mirror of
https://github.com/openai/codex.git
synced 2026-03-30 03:56:35 +03:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ae9f99d41 | ||
|
|
b480a0eeae | ||
|
|
4e38c6a73b |
67
.bazelrc
67
.bazelrc
@@ -18,7 +18,10 @@ common --enable_platform_specific_config
|
||||
common:linux --host_platform=//:local_linux
|
||||
common:windows --host_platform=//:local_windows
|
||||
common --@rules_cc//cc/toolchains/args/archiver_flags:use_libtool_on_macos=False
|
||||
common --@llvm//config:experimental_stub_libgcc_s
|
||||
common --@toolchains_llvm_bootstrapped//config:experimental_stub_libgcc_s
|
||||
|
||||
# We need to use the sh toolchain on windows so we don't send host bash paths to the linux executor.
|
||||
common:windows --@rules_rust//rust/settings:experimental_use_sh_toolchain_for_bootstrap_process_wrapper
|
||||
|
||||
# TODO(zbarsky): rules_rust doesn't implement this flag properly with remote exec...
|
||||
# common --@rules_rust//rust/settings:pipelined_compilation
|
||||
@@ -53,65 +56,3 @@ common --jobs=30
|
||||
common:remote --extra_execution_platforms=//:rbe
|
||||
common:remote --remote_executor=grpcs://remote.buildbuddy.io
|
||||
common:remote --jobs=800
|
||||
# TODO(team): Evaluate if this actually helps, zbarsky is not sure, everything seems bottlenecked on `core` either way.
|
||||
# Enable pipelined compilation since we are not bound by local CPU count.
|
||||
#common:remote --@rules_rust//rust/settings:pipelined_compilation
|
||||
|
||||
# GitHub Actions CI configs.
|
||||
common:ci --remote_download_minimal
|
||||
common:ci --keep_going
|
||||
common:ci --verbose_failures
|
||||
common:ci --build_metadata=REPO_URL=https://github.com/openai/codex.git
|
||||
common:ci --build_metadata=ROLE=CI
|
||||
common:ci --build_metadata=VISIBILITY=PUBLIC
|
||||
|
||||
# Disable disk cache in CI since we have a remote one and aren't using persistent workers.
|
||||
common:ci --disk_cache=
|
||||
|
||||
# Shared config for the main Bazel CI workflow.
|
||||
common:ci-bazel --config=ci
|
||||
common:ci-bazel --build_metadata=TAG_workflow=bazel
|
||||
|
||||
# Shared config for Bazel-backed Rust linting.
|
||||
build:clippy --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect
|
||||
build:clippy --output_groups=+clippy_checks
|
||||
build:clippy --@rules_rust//rust/settings:clippy.toml=//codex-rs:clippy.toml
|
||||
|
||||
# Shared config for Bazel-backed argument-comment-lint.
|
||||
build:argument-comment-lint --aspects=//tools/argument-comment-lint:lint_aspect.bzl%rust_argument_comment_lint_aspect
|
||||
build:argument-comment-lint --output_groups=argument_comment_lint_checks
|
||||
build:argument-comment-lint --@rules_rust//rust/toolchain/channel=nightly
|
||||
|
||||
# Rearrange caches on Windows so they're on the same volume as the checkout.
|
||||
common:ci-windows --config=ci-bazel
|
||||
common:ci-windows --build_metadata=TAG_os=windows
|
||||
common:ci-windows --repo_contents_cache=D:/a/.cache/bazel-repo-contents-cache
|
||||
common:ci-windows --repository_cache=D:/a/.cache/bazel-repo-cache
|
||||
|
||||
# We prefer to run the build actions entirely remotely so we can dial up the concurrency.
|
||||
# We have platform-specific tests, so we want to execute the tests on all platforms using the strongest sandboxing available on each platform.
|
||||
|
||||
# On linux, we can do a full remote build/test, by targeting the right (x86/arm) runners, so we have coverage of both.
|
||||
# Linux crossbuilds don't work until we untangle the libc constraint mess.
|
||||
common:ci-linux --config=ci-bazel
|
||||
common:ci-linux --build_metadata=TAG_os=linux
|
||||
common:ci-linux --config=remote
|
||||
common:ci-linux --strategy=remote
|
||||
common:ci-linux --platforms=//:rbe
|
||||
|
||||
# On mac, we can run all the build actions remotely but test actions locally.
|
||||
common:ci-macos --config=ci-bazel
|
||||
common:ci-macos --build_metadata=TAG_os=macos
|
||||
common:ci-macos --config=remote
|
||||
common:ci-macos --strategy=remote
|
||||
common:ci-macos --strategy=TestRunner=darwin-sandbox,local
|
||||
|
||||
# Linux-only V8 CI config.
|
||||
common:ci-v8 --config=ci
|
||||
common:ci-v8 --build_metadata=TAG_workflow=v8
|
||||
common:ci-v8 --build_metadata=TAG_os=linux
|
||||
common:ci-v8 --config=remote
|
||||
common:ci-v8 --strategy=remote
|
||||
|
||||
# Optional per-user local overrides.
|
||||
try-import %workspace%/user.bazelrc
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new,*meriyah.umd.min.js
|
||||
check-hidden = true
|
||||
ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b
|
||||
ignore-words-list = ratatui,ser,iTerm,iterm2,iterm,te,TE,PASE,SEH
|
||||
ignore-words-list = ratatui,ser,iTerm,iterm2,iterm,te,TE
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
---
|
||||
name: babysit-pr
|
||||
description: Babysit a GitHub pull request after creation by continuously polling CI checks/workflow runs, new review comments, and mergeability state until the PR is ready to merge (or merged/closed). Diagnose failures, retry likely flaky failures up to 3 times, auto-fix/push branch-related issues when appropriate, and stop only when user help is required (for example CI infrastructure issues, exhausted flaky retries, or ambiguous/blocking situations). Use when the user asks Codex to monitor a PR, watch CI, handle review comments, or keep an eye on failures and feedback on an open PR.
|
||||
---
|
||||
|
||||
# PR Babysitter
|
||||
|
||||
## Objective
|
||||
Babysit a PR persistently until one of these terminal outcomes occurs:
|
||||
|
||||
- The PR is merged or closed.
|
||||
- CI is successful, there are no unaddressed review comments surfaced by the watcher, required review approval is not blocking merge, and there are no potential merge conflicts (PR is mergeable / not reporting conflict risk).
|
||||
- A situation requires user help (for example CI infrastructure issues, repeated flaky failures after retry budget is exhausted, permission problems, or ambiguity that cannot be resolved safely).
|
||||
|
||||
Do not stop merely because a single snapshot returns `idle` while checks are still pending.
|
||||
|
||||
## Inputs
|
||||
Accept any of the following:
|
||||
|
||||
- No PR argument: infer the PR from the current branch (`--pr auto`)
|
||||
- PR number
|
||||
- PR URL
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. When the user asks to "monitor"/"watch"/"babysit" a PR, start with the watcher's continuous mode (`--watch`) unless you are intentionally doing a one-shot diagnostic snapshot.
|
||||
2. Run the watcher script to snapshot PR/CI/review state (or consume each streamed snapshot from `--watch`).
|
||||
3. Inspect the `actions` list in the JSON response.
|
||||
4. If `diagnose_ci_failure` is present, inspect failed run logs and classify the failure.
|
||||
5. If the failure is likely caused by the current branch, patch code locally, commit, and push.
|
||||
6. If `process_review_comment` is present, inspect surfaced review items and decide whether to address them.
|
||||
7. If a review item is actionable and correct, patch code locally, commit, push, and then mark the associated review thread/comment as resolved once the fix is on GitHub.
|
||||
8. If a review item from another author is non-actionable, already addressed, or not valid, post one reply on the comment/thread explaining that decision (for example answering the question or explaining why no change is needed). If the watcher later surfaces your own reply, treat that self-authored item as already handled and do not reply again.
|
||||
9. If the failure is likely flaky/unrelated and `retry_failed_checks` is present, rerun failed jobs with `--retry-failed-now`.
|
||||
10. If both actionable review feedback and `retry_failed_checks` are present, prioritize review feedback first; a new commit will retrigger CI, so avoid rerunning flaky checks on the old SHA unless you intentionally defer the review change.
|
||||
11. On every loop, verify mergeability / merge-conflict status (for example via `gh pr view`) in addition to CI and review state.
|
||||
12. After any push or rerun action, immediately return to step 1 and continue polling on the updated SHA/state.
|
||||
13. If you had been using `--watch` before pausing to patch/commit/push, relaunch `--watch` yourself in the same turn immediately after the push (do not wait for the user to re-invoke the skill).
|
||||
14. Repeat polling until the PR is green + review-clean + mergeable, `stop_pr_closed` appears, or a user-help-required blocker is reached.
|
||||
15. Maintain terminal/session ownership: while babysitting is active, keep consuming watcher output in the same turn; do not leave a detached `--watch` process running and then end the turn as if monitoring were complete.
|
||||
|
||||
## Commands
|
||||
|
||||
### One-shot snapshot
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr auto --once
|
||||
```
|
||||
|
||||
### Continuous watch (JSONL)
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr auto --watch
|
||||
```
|
||||
|
||||
### Trigger flaky retry cycle (only when watcher indicates)
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr auto --retry-failed-now
|
||||
```
|
||||
|
||||
### Explicit PR target
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr <number-or-url> --once
|
||||
```
|
||||
|
||||
## CI Failure Classification
|
||||
Use `gh` commands to inspect failed runs before deciding to rerun.
|
||||
|
||||
- `gh run view <run-id> --json jobs,name,workflowName,conclusion,status,url,headSha`
|
||||
- `gh run view <run-id> --log-failed`
|
||||
|
||||
Prefer treating failures as branch-related when logs point to changed code (compile/test/lint/typecheck/snapshots/static analysis in touched areas).
|
||||
|
||||
Prefer treating failures as flaky/unrelated when logs show transient infra/external issues (timeouts, runner provisioning failures, registry/network outages, GitHub Actions infra errors).
|
||||
|
||||
If classification is ambiguous, perform one manual diagnosis attempt before choosing rerun.
|
||||
|
||||
Read `.codex/skills/babysit-pr/references/heuristics.md` for a concise checklist.
|
||||
|
||||
## Review Comment Handling
|
||||
The watcher surfaces review items from:
|
||||
|
||||
- PR issue comments
|
||||
- Inline review comments
|
||||
- Review submissions (COMMENT / APPROVED / CHANGES_REQUESTED)
|
||||
|
||||
It intentionally surfaces Codex reviewer bot feedback (for example comments/reviews from `chatgpt-codex-connector[bot]`) in addition to human reviewer feedback. Most unrelated bot noise should still be ignored.
|
||||
For safety, the watcher only auto-surfaces trusted human review authors (for example repo OWNER/MEMBER/COLLABORATOR, plus the authenticated operator) and approved review bots such as Codex.
|
||||
On a fresh watcher state file, existing pending review feedback may be surfaced immediately (not only comments that arrive after monitoring starts). This is intentional so already-open review comments are not missed.
|
||||
|
||||
When you agree with a comment and it is actionable:
|
||||
|
||||
1. Patch code locally.
|
||||
2. Commit with `codex: address PR review feedback (#<n>)`.
|
||||
3. Push to the PR head branch.
|
||||
4. After the push succeeds, mark the associated GitHub review thread/comment as resolved.
|
||||
5. Resume watching on the new SHA immediately (do not stop after reporting the push).
|
||||
6. If monitoring was running in `--watch` mode, restart `--watch` immediately after the push in the same turn; do not wait for the user to ask again.
|
||||
|
||||
If you disagree or the comment is non-actionable/already addressed, reply once directly on the GitHub comment/thread so the reviewer gets an explicit answer, then continue the watcher loop. If the watcher later surfaces your own reply because the authenticated operator is treated as a trusted review author, treat that self-authored item as already handled and do not reply again.
|
||||
If a code review comment/thread is already marked as resolved in GitHub, treat it as non-actionable and safely ignore it unless new unresolved follow-up feedback appears.
|
||||
|
||||
## Git Safety Rules
|
||||
|
||||
- Work only on the PR head branch.
|
||||
- Avoid destructive git commands.
|
||||
- Do not switch branches unless necessary to recover context.
|
||||
- Before editing, check for unrelated uncommitted changes. If present, stop and ask the user.
|
||||
- After each successful fix, commit and `git push`, then re-run the watcher.
|
||||
- If you interrupted a live `--watch` session to make the fix, restart `--watch` immediately after the push in the same turn.
|
||||
- Do not run multiple concurrent `--watch` processes for the same PR/state file; keep one watcher session active and reuse it until it stops or you intentionally restart it.
|
||||
- A push is not a terminal outcome; continue the monitoring loop unless a strict stop condition is met.
|
||||
|
||||
Commit message defaults:
|
||||
|
||||
- `codex: fix CI failure on PR #<n>`
|
||||
- `codex: address PR review feedback (#<n>)`
|
||||
|
||||
## Monitoring Loop Pattern
|
||||
Use this loop in a live Codex session:
|
||||
|
||||
1. Run `--once`.
|
||||
2. Read `actions`.
|
||||
3. First check whether the PR is now merged or otherwise closed; if so, report that terminal state and stop polling immediately.
|
||||
4. Check CI summary, new review items, and mergeability/conflict status.
|
||||
5. Diagnose CI failures and classify branch-related vs flaky/unrelated.
|
||||
6. For each surfaced review item from another author, either reply once with an explanation if it is non-actionable or patch/commit/push and then resolve it if it is actionable. If a later snapshot surfaces your own reply, treat it as informational and continue without responding again.
|
||||
7. Process actionable review comments before flaky reruns when both are present; if a review fix requires a commit, push it and skip rerunning failed checks on the old SHA.
|
||||
8. Retry failed checks only when `retry_failed_checks` is present and you are not about to replace the current SHA with a review/CI fix commit.
|
||||
9. If you pushed a commit, resolved a review thread, replied to a review comment, or triggered a rerun, report the action briefly and continue polling (do not stop).
|
||||
10. After a review-fix push, proactively restart continuous monitoring (`--watch`) in the same turn unless a strict stop condition has already been reached.
|
||||
11. If everything is passing, mergeable, not blocked on required review approval, and there are no unaddressed review items, report success and stop.
|
||||
12. If blocked on a user-help-required issue (infra outage, exhausted flaky retries, unclear reviewer request, permissions), report the blocker and stop.
|
||||
13. Otherwise sleep according to the polling cadence below and repeat.
|
||||
|
||||
When the user explicitly asks to monitor/watch/babysit a PR, prefer `--watch` so polling continues autonomously in one command. Use repeated `--once` snapshots only for debugging, local testing, or when the user explicitly asks for a one-shot check.
|
||||
Do not stop to ask the user whether to continue polling; continue autonomously until a strict stop condition is met or the user explicitly interrupts.
|
||||
Do not hand control back to the user after a review-fix push just because a new SHA was created; restarting the watcher and re-entering the poll loop is part of the same babysitting task.
|
||||
If a `--watch` process is still running and no strict stop condition has been reached, the babysitting task is still in progress; keep streaming/consuming watcher output instead of ending the turn.
|
||||
|
||||
## Polling Cadence
|
||||
Use adaptive polling and continue monitoring even after CI turns green:
|
||||
|
||||
- While CI is not green (pending/running/queued or failing): poll every 1 minute.
|
||||
- After CI turns green: start at every 1 minute, then back off exponentially when there is no change (for example 1m, 2m, 4m, 8m, 16m, 32m), capping at every 1 hour.
|
||||
- Reset the green-state polling interval back to 1 minute whenever anything changes (new commit/SHA, check status changes, new review comments, mergeability changes, review decision changes).
|
||||
- If CI stops being green again (new commit, rerun, or regression): return to 1-minute polling.
|
||||
- If any poll shows the PR is merged or otherwise closed: stop polling immediately and report the terminal state.
|
||||
|
||||
## Stop Conditions (Strict)
|
||||
Stop only when one of the following is true:
|
||||
|
||||
- PR merged or closed (stop as soon as a poll/snapshot confirms this).
|
||||
- PR is ready to merge: CI succeeded, no surfaced unaddressed review comments, not blocked on required review approval, and no merge conflict risk.
|
||||
- User intervention is required and Codex cannot safely proceed alone.
|
||||
|
||||
Keep polling when:
|
||||
|
||||
- `actions` contains only `idle` but checks are still pending.
|
||||
- CI is still running/queued.
|
||||
- Review state is quiet but CI is not terminal.
|
||||
- CI is green but mergeability is unknown/pending.
|
||||
- CI is green and mergeable, but the PR is still open and you are waiting for possible new review comments or merge-conflict changes per the green-state cadence.
|
||||
- The PR is green but blocked on review approval (`REVIEW_REQUIRED` / similar); continue polling on the green-state cadence and surface any new review comments without asking for confirmation to keep watching.
|
||||
|
||||
## Output Expectations
|
||||
Provide concise progress updates while monitoring and a final summary that includes:
|
||||
|
||||
- During long unchanged monitoring periods, avoid emitting a full update on every poll; summarize only status changes plus occasional heartbeat updates.
|
||||
- Treat push confirmations, intermediate CI snapshots, and review-action updates as progress updates only; do not emit the final summary or end the babysitting session unless a strict stop condition is met.
|
||||
- A user request to "monitor" is not satisfied by a couple of sample polls; remain in the loop until a strict stop condition or an explicit user interruption.
|
||||
- A review-fix commit + push is not a completion event; immediately resume live monitoring (`--watch`) in the same turn and continue reporting progress updates.
|
||||
- When CI first transitions to all green for the current SHA, emit a one-time celebratory progress update (do not repeat it on every green poll). Preferred style: `🚀 CI is all green! 33/33 passed. Still on watch for review approval.`
|
||||
- Do not send the final summary while a watcher terminal is still running unless the watcher has emitted/confirmed a strict stop condition; otherwise continue with progress updates.
|
||||
|
||||
- Final PR SHA
|
||||
- CI status summary
|
||||
- Mergeability / conflict status
|
||||
- Fixes pushed
|
||||
- Flaky retry cycles used
|
||||
- Remaining unresolved failures or review comments
|
||||
|
||||
## References
|
||||
|
||||
- Heuristics and decision tree: `.codex/skills/babysit-pr/references/heuristics.md`
|
||||
- GitHub CLI/API details used by the watcher: `.codex/skills/babysit-pr/references/github-api-notes.md`
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "PR Babysitter"
|
||||
short_description: "Watch PR CI, reviews, and merge conflicts"
|
||||
default_prompt: "Babysit the current PR: monitor CI, reviewer comments, and merge-conflict status (prefer the watcher’s --watch mode for live monitoring); fix valid issues, push updates, and rerun flaky failures up to 3 times. Keep exactly one watcher session active for the PR (do not leave duplicate --watch terminals running). If you pause monitoring to patch review/CI feedback, restart --watch yourself immediately after the push in the same turn. If a watcher is still running and no strict stop condition has been reached, the task is still in progress: keep consuming watcher output and sending progress updates instead of ending the turn. Continue polling autonomously after any push/rerun until a strict terminal stop condition is reached or the user interrupts."
|
||||
@@ -1,72 +0,0 @@
|
||||
# GitHub CLI / API Notes For `babysit-pr`
|
||||
|
||||
## Primary commands used
|
||||
|
||||
### PR metadata
|
||||
|
||||
- `gh pr view --json number,url,state,mergedAt,closedAt,headRefName,headRefOid,headRepository,headRepositoryOwner`
|
||||
|
||||
Used to resolve PR number, URL, branch, head SHA, and closed/merged state.
|
||||
|
||||
### PR checks summary
|
||||
|
||||
- `gh pr checks --json name,state,bucket,link,workflow,event,startedAt,completedAt`
|
||||
|
||||
Used to compute pending/failed/passed counts and whether the current CI round is terminal.
|
||||
|
||||
### Workflow runs for head SHA
|
||||
|
||||
- `gh api repos/{owner}/{repo}/actions/runs -X GET -f head_sha=<sha> -f per_page=100`
|
||||
|
||||
Used to discover failed workflow runs and rerunnable run IDs.
|
||||
|
||||
### Failed log inspection
|
||||
|
||||
- `gh run view <run-id> --json jobs,name,workflowName,conclusion,status,url,headSha`
|
||||
- `gh run view <run-id> --log-failed`
|
||||
|
||||
Used by Codex to classify branch-related vs flaky/unrelated failures.
|
||||
|
||||
### Retry failed jobs only
|
||||
|
||||
- `gh run rerun <run-id> --failed`
|
||||
|
||||
Reruns only failed jobs (and dependencies) for a workflow run.
|
||||
|
||||
## Review-related endpoints
|
||||
|
||||
- Issue comments on PR:
|
||||
- `gh api repos/{owner}/{repo}/issues/<pr_number>/comments?per_page=100`
|
||||
- Inline PR review comments:
|
||||
- `gh api repos/{owner}/{repo}/pulls/<pr_number>/comments?per_page=100`
|
||||
- Review submissions:
|
||||
- `gh api repos/{owner}/{repo}/pulls/<pr_number>/reviews?per_page=100`
|
||||
|
||||
## JSON fields consumed by the watcher
|
||||
|
||||
### `gh pr view`
|
||||
|
||||
- `number`
|
||||
- `url`
|
||||
- `state`
|
||||
- `mergedAt`
|
||||
- `closedAt`
|
||||
- `headRefName`
|
||||
- `headRefOid`
|
||||
|
||||
### `gh pr checks`
|
||||
|
||||
- `bucket` (`pass`, `fail`, `pending`, `skipping`)
|
||||
- `state`
|
||||
- `name`
|
||||
- `workflow`
|
||||
- `link`
|
||||
|
||||
### Actions runs API (`workflow_runs[]`)
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `status`
|
||||
- `conclusion`
|
||||
- `html_url`
|
||||
- `head_sha`
|
||||
@@ -1,58 +0,0 @@
|
||||
# CI / Review Heuristics
|
||||
|
||||
## CI classification checklist
|
||||
|
||||
Treat as **branch-related** when logs clearly indicate a regression caused by the PR branch:
|
||||
|
||||
- Compile/typecheck/lint failures in files or modules touched by the branch
|
||||
- Deterministic unit/integration test failures in changed areas
|
||||
- Snapshot output changes caused by UI/text changes in the branch
|
||||
- Static analysis violations introduced by the latest push
|
||||
- Build script/config changes in the PR causing a deterministic failure
|
||||
|
||||
Treat as **likely flaky or unrelated** when evidence points to transient or external issues:
|
||||
|
||||
- DNS/network/registry timeout errors while fetching dependencies
|
||||
- Runner image provisioning or startup failures
|
||||
- GitHub Actions infrastructure/service outages
|
||||
- Cloud/service rate limits or transient API outages
|
||||
- Non-deterministic failures in unrelated integration tests with known flake patterns
|
||||
|
||||
If uncertain, inspect failed logs once before choosing rerun.
|
||||
|
||||
## Decision tree (fix vs rerun vs stop)
|
||||
|
||||
1. If PR is merged/closed: stop.
|
||||
2. If there are failed checks:
|
||||
- Diagnose first.
|
||||
- If branch-related: fix locally, commit, push.
|
||||
- If likely flaky/unrelated and all checks for the current SHA are terminal: rerun failed jobs.
|
||||
- If checks are still pending: wait.
|
||||
3. If flaky reruns for the same SHA reach the configured limit (default 3): stop and report persistent failure.
|
||||
4. Independently, process any new human review comments.
|
||||
|
||||
## Review comment agreement criteria
|
||||
|
||||
Address the comment when:
|
||||
|
||||
- The comment is technically correct.
|
||||
- The change is actionable in the current branch.
|
||||
- The requested change does not conflict with the user’s intent or recent guidance.
|
||||
- The change can be made safely without unrelated refactors.
|
||||
|
||||
Do not auto-fix when:
|
||||
|
||||
- The comment is ambiguous and needs clarification.
|
||||
- The request conflicts with explicit user instructions.
|
||||
- The proposed change requires product/design decisions the user has not made.
|
||||
- The codebase is in a dirty/unrelated state that makes safe editing uncertain.
|
||||
|
||||
## Stop-and-ask conditions
|
||||
|
||||
Stop and ask the user instead of continuing automatically when:
|
||||
|
||||
- The local worktree has unrelated uncommitted changes.
|
||||
- `gh` auth/permissions fail.
|
||||
- The PR branch cannot be pushed.
|
||||
- CI failures persist after the flaky retry budget.
|
||||
- Reviewer feedback requires a product decision or cross-team coordination.
|
||||
@@ -1,805 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Watch GitHub PR CI and review activity for Codex PR babysitting workflows."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
FAILED_RUN_CONCLUSIONS = {
|
||||
"failure",
|
||||
"timed_out",
|
||||
"cancelled",
|
||||
"action_required",
|
||||
"startup_failure",
|
||||
"stale",
|
||||
}
|
||||
PENDING_CHECK_STATES = {
|
||||
"QUEUED",
|
||||
"IN_PROGRESS",
|
||||
"PENDING",
|
||||
"WAITING",
|
||||
"REQUESTED",
|
||||
}
|
||||
REVIEW_BOT_LOGIN_KEYWORDS = {
|
||||
"codex",
|
||||
}
|
||||
TRUSTED_AUTHOR_ASSOCIATIONS = {
|
||||
"OWNER",
|
||||
"MEMBER",
|
||||
"COLLABORATOR",
|
||||
}
|
||||
MERGE_BLOCKING_REVIEW_DECISIONS = {
|
||||
"REVIEW_REQUIRED",
|
||||
"CHANGES_REQUESTED",
|
||||
}
|
||||
MERGE_CONFLICT_OR_BLOCKING_STATES = {
|
||||
"BLOCKED",
|
||||
"DIRTY",
|
||||
"DRAFT",
|
||||
"UNKNOWN",
|
||||
}
|
||||
GREEN_STATE_MAX_POLL_SECONDS = 60 * 60
|
||||
|
||||
|
||||
class GhCommandError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Normalize PR/CI/review state for Codex PR babysitting and optionally "
|
||||
"trigger flaky reruns."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--pr", default="auto", help="auto, PR number, or PR URL")
|
||||
parser.add_argument("--repo", help="Optional OWNER/REPO override")
|
||||
parser.add_argument("--poll-seconds", type=int, default=30, help="Watch poll interval")
|
||||
parser.add_argument(
|
||||
"--max-flaky-retries",
|
||||
type=int,
|
||||
default=3,
|
||||
help="Max rerun cycles per head SHA before stop recommendation",
|
||||
)
|
||||
parser.add_argument("--state-file", help="Path to state JSON file")
|
||||
parser.add_argument("--once", action="store_true", help="Emit one snapshot and exit")
|
||||
parser.add_argument("--watch", action="store_true", help="Continuously emit JSONL snapshots")
|
||||
parser.add_argument(
|
||||
"--retry-failed-now",
|
||||
action="store_true",
|
||||
help="Rerun failed jobs for current failed workflow runs when policy allows",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Emit machine-readable output (default behavior for --once and --retry-failed-now)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.poll_seconds <= 0:
|
||||
parser.error("--poll-seconds must be > 0")
|
||||
if args.max_flaky_retries < 0:
|
||||
parser.error("--max-flaky-retries must be >= 0")
|
||||
if args.watch and args.retry_failed_now:
|
||||
parser.error("--watch cannot be combined with --retry-failed-now")
|
||||
if not args.once and not args.watch and not args.retry_failed_now:
|
||||
args.once = True
|
||||
return args
|
||||
|
||||
|
||||
def _format_gh_error(cmd, err):
|
||||
stdout = (err.stdout or "").strip()
|
||||
stderr = (err.stderr or "").strip()
|
||||
parts = [f"GitHub CLI command failed: {' '.join(cmd)}"]
|
||||
if stdout:
|
||||
parts.append(f"stdout: {stdout}")
|
||||
if stderr:
|
||||
parts.append(f"stderr: {stderr}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def gh_text(args, repo=None):
|
||||
cmd = ["gh"]
|
||||
# `gh api` does not accept `-R/--repo` on all gh versions. The watcher's
|
||||
# API calls use explicit endpoints (e.g. repos/{owner}/{repo}/...), so the
|
||||
# repo flag is unnecessary there.
|
||||
if repo and (not args or args[0] != "api"):
|
||||
cmd.extend(["-R", repo])
|
||||
cmd.extend(args)
|
||||
try:
|
||||
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
except FileNotFoundError as err:
|
||||
raise GhCommandError("`gh` command not found") from err
|
||||
except subprocess.CalledProcessError as err:
|
||||
raise GhCommandError(_format_gh_error(cmd, err)) from err
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def gh_json(args, repo=None):
|
||||
raw = gh_text(args, repo=repo).strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError as err:
|
||||
raise GhCommandError(f"Failed to parse JSON from gh output for {' '.join(args)}") from err
|
||||
|
||||
|
||||
def parse_pr_spec(pr_spec):
|
||||
if pr_spec == "auto":
|
||||
return {"mode": "auto", "value": None}
|
||||
if re.fullmatch(r"\d+", pr_spec):
|
||||
return {"mode": "number", "value": pr_spec}
|
||||
parsed = urlparse(pr_spec)
|
||||
if parsed.scheme and parsed.netloc and "/pull/" in parsed.path:
|
||||
return {"mode": "url", "value": pr_spec}
|
||||
raise ValueError("--pr must be 'auto', a PR number, or a PR URL")
|
||||
|
||||
|
||||
def pr_view_fields():
|
||||
return (
|
||||
"number,url,state,mergedAt,closedAt,headRefName,headRefOid,"
|
||||
"headRepository,headRepositoryOwner,mergeable,mergeStateStatus,reviewDecision"
|
||||
)
|
||||
|
||||
|
||||
def checks_fields():
|
||||
return "name,state,bucket,link,workflow,event,startedAt,completedAt"
|
||||
|
||||
|
||||
def resolve_pr(pr_spec, repo_override=None):
|
||||
parsed = parse_pr_spec(pr_spec)
|
||||
cmd = ["pr", "view"]
|
||||
if parsed["value"] is not None:
|
||||
cmd.append(parsed["value"])
|
||||
cmd.extend(["--json", pr_view_fields()])
|
||||
data = gh_json(cmd, repo=repo_override)
|
||||
if not isinstance(data, dict):
|
||||
raise GhCommandError("Unexpected PR payload from `gh pr view`")
|
||||
|
||||
pr_url = str(data.get("url") or "")
|
||||
repo = (
|
||||
repo_override
|
||||
or extract_repo_from_pr_url(pr_url)
|
||||
or extract_repo_from_pr_view(data)
|
||||
)
|
||||
if not repo:
|
||||
raise GhCommandError("Unable to determine OWNER/REPO for the PR")
|
||||
|
||||
state = str(data.get("state") or "")
|
||||
merged = bool(data.get("mergedAt"))
|
||||
closed = bool(data.get("closedAt")) or state.upper() == "CLOSED"
|
||||
|
||||
return {
|
||||
"number": int(data["number"]),
|
||||
"url": pr_url,
|
||||
"repo": repo,
|
||||
"head_sha": str(data.get("headRefOid") or ""),
|
||||
"head_branch": str(data.get("headRefName") or ""),
|
||||
"state": state,
|
||||
"merged": merged,
|
||||
"closed": closed,
|
||||
"mergeable": str(data.get("mergeable") or ""),
|
||||
"merge_state_status": str(data.get("mergeStateStatus") or ""),
|
||||
"review_decision": str(data.get("reviewDecision") or ""),
|
||||
}
|
||||
|
||||
|
||||
def extract_repo_from_pr_view(data):
|
||||
head_repo = data.get("headRepository")
|
||||
head_owner = data.get("headRepositoryOwner")
|
||||
owner = None
|
||||
name = None
|
||||
if isinstance(head_owner, dict):
|
||||
owner = head_owner.get("login") or head_owner.get("name")
|
||||
elif isinstance(head_owner, str):
|
||||
owner = head_owner
|
||||
if isinstance(head_repo, dict):
|
||||
name = head_repo.get("name")
|
||||
repo_owner = head_repo.get("owner")
|
||||
if not owner and isinstance(repo_owner, dict):
|
||||
owner = repo_owner.get("login") or repo_owner.get("name")
|
||||
elif isinstance(head_repo, str):
|
||||
name = head_repo
|
||||
if owner and name:
|
||||
return f"{owner}/{name}"
|
||||
return None
|
||||
def extract_repo_from_pr_url(pr_url):
|
||||
parsed = urlparse(pr_url)
|
||||
parts = [p for p in parsed.path.split("/") if p]
|
||||
if len(parts) >= 4 and parts[2] == "pull":
|
||||
return f"{parts[0]}/{parts[1]}"
|
||||
return None
|
||||
|
||||
|
||||
def load_state(path):
|
||||
if path.exists():
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except json.JSONDecodeError as err:
|
||||
raise RuntimeError(f"State file is not valid JSON: {path}") from err
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError(f"State file must contain an object: {path}")
|
||||
return data, False
|
||||
return {
|
||||
"pr": {},
|
||||
"started_at": None,
|
||||
"last_seen_head_sha": None,
|
||||
"retries_by_sha": {},
|
||||
"seen_issue_comment_ids": [],
|
||||
"seen_review_comment_ids": [],
|
||||
"seen_review_ids": [],
|
||||
"last_snapshot_at": None,
|
||||
}, True
|
||||
|
||||
|
||||
def save_state(path, state):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = json.dumps(state, indent=2, sort_keys=True) + "\n"
|
||||
fd, tmp_name = tempfile.mkstemp(prefix=f"{path.name}.", suffix=".tmp", dir=path.parent)
|
||||
tmp_path = Path(tmp_name)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as tmp_file:
|
||||
tmp_file.write(payload)
|
||||
os.replace(tmp_path, path)
|
||||
except Exception:
|
||||
try:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def default_state_file_for(pr):
|
||||
repo_slug = pr["repo"].replace("/", "-")
|
||||
return Path(f"/tmp/codex-babysit-pr-{repo_slug}-pr{pr['number']}.json")
|
||||
|
||||
|
||||
def get_pr_checks(pr_spec, repo):
|
||||
parsed = parse_pr_spec(pr_spec)
|
||||
cmd = ["pr", "checks"]
|
||||
if parsed["value"] is not None:
|
||||
cmd.append(parsed["value"])
|
||||
cmd.extend(["--json", checks_fields()])
|
||||
data = gh_json(cmd, repo=repo)
|
||||
if data is None:
|
||||
return []
|
||||
if not isinstance(data, list):
|
||||
raise GhCommandError("Unexpected payload from `gh pr checks`")
|
||||
return data
|
||||
|
||||
|
||||
def is_pending_check(check):
|
||||
bucket = str(check.get("bucket") or "").lower()
|
||||
state = str(check.get("state") or "").upper()
|
||||
return bucket == "pending" or state in PENDING_CHECK_STATES
|
||||
|
||||
|
||||
def summarize_checks(checks):
|
||||
pending_count = 0
|
||||
failed_count = 0
|
||||
passed_count = 0
|
||||
for check in checks:
|
||||
bucket = str(check.get("bucket") or "").lower()
|
||||
if is_pending_check(check):
|
||||
pending_count += 1
|
||||
if bucket == "fail":
|
||||
failed_count += 1
|
||||
if bucket == "pass":
|
||||
passed_count += 1
|
||||
return {
|
||||
"pending_count": pending_count,
|
||||
"failed_count": failed_count,
|
||||
"passed_count": passed_count,
|
||||
"all_terminal": pending_count == 0,
|
||||
}
|
||||
|
||||
|
||||
def get_workflow_runs_for_sha(repo, head_sha):
|
||||
endpoint = f"repos/{repo}/actions/runs"
|
||||
data = gh_json(
|
||||
["api", endpoint, "-X", "GET", "-f", f"head_sha={head_sha}", "-f", "per_page=100"],
|
||||
repo=repo,
|
||||
)
|
||||
if not isinstance(data, dict):
|
||||
raise GhCommandError("Unexpected payload from actions runs API")
|
||||
runs = data.get("workflow_runs") or []
|
||||
if not isinstance(runs, list):
|
||||
raise GhCommandError("Expected `workflow_runs` to be a list")
|
||||
return runs
|
||||
|
||||
|
||||
def failed_runs_from_workflow_runs(runs, head_sha):
|
||||
failed_runs = []
|
||||
for run in runs:
|
||||
if not isinstance(run, dict):
|
||||
continue
|
||||
if str(run.get("head_sha") or "") != head_sha:
|
||||
continue
|
||||
conclusion = str(run.get("conclusion") or "")
|
||||
if conclusion not in FAILED_RUN_CONCLUSIONS:
|
||||
continue
|
||||
failed_runs.append(
|
||||
{
|
||||
"run_id": run.get("id"),
|
||||
"workflow_name": run.get("name") or run.get("display_title") or "",
|
||||
"status": str(run.get("status") or ""),
|
||||
"conclusion": conclusion,
|
||||
"html_url": str(run.get("html_url") or ""),
|
||||
}
|
||||
)
|
||||
failed_runs.sort(key=lambda item: (str(item.get("workflow_name") or ""), str(item.get("run_id") or "")))
|
||||
return failed_runs
|
||||
|
||||
|
||||
def get_authenticated_login():
|
||||
data = gh_json(["api", "user"])
|
||||
if not isinstance(data, dict) or not data.get("login"):
|
||||
raise GhCommandError("Unable to determine authenticated GitHub login from `gh api user`")
|
||||
return str(data["login"])
|
||||
|
||||
|
||||
def comment_endpoints(repo, pr_number):
|
||||
return {
|
||||
"issue_comment": f"repos/{repo}/issues/{pr_number}/comments",
|
||||
"review_comment": f"repos/{repo}/pulls/{pr_number}/comments",
|
||||
"review": f"repos/{repo}/pulls/{pr_number}/reviews",
|
||||
}
|
||||
|
||||
|
||||
def gh_api_list_paginated(endpoint, repo=None, per_page=100):
|
||||
items = []
|
||||
page = 1
|
||||
while True:
|
||||
sep = "&" if "?" in endpoint else "?"
|
||||
page_endpoint = f"{endpoint}{sep}per_page={per_page}&page={page}"
|
||||
payload = gh_json(["api", page_endpoint], repo=repo)
|
||||
if payload is None:
|
||||
break
|
||||
if not isinstance(payload, list):
|
||||
raise GhCommandError(f"Unexpected paginated payload from gh api {endpoint}")
|
||||
items.extend(payload)
|
||||
if len(payload) < per_page:
|
||||
break
|
||||
page += 1
|
||||
return items
|
||||
|
||||
|
||||
def normalize_issue_comments(items):
|
||||
out = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"kind": "issue_comment",
|
||||
"id": str(item.get("id") or ""),
|
||||
"author": extract_login(item.get("user")),
|
||||
"author_association": str(item.get("author_association") or ""),
|
||||
"created_at": str(item.get("created_at") or ""),
|
||||
"body": str(item.get("body") or ""),
|
||||
"path": None,
|
||||
"line": None,
|
||||
"url": str(item.get("html_url") or ""),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_review_comments(items):
|
||||
out = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
line = item.get("line")
|
||||
if line is None:
|
||||
line = item.get("original_line")
|
||||
out.append(
|
||||
{
|
||||
"kind": "review_comment",
|
||||
"id": str(item.get("id") or ""),
|
||||
"author": extract_login(item.get("user")),
|
||||
"author_association": str(item.get("author_association") or ""),
|
||||
"created_at": str(item.get("created_at") or ""),
|
||||
"body": str(item.get("body") or ""),
|
||||
"path": item.get("path"),
|
||||
"line": line,
|
||||
"url": str(item.get("html_url") or ""),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_reviews(items):
|
||||
out = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"kind": "review",
|
||||
"id": str(item.get("id") or ""),
|
||||
"author": extract_login(item.get("user")),
|
||||
"author_association": str(item.get("author_association") or ""),
|
||||
"created_at": str(item.get("submitted_at") or item.get("created_at") or ""),
|
||||
"body": str(item.get("body") or ""),
|
||||
"path": None,
|
||||
"line": None,
|
||||
"url": str(item.get("html_url") or ""),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def extract_login(user_obj):
|
||||
if isinstance(user_obj, dict):
|
||||
return str(user_obj.get("login") or "")
|
||||
return ""
|
||||
|
||||
|
||||
def is_bot_login(login):
|
||||
return bool(login) and login.endswith("[bot]")
|
||||
|
||||
|
||||
def is_actionable_review_bot_login(login):
|
||||
if not is_bot_login(login):
|
||||
return False
|
||||
lower_login = login.lower()
|
||||
return any(keyword in lower_login for keyword in REVIEW_BOT_LOGIN_KEYWORDS)
|
||||
|
||||
|
||||
def is_trusted_human_review_author(item, authenticated_login):
|
||||
author = str(item.get("author") or "")
|
||||
if not author:
|
||||
return False
|
||||
if authenticated_login and author == authenticated_login:
|
||||
return True
|
||||
association = str(item.get("author_association") or "").upper()
|
||||
return association in TRUSTED_AUTHOR_ASSOCIATIONS
|
||||
|
||||
|
||||
def fetch_new_review_items(pr, state, fresh_state, authenticated_login=None):
|
||||
repo = pr["repo"]
|
||||
pr_number = pr["number"]
|
||||
endpoints = comment_endpoints(repo, pr_number)
|
||||
|
||||
issue_payload = gh_api_list_paginated(endpoints["issue_comment"], repo=repo)
|
||||
review_comment_payload = gh_api_list_paginated(endpoints["review_comment"], repo=repo)
|
||||
review_payload = gh_api_list_paginated(endpoints["review"], repo=repo)
|
||||
|
||||
issue_items = normalize_issue_comments(issue_payload)
|
||||
review_comment_items = normalize_review_comments(review_comment_payload)
|
||||
review_items = normalize_reviews(review_payload)
|
||||
all_items = issue_items + review_comment_items + review_items
|
||||
|
||||
seen_issue = {str(x) for x in state.get("seen_issue_comment_ids") or []}
|
||||
seen_review_comment = {str(x) for x in state.get("seen_review_comment_ids") or []}
|
||||
seen_review = {str(x) for x in state.get("seen_review_ids") or []}
|
||||
|
||||
# On a brand-new state file, surface existing review activity instead of
|
||||
# silently treating it as seen. This avoids missing already-pending review
|
||||
# feedback when monitoring starts after comments were posted.
|
||||
|
||||
new_items = []
|
||||
for item in all_items:
|
||||
item_id = item.get("id")
|
||||
if not item_id:
|
||||
continue
|
||||
author = item.get("author") or ""
|
||||
if not author:
|
||||
continue
|
||||
if is_bot_login(author):
|
||||
if not is_actionable_review_bot_login(author):
|
||||
continue
|
||||
elif not is_trusted_human_review_author(item, authenticated_login):
|
||||
continue
|
||||
|
||||
kind = item["kind"]
|
||||
if kind == "issue_comment" and item_id in seen_issue:
|
||||
continue
|
||||
if kind == "review_comment" and item_id in seen_review_comment:
|
||||
continue
|
||||
if kind == "review" and item_id in seen_review:
|
||||
continue
|
||||
|
||||
new_items.append(item)
|
||||
if kind == "issue_comment":
|
||||
seen_issue.add(item_id)
|
||||
elif kind == "review_comment":
|
||||
seen_review_comment.add(item_id)
|
||||
elif kind == "review":
|
||||
seen_review.add(item_id)
|
||||
|
||||
new_items.sort(key=lambda item: (item.get("created_at") or "", item.get("kind") or "", item.get("id") or ""))
|
||||
state["seen_issue_comment_ids"] = sorted(seen_issue)
|
||||
state["seen_review_comment_ids"] = sorted(seen_review_comment)
|
||||
state["seen_review_ids"] = sorted(seen_review)
|
||||
return new_items
|
||||
|
||||
|
||||
def current_retry_count(state, head_sha):
|
||||
retries = state.get("retries_by_sha") or {}
|
||||
value = retries.get(head_sha, 0)
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def set_retry_count(state, head_sha, count):
|
||||
retries = state.get("retries_by_sha")
|
||||
if not isinstance(retries, dict):
|
||||
retries = {}
|
||||
retries[head_sha] = int(count)
|
||||
state["retries_by_sha"] = retries
|
||||
|
||||
|
||||
def unique_actions(actions):
|
||||
out = []
|
||||
seen = set()
|
||||
for action in actions:
|
||||
if action not in seen:
|
||||
out.append(action)
|
||||
seen.add(action)
|
||||
return out
|
||||
|
||||
|
||||
def is_pr_ready_to_merge(pr, checks_summary, new_review_items):
|
||||
if pr["closed"] or pr["merged"]:
|
||||
return False
|
||||
if not checks_summary["all_terminal"]:
|
||||
return False
|
||||
if checks_summary["failed_count"] > 0 or checks_summary["pending_count"] > 0:
|
||||
return False
|
||||
if new_review_items:
|
||||
return False
|
||||
if str(pr.get("mergeable") or "") != "MERGEABLE":
|
||||
return False
|
||||
if str(pr.get("merge_state_status") or "") in MERGE_CONFLICT_OR_BLOCKING_STATES:
|
||||
return False
|
||||
if str(pr.get("review_decision") or "") in MERGE_BLOCKING_REVIEW_DECISIONS:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def recommend_actions(pr, checks_summary, failed_runs, new_review_items, retries_used, max_retries):
|
||||
actions = []
|
||||
if pr["closed"] or pr["merged"]:
|
||||
if new_review_items:
|
||||
actions.append("process_review_comment")
|
||||
actions.append("stop_pr_closed")
|
||||
return unique_actions(actions)
|
||||
|
||||
if is_pr_ready_to_merge(pr, checks_summary, new_review_items):
|
||||
actions.append("stop_ready_to_merge")
|
||||
return unique_actions(actions)
|
||||
|
||||
if new_review_items:
|
||||
actions.append("process_review_comment")
|
||||
|
||||
has_failed_pr_checks = checks_summary["failed_count"] > 0
|
||||
if has_failed_pr_checks:
|
||||
if checks_summary["all_terminal"] and retries_used >= max_retries:
|
||||
actions.append("stop_exhausted_retries")
|
||||
else:
|
||||
actions.append("diagnose_ci_failure")
|
||||
if checks_summary["all_terminal"] and failed_runs and retries_used < max_retries:
|
||||
actions.append("retry_failed_checks")
|
||||
|
||||
if not actions:
|
||||
actions.append("idle")
|
||||
return unique_actions(actions)
|
||||
|
||||
|
||||
def collect_snapshot(args):
|
||||
pr = resolve_pr(args.pr, repo_override=args.repo)
|
||||
state_path = Path(args.state_file) if args.state_file else default_state_file_for(pr)
|
||||
state, fresh_state = load_state(state_path)
|
||||
|
||||
if not state.get("started_at"):
|
||||
state["started_at"] = int(time.time())
|
||||
|
||||
# `gh pr checks -R <repo>` requires an explicit PR/branch/url argument.
|
||||
# After resolving `--pr auto`, reuse the concrete PR number.
|
||||
checks = get_pr_checks(str(pr["number"]), repo=pr["repo"])
|
||||
checks_summary = summarize_checks(checks)
|
||||
workflow_runs = get_workflow_runs_for_sha(pr["repo"], pr["head_sha"])
|
||||
failed_runs = failed_runs_from_workflow_runs(workflow_runs, pr["head_sha"])
|
||||
authenticated_login = get_authenticated_login()
|
||||
new_review_items = fetch_new_review_items(
|
||||
pr,
|
||||
state,
|
||||
fresh_state=fresh_state,
|
||||
authenticated_login=authenticated_login,
|
||||
)
|
||||
|
||||
retries_used = current_retry_count(state, pr["head_sha"])
|
||||
actions = recommend_actions(
|
||||
pr,
|
||||
checks_summary,
|
||||
failed_runs,
|
||||
new_review_items,
|
||||
retries_used,
|
||||
args.max_flaky_retries,
|
||||
)
|
||||
|
||||
state["pr"] = {"repo": pr["repo"], "number": pr["number"]}
|
||||
state["last_seen_head_sha"] = pr["head_sha"]
|
||||
state["last_snapshot_at"] = int(time.time())
|
||||
save_state(state_path, state)
|
||||
|
||||
snapshot = {
|
||||
"pr": pr,
|
||||
"checks": checks_summary,
|
||||
"failed_runs": failed_runs,
|
||||
"new_review_items": new_review_items,
|
||||
"actions": actions,
|
||||
"retry_state": {
|
||||
"current_sha_retries_used": retries_used,
|
||||
"max_flaky_retries": args.max_flaky_retries,
|
||||
},
|
||||
}
|
||||
return snapshot, state_path
|
||||
|
||||
|
||||
def retry_failed_now(args):
|
||||
snapshot, state_path = collect_snapshot(args)
|
||||
pr = snapshot["pr"]
|
||||
checks_summary = snapshot["checks"]
|
||||
failed_runs = snapshot["failed_runs"]
|
||||
retries_used = snapshot["retry_state"]["current_sha_retries_used"]
|
||||
max_retries = snapshot["retry_state"]["max_flaky_retries"]
|
||||
|
||||
result = {
|
||||
"snapshot": snapshot,
|
||||
"state_file": str(state_path),
|
||||
"rerun_attempted": False,
|
||||
"rerun_count": 0,
|
||||
"rerun_run_ids": [],
|
||||
"reason": None,
|
||||
}
|
||||
|
||||
if pr["closed"] or pr["merged"]:
|
||||
result["reason"] = "pr_closed"
|
||||
return result
|
||||
if checks_summary["failed_count"] <= 0:
|
||||
result["reason"] = "no_failed_pr_checks"
|
||||
return result
|
||||
if not failed_runs:
|
||||
result["reason"] = "no_failed_runs"
|
||||
return result
|
||||
if not checks_summary["all_terminal"]:
|
||||
result["reason"] = "checks_still_pending"
|
||||
return result
|
||||
if retries_used >= max_retries:
|
||||
result["reason"] = "retry_budget_exhausted"
|
||||
return result
|
||||
|
||||
for run in failed_runs:
|
||||
run_id = run.get("run_id")
|
||||
if run_id in (None, ""):
|
||||
continue
|
||||
gh_text(["run", "rerun", str(run_id), "--failed"], repo=pr["repo"])
|
||||
result["rerun_run_ids"].append(run_id)
|
||||
|
||||
if result["rerun_run_ids"]:
|
||||
state, _ = load_state(state_path)
|
||||
new_count = current_retry_count(state, pr["head_sha"]) + 1
|
||||
set_retry_count(state, pr["head_sha"], new_count)
|
||||
state["last_snapshot_at"] = int(time.time())
|
||||
save_state(state_path, state)
|
||||
result["rerun_attempted"] = True
|
||||
result["rerun_count"] = len(result["rerun_run_ids"])
|
||||
result["reason"] = "rerun_triggered"
|
||||
else:
|
||||
result["reason"] = "failed_runs_missing_ids"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def print_json(obj):
|
||||
sys.stdout.write(json.dumps(obj, sort_keys=True) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def print_event(event, payload):
|
||||
print_json({"event": event, "payload": payload})
|
||||
|
||||
|
||||
def is_ci_green(snapshot):
|
||||
checks = snapshot.get("checks") or {}
|
||||
return (
|
||||
bool(checks.get("all_terminal"))
|
||||
and int(checks.get("failed_count") or 0) == 0
|
||||
and int(checks.get("pending_count") or 0) == 0
|
||||
)
|
||||
|
||||
|
||||
def snapshot_change_key(snapshot):
|
||||
pr = snapshot.get("pr") or {}
|
||||
checks = snapshot.get("checks") or {}
|
||||
review_items = snapshot.get("new_review_items") or []
|
||||
return (
|
||||
str(pr.get("head_sha") or ""),
|
||||
str(pr.get("state") or ""),
|
||||
str(pr.get("mergeable") or ""),
|
||||
str(pr.get("merge_state_status") or ""),
|
||||
str(pr.get("review_decision") or ""),
|
||||
int(checks.get("passed_count") or 0),
|
||||
int(checks.get("failed_count") or 0),
|
||||
int(checks.get("pending_count") or 0),
|
||||
tuple(
|
||||
(str(item.get("kind") or ""), str(item.get("id") or ""))
|
||||
for item in review_items
|
||||
if isinstance(item, dict)
|
||||
),
|
||||
tuple(snapshot.get("actions") or []),
|
||||
)
|
||||
|
||||
|
||||
def run_watch(args):
|
||||
poll_seconds = args.poll_seconds
|
||||
last_change_key = None
|
||||
while True:
|
||||
snapshot, state_path = collect_snapshot(args)
|
||||
print_event(
|
||||
"snapshot",
|
||||
{
|
||||
"snapshot": snapshot,
|
||||
"state_file": str(state_path),
|
||||
"next_poll_seconds": poll_seconds,
|
||||
},
|
||||
)
|
||||
actions = set(snapshot.get("actions") or [])
|
||||
if (
|
||||
"stop_pr_closed" in actions
|
||||
or "stop_exhausted_retries" in actions
|
||||
or "stop_ready_to_merge" in actions
|
||||
):
|
||||
print_event("stop", {"actions": snapshot.get("actions"), "pr": snapshot.get("pr")})
|
||||
return 0
|
||||
|
||||
current_change_key = snapshot_change_key(snapshot)
|
||||
changed = current_change_key != last_change_key
|
||||
green = is_ci_green(snapshot)
|
||||
|
||||
if not green:
|
||||
poll_seconds = args.poll_seconds
|
||||
elif changed or last_change_key is None:
|
||||
poll_seconds = args.poll_seconds
|
||||
else:
|
||||
poll_seconds = min(poll_seconds * 2, GREEN_STATE_MAX_POLL_SECONDS)
|
||||
|
||||
last_change_key = current_change_key
|
||||
time.sleep(poll_seconds)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
try:
|
||||
if args.retry_failed_now:
|
||||
print_json(retry_failed_now(args))
|
||||
return 0
|
||||
if args.watch:
|
||||
return run_watch(args)
|
||||
snapshot, state_path = collect_snapshot(args)
|
||||
snapshot["state_file"] = str(state_path)
|
||||
print_json(snapshot)
|
||||
return 0
|
||||
except (GhCommandError, RuntimeError, ValueError) as err:
|
||||
sys.stderr.write(f"gh_pr_watch.py error: {err}\n")
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
sys.stderr.write("gh_pr_watch.py interrupted\n")
|
||||
return 130
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: remote-tests
|
||||
description: How to run tests using remote executor.
|
||||
---
|
||||
|
||||
Some codex integration tests support a running against a remote executor.
|
||||
This means that when CODEX_TEST_REMOTE_ENV environment variable is set they will attempt to start an executor process in a docker container CODEX_TEST_REMOTE_ENV points to and use it in tests.
|
||||
|
||||
Docker container is built and initialized via ./scripts/test-remote-env.sh
|
||||
|
||||
Currently running remote tests is only supported on Linux, so you need to use a devbox to run them
|
||||
|
||||
You can list devboxes via `applied_devbox ls`, pick the one with `codex` in the name.
|
||||
Connect to devbox via `ssh <devbox_name>`.
|
||||
Reuse the same checkout of codex in `~/code/codex`. Reset files if needed. Multiple checkouts take longer to build and take up more space.
|
||||
Check whether the SHA and modified files are in sync between remote and local.
|
||||
@@ -11,7 +11,7 @@ RUN apt-get update && \
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential curl git ca-certificates \
|
||||
pkg-config libcap-dev clang musl-tools libssl-dev just && \
|
||||
pkg-config clang musl-tools libssl-dev just && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Ubuntu 24.04 ships with user 'ubuntu' already created with UID 1000.
|
||||
|
||||
3
.github/actions/linux-code-sign/action.yml
vendored
3
.github/actions/linux-code-sign/action.yml
vendored
@@ -17,7 +17,6 @@ runs:
|
||||
- name: Cosign Linux artifacts
|
||||
shell: bash
|
||||
env:
|
||||
ARTIFACTS_DIR: ${{ inputs.artifacts-dir }}
|
||||
COSIGN_EXPERIMENTAL: "1"
|
||||
COSIGN_YES: "true"
|
||||
COSIGN_OIDC_CLIENT_ID: "sigstore"
|
||||
@@ -25,7 +24,7 @@ runs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
dest="$ARTIFACTS_DIR"
|
||||
dest="${{ inputs.artifacts-dir }}"
|
||||
if [[ ! -d "$dest" ]]; then
|
||||
echo "Destination $dest does not exist"
|
||||
exit 1
|
||||
|
||||
17
.github/actions/macos-code-sign/action.yml
vendored
17
.github/actions/macos-code-sign/action.yml
vendored
@@ -117,8 +117,6 @@ runs:
|
||||
- name: Sign macOS binaries
|
||||
if: ${{ inputs.sign-binaries == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
TARGET: ${{ inputs.target }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -132,18 +130,15 @@ runs:
|
||||
keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}")
|
||||
fi
|
||||
|
||||
entitlements_path="$GITHUB_ACTION_PATH/codex.entitlements.plist"
|
||||
|
||||
for binary in codex codex-responses-api-proxy; do
|
||||
path="codex-rs/target/${TARGET}/release/${binary}"
|
||||
codesign --force --options runtime --timestamp --entitlements "$entitlements_path" --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path"
|
||||
path="codex-rs/target/${{ inputs.target }}/release/${binary}"
|
||||
codesign --force --options runtime --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path"
|
||||
done
|
||||
|
||||
- name: Notarize macOS binaries
|
||||
if: ${{ inputs.sign-binaries == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
TARGET: ${{ inputs.target }}
|
||||
APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }}
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }}
|
||||
@@ -168,7 +163,7 @@ runs:
|
||||
|
||||
notarize_binary() {
|
||||
local binary="$1"
|
||||
local source_path="codex-rs/target/${TARGET}/release/${binary}"
|
||||
local source_path="codex-rs/target/${{ inputs.target }}/release/${binary}"
|
||||
local archive_path="${RUNNER_TEMP}/${binary}.zip"
|
||||
|
||||
if [[ ! -f "$source_path" ]]; then
|
||||
@@ -189,7 +184,6 @@ runs:
|
||||
if: ${{ inputs.sign-dmg == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
TARGET: ${{ inputs.target }}
|
||||
APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }}
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }}
|
||||
@@ -212,8 +206,7 @@ runs:
|
||||
|
||||
source "$GITHUB_ACTION_PATH/notary_helpers.sh"
|
||||
|
||||
dmg_name="codex-${TARGET}.dmg"
|
||||
dmg_path="codex-rs/target/${TARGET}/release/${dmg_name}"
|
||||
dmg_path="codex-rs/target/${{ inputs.target }}/release/codex-${{ inputs.target }}.dmg"
|
||||
|
||||
if [[ ! -f "$dmg_path" ]]; then
|
||||
echo "dmg $dmg_path not found"
|
||||
@@ -226,7 +219,7 @@ runs:
|
||||
fi
|
||||
|
||||
codesign --force --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$dmg_path"
|
||||
notarize_submission "$dmg_name" "$dmg_path" "$notary_key_path"
|
||||
notarize_submission "codex-${{ inputs.target }}.dmg" "$dmg_path" "$notary_key_path"
|
||||
xcrun stapler staple "$dmg_path"
|
||||
|
||||
- name: Remove signing keychain
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
69
.github/actions/setup-bazel-ci/action.yml
vendored
69
.github/actions/setup-bazel-ci/action.yml
vendored
@@ -1,69 +0,0 @@
|
||||
name: setup-bazel-ci
|
||||
description: Prepare a Bazel CI runner with shared caches and optional test prerequisites.
|
||||
inputs:
|
||||
target:
|
||||
description: Target triple used for cache namespacing.
|
||||
required: true
|
||||
install-test-prereqs:
|
||||
description: Install Node.js and DotSlash for Bazel-backed test jobs.
|
||||
required: false
|
||||
default: "false"
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: Whether the Bazel repository cache key was restored exactly.
|
||||
value: ${{ steps.cache_bazel_repository_restore.outputs.cache-hit }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up Node.js for js_repl tests
|
||||
if: inputs.install-test-prereqs == 'true'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: codex-rs/node-version.txt
|
||||
|
||||
# Some integration tests rely on DotSlash being installed.
|
||||
# See https://github.com/openai/codex/pull/7617.
|
||||
- name: Install DotSlash
|
||||
if: inputs.install-test-prereqs == 'true'
|
||||
uses: facebook/install-dotslash@v2
|
||||
|
||||
- name: Make DotSlash available in PATH (Unix)
|
||||
if: inputs.install-test-prereqs == 'true' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: cp "$(which dotslash)" /usr/local/bin
|
||||
|
||||
- name: Make DotSlash available in PATH (Windows)
|
||||
if: inputs.install-test-prereqs == 'true' && runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe"
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: bazelbuild/setup-bazelisk@v3
|
||||
|
||||
# Restore bazel repository cache so we don't have to redownload all the external dependencies
|
||||
# on every CI run.
|
||||
- name: Restore bazel repository cache
|
||||
id: cache_bazel_repository_restore
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
key: bazel-cache-${{ inputs.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
|
||||
restore-keys: |
|
||||
bazel-cache-${{ inputs.target }}
|
||||
|
||||
- name: Configure Bazel output root (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Use the shortest available drive to reduce argv/path length issues,
|
||||
# but avoid the drive root because some Windows test launchers mis-handle
|
||||
# MANIFEST paths there.
|
||||
$bazelOutputUserRoot = if (Test-Path 'D:\') { 'D:\b' } else { 'C:\b' }
|
||||
"BAZEL_OUTPUT_USER_ROOT=$bazelOutputUserRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
- name: Enable Git long paths (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: git config --global core.longpaths true
|
||||
9
.github/blob-size-allowlist.txt
vendored
9
.github/blob-size-allowlist.txt
vendored
@@ -1,9 +0,0 @@
|
||||
# Paths are matched exactly, relative to the repository root.
|
||||
# Keep this list short and limited to intentional large checked-in assets.
|
||||
|
||||
.github/codex-cli-splash.png
|
||||
MODULE.bazel.lock
|
||||
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
|
||||
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
|
||||
codex-rs/tui/tests/fixtures/oss-story.jsonl
|
||||
codex-rs/tui_app_server/tests/fixtures/oss-story.jsonl
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"outputs": {
|
||||
"argument-comment-lint": {
|
||||
"platforms": {
|
||||
"macos-aarch64": {
|
||||
"regex": "^argument-comment-lint-aarch64-apple-darwin\\.tar\\.gz$",
|
||||
"path": "argument-comment-lint/bin/argument-comment-lint"
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"regex": "^argument-comment-lint-x86_64-unknown-linux-gnu\\.tar\\.gz$",
|
||||
"path": "argument-comment-lint/bin/argument-comment-lint"
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"regex": "^argument-comment-lint-aarch64-unknown-linux-gnu\\.tar\\.gz$",
|
||||
"path": "argument-comment-lint/bin/argument-comment-lint"
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"regex": "^argument-comment-lint-x86_64-pc-windows-msvc\\.zip$",
|
||||
"path": "argument-comment-lint/bin/argument-comment-lint.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
.github/dotslash-zsh-config.json
vendored
23
.github/dotslash-zsh-config.json
vendored
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"outputs": {
|
||||
"codex-zsh": {
|
||||
"platforms": {
|
||||
"macos-aarch64": {
|
||||
"name": "codex-zsh-aarch64-apple-darwin.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"path": "codex-zsh/bin/zsh"
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"name": "codex-zsh-x86_64-unknown-linux-musl.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"path": "codex-zsh/bin/zsh"
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"name": "codex-zsh-aarch64-unknown-linux-musl.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"path": "codex-zsh/bin/zsh"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
.github/scripts/build-zsh-release-artifact.sh
vendored
61
.github/scripts/build-zsh-release-artifact.sh
vendored
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$#" -ne 1 ]]; then
|
||||
echo "usage: $0 <archive-path>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
archive_path="$1"
|
||||
workspace="${GITHUB_WORKSPACE:?missing GITHUB_WORKSPACE}"
|
||||
zsh_commit="${ZSH_COMMIT:?missing ZSH_COMMIT}"
|
||||
zsh_patch="${ZSH_PATCH:?missing ZSH_PATCH}"
|
||||
temp_root="${RUNNER_TEMP:-/tmp}"
|
||||
work_root="$(mktemp -d "${temp_root%/}/codex-zsh-release.XXXXXX")"
|
||||
trap 'rm -rf "$work_root"' EXIT
|
||||
|
||||
source_root="${work_root}/zsh"
|
||||
package_root="${work_root}/codex-zsh"
|
||||
wrapper_path="${work_root}/exec-wrapper"
|
||||
stdout_path="${work_root}/stdout.txt"
|
||||
wrapper_log_path="${work_root}/wrapper.log"
|
||||
|
||||
git clone https://git.code.sf.net/p/zsh/code "$source_root"
|
||||
cd "$source_root"
|
||||
git checkout "$zsh_commit"
|
||||
git apply "${workspace}/${zsh_patch}"
|
||||
./Util/preconfig
|
||||
./configure
|
||||
|
||||
cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)"
|
||||
make -j"${cores}"
|
||||
|
||||
cat > "$wrapper_path" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${CODEX_WRAPPER_LOG:?missing CODEX_WRAPPER_LOG}"
|
||||
printf '%s\n' "$@" > "$CODEX_WRAPPER_LOG"
|
||||
file="$1"
|
||||
shift
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
exec "$file"
|
||||
fi
|
||||
arg0="$1"
|
||||
shift
|
||||
exec -a "$arg0" "$file" "$@"
|
||||
EOF
|
||||
chmod +x "$wrapper_path"
|
||||
|
||||
CODEX_WRAPPER_LOG="$wrapper_log_path" \
|
||||
EXEC_WRAPPER="$wrapper_path" \
|
||||
"${source_root}/Src/zsh" -fc '/bin/echo smoke-zsh' > "$stdout_path"
|
||||
|
||||
grep -Fx "smoke-zsh" "$stdout_path"
|
||||
grep -Fx "/bin/echo" "$wrapper_log_path"
|
||||
|
||||
mkdir -p "$package_root/bin" "$(dirname "${workspace}/${archive_path}")"
|
||||
cp "${source_root}/Src/zsh" "$package_root/bin/zsh"
|
||||
chmod +x "$package_root/bin/zsh"
|
||||
|
||||
(cd "$work_root" && tar -czf "${workspace}/${archive_path}" codex-zsh)
|
||||
202
.github/scripts/run-bazel-ci.sh
vendored
202
.github/scripts/run-bazel-ci.sh
vendored
@@ -1,202 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
print_failed_bazel_test_logs=0
|
||||
use_node_test_env=0
|
||||
remote_download_toplevel=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--print-failed-test-logs)
|
||||
print_failed_bazel_test_logs=1
|
||||
shift
|
||||
;;
|
||||
--use-node-test-env)
|
||||
use_node_test_env=1
|
||||
shift
|
||||
;;
|
||||
--remote-download-toplevel)
|
||||
remote_download_toplevel=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: $0 [--print-failed-test-logs] [--use-node-test-env] [--remote-download-toplevel] -- <bazel args> -- <targets>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bazel_startup_args=()
|
||||
if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
|
||||
bazel_startup_args+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
|
||||
ci_config=ci-linux
|
||||
case "${RUNNER_OS:-}" in
|
||||
macOS)
|
||||
ci_config=ci-macos
|
||||
;;
|
||||
Windows)
|
||||
ci_config=ci-windows
|
||||
;;
|
||||
esac
|
||||
|
||||
print_bazel_test_log_tails() {
|
||||
local console_log="$1"
|
||||
local testlogs_dir
|
||||
local -a bazel_info_cmd=(bazel)
|
||||
|
||||
if (( ${#bazel_startup_args[@]} > 0 )); then
|
||||
bazel_info_cmd+=("${bazel_startup_args[@]}")
|
||||
fi
|
||||
|
||||
testlogs_dir="$("${bazel_info_cmd[@]}" info bazel-testlogs 2>/dev/null || echo bazel-testlogs)"
|
||||
|
||||
local failed_targets=()
|
||||
while IFS= read -r target; do
|
||||
failed_targets+=("$target")
|
||||
done < <(
|
||||
grep -E '^FAIL: //' "$console_log" \
|
||||
| sed -E 's#^FAIL: (//[^ ]+).*#\1#' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
if [[ ${#failed_targets[@]} -eq 0 ]]; then
|
||||
echo "No failed Bazel test targets were found in console output."
|
||||
return
|
||||
fi
|
||||
|
||||
for target in "${failed_targets[@]}"; do
|
||||
local rel_path="${target#//}"
|
||||
rel_path="${rel_path/:/\/}"
|
||||
local test_log="${testlogs_dir}/${rel_path}/test.log"
|
||||
|
||||
echo "::group::Bazel test log tail for ${target}"
|
||||
if [[ -f "$test_log" ]]; then
|
||||
tail -n 200 "$test_log"
|
||||
else
|
||||
echo "Missing test log: $test_log"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
done
|
||||
}
|
||||
|
||||
bazel_args=()
|
||||
bazel_targets=()
|
||||
found_target_separator=0
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--" && $found_target_separator -eq 0 ]]; then
|
||||
found_target_separator=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ $found_target_separator -eq 0 ]]; then
|
||||
bazel_args+=("$arg")
|
||||
else
|
||||
bazel_targets+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then
|
||||
echo "Expected Bazel args and targets separated by --" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $use_node_test_env -eq 1 && "${RUNNER_OS:-}" != "Windows" ]]; then
|
||||
# Bazel test sandboxes on macOS may resolve an older Homebrew `node`
|
||||
# before the `actions/setup-node` runtime on PATH.
|
||||
node_bin="$(which node)"
|
||||
bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}")
|
||||
fi
|
||||
|
||||
post_config_bazel_args=()
|
||||
if [[ $remote_download_toplevel -eq 1 ]]; then
|
||||
# Override the CI config's remote_download_minimal setting when callers need
|
||||
# the built artifact to exist on disk after the command completes.
|
||||
post_config_bazel_args+=(--remote_download_toplevel)
|
||||
fi
|
||||
|
||||
bazel_console_log="$(mktemp)"
|
||||
trap 'rm -f "$bazel_console_log"' EXIT
|
||||
|
||||
bazel_cmd=(bazel)
|
||||
if (( ${#bazel_startup_args[@]} > 0 )); then
|
||||
bazel_cmd+=("${bazel_startup_args[@]}")
|
||||
fi
|
||||
|
||||
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
echo "BuildBuddy API key is available; using remote Bazel configuration."
|
||||
# Work around Bazel 9 remote repo contents cache / overlay materialization failures
|
||||
# seen in CI (for example "is not a symlink" or permission errors while
|
||||
# materializing external repos such as rules_perl). We still use BuildBuddy for
|
||||
# remote execution/cache; this only disables the startup-level repo contents cache.
|
||||
bazel_run_args=(
|
||||
"${bazel_args[@]}"
|
||||
"--config=${ci_config}"
|
||||
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
|
||||
)
|
||||
if (( ${#post_config_bazel_args[@]} > 0 )); then
|
||||
bazel_run_args+=("${post_config_bazel_args[@]}")
|
||||
fi
|
||||
set +e
|
||||
"${bazel_cmd[@]}" \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
"${bazel_run_args[@]}" \
|
||||
-- \
|
||||
"${bazel_targets[@]}" \
|
||||
2>&1 | tee "$bazel_console_log"
|
||||
bazel_status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
else
|
||||
echo "BuildBuddy API key is not available; using local Bazel configuration."
|
||||
# Keep fork/community PRs on Bazel but disable remote services that are
|
||||
# configured in .bazelrc and require auth.
|
||||
#
|
||||
# Flag docs:
|
||||
# - Command-line reference: https://bazel.build/reference/command-line-reference
|
||||
# - Remote caching overview: https://bazel.build/remote/caching
|
||||
# - Remote execution overview: https://bazel.build/remote/rbe
|
||||
# - Build Event Protocol overview: https://bazel.build/remote/bep
|
||||
#
|
||||
# --noexperimental_remote_repo_contents_cache:
|
||||
# disable remote repo contents cache enabled in .bazelrc startup options.
|
||||
# https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache
|
||||
# --remote_cache= and --remote_executor=:
|
||||
# clear remote cache/execution endpoints configured in .bazelrc.
|
||||
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache
|
||||
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor
|
||||
bazel_run_args=(
|
||||
"${bazel_args[@]}"
|
||||
--remote_cache=
|
||||
--remote_executor=
|
||||
)
|
||||
if (( ${#post_config_bazel_args[@]} > 0 )); then
|
||||
bazel_run_args+=("${post_config_bazel_args[@]}")
|
||||
fi
|
||||
set +e
|
||||
"${bazel_cmd[@]}" \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
"${bazel_run_args[@]}" \
|
||||
-- \
|
||||
"${bazel_targets[@]}" \
|
||||
2>&1 | tee "$bazel_console_log"
|
||||
bazel_status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
fi
|
||||
|
||||
if [[ ${bazel_status:-0} -ne 0 ]]; then
|
||||
if [[ $print_failed_bazel_test_logs -eq 1 ]]; then
|
||||
print_bazel_test_log_tails "$bazel_console_log"
|
||||
fi
|
||||
exit "$bazel_status"
|
||||
fi
|
||||
287
.github/scripts/rusty_v8_bazel.py
vendored
287
.github/scripts/rusty_v8_bazel.py
vendored
@@ -1,287 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MUSL_RUNTIME_ARCHIVE_LABELS = [
|
||||
"@llvm//runtimes/libcxx:libcxx.static",
|
||||
"@llvm//runtimes/libcxx:libcxxabi.static",
|
||||
]
|
||||
LLVM_AR_LABEL = "@llvm//tools:llvm-ar"
|
||||
LLVM_RANLIB_LABEL = "@llvm//tools:llvm-ranlib"
|
||||
|
||||
|
||||
def bazel_execroot() -> Path:
|
||||
result = subprocess.run(
|
||||
["bazel", "info", "execution_root"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return Path(result.stdout.strip())
|
||||
|
||||
|
||||
def bazel_output_base() -> Path:
|
||||
result = subprocess.run(
|
||||
["bazel", "info", "output_base"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return Path(result.stdout.strip())
|
||||
|
||||
|
||||
def bazel_output_path(path: str) -> Path:
|
||||
if path.startswith("external/"):
|
||||
return bazel_output_base() / path
|
||||
return bazel_execroot() / path
|
||||
|
||||
|
||||
def bazel_output_files(
|
||||
platform: str,
|
||||
labels: list[str],
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> list[Path]:
|
||||
expression = "set(" + " ".join(labels) + ")"
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bazel",
|
||||
"cquery",
|
||||
"-c",
|
||||
compilation_mode,
|
||||
f"--platforms=@llvm//platforms:{platform}",
|
||||
"--output=files",
|
||||
expression,
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return [bazel_output_path(line.strip()) for line in result.stdout.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def bazel_build(
|
||||
platform: str,
|
||||
labels: list[str],
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
"bazel",
|
||||
"build",
|
||||
"-c",
|
||||
compilation_mode,
|
||||
f"--platforms=@llvm//platforms:{platform}",
|
||||
*labels,
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def ensure_bazel_output_files(
|
||||
platform: str,
|
||||
labels: list[str],
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> list[Path]:
|
||||
outputs = bazel_output_files(platform, labels, compilation_mode)
|
||||
if all(path.exists() for path in outputs):
|
||||
return outputs
|
||||
|
||||
bazel_build(platform, labels, compilation_mode)
|
||||
outputs = bazel_output_files(platform, labels, compilation_mode)
|
||||
missing = [str(path) for path in outputs if not path.exists()]
|
||||
if missing:
|
||||
raise SystemExit(f"missing built outputs for {labels}: {missing}")
|
||||
return outputs
|
||||
|
||||
|
||||
def release_pair_label(target: str) -> str:
|
||||
target_suffix = target.replace("-", "_")
|
||||
return f"//third_party/v8:rusty_v8_release_pair_{target_suffix}"
|
||||
|
||||
|
||||
def resolved_v8_crate_version() -> str:
|
||||
cargo_lock = tomllib.loads((ROOT / "codex-rs" / "Cargo.lock").read_text())
|
||||
versions = sorted(
|
||||
{
|
||||
package["version"]
|
||||
for package in cargo_lock["package"]
|
||||
if package["name"] == "v8"
|
||||
}
|
||||
)
|
||||
if len(versions) == 1:
|
||||
return versions[0]
|
||||
if len(versions) > 1:
|
||||
raise SystemExit(f"expected exactly one resolved v8 version, found: {versions}")
|
||||
|
||||
module_bazel = (ROOT / "MODULE.bazel").read_text()
|
||||
matches = sorted(
|
||||
set(
|
||||
re.findall(
|
||||
r'https://static\.crates\.io/crates/v8/v8-([0-9]+\.[0-9]+\.[0-9]+)\.crate',
|
||||
module_bazel,
|
||||
)
|
||||
)
|
||||
)
|
||||
if len(matches) != 1:
|
||||
raise SystemExit(
|
||||
"expected exactly one pinned v8 crate version in MODULE.bazel, "
|
||||
f"found: {matches}"
|
||||
)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def staged_archive_name(target: str, source_path: Path) -> str:
|
||||
if source_path.suffix == ".lib":
|
||||
return f"rusty_v8_release_{target}.lib.gz"
|
||||
return f"librusty_v8_release_{target}.a.gz"
|
||||
|
||||
|
||||
def is_musl_archive_target(target: str, source_path: Path) -> bool:
|
||||
return target.endswith("-unknown-linux-musl") and source_path.suffix == ".a"
|
||||
|
||||
|
||||
def single_bazel_output_file(
|
||||
platform: str,
|
||||
label: str,
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> Path:
|
||||
outputs = ensure_bazel_output_files(platform, [label], compilation_mode)
|
||||
if len(outputs) != 1:
|
||||
raise SystemExit(f"expected exactly one output for {label}, found {outputs}")
|
||||
return outputs[0]
|
||||
|
||||
|
||||
def merged_musl_archive(
|
||||
platform: str,
|
||||
lib_path: Path,
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> Path:
|
||||
llvm_ar = single_bazel_output_file(platform, LLVM_AR_LABEL, compilation_mode)
|
||||
llvm_ranlib = single_bazel_output_file(platform, LLVM_RANLIB_LABEL, compilation_mode)
|
||||
runtime_archives = [
|
||||
single_bazel_output_file(platform, label, compilation_mode)
|
||||
for label in MUSL_RUNTIME_ARCHIVE_LABELS
|
||||
]
|
||||
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="rusty-v8-musl-stage-"))
|
||||
merged_archive = temp_dir / lib_path.name
|
||||
merge_commands = "\n".join(
|
||||
[
|
||||
f"create {merged_archive}",
|
||||
f"addlib {lib_path}",
|
||||
*[f"addlib {archive}" for archive in runtime_archives],
|
||||
"save",
|
||||
"end",
|
||||
]
|
||||
)
|
||||
subprocess.run(
|
||||
[str(llvm_ar), "-M"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
input=merge_commands,
|
||||
text=True,
|
||||
)
|
||||
subprocess.run([str(llvm_ranlib), str(merged_archive)], cwd=ROOT, check=True)
|
||||
return merged_archive
|
||||
|
||||
|
||||
def stage_release_pair(
|
||||
platform: str,
|
||||
target: str,
|
||||
output_dir: Path,
|
||||
compilation_mode: str = "fastbuild",
|
||||
) -> None:
|
||||
outputs = ensure_bazel_output_files(
|
||||
platform,
|
||||
[release_pair_label(target)],
|
||||
compilation_mode,
|
||||
)
|
||||
|
||||
try:
|
||||
lib_path = next(path for path in outputs if path.suffix in {".a", ".lib"})
|
||||
except StopIteration as exc:
|
||||
raise SystemExit(f"missing static library output for {target}") from exc
|
||||
|
||||
try:
|
||||
binding_path = next(path for path in outputs if path.suffix == ".rs")
|
||||
except StopIteration as exc:
|
||||
raise SystemExit(f"missing Rust binding output for {target}") from exc
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
staged_library = output_dir / staged_archive_name(target, lib_path)
|
||||
staged_binding = output_dir / f"src_binding_release_{target}.rs"
|
||||
source_archive = (
|
||||
merged_musl_archive(platform, lib_path, compilation_mode)
|
||||
if is_musl_archive_target(target, lib_path)
|
||||
else lib_path
|
||||
)
|
||||
|
||||
with source_archive.open("rb") as src, staged_library.open("wb") as dst:
|
||||
with gzip.GzipFile(
|
||||
filename="",
|
||||
mode="wb",
|
||||
fileobj=dst,
|
||||
compresslevel=6,
|
||||
mtime=0,
|
||||
) as gz:
|
||||
shutil.copyfileobj(src, gz)
|
||||
|
||||
shutil.copyfile(binding_path, staged_binding)
|
||||
|
||||
print(staged_library)
|
||||
print(staged_binding)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
stage_release_pair_parser = subparsers.add_parser("stage-release-pair")
|
||||
stage_release_pair_parser.add_argument("--platform", required=True)
|
||||
stage_release_pair_parser.add_argument("--target", required=True)
|
||||
stage_release_pair_parser.add_argument("--output-dir", required=True)
|
||||
stage_release_pair_parser.add_argument(
|
||||
"--compilation-mode",
|
||||
default="fastbuild",
|
||||
choices=["fastbuild", "opt", "dbg"],
|
||||
)
|
||||
|
||||
subparsers.add_parser("resolved-v8-crate-version")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if args.command == "stage-release-pair":
|
||||
stage_release_pair(
|
||||
platform=args.platform,
|
||||
target=args.target,
|
||||
output_dir=Path(args.output_dir),
|
||||
compilation_mode=args.compilation_mode,
|
||||
)
|
||||
return 0
|
||||
if args.command == "resolved-v8-crate-version":
|
||||
print(resolved_v8_crate_version())
|
||||
return 0
|
||||
raise SystemExit(f"unsupported command: {args.command}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
33
.github/workflows/README.md
vendored
33
.github/workflows/README.md
vendored
@@ -1,33 +0,0 @@
|
||||
# Workflow Strategy
|
||||
|
||||
The workflows in this directory are split so that pull requests get fast, review-friendly signal while `main` still gets the full cross-platform verification pass.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- `bazel.yml` is the main pre-merge verification path for Rust code.
|
||||
It runs Bazel `test` and Bazel `clippy` on the supported Bazel targets.
|
||||
- `rust-ci.yml` keeps the Cargo-native PR checks intentionally small:
|
||||
- `cargo fmt --check`
|
||||
- `cargo shear`
|
||||
- `argument-comment-lint` on Linux, macOS, and Windows
|
||||
- `tools/argument-comment-lint` package tests when the lint or its workflow wiring changes
|
||||
|
||||
The PR workflow still keeps the Linux lint lane on the default-targets-only invocation for now, but the released linter runs on Linux, macOS, and Windows before merge.
|
||||
|
||||
## Post-Merge On `main`
|
||||
|
||||
- `bazel.yml` also runs on pushes to `main`.
|
||||
This re-verifies the merged Bazel path and helps keep the BuildBuddy caches warm.
|
||||
- `rust-ci-full.yml` is the full Cargo-native verification workflow.
|
||||
It keeps the heavier checks off the PR path while still validating them after merge:
|
||||
- the full Cargo `clippy` matrix
|
||||
- the full Cargo `nextest` matrix
|
||||
- release-profile Cargo builds
|
||||
- cross-platform `argument-comment-lint`
|
||||
- Linux remote-env tests
|
||||
|
||||
## Rule Of Thumb
|
||||
|
||||
- If a build/test/clippy check can be expressed in Bazel, prefer putting the PR-time version in `bazel.yml`.
|
||||
- Keep `rust-ci.yml` fast enough that it usually does not dominate PR latency.
|
||||
- Reserve `rust-ci-full.yml` for heavyweight Cargo-native coverage that Bazel does not replace yet.
|
||||
196
.github/workflows/bazel.yml
vendored
196
.github/workflows/bazel.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Bazel
|
||||
name: Bazel (experimental)
|
||||
|
||||
# Note this workflow was originally derived from:
|
||||
# https://github.com/cerisier/toolchains_llvm_bootstrapped/blob/main/.github/workflows/ci.yaml
|
||||
@@ -17,7 +17,6 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.ref_name != 'main' }}
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -29,127 +28,122 @@ jobs:
|
||||
target: x86_64-apple-darwin
|
||||
|
||||
# Linux
|
||||
- os: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
# 2026-02-27 Bazel tests have been flaky on arm in CI.
|
||||
# Disable until we can investigate and stabilize them.
|
||||
# - os: ubuntu-24.04-arm
|
||||
# target: aarch64-unknown-linux-musl
|
||||
# - os: ubuntu-24.04-arm
|
||||
# target: aarch64-unknown-linux-gnu
|
||||
|
||||
# Windows
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-gnullvm
|
||||
# TODO: Enable Windows once we fix the toolchain issues there.
|
||||
#- os: windows-latest
|
||||
# target: x86_64-pc-windows-gnullvm
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
# Configure a human readable name for each job
|
||||
name: Local Bazel build on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Bazel CI
|
||||
id: setup_bazel
|
||||
uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
install-test-prereqs: "true"
|
||||
# Some integration tests rely on DotSlash being installed.
|
||||
# See https://github.com/openai/codex/pull/7617.
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@v2
|
||||
|
||||
- name: Make DotSlash available in PATH (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: cp "$(which dotslash)" /usr/local/bin
|
||||
|
||||
- name: Make DotSlash available in PATH (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe"
|
||||
|
||||
# Install Bazel via Bazelisk
|
||||
- name: Set up Bazel
|
||||
uses: bazelbuild/setup-bazelisk@v3
|
||||
|
||||
- name: Check MODULE.bazel.lock is up to date
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
shell: bash
|
||||
run: ./scripts/check-module-bazel-lock.sh
|
||||
|
||||
# TODO(mbolin): Bring this back once we have caching working. Currently,
|
||||
# we never seem to get a cache hit but we still end up paying the cost of
|
||||
# uploading at the end of the build, which takes over a minute!
|
||||
#
|
||||
# Cache build and external artifacts so that the next ci build is incremental.
|
||||
# Because github action caches cannot be updated after a build, we need to
|
||||
# store the contents of each build in a unique cache key, then fall back to loading
|
||||
# it on the next ci run. We use hashFiles(...) in the key and restore-keys- with
|
||||
# the prefix to load the most recent cache for the branch on a cache miss. You
|
||||
# should customize the contents of hashFiles to capture any bazel input sources,
|
||||
# although this doesn't need to be perfect. If none of the input sources change
|
||||
# then a cache hit will load an existing cache and bazel won't have to do any work.
|
||||
# In the case of a cache miss, you want the fallback cache to contain most of the
|
||||
# previously built artifacts to minimize build time. The more precise you are with
|
||||
# hashFiles sources the less work bazel will have to do.
|
||||
# - name: Mount bazel caches
|
||||
# uses: actions/cache@v5
|
||||
# with:
|
||||
# path: |
|
||||
# ~/.cache/bazel-repo-cache
|
||||
# ~/.cache/bazel-repo-contents-cache
|
||||
# key: bazel-cache-${{ matrix.os }}-${{ hashFiles('**/BUILD.bazel', '**/*.bzl', 'MODULE.bazel') }}
|
||||
# restore-keys: |
|
||||
# bazel-cache-${{ matrix.os }}
|
||||
|
||||
- name: Configure Bazel startup args (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Use a very short path to reduce argv/path length issues.
|
||||
"BAZEL_STARTUP_ARGS=--output_user_root=C:\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
- name: bazel test //...
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
bazel_targets=(
|
||||
bazel_args=(
|
||||
test
|
||||
//...
|
||||
# Keep standalone V8 library targets out of the ordinary Bazel CI
|
||||
# path. V8 consumers under `//codex-rs/...` still participate
|
||||
# transitively through `//...`.
|
||||
-//third_party/v8:all
|
||||
--test_verbose_timeout_warnings
|
||||
--build_metadata=REPO_URL=https://github.com/openai/codex.git
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
--build_metadata=ROLE=CI
|
||||
--build_metadata=VISIBILITY=PUBLIC
|
||||
)
|
||||
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
--print-failed-test-logs \
|
||||
--use-node-test-env \
|
||||
-- \
|
||||
test \
|
||||
--test_tag_filters=-argument-comment-lint \
|
||||
--test_verbose_timeout_warnings \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
-- \
|
||||
"${bazel_targets[@]}"
|
||||
|
||||
# Save bazel repository cache explicitly; make non-fatal so cache uploading
|
||||
# never fails the overall job. Only save when key wasn't hit.
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
|
||||
|
||||
clippy:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Keep Linux lint coverage on x64 and add the arm64 macOS path that
|
||||
# the Bazel test job already exercises. Add Windows gnullvm as well
|
||||
# so PRs get Bazel-native lint signal on the same Windows toolchain
|
||||
# that the Bazel test job uses.
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-gnullvm
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Bazel CI
|
||||
id: setup_bazel
|
||||
uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: bazel build --config=clippy //codex-rs/...
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
# Keep the initial Bazel clippy scope on codex-rs and out of the
|
||||
# V8 proof-of-concept target for now.
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
build \
|
||||
--config=clippy \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
--build_metadata=TAG_job=clippy \
|
||||
-- \
|
||||
//codex-rs/... \
|
||||
-//codex-rs/v8-poc:all
|
||||
|
||||
# Save bazel repository cache explicitly; make non-fatal so cache uploading
|
||||
# never fails the overall job. Only save when key wasn't hit.
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
|
||||
if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
echo "BuildBuddy API key is available; using remote Bazel configuration."
|
||||
bazel $BAZEL_STARTUP_ARGS \
|
||||
--bazelrc=.github/workflows/ci.bazelrc \
|
||||
"${bazel_args[@]}" \
|
||||
"--remote_header=x-buildbuddy-api-key=$BUILDBUDDY_API_KEY"
|
||||
else
|
||||
echo "BuildBuddy API key is not available; using local Bazel configuration."
|
||||
# Keep fork/community PRs on Bazel but disable remote services that are
|
||||
# configured in .bazelrc and require auth.
|
||||
#
|
||||
# Flag docs:
|
||||
# - Command-line reference: https://bazel.build/reference/command-line-reference
|
||||
# - Remote caching overview: https://bazel.build/remote/caching
|
||||
# - Remote execution overview: https://bazel.build/remote/rbe
|
||||
# - Build Event Protocol overview: https://bazel.build/remote/bep
|
||||
#
|
||||
# --noexperimental_remote_repo_contents_cache:
|
||||
# disable remote repo contents cache enabled in .bazelrc startup options.
|
||||
# https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache
|
||||
# --remote_cache= and --remote_executor=:
|
||||
# clear remote cache/execution endpoints configured in .bazelrc.
|
||||
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache
|
||||
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor
|
||||
bazel $BAZEL_STARTUP_ARGS \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
"${bazel_args[@]}" \
|
||||
--remote_cache= \
|
||||
--remote_executor=
|
||||
fi
|
||||
|
||||
32
.github/workflows/blob-size-policy.yml
vendored
32
.github/workflows/blob-size-policy.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: blob-size-policy
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Blob size policy
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine PR comparison range
|
||||
id: range
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "base=$(git rev-parse HEAD^1)" >> "$GITHUB_OUTPUT"
|
||||
echo "head=$(git rev-parse HEAD^2)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check changed blob sizes
|
||||
env:
|
||||
BASE_SHA: ${{ steps.range.outputs.base }}
|
||||
HEAD_SHA: ${{ steps.range.outputs.head }}
|
||||
run: |
|
||||
python3 scripts/check_blob_size.py \
|
||||
--base "$BASE_SHA" \
|
||||
--head "$HEAD_SHA" \
|
||||
--max-bytes 512000 \
|
||||
--allowlist .github/blob-size-allowlist.txt
|
||||
6
.github/workflows/cargo-deny.yml
vendored
6
.github/workflows/cargo-deny.yml
vendored
@@ -14,13 +14,13 @@ jobs:
|
||||
working-directory: ./codex-rs
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2
|
||||
uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
rust-version: stable
|
||||
manifest-path: ./codex-rs/Cargo.toml
|
||||
|
||||
27
.github/workflows/ci.bazelrc
vendored
Normal file
27
.github/workflows/ci.bazelrc
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
common --remote_download_minimal
|
||||
common --keep_going
|
||||
common --verbose_failures
|
||||
|
||||
# Disable disk cache since we have remote one and aren't using persistent workers.
|
||||
common --disk_cache=
|
||||
|
||||
# Rearrange caches on Windows so they're on the same volume as the checkout.
|
||||
common:windows --repo_contents_cache=D:/a/.cache/bazel-repo-contents-cache
|
||||
common:windows --repository_cache=D:/a/.cache/bazel-repo-cache
|
||||
|
||||
# We prefer to run the build actions entirely remotely so we can dial up the concurrency.
|
||||
# We have platform-specific tests, so we want to execute the tests on all platforms using the strongest sandboxing available on each platform.
|
||||
|
||||
# On linux, we can do a full remote build/test, by targeting the right (x86/arm) runners, so we have coverage of both.
|
||||
# Linux crossbuilds don't work until we untangle the libc constraint mess.
|
||||
common:linux --config=remote
|
||||
common:linux --strategy=remote
|
||||
common:linux --platforms=//:rbe
|
||||
|
||||
# On mac, we can run all the build actions remotely but test actions locally.
|
||||
common:macos --config=remote
|
||||
common:macos --strategy=remote
|
||||
common:macos --strategy=TestRunner=darwin-sandbox,local
|
||||
|
||||
# On windows we cannot cross-build the tests but run them locally due to what appears to be a Bazel bug
|
||||
# (windows vs unix path confusion)
|
||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -12,15 +12,15 @@ jobs:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# stage_npm_packages.py requires DotSlash when staging releases.
|
||||
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
- uses: facebook/install-dotslash@v2
|
||||
|
||||
- name: Stage npm package
|
||||
id: stage_npm_package
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Use a rust-release version that includes all native binaries.
|
||||
CODEX_VERSION=0.115.0
|
||||
CODEX_VERSION=0.74.0
|
||||
OUTPUT_DIR="${RUNNER_TEMP}"
|
||||
python3 ./scripts/stage_npm_packages.py \
|
||||
--release-version "$CODEX_VERSION" \
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload staged npm package artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: codex-npm-staging
|
||||
path: ${{ steps.stage_npm_package.outputs.pack_output }}
|
||||
|
||||
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
if: ${{ github.repository_owner == 'openai' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
|
||||
- uses: contributor-assistant/github-action@v2.6.1
|
||||
# Run on close only if the PR was merged. This will lock the PR to preserve
|
||||
# the CLA agreement. We don't want to lock PRs that have been closed without
|
||||
# merging because the contributor may want to respond with additional comments.
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close inactive PRs from contributors
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/codespell.yml
vendored
2
.github/workflows/codespell.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
- name: Annotate locations with typos
|
||||
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1
|
||||
- name: Codespell
|
||||
|
||||
120
.github/workflows/issue-deduplicator.yml
vendored
120
.github/workflows/issue-deduplicator.yml
vendored
@@ -7,19 +7,17 @@ on:
|
||||
- labeled
|
||||
|
||||
jobs:
|
||||
gather-duplicates-all:
|
||||
name: Identify potential duplicates (all issues)
|
||||
gather-duplicates:
|
||||
name: Identify potential duplicates
|
||||
# Prevent runs on forks (requires OpenAI API key, wastes Actions minutes)
|
||||
if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-deduplicate'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
issues_json: ${{ steps.normalize-all.outputs.issues_json }}
|
||||
reason: ${{ steps.normalize-all.outputs.reason }}
|
||||
has_matches: ${{ steps.normalize-all.outputs.has_matches }}
|
||||
codex_output: ${{ steps.select-final.outputs.codex_output }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare Codex inputs
|
||||
env:
|
||||
@@ -31,6 +29,7 @@ jobs:
|
||||
|
||||
CURRENT_ISSUE_FILE=codex-current-issue.json
|
||||
EXISTING_ALL_FILE=codex-existing-issues-all.json
|
||||
EXISTING_OPEN_FILE=codex-existing-issues-open.json
|
||||
|
||||
gh issue list --repo "$REPO" \
|
||||
--json number,title,body,createdAt,updatedAt,state,labels \
|
||||
@@ -48,6 +47,22 @@ jobs:
|
||||
}]' \
|
||||
> "$EXISTING_ALL_FILE"
|
||||
|
||||
gh issue list --repo "$REPO" \
|
||||
--json number,title,body,createdAt,updatedAt,state,labels \
|
||||
--limit 1000 \
|
||||
--state open \
|
||||
--search "sort:created-desc" \
|
||||
| jq '[.[] | {
|
||||
number,
|
||||
title,
|
||||
body: ((.body // "")[0:4000]),
|
||||
createdAt,
|
||||
updatedAt,
|
||||
state,
|
||||
labels: ((.labels // []) | map(.name))
|
||||
}]' \
|
||||
> "$EXISTING_OPEN_FILE"
|
||||
|
||||
gh issue view "$ISSUE_NUMBER" \
|
||||
--repo "$REPO" \
|
||||
--json number,title,body \
|
||||
@@ -56,12 +71,13 @@ jobs:
|
||||
|
||||
echo "Prepared duplicate detection input files."
|
||||
echo "all_issue_count=$(jq 'length' "$EXISTING_ALL_FILE")"
|
||||
echo "open_issue_count=$(jq 'length' "$EXISTING_OPEN_FILE")"
|
||||
|
||||
# Prompt instructions are intentionally inline in this workflow. The old
|
||||
# .github/prompts/issue-deduplicator.txt file is obsolete and removed.
|
||||
- id: codex-all
|
||||
name: Find duplicates (pass 1, all issues)
|
||||
uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
|
||||
uses: openai/codex-action@main
|
||||
with:
|
||||
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
|
||||
allow-users: "*"
|
||||
@@ -142,60 +158,10 @@ jobs:
|
||||
echo "has_matches=$has_matches"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
gather-duplicates-open:
|
||||
name: Identify potential duplicates (open issues fallback)
|
||||
# Pass 1 may drop sudo on the runner, so run the fallback in a fresh job.
|
||||
needs: gather-duplicates-all
|
||||
if: ${{ needs.gather-duplicates-all.result == 'success' && needs.gather-duplicates-all.outputs.has_matches != 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
issues_json: ${{ steps.normalize-open.outputs.issues_json }}
|
||||
reason: ${{ steps.normalize-open.outputs.reason }}
|
||||
has_matches: ${{ steps.normalize-open.outputs.has_matches }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Prepare Codex inputs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
|
||||
CURRENT_ISSUE_FILE=codex-current-issue.json
|
||||
EXISTING_OPEN_FILE=codex-existing-issues-open.json
|
||||
|
||||
gh issue list --repo "$REPO" \
|
||||
--json number,title,body,createdAt,updatedAt,state,labels \
|
||||
--limit 1000 \
|
||||
--state open \
|
||||
--search "sort:created-desc" \
|
||||
| jq '[.[] | {
|
||||
number,
|
||||
title,
|
||||
body: ((.body // "")[0:4000]),
|
||||
createdAt,
|
||||
updatedAt,
|
||||
state,
|
||||
labels: ((.labels // []) | map(.name))
|
||||
}]' \
|
||||
> "$EXISTING_OPEN_FILE"
|
||||
|
||||
gh issue view "$ISSUE_NUMBER" \
|
||||
--repo "$REPO" \
|
||||
--json number,title,body \
|
||||
| jq '{number, title, body: ((.body // "")[0:4000])}' \
|
||||
> "$CURRENT_ISSUE_FILE"
|
||||
|
||||
echo "Prepared fallback duplicate detection input files."
|
||||
echo "open_issue_count=$(jq 'length' "$EXISTING_OPEN_FILE")"
|
||||
|
||||
- id: codex-open
|
||||
name: Find duplicates (pass 2, open issues)
|
||||
uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
|
||||
if: ${{ steps.normalize-all.outputs.has_matches != 'true' }}
|
||||
uses: openai/codex-action@main
|
||||
with:
|
||||
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
|
||||
allow-users: "*"
|
||||
@@ -234,6 +200,7 @@ jobs:
|
||||
|
||||
- id: normalize-open
|
||||
name: Normalize pass 2 output
|
||||
if: ${{ steps.normalize-all.outputs.has_matches != 'true' }}
|
||||
env:
|
||||
CODEX_OUTPUT: ${{ steps.codex-open.outputs.final-message }}
|
||||
CURRENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
@@ -276,27 +243,15 @@ jobs:
|
||||
echo "has_matches=$has_matches"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
select-final:
|
||||
name: Select final duplicate set
|
||||
needs:
|
||||
- gather-duplicates-all
|
||||
- gather-duplicates-open
|
||||
if: ${{ always() && needs.gather-duplicates-all.result == 'success' && (needs.gather-duplicates-open.result == 'success' || needs.gather-duplicates-open.result == 'skipped') }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
codex_output: ${{ steps.select-final.outputs.codex_output }}
|
||||
steps:
|
||||
- id: select-final
|
||||
name: Select final duplicate set
|
||||
env:
|
||||
PASS1_ISSUES: ${{ needs.gather-duplicates-all.outputs.issues_json }}
|
||||
PASS1_REASON: ${{ needs.gather-duplicates-all.outputs.reason }}
|
||||
PASS2_ISSUES: ${{ needs.gather-duplicates-open.outputs.issues_json }}
|
||||
PASS2_REASON: ${{ needs.gather-duplicates-open.outputs.reason }}
|
||||
PASS1_HAS_MATCHES: ${{ needs.gather-duplicates-all.outputs.has_matches }}
|
||||
PASS2_HAS_MATCHES: ${{ needs.gather-duplicates-open.outputs.has_matches }}
|
||||
PASS1_ISSUES: ${{ steps.normalize-all.outputs.issues_json }}
|
||||
PASS1_REASON: ${{ steps.normalize-all.outputs.reason }}
|
||||
PASS2_ISSUES: ${{ steps.normalize-open.outputs.issues_json }}
|
||||
PASS2_REASON: ${{ steps.normalize-open.outputs.reason }}
|
||||
PASS1_HAS_MATCHES: ${{ steps.normalize-all.outputs.has_matches }}
|
||||
PASS2_HAS_MATCHES: ${{ steps.normalize-open.outputs.has_matches }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
|
||||
@@ -334,17 +289,17 @@ jobs:
|
||||
|
||||
comment-on-issue:
|
||||
name: Comment with potential duplicates
|
||||
needs: select-final
|
||||
if: ${{ always() && needs.select-final.result == 'success' }}
|
||||
needs: gather-duplicates
|
||||
if: ${{ needs.gather-duplicates.result != 'skipped' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Comment on issue
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
CODEX_OUTPUT: ${{ needs.select-final.outputs.codex_output }}
|
||||
CODEX_OUTPUT: ${{ needs.gather-duplicates.outputs.codex_output }}
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
@@ -396,7 +351,6 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
run: |
|
||||
gh issue edit "$ISSUE_NUMBER" --remove-label codex-deduplicate || true
|
||||
gh issue edit "${{ github.event.issue.number }}" --remove-label codex-deduplicate || true
|
||||
echo "Attempted to remove label: codex-deduplicate"
|
||||
|
||||
4
.github/workflows/issue-labeler.yml
vendored
4
.github/workflows/issue-labeler.yml
vendored
@@ -17,10 +17,10 @@ jobs:
|
||||
outputs:
|
||||
codex_output: ${{ steps.codex.outputs.final-message }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- id: codex
|
||||
uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
|
||||
uses: openai/codex-action@main
|
||||
with:
|
||||
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
|
||||
allow-users: "*"
|
||||
|
||||
760
.github/workflows/rust-ci-full.yml
vendored
760
.github/workflows/rust-ci-full.yml
vendored
@@ -1,760 +0,0 @@
|
||||
name: rust-ci-full
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
# CI builds in debug (dev) for faster signal.
|
||||
|
||||
jobs:
|
||||
# --- CI that doesn't need specific targets ---------------------------------
|
||||
general:
|
||||
name: Format / etc
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: cargo fmt
|
||||
run: cargo fmt -- --config imports_granularity=Item --check
|
||||
|
||||
cargo_shear:
|
||||
name: cargo shear
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: cargo-shear
|
||||
version: 1.5.1
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
|
||||
argument_comment_lint_package:
|
||||
name: Argument comment lint package
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
components: llvm-tools-preview, rustc-dev, rust-src
|
||||
- name: Cache cargo-dylint tooling
|
||||
id: cargo_dylint_cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-dylint
|
||||
~/.cargo/bin/dylint-link
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: argument-comment-lint-${{ runner.os }}-${{ hashFiles('tools/argument-comment-lint/Cargo.lock', 'tools/argument-comment-lint/rust-toolchain', '.github/workflows/rust-ci.yml', '.github/workflows/rust-ci-full.yml') }}
|
||||
- name: Install cargo-dylint tooling
|
||||
if: ${{ steps.cargo_dylint_cache.outputs.cache-hit != 'true' }}
|
||||
run: cargo install --locked cargo-dylint dylint-link
|
||||
- name: Check Python wrapper syntax
|
||||
run: python3 -m py_compile tools/argument-comment-lint/wrapper_common.py tools/argument-comment-lint/run.py tools/argument-comment-lint/run-prebuilt-linter.py tools/argument-comment-lint/test_wrapper_common.py
|
||||
- name: Test Python wrapper helpers
|
||||
run: python3 -m unittest discover -s tools/argument-comment-lint -p 'test_*.py'
|
||||
- name: Test argument comment lint package
|
||||
working-directory: tools/argument-comment-lint
|
||||
run: cargo test
|
||||
|
||||
argument_comment_lint_prebuilt:
|
||||
name: Argument comment lint - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux
|
||||
runner: ubuntu-24.04
|
||||
- name: macOS
|
||||
runner: macos-15-xlarge
|
||||
- name: Windows
|
||||
runner: windows-x64
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: ${{ runner.os }}
|
||||
install-test-prereqs: true
|
||||
- name: Install Linux sandbox build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
|
||||
- name: Run argument comment lint on codex-rs via Bazel
|
||||
shell: bash
|
||||
run: |
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
build \
|
||||
--config=argument-comment-lint \
|
||||
--keep_going \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
-- \
|
||||
//codex-rs/...
|
||||
|
||||
# --- CI to validate on different os/targets --------------------------------
|
||||
lint_build:
|
||||
name: Lint/Build — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.profile == 'release' && ' (release)' || '' }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
# Speed up repeated builds across CI runs by caching compiled objects, except on
|
||||
# arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
|
||||
# mixed-architecture archives under sccache.
|
||||
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
|
||||
CARGO_INCREMENTAL: "0"
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
# In rust-ci, representative release-profile checks use thin LTO for faster feedback.
|
||||
CARGO_PROFILE_RELEASE_LTO: ${{ matrix.profile == 'release' && 'thin' || 'fat' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
profile: dev
|
||||
- runner: macos-15-xlarge
|
||||
target: x86_64-apple-darwin
|
||||
profile: dev
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-x64
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-x64
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-arm64
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-arm64
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
|
||||
# Also run representative release builds on Mac and Linux because
|
||||
# there could be release-only build errors we want to catch.
|
||||
# Hopefully this also pre-populates the build cache to speed up
|
||||
# releases.
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
profile: release
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
profile: release
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-x64
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
profile: release
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-arm64
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
profile: release
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
profile: release
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
packages=(pkg-config libcap-dev)
|
||||
if [[ "${{ matrix.target }}" == 'x86_64-unknown-linux-musl' || "${{ matrix.target }}" == 'aarch64-unknown-linux-musl' ]]; then
|
||||
packages+=(libubsan1)
|
||||
fi
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
|
||||
fi
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
components: clippy
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Use hermetic Cargo home (musl)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo_home="${GITHUB_WORKSPACE}/.cargo-home"
|
||||
mkdir -p "${cargo_home}/bin"
|
||||
echo "CARGO_HOME=${cargo_home}" >> "$GITHUB_ENV"
|
||||
echo "${cargo_home}/bin" >> "$GITHUB_PATH"
|
||||
: > "${cargo_home}/config.toml"
|
||||
|
||||
- name: Compute lockfile hash
|
||||
id: lockhash
|
||||
working-directory: codex-rs
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Explicit cache restore: split cargo home vs target, so we can
|
||||
# avoid caching the large target dir on the gnu-dev job.
|
||||
- name: Restore cargo home cache
|
||||
id: cache_cargo_home_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
${{ github.workspace }}/.cargo-home/bin/
|
||||
${{ github.workspace }}/.cargo-home/registry/index/
|
||||
${{ github.workspace }}/.cargo-home/registry/cache/
|
||||
${{ github.workspace }}/.cargo-home/git/db/
|
||||
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
|
||||
restore-keys: |
|
||||
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
# Install and restore sccache cache
|
||||
- name: Install sccache
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: sccache
|
||||
version: 0.7.5
|
||||
|
||||
- name: Configure sccache backend
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
|
||||
echo "Using sccache GitHub backend"
|
||||
else
|
||||
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
|
||||
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
|
||||
echo "Using sccache local disk + actions/cache fallback"
|
||||
fi
|
||||
|
||||
- name: Enable sccache wrapper
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
shell: bash
|
||||
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Restore sccache cache (fallback)
|
||||
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
|
||||
id: cache_sccache_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Disable sccache wrapper (musl)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
|
||||
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Prepare APT cache directories (musl)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo mkdir -p /var/cache/apt/archives /var/lib/apt/lists
|
||||
sudo chown -R "$USER:$USER" /var/cache/apt /var/lib/apt/lists
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Restore APT cache (musl)
|
||||
id: cache_apt_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
/var/cache/apt
|
||||
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Install Zig
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
with:
|
||||
version: 0.14.0
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Install musl build tools
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
TARGET: ${{ matrix.target }}
|
||||
APT_UPDATE_ARGS: -o Acquire::Retries=3
|
||||
APT_INSTALL_ARGS: --no-install-recommends
|
||||
shell: bash
|
||||
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Configure rustc UBSan wrapper (musl host)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ubsan=""
|
||||
if command -v ldconfig >/dev/null 2>&1; then
|
||||
ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
|
||||
fi
|
||||
wrapper_root="${RUNNER_TEMP:-/tmp}"
|
||||
wrapper="${wrapper_root}/rustc-ubsan-wrapper"
|
||||
cat > "${wrapper}" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ -n "${ubsan}" ]]; then
|
||||
export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
|
||||
fi
|
||||
exec "\$1" "\${@:2}"
|
||||
EOF
|
||||
chmod +x "${wrapper}"
|
||||
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
|
||||
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Clear sanitizer flags (musl)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
|
||||
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
|
||||
# Override any runner-level Cargo config rustflags as well.
|
||||
echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
|
||||
sanitize_flags() {
|
||||
local input="$1"
|
||||
input="${input//-fsanitize=undefined/}"
|
||||
input="${input//-fno-sanitize-recover=undefined/}"
|
||||
input="${input//-fno-sanitize-trap=undefined/}"
|
||||
echo "$input"
|
||||
}
|
||||
|
||||
cflags="$(sanitize_flags "${CFLAGS-}")"
|
||||
cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
|
||||
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
|
||||
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
|
||||
name: Configure musl rusty_v8 artifact overrides
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
|
||||
release_tag="rusty-v8-v${version}"
|
||||
base_url="https://github.com/openai/codex/releases/download/${release_tag}"
|
||||
archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz"
|
||||
binding_dir="${RUNNER_TEMP}/rusty_v8"
|
||||
binding_path="${binding_dir}/src_binding_release_${TARGET}.rs"
|
||||
mkdir -p "${binding_dir}"
|
||||
curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}"
|
||||
echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV"
|
||||
echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install cargo-chef
|
||||
if: ${{ matrix.profile == 'release' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: cargo-chef
|
||||
version: 0.1.71
|
||||
|
||||
- name: Pre-warm dependency cache (cargo-chef)
|
||||
if: ${{ matrix.profile == 'release' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RECIPE="${RUNNER_TEMP}/chef-recipe.json"
|
||||
cargo chef prepare --recipe-path "$RECIPE"
|
||||
cargo chef cook --recipe-path "$RECIPE" --target ${{ matrix.target }} --release --all-features
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo clippy --target ${{ matrix.target }} --all-features --tests --profile ${{ matrix.profile }} --timings -- -D warnings
|
||||
|
||||
- name: Upload Cargo timings (clippy)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
# Save caches explicitly; make non-fatal so cache packaging
|
||||
# never fails the overall job. Only save when key wasn't hit.
|
||||
- name: Save cargo home cache
|
||||
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
${{ github.workspace }}/.cargo-home/bin/
|
||||
${{ github.workspace }}/.cargo-home/registry/index/
|
||||
${{ github.workspace }}/.cargo-home/registry/cache/
|
||||
${{ github.workspace }}/.cargo-home/git/db/
|
||||
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
|
||||
|
||||
- name: Save sccache cache (fallback)
|
||||
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
|
||||
- name: sccache stats
|
||||
if: always() && env.USE_SCCACHE == 'true'
|
||||
continue-on-error: true
|
||||
run: sccache --show-stats || true
|
||||
|
||||
- name: sccache summary
|
||||
if: always() && env.USE_SCCACHE == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "### sccache stats — ${{ matrix.target }} (${{ matrix.profile }})";
|
||||
echo;
|
||||
echo '```';
|
||||
sccache --show-stats || true;
|
||||
echo '```';
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Save APT cache (musl)
|
||||
if: always() && !cancelled() && (matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl') && steps.cache_apt_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
/var/cache/apt
|
||||
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
|
||||
|
||||
tests:
|
||||
name: Tests — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.remote_env == 'true' && ' (remote)' || '' }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
# Perhaps we can bring this back down to 30m once we finish the cutover
|
||||
# from tui_app_server/ to tui/. Incidentally, windows-arm64 was the main
|
||||
# offender for exceeding the timeout.
|
||||
timeout-minutes: 45
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
# Speed up repeated builds across CI runs by caching compiled objects, except on
|
||||
# arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
|
||||
# mixed-architecture archives under sccache.
|
||||
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
|
||||
CARGO_INCREMENTAL: "0"
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
profile: dev
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
profile: dev
|
||||
remote_env: "true"
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-x64
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-arm64
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up Node.js for js_repl tests
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: codex-rs/node-version.txt
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
|
||||
fi
|
||||
|
||||
# Some integration tests rely on DotSlash being installed.
|
||||
# See https://github.com/openai/codex/pull/7617.
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Compute lockfile hash
|
||||
id: lockhash
|
||||
working-directory: codex-rs
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore cargo home cache
|
||||
id: cache_cargo_home_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
|
||||
restore-keys: |
|
||||
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
- name: Install sccache
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: sccache
|
||||
version: 0.7.5
|
||||
|
||||
- name: Configure sccache backend
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
|
||||
echo "Using sccache GitHub backend"
|
||||
else
|
||||
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
|
||||
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
|
||||
echo "Using sccache local disk + actions/cache fallback"
|
||||
fi
|
||||
|
||||
- name: Enable sccache wrapper
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
shell: bash
|
||||
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Restore sccache cache (fallback)
|
||||
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
|
||||
id: cache_sccache_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: nextest
|
||||
version: 0.9.103
|
||||
|
||||
- name: Enable unprivileged user namespaces (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
# Required for bubblewrap to work on Linux CI runners.
|
||||
sudo sysctl -w kernel.unprivileged_userns_clone=1
|
||||
# Ubuntu 24.04+ can additionally gate unprivileged user namespaces
|
||||
# behind AppArmor.
|
||||
if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
fi
|
||||
|
||||
- name: Set up remote test env (Docker)
|
||||
if: ${{ runner.os == 'Linux' && matrix.remote_env == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export CODEX_TEST_REMOTE_ENV_CONTAINER_NAME=codex-remote-test-env
|
||||
source "${GITHUB_WORKSPACE}/scripts/test-remote-env.sh"
|
||||
echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: tests
|
||||
id: test
|
||||
run: cargo nextest run --all-features --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
NEXTEST_STATUS_LEVEL: leak
|
||||
|
||||
- name: Upload Cargo timings (nextest)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: cargo-timings-rust-ci-nextest-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Save cargo home cache
|
||||
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
|
||||
|
||||
- name: Save sccache cache (fallback)
|
||||
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
|
||||
- name: sccache stats
|
||||
if: always() && env.USE_SCCACHE == 'true'
|
||||
continue-on-error: true
|
||||
run: sccache --show-stats || true
|
||||
|
||||
- name: sccache summary
|
||||
if: always() && env.USE_SCCACHE == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "### sccache stats — ${{ matrix.target }} (tests)";
|
||||
echo;
|
||||
echo '```';
|
||||
sccache --show-stats || true;
|
||||
echo '```';
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Tear down remote test env
|
||||
if: ${{ always() && runner.os == 'Linux' && matrix.remote_env == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set +e
|
||||
if [[ "${{ steps.test.outcome }}" != "success" ]]; then
|
||||
docker logs codex-remote-test-env || true
|
||||
fi
|
||||
docker rm -f codex-remote-test-env >/dev/null 2>&1 || true
|
||||
|
||||
- name: verify tests passed
|
||||
if: steps.test.outcome == 'failure'
|
||||
run: |
|
||||
echo "Tests failed. See logs for details."
|
||||
exit 1
|
||||
|
||||
# --- Gatherer job for the full post-merge workflow --------------------------
|
||||
results:
|
||||
name: Full CI results
|
||||
needs:
|
||||
[
|
||||
general,
|
||||
cargo_shear,
|
||||
argument_comment_lint_package,
|
||||
argument_comment_lint_prebuilt,
|
||||
lint_build,
|
||||
tests,
|
||||
]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Summarize
|
||||
shell: bash
|
||||
run: |
|
||||
echo "argpkg : ${{ needs.argument_comment_lint_package.result }}"
|
||||
echo "arglint: ${{ needs.argument_comment_lint_prebuilt.result }}"
|
||||
echo "general: ${{ needs.general.result }}"
|
||||
echo "shear : ${{ needs.cargo_shear.result }}"
|
||||
echo "lint : ${{ needs.lint_build.result }}"
|
||||
echo "tests : ${{ needs.tests.result }}"
|
||||
[[ '${{ needs.argument_comment_lint_package.result }}' == 'success' ]] || { echo 'argument_comment_lint_package failed'; exit 1; }
|
||||
[[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
|
||||
[[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
|
||||
[[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
|
||||
[[ '${{ needs.lint_build.result }}' == 'success' ]] || { echo 'lint_build failed'; exit 1; }
|
||||
[[ '${{ needs.tests.result }}' == 'success' ]] || { echo 'tests failed'; exit 1; }
|
||||
|
||||
- name: sccache summary note
|
||||
if: always()
|
||||
run: |
|
||||
echo "Per-job sccache stats are attached to each matrix job's Step Summary."
|
||||
702
.github/workflows/rust-ci.yml
vendored
702
.github/workflows/rust-ci.yml
vendored
@@ -1,20 +1,23 @@
|
||||
name: rust-ci
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
# CI builds in debug (dev) for faster signal.
|
||||
|
||||
jobs:
|
||||
# --- Detect what changed so the fast PR workflow only runs relevant jobs ----
|
||||
# --- Detect what changed to detect which tests to run (always runs) -------------------------------------
|
||||
changed:
|
||||
name: Detect changed areas
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
argument_comment_lint: ${{ steps.detect.outputs.argument_comment_lint }}
|
||||
argument_comment_lint_package: ${{ steps.detect.outputs.argument_comment_lint_package }}
|
||||
codex: ${{ steps.detect.outputs.codex }}
|
||||
workflows: ${{ steps.detect.outputs.workflows }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Detect changed paths (no external action)
|
||||
@@ -28,40 +31,35 @@ jobs:
|
||||
HEAD_SHA='${{ github.event.pull_request.head.sha }}'
|
||||
echo "Base SHA: $BASE_SHA"
|
||||
echo "Head SHA: $HEAD_SHA"
|
||||
# List files changed between base and PR head
|
||||
mapfile -t files < <(git diff --name-only --no-renames "$BASE_SHA" "$HEAD_SHA")
|
||||
else
|
||||
# On manual runs, default to the full fast-PR bundle.
|
||||
files=("codex-rs/force" "tools/argument-comment-lint/force" ".github/force")
|
||||
# On push / manual runs, default to running everything
|
||||
files=("codex-rs/force" ".github/force")
|
||||
fi
|
||||
|
||||
codex=false
|
||||
argument_comment_lint=false
|
||||
argument_comment_lint_package=false
|
||||
workflows=false
|
||||
for f in "${files[@]}"; do
|
||||
[[ $f == codex-rs/* ]] && codex=true
|
||||
[[ $f == codex-rs/* || $f == tools/argument-comment-lint/* || $f == justfile ]] && argument_comment_lint=true
|
||||
[[ $f == tools/argument-comment-lint/* || $f == .github/workflows/rust-ci.yml || $f == .github/workflows/rust-ci-full.yml ]] && argument_comment_lint_package=true
|
||||
[[ $f == .github/* ]] && workflows=true
|
||||
done
|
||||
|
||||
echo "argument_comment_lint=$argument_comment_lint" >> "$GITHUB_OUTPUT"
|
||||
echo "argument_comment_lint_package=$argument_comment_lint_package" >> "$GITHUB_OUTPUT"
|
||||
echo "codex=$codex" >> "$GITHUB_OUTPUT"
|
||||
echo "workflows=$workflows" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# --- Fast Cargo-native PR checks -------------------------------------------
|
||||
# --- CI that doesn't need specific targets ---------------------------------
|
||||
general:
|
||||
name: Format / etc
|
||||
runs-on: ubuntu-24.04
|
||||
needs: changed
|
||||
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' }}
|
||||
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: cargo fmt
|
||||
@@ -71,13 +69,13 @@ jobs:
|
||||
name: cargo shear
|
||||
runs-on: ubuntu-24.04
|
||||
needs: changed
|
||||
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' }}
|
||||
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: cargo-shear
|
||||
@@ -85,148 +83,602 @@ jobs:
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
|
||||
argument_comment_lint_package:
|
||||
name: Argument comment lint package
|
||||
runs-on: ubuntu-24.04
|
||||
needs: changed
|
||||
if: ${{ needs.changed.outputs.argument_comment_lint_package == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- name: Install nightly argument-comment-lint toolchain
|
||||
shell: bash
|
||||
run: |
|
||||
rustup toolchain install nightly-2025-09-18 \
|
||||
--profile minimal \
|
||||
--component llvm-tools-preview \
|
||||
--component rustc-dev \
|
||||
--component rust-src \
|
||||
--no-self-update
|
||||
rustup default nightly-2025-09-18
|
||||
- name: Cache cargo-dylint tooling
|
||||
id: cargo_dylint_cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-dylint
|
||||
~/.cargo/bin/dylint-link
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: argument-comment-lint-${{ runner.os }}-${{ hashFiles('tools/argument-comment-lint/Cargo.lock', 'tools/argument-comment-lint/rust-toolchain', '.github/workflows/rust-ci.yml', '.github/workflows/rust-ci-full.yml') }}
|
||||
- name: Install cargo-dylint tooling
|
||||
if: ${{ steps.cargo_dylint_cache.outputs.cache-hit != 'true' }}
|
||||
run: cargo install --locked cargo-dylint dylint-link
|
||||
- name: Check Python wrapper syntax
|
||||
run: python3 -m py_compile tools/argument-comment-lint/wrapper_common.py tools/argument-comment-lint/run.py tools/argument-comment-lint/run-prebuilt-linter.py tools/argument-comment-lint/test_wrapper_common.py
|
||||
- name: Test Python wrapper helpers
|
||||
run: python3 -m unittest discover -s tools/argument-comment-lint -p 'test_*.py'
|
||||
- name: Test argument comment lint package
|
||||
working-directory: tools/argument-comment-lint
|
||||
run: cargo test
|
||||
|
||||
argument_comment_lint_prebuilt:
|
||||
name: Argument comment lint - ${{ matrix.name }}
|
||||
# --- CI to validate on different os/targets --------------------------------
|
||||
lint_build:
|
||||
name: Lint/Build — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.profile == 'release' && ' (release)' || '' }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
timeout-minutes: 30
|
||||
needs: changed
|
||||
if: ${{ needs.changed.outputs.argument_comment_lint == 'true' || needs.changed.outputs.workflows == 'true' }}
|
||||
# Keep job-level if to avoid spinning up runners when not needed
|
||||
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
# Speed up repeated builds across CI runs by caching compiled objects (non-Windows).
|
||||
USE_SCCACHE: ${{ startsWith(matrix.runner, 'windows') && 'false' || 'true' }}
|
||||
CARGO_INCREMENTAL: "0"
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
# In rust-ci, representative release-profile checks use thin LTO for faster feedback.
|
||||
CARGO_PROFILE_RELEASE_LTO: ${{ matrix.profile == 'release' && 'thin' || 'fat' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux
|
||||
runner: ubuntu-24.04
|
||||
timeout_minutes: 30
|
||||
- name: macOS
|
||||
runner: macos-15-xlarge
|
||||
timeout_minutes: 30
|
||||
- name: Windows
|
||||
runner: windows-x64
|
||||
timeout_minutes: 30
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
profile: dev
|
||||
- runner: macos-15-xlarge
|
||||
target: x86_64-apple-darwin
|
||||
profile: dev
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-x64
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-x64
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-arm64
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-arm64
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
|
||||
# Also run representative release builds on Mac and Linux because
|
||||
# there could be release-only build errors we want to catch.
|
||||
# Hopefully this also pre-populates the build cache to speed up
|
||||
# releases.
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
profile: release
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
profile: release
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-x64
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
profile: release
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-arm64
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
profile: release
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
profile: release
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: ${{ runner.os }}
|
||||
install-test-prereqs: true
|
||||
- name: Install Linux sandbox build dependencies
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
|
||||
- name: Install nightly argument-comment-lint toolchain
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
set -euo pipefail
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
packages=(pkg-config libcap-dev)
|
||||
if [[ "${{ matrix.target }}" == 'x86_64-unknown-linux-musl' || "${{ matrix.target }}" == 'aarch64-unknown-linux-musl' ]]; then
|
||||
packages+=(libubsan1)
|
||||
fi
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
|
||||
fi
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
components: clippy
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Use hermetic Cargo home (musl)
|
||||
shell: bash
|
||||
run: |
|
||||
rustup toolchain install nightly-2025-09-18 \
|
||||
--profile minimal \
|
||||
--component llvm-tools-preview \
|
||||
--component rustc-dev \
|
||||
--component rust-src \
|
||||
--no-self-update
|
||||
rustup default nightly-2025-09-18
|
||||
- name: Run argument comment lint on codex-rs via Bazel
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
set -euo pipefail
|
||||
cargo_home="${GITHUB_WORKSPACE}/.cargo-home"
|
||||
mkdir -p "${cargo_home}/bin"
|
||||
echo "CARGO_HOME=${cargo_home}" >> "$GITHUB_ENV"
|
||||
echo "${cargo_home}/bin" >> "$GITHUB_PATH"
|
||||
: > "${cargo_home}/config.toml"
|
||||
|
||||
- name: Compute lockfile hash
|
||||
id: lockhash
|
||||
working-directory: codex-rs
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Explicit cache restore: split cargo home vs target, so we can
|
||||
# avoid caching the large target dir on the gnu-dev job.
|
||||
- name: Restore cargo home cache
|
||||
id: cache_cargo_home_restore
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
${{ github.workspace }}/.cargo-home/bin/
|
||||
${{ github.workspace }}/.cargo-home/registry/index/
|
||||
${{ github.workspace }}/.cargo-home/registry/cache/
|
||||
${{ github.workspace }}/.cargo-home/git/db/
|
||||
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
|
||||
restore-keys: |
|
||||
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
# Install and restore sccache cache
|
||||
- name: Install sccache
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: sccache
|
||||
version: 0.7.5
|
||||
|
||||
- name: Configure sccache backend
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
|
||||
echo "Using sccache GitHub backend"
|
||||
else
|
||||
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
|
||||
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
|
||||
echo "Using sccache local disk + actions/cache fallback"
|
||||
fi
|
||||
|
||||
- name: Enable sccache wrapper
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
shell: bash
|
||||
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Restore sccache cache (fallback)
|
||||
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
|
||||
id: cache_sccache_restore
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Disable sccache wrapper (musl)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
|
||||
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Prepare APT cache directories (musl)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo mkdir -p /var/cache/apt/archives /var/lib/apt/lists
|
||||
sudo chown -R "$USER:$USER" /var/cache/apt /var/lib/apt/lists
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Restore APT cache (musl)
|
||||
id: cache_apt_restore
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: |
|
||||
/var/cache/apt
|
||||
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Install Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.14.0
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Install musl build tools
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
TARGET: ${{ matrix.target }}
|
||||
APT_UPDATE_ARGS: -o Acquire::Retries=3
|
||||
APT_INSTALL_ARGS: --no-install-recommends
|
||||
shell: bash
|
||||
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Configure rustc UBSan wrapper (musl host)
|
||||
shell: bash
|
||||
run: |
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
build \
|
||||
--config=argument-comment-lint \
|
||||
--keep_going \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
-- \
|
||||
//codex-rs/...
|
||||
- name: Run argument comment lint on codex-rs via packaged wrapper
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
set -euo pipefail
|
||||
ubsan=""
|
||||
if command -v ldconfig >/dev/null 2>&1; then
|
||||
ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
|
||||
fi
|
||||
wrapper_root="${RUNNER_TEMP:-/tmp}"
|
||||
wrapper="${wrapper_root}/rustc-ubsan-wrapper"
|
||||
cat > "${wrapper}" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ -n "${ubsan}" ]]; then
|
||||
export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
|
||||
fi
|
||||
exec "\$1" "\${@:2}"
|
||||
EOF
|
||||
chmod +x "${wrapper}"
|
||||
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
|
||||
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Clear sanitizer flags (musl)
|
||||
shell: bash
|
||||
run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
|
||||
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
|
||||
# Override any runner-level Cargo config rustflags as well.
|
||||
echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
|
||||
sanitize_flags() {
|
||||
local input="$1"
|
||||
input="${input//-fsanitize=undefined/}"
|
||||
input="${input//-fno-sanitize-recover=undefined/}"
|
||||
input="${input//-fno-sanitize-trap=undefined/}"
|
||||
echo "$input"
|
||||
}
|
||||
|
||||
cflags="$(sanitize_flags "${CFLAGS-}")"
|
||||
cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
|
||||
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
|
||||
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install cargo-chef
|
||||
if: ${{ matrix.profile == 'release' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: cargo-chef
|
||||
version: 0.1.71
|
||||
|
||||
- name: Pre-warm dependency cache (cargo-chef)
|
||||
if: ${{ matrix.profile == 'release' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RECIPE="${RUNNER_TEMP}/chef-recipe.json"
|
||||
cargo chef prepare --recipe-path "$RECIPE"
|
||||
cargo chef cook --recipe-path "$RECIPE" --target ${{ matrix.target }} --release --all-features
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo clippy --target ${{ matrix.target }} --all-features --tests --profile ${{ matrix.profile }} --timings -- -D warnings
|
||||
|
||||
- name: Upload Cargo timings (clippy)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
# Save caches explicitly; make non-fatal so cache packaging
|
||||
# never fails the overall job. Only save when key wasn't hit.
|
||||
- name: Save cargo home cache
|
||||
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
${{ github.workspace }}/.cargo-home/bin/
|
||||
${{ github.workspace }}/.cargo-home/registry/index/
|
||||
${{ github.workspace }}/.cargo-home/registry/cache/
|
||||
${{ github.workspace }}/.cargo-home/git/db/
|
||||
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
|
||||
|
||||
- name: Save sccache cache (fallback)
|
||||
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
|
||||
- name: sccache stats
|
||||
if: always() && env.USE_SCCACHE == 'true'
|
||||
continue-on-error: true
|
||||
run: sccache --show-stats || true
|
||||
|
||||
- name: sccache summary
|
||||
if: always() && env.USE_SCCACHE == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "### sccache stats — ${{ matrix.target }} (${{ matrix.profile }})";
|
||||
echo;
|
||||
echo '```';
|
||||
sccache --show-stats || true;
|
||||
echo '```';
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Save APT cache (musl)
|
||||
if: always() && !cancelled() && (matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl') && steps.cache_apt_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: |
|
||||
/var/cache/apt
|
||||
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
|
||||
|
||||
tests:
|
||||
name: Tests — ${{ matrix.runner }} - ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
needs: changed
|
||||
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
# Speed up repeated builds across CI runs by caching compiled objects (non-Windows).
|
||||
USE_SCCACHE: ${{ startsWith(matrix.runner, 'windows') && 'false' || 'true' }}
|
||||
CARGO_INCREMENTAL: "0"
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
profile: dev
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-x64
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-arm64
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
- runner: windows-arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
profile: dev
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
|
||||
fi
|
||||
# Some integration tests rely on DotSlash being installed.
|
||||
# See https://github.com/openai/codex/pull/7617.
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@v2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Compute lockfile hash
|
||||
id: lockhash
|
||||
working-directory: codex-rs
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore cargo home cache
|
||||
id: cache_cargo_home_restore
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
|
||||
restore-keys: |
|
||||
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
- name: Install sccache
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: sccache
|
||||
version: 0.7.5
|
||||
|
||||
- name: Configure sccache backend
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
|
||||
echo "Using sccache GitHub backend"
|
||||
else
|
||||
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
|
||||
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
|
||||
echo "Using sccache local disk + actions/cache fallback"
|
||||
fi
|
||||
|
||||
- name: Enable sccache wrapper
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
shell: bash
|
||||
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Restore sccache cache (fallback)
|
||||
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
|
||||
id: cache_sccache_restore
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
with:
|
||||
tool: nextest
|
||||
version: 0.9.103
|
||||
|
||||
- name: Enable unprivileged user namespaces (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
# Required for bubblewrap to work on Linux CI runners.
|
||||
sudo sysctl -w kernel.unprivileged_userns_clone=1
|
||||
# Ubuntu 24.04+ can additionally gate unprivileged user namespaces
|
||||
# behind AppArmor.
|
||||
if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
fi
|
||||
|
||||
- name: tests
|
||||
id: test
|
||||
run: cargo nextest run --all-features --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
NEXTEST_STATUS_LEVEL: leak
|
||||
|
||||
- name: Upload Cargo timings (nextest)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: cargo-timings-rust-ci-nextest-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Save cargo home cache
|
||||
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
|
||||
|
||||
- name: Save sccache cache (fallback)
|
||||
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
|
||||
- name: sccache stats
|
||||
if: always() && env.USE_SCCACHE == 'true'
|
||||
continue-on-error: true
|
||||
run: sccache --show-stats || true
|
||||
|
||||
- name: sccache summary
|
||||
if: always() && env.USE_SCCACHE == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "### sccache stats — ${{ matrix.target }} (tests)";
|
||||
echo;
|
||||
echo '```';
|
||||
sccache --show-stats || true;
|
||||
echo '```';
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: verify tests passed
|
||||
if: steps.test.outcome == 'failure'
|
||||
run: |
|
||||
echo "Tests failed. See logs for details."
|
||||
exit 1
|
||||
|
||||
# --- Gatherer job that you mark as the ONLY required status -----------------
|
||||
results:
|
||||
name: CI results (required)
|
||||
needs:
|
||||
[
|
||||
changed,
|
||||
general,
|
||||
cargo_shear,
|
||||
argument_comment_lint_package,
|
||||
argument_comment_lint_prebuilt,
|
||||
]
|
||||
needs: [changed, general, cargo_shear, lint_build, tests]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Summarize
|
||||
shell: bash
|
||||
run: |
|
||||
echo "argpkg : ${{ needs.argument_comment_lint_package.result }}"
|
||||
echo "arglint: ${{ needs.argument_comment_lint_prebuilt.result }}"
|
||||
echo "general: ${{ needs.general.result }}"
|
||||
echo "shear : ${{ needs.cargo_shear.result }}"
|
||||
echo "lint : ${{ needs.lint_build.result }}"
|
||||
echo "tests : ${{ needs.tests.result }}"
|
||||
|
||||
# If nothing relevant changed (PR touching only root README, etc.),
|
||||
# declare success regardless of other jobs.
|
||||
if [[ '${{ needs.changed.outputs.argument_comment_lint }}' != 'true' && '${{ needs.changed.outputs.codex }}' != 'true' && '${{ needs.changed.outputs.workflows }}' != 'true' ]]; then
|
||||
if [[ '${{ needs.changed.outputs.codex }}' != 'true' && '${{ needs.changed.outputs.workflows }}' != 'true' && '${{ github.event_name }}' != 'push' ]]; then
|
||||
echo 'No relevant changes -> CI not required.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ '${{ needs.changed.outputs.argument_comment_lint_package }}' == 'true' ]]; then
|
||||
[[ '${{ needs.argument_comment_lint_package.result }}' == 'success' ]] || { echo 'argument_comment_lint_package failed'; exit 1; }
|
||||
fi
|
||||
# Otherwise require the jobs to have succeeded
|
||||
[[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
|
||||
[[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
|
||||
[[ '${{ needs.lint_build.result }}' == 'success' ]] || { echo 'lint_build failed'; exit 1; }
|
||||
[[ '${{ needs.tests.result }}' == 'success' ]] || { echo 'tests failed'; exit 1; }
|
||||
|
||||
if [[ '${{ needs.changed.outputs.argument_comment_lint }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' ]]; then
|
||||
[[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
|
||||
fi
|
||||
|
||||
if [[ '${{ needs.changed.outputs.codex }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' ]]; then
|
||||
[[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
|
||||
[[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
|
||||
fi
|
||||
- name: sccache summary note
|
||||
if: always()
|
||||
run: |
|
||||
echo "Per-job sccache stats are attached to each matrix job's Step Summary."
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
name: rust-release-argument-comment-lint
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
publish:
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
skip:
|
||||
if: ${{ !inputs.publish }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Skipping argument-comment-lint release assets for prerelease tag"
|
||||
|
||||
build:
|
||||
if: ${{ inputs.publish }}
|
||||
name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
archive_name: argument-comment-lint-aarch64-apple-darwin.tar.gz
|
||||
lib_name: libargument_comment_lint@nightly-2025-09-18-aarch64-apple-darwin.dylib
|
||||
runner_binary: argument-comment-lint
|
||||
cargo_dylint_binary: cargo-dylint
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
archive_name: argument-comment-lint-x86_64-unknown-linux-gnu.tar.gz
|
||||
lib_name: libargument_comment_lint@nightly-2025-09-18-x86_64-unknown-linux-gnu.so
|
||||
runner_binary: argument-comment-lint
|
||||
cargo_dylint_binary: cargo-dylint
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
archive_name: argument-comment-lint-aarch64-unknown-linux-gnu.tar.gz
|
||||
lib_name: libargument_comment_lint@nightly-2025-09-18-aarch64-unknown-linux-gnu.so
|
||||
runner_binary: argument-comment-lint
|
||||
cargo_dylint_binary: cargo-dylint
|
||||
- runner: windows-x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
archive_name: argument-comment-lint-x86_64-pc-windows-msvc.zip
|
||||
lib_name: argument_comment_lint@nightly-2025-09-18-x86_64-pc-windows-msvc.dll
|
||||
runner_binary: argument-comment-lint.exe
|
||||
cargo_dylint_binary: cargo-dylint.exe
|
||||
runs_on:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
targets: ${{ matrix.target }}
|
||||
components: llvm-tools-preview, rustc-dev, rust-src
|
||||
|
||||
- name: Install tooling
|
||||
shell: bash
|
||||
run: |
|
||||
install_root="${RUNNER_TEMP}/argument-comment-lint-tools"
|
||||
cargo install --locked cargo-dylint --root "$install_root"
|
||||
cargo install --locked dylint-link
|
||||
echo "INSTALL_ROOT=$install_root" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cargo build
|
||||
working-directory: tools/argument-comment-lint
|
||||
shell: bash
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Stage artifact
|
||||
shell: bash
|
||||
run: |
|
||||
dest="dist/argument-comment-lint/${{ matrix.target }}"
|
||||
mkdir -p "$dest"
|
||||
package_root="${RUNNER_TEMP}/argument-comment-lint"
|
||||
rm -rf "$package_root"
|
||||
mkdir -p "$package_root/bin" "$package_root/lib"
|
||||
|
||||
cp "tools/argument-comment-lint/target/${{ matrix.target }}/release/${{ matrix.runner_binary }}" \
|
||||
"$package_root/bin/${{ matrix.runner_binary }}"
|
||||
cp "${INSTALL_ROOT}/bin/${{ matrix.cargo_dylint_binary }}" \
|
||||
"$package_root/bin/${{ matrix.cargo_dylint_binary }}"
|
||||
cp "tools/argument-comment-lint/target/${{ matrix.target }}/release/${{ matrix.lib_name }}" \
|
||||
"$package_root/lib/${{ matrix.lib_name }}"
|
||||
|
||||
archive_path="$dest/${{ matrix.archive_name }}"
|
||||
if [[ "${{ runner.os }}" == "Windows" ]]; then
|
||||
(cd "${RUNNER_TEMP}" && 7z a "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint >/dev/null)
|
||||
else
|
||||
(cd "${RUNNER_TEMP}" && tar -czf "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint)
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: argument-comment-lint-${{ matrix.target }}
|
||||
path: dist/argument-comment-lint/${{ matrix.target }}/*
|
||||
4
.github/workflows/rust-release-prepare.yml
vendored
4
.github/workflows/rust-release-prepare.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
if: github.repository == 'openai/codex'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
curl --http1.1 --fail --show-error --location "${headers[@]}" "${url}" | jq '.' > codex-rs/core/models.json
|
||||
|
||||
- name: Open pull request (if changed)
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
commit-message: "Update models.json"
|
||||
title: "Update models.json"
|
||||
|
||||
18
.github/workflows/rust-release-windows.yml
vendored
18
.github/workflows/rust-release-windows.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v6
|
||||
- name: Print runner specs (Windows)
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
Write-Host "Total RAM: $ramGiB GiB"
|
||||
Write-Host "Disk usage:"
|
||||
Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize Name, @{Name='Size(GB)';Expression={[math]::Round(($_.Used + $_.Free) / 1GB, 1)}}, @{Name='Free(GB)';Expression={[math]::Round($_.Free / 1GB, 1)}}
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
cargo build --target ${{ matrix.target }} --release --timings ${{ matrix.build_args }}
|
||||
|
||||
- name: Upload Cargo timings
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: cargo-timings-rust-release-windows-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Windows binaries
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: |
|
||||
@@ -147,16 +147,16 @@ jobs:
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download prebuilt Windows primary binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-primary
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows helper binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-helpers
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe"
|
||||
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
uses: facebook/install-dotslash@v2
|
||||
|
||||
- name: Compress artifacts
|
||||
shell: bash
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
"${GITHUB_WORKSPACE}/.github/workflows/zstd" -T0 -19 "$dest/$base"
|
||||
done
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: |
|
||||
|
||||
95
.github/workflows/rust-release-zsh.yml
vendored
95
.github/workflows/rust-release-zsh.yml
vendored
@@ -1,95 +0,0 @@
|
||||
name: rust-release-zsh
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
ZSH_COMMIT: 77045ef899e53b9598bebc5a41db93a548a40ca6
|
||||
ZSH_PATCH: codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build zsh (Linux) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: ubuntu:24.04
|
||||
archive_name: codex-zsh-x86_64-unknown-linux-musl.tar.gz
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: arm64v8/ubuntu:24.04
|
||||
archive_name: codex-zsh-aarch64-unknown-linux-musl.tar.gz
|
||||
|
||||
steps:
|
||||
- name: Install build prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
autoconf \
|
||||
bison \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
gettext \
|
||||
git \
|
||||
libncursesw5-dev
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Build, smoke-test, and stage zsh artifact
|
||||
shell: bash
|
||||
run: |
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
|
||||
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: codex-zsh-${{ matrix.target }}
|
||||
path: dist/zsh/${{ matrix.target }}/*
|
||||
|
||||
darwin:
|
||||
name: Build zsh (macOS) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
variant: macos-15
|
||||
archive_name: codex-zsh-aarch64-apple-darwin.tar.gz
|
||||
|
||||
steps:
|
||||
- name: Install build prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v autoconf >/dev/null 2>&1; then
|
||||
brew install autoconf
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Build, smoke-test, and stage zsh artifact
|
||||
shell: bash
|
||||
run: |
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
|
||||
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: codex-zsh-${{ matrix.target }}
|
||||
path: dist/zsh/${{ matrix.target }}/*
|
||||
129
.github/workflows/rust-release.yml
vendored
129
.github/workflows/rust-release.yml
vendored
@@ -19,8 +19,8 @@ jobs:
|
||||
tag-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dtolnay/rust-toolchain@c2b55edffaf41a251c410bb32bed22afefa800f1 # 1.92
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@1.92
|
||||
- name: Validate tag matches Cargo.toml version
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -57,9 +57,7 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
# 2026-03-04: temporarily change releases to use thin LTO because
|
||||
# Ubuntu ARM is timing out at 60 minutes.
|
||||
CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }}
|
||||
CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -79,7 +77,7 @@ jobs:
|
||||
target: aarch64-unknown-linux-gnu
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v6
|
||||
- name: Print runner specs (Linux)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -125,7 +123,7 @@ jobs:
|
||||
sudo apt-get update -y
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1
|
||||
fi
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
@@ -142,7 +140,7 @@ jobs:
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Install Zig
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.14.0
|
||||
|
||||
@@ -180,12 +178,6 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Avoid problematic aws-lc jitter entropy code path on musl builders.
|
||||
echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV"
|
||||
target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}"
|
||||
target_no_jitter="${target_no_jitter//-/_}"
|
||||
echo "${target_no_jitter}=1" >> "$GITHUB_ENV"
|
||||
|
||||
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
|
||||
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
@@ -210,32 +202,13 @@ jobs:
|
||||
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
|
||||
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
|
||||
name: Configure musl rusty_v8 artifact overrides
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
|
||||
release_tag="rusty-v8-v${version}"
|
||||
base_url="https://github.com/openai/codex/releases/download/${release_tag}"
|
||||
archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz"
|
||||
binding_dir="${RUNNER_TEMP}/rusty_v8"
|
||||
binding_path="${binding_dir}/src_binding_release_${TARGET}.rs"
|
||||
mkdir -p "${binding_dir}"
|
||||
curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}"
|
||||
echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV"
|
||||
echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cargo build
|
||||
shell: bash
|
||||
run: |
|
||||
echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}"
|
||||
cargo build --target ${{ matrix.target }} --release --timings --bin codex --bin codex-responses-api-proxy
|
||||
|
||||
- name: Upload Cargo timings
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: cargo-timings-rust-release-${{ matrix.target }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -374,7 +347,7 @@ jobs:
|
||||
zstd -T0 -19 --rm "$dest/$base"
|
||||
done
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
# Upload the per-binary .zst files as well as the new .tar.gz
|
||||
@@ -389,24 +362,20 @@ jobs:
|
||||
release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }}
|
||||
secrets: inherit
|
||||
|
||||
argument-comment-lint-release-assets:
|
||||
name: argument-comment-lint release assets
|
||||
shell-tool-mcp:
|
||||
name: shell-tool-mcp
|
||||
needs: tag-check
|
||||
uses: ./.github/workflows/rust-release-argument-comment-lint.yml
|
||||
uses: ./.github/workflows/shell-tool-mcp.yml
|
||||
with:
|
||||
release-tag: ${{ github.ref_name }}
|
||||
publish: true
|
||||
|
||||
zsh-release-assets:
|
||||
name: zsh release assets
|
||||
needs: tag-check
|
||||
uses: ./.github/workflows/rust-release-zsh.yml
|
||||
secrets: inherit
|
||||
|
||||
release:
|
||||
needs:
|
||||
- build
|
||||
- build-windows
|
||||
- argument-comment-lint-release-assets
|
||||
- zsh-release-assets
|
||||
- shell-tool-mcp
|
||||
name: release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -420,7 +389,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Generate release notes from tag commit message
|
||||
id: release_notes
|
||||
@@ -442,15 +411,18 @@ jobs:
|
||||
|
||||
echo "path=${notes_path}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
|
||||
- name: List
|
||||
run: ls -R dist/
|
||||
|
||||
# This is a temporary fix: we should modify shell-tool-mcp.yml so these
|
||||
# files do not end up in dist/ in the first place.
|
||||
- name: Delete entries from dist/ that should not go in the release
|
||||
run: |
|
||||
rm -rf dist/shell-tool-mcp*
|
||||
rm -rf dist/windows-binaries*
|
||||
# cargo-timing.html appears under multiple target-specific directories.
|
||||
# If included in files: dist/**, release upload races on duplicate
|
||||
@@ -492,12 +464,12 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js for npm packaging
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -505,25 +477,19 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# stage_npm_packages.py requires DotSlash when staging releases.
|
||||
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
- uses: facebook/install-dotslash@v2
|
||||
- name: Stage npm packages
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_VERSION: ${{ steps.release_name.outputs.name }}
|
||||
run: |
|
||||
./scripts/stage_npm_packages.py \
|
||||
--release-version "$RELEASE_VERSION" \
|
||||
--release-version "${{ steps.release_name.outputs.name }}" \
|
||||
--package codex \
|
||||
--package codex-responses-api-proxy \
|
||||
--package codex-sdk
|
||||
|
||||
- name: Stage installer scripts
|
||||
run: |
|
||||
cp scripts/install/install.sh dist/install.sh
|
||||
cp scripts/install/install.ps1 dist/install.ps1
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: ${{ steps.release_name.outputs.name }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
@@ -533,27 +499,13 @@ jobs:
|
||||
# (e.g. -alpha, -beta). Otherwise publish a normal release.
|
||||
prerelease: ${{ contains(steps.release_name.outputs.name, '-') }}
|
||||
|
||||
- uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
|
||||
- uses: facebook/dotslash-publish-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
config: .github/dotslash-config.json
|
||||
|
||||
- uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
config: .github/dotslash-zsh-config.json
|
||||
|
||||
- uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
config: .github/dotslash-argument-comment-lint-config.json
|
||||
|
||||
- name: Trigger developers.openai.com deploy
|
||||
# Only trigger the deploy if the release is not a pre-release.
|
||||
# The deploy is used to update the developers.openai.com website with the new config schema json file.
|
||||
@@ -582,7 +534,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
@@ -595,12 +547,10 @@ jobs:
|
||||
- name: Download npm tarballs from release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.release.outputs.tag }}
|
||||
RELEASE_VERSION: ${{ needs.release.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$RELEASE_VERSION"
|
||||
tag="$RELEASE_TAG"
|
||||
version="${{ needs.release.outputs.version }}"
|
||||
tag="${{ needs.release.outputs.tag }}"
|
||||
mkdir -p dist/npm
|
||||
patterns=(
|
||||
"codex-npm-${version}.tgz"
|
||||
@@ -679,29 +629,6 @@ jobs:
|
||||
exit "${publish_status}"
|
||||
done
|
||||
|
||||
winget:
|
||||
name: winget
|
||||
needs: release
|
||||
# Only publish stable/mainline releases to WinGet; pre-releases include a
|
||||
# '-' in the semver string (e.g., 1.2.3-alpha.1).
|
||||
if: ${{ !contains(needs.release.outputs.version, '-') }}
|
||||
# This job only invokes a GitHub Action to open/update the winget-pkgs PR;
|
||||
# it does not execute Windows-only tooling, so Linux is sufficient.
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Publish to WinGet
|
||||
uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5
|
||||
with:
|
||||
identifier: OpenAI.Codex
|
||||
version: ${{ needs.release.outputs.version }}
|
||||
release-tag: ${{ needs.release.outputs.tag }}
|
||||
fork-user: openai-oss-forks
|
||||
installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$'
|
||||
token: ${{ secrets.WINGET_PUBLISH_PAT }}
|
||||
|
||||
update-branch:
|
||||
name: Update latest-alpha-cli branch
|
||||
permissions:
|
||||
|
||||
188
.github/workflows/rusty-v8-release.yml
vendored
188
.github/workflows/rusty-v8-release.yml
vendored
@@ -1,188 +0,0 @@
|
||||
name: rusty-v8-release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: Optional release tag. Defaults to rusty-v8-v<resolved_v8_version>.
|
||||
required: false
|
||||
type: string
|
||||
publish:
|
||||
description: Publish the staged musl artifacts to a GitHub release.
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}::${{ inputs.release_tag || github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_tag: ${{ steps.release_tag.outputs.release_tag }}
|
||||
v8_version: ${{ steps.v8_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Resolve exact v8 crate version
|
||||
id: v8_version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)"
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Resolve release tag
|
||||
id: release_tag
|
||||
env:
|
||||
RELEASE_TAG_INPUT: ${{ inputs.release_tag }}
|
||||
V8_VERSION: ${{ steps.v8_version.outputs.version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
release_tag="${RELEASE_TAG_INPUT}"
|
||||
if [[ -z "${release_tag}" ]]; then
|
||||
release_tag="rusty-v8-v${V8_VERSION}"
|
||||
fi
|
||||
|
||||
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux_amd64_musl
|
||||
target: x86_64-unknown-linux-musl
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux_arm64_musl
|
||||
target: aarch64-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: bazelbuild/setup-bazelisk@6ecf4fd8b7d1f9721785f1dd656a689acf9add47 # v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Build Bazel V8 release pair
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
target_suffix="${TARGET//-/_}"
|
||||
pair_target="//third_party/v8:rusty_v8_release_pair_${target_suffix}"
|
||||
extra_targets=()
|
||||
if [[ "${TARGET}" == *-unknown-linux-musl ]]; then
|
||||
extra_targets=(
|
||||
"@llvm//runtimes/libcxx:libcxx.static"
|
||||
"@llvm//runtimes/libcxx:libcxxabi.static"
|
||||
)
|
||||
fi
|
||||
|
||||
bazel_args=(
|
||||
build
|
||||
-c
|
||||
opt
|
||||
"--platforms=@llvm//platforms:${PLATFORM}"
|
||||
"${pair_target}"
|
||||
"${extra_targets[@]}"
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
)
|
||||
|
||||
bazel \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
"${bazel_args[@]}" \
|
||||
--config=ci-v8 \
|
||||
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
|
||||
|
||||
- name: Stage release pair
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
|
||||
--platform "${PLATFORM}" \
|
||||
--target "${TARGET}" \
|
||||
--compilation-mode opt \
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
|
||||
publish-release:
|
||||
if: ${{ inputs.publish }}
|
||||
needs:
|
||||
- metadata
|
||||
- build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: Ensure publishing from default branch
|
||||
if: ${{ github.ref_name != github.event.repository.default_branch }}
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Publishing is only allowed from ${DEFAULT_BRANCH}; current ref is ${GITHUB_REF_NAME}." >&2
|
||||
exit 1
|
||||
|
||||
- name: Ensure release tag is new
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ needs.metadata.outputs.release_tag }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then
|
||||
echo "Release tag ${RELEASE_TAG} already exists; musl artifact tags are immutable." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
path: dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
with:
|
||||
tag_name: ${{ needs.metadata.outputs.release_tag }}
|
||||
name: ${{ needs.metadata.outputs.release_tag }}
|
||||
files: dist/**
|
||||
# Keep V8 artifact releases out of Codex's normal "latest release" channel.
|
||||
prerelease: true
|
||||
88
.github/workflows/sdk.yml
vendored
88
.github/workflows/sdk.yml
vendored
@@ -7,13 +7,11 @@ on:
|
||||
|
||||
jobs:
|
||||
sdks:
|
||||
runs-on:
|
||||
group: codex-runners
|
||||
labels: codex-linux-x64
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Linux bwrap build dependencies
|
||||
shell: bash
|
||||
@@ -23,82 +21,21 @@ jobs:
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Set up Bazel CI
|
||||
id: setup_bazel
|
||||
uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
|
||||
- name: Build codex with Bazel
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Use the shared CI wrapper so fork PRs fall back cleanly when
|
||||
# BuildBuddy credentials are unavailable. This workflow needs the
|
||||
# built `codex` binary on disk afterwards, so ask the wrapper to
|
||||
# override CI's default remote_download_minimal behavior.
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
--remote-download-toplevel \
|
||||
-- \
|
||||
build \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
--build_metadata=TAG_job=sdk \
|
||||
-- \
|
||||
//codex-rs/cli:codex
|
||||
|
||||
# Resolve the exact output file using the same wrapper/config path as
|
||||
# the build instead of guessing which Bazel convenience symlink is
|
||||
# available on the runner.
|
||||
cquery_output="$(
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
cquery \
|
||||
--output=files \
|
||||
-- \
|
||||
//codex-rs/cli:codex \
|
||||
| grep -E '^(/|bazel-out/)' \
|
||||
| tail -n 1
|
||||
)"
|
||||
if [[ "${cquery_output}" = /* ]]; then
|
||||
codex_bazel_output_path="${cquery_output}"
|
||||
else
|
||||
codex_bazel_output_path="${GITHUB_WORKSPACE}/${cquery_output}"
|
||||
fi
|
||||
if [[ -z "${codex_bazel_output_path}" ]]; then
|
||||
echo "Bazel did not report an output path for //codex-rs/cli:codex." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -e "${codex_bazel_output_path}" ]]; then
|
||||
echo "Unable to locate the Bazel-built codex binary at ${codex_bazel_output_path}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stage the binary into the workspace and point the SDK tests at that
|
||||
# stable path. The tests spawn `codex` directly many times, so using a
|
||||
# normal executable path is more reliable than invoking Bazel for each
|
||||
# test process.
|
||||
install_dir="${GITHUB_WORKSPACE}/.tmp/sdk-ci"
|
||||
mkdir -p "${install_dir}"
|
||||
install -m 755 "${codex_bazel_output_path}" "${install_dir}/codex"
|
||||
echo "CODEX_EXEC_PATH=${install_dir}/codex" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Warm up Bazel-built codex
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
"${CODEX_EXEC_PATH}" --version
|
||||
- name: build codex
|
||||
run: cargo build --bin codex
|
||||
working-directory: codex-rs
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -111,12 +48,3 @@ jobs:
|
||||
|
||||
- name: Test SDK packages
|
||||
run: pnpm -r --filter ./sdk/typescript run test
|
||||
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
key: bazel-cache-x86_64-unknown-linux-gnu-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
|
||||
|
||||
48
.github/workflows/shell-tool-mcp-ci.yml
vendored
Normal file
48
.github/workflows/shell-tool-mcp-ci.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: shell-tool-mcp CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "shell-tool-mcp/**"
|
||||
- ".github/workflows/shell-tool-mcp-ci.yml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "shell-tool-mcp/**"
|
||||
- ".github/workflows/shell-tool-mcp-ci.yml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Format check
|
||||
run: pnpm --filter @openai/codex-shell-tool-mcp run format
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm --filter @openai/codex-shell-tool-mcp test
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter @openai/codex-shell-tool-mcp run build
|
||||
676
.github/workflows/shell-tool-mcp.yml
vendored
Normal file
676
.github/workflows/shell-tool-mcp.yml
vendored
Normal file
@@ -0,0 +1,676 @@
|
||||
name: shell-tool-mcp
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release-version:
|
||||
description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v.
|
||||
required: false
|
||||
type: string
|
||||
release-tag:
|
||||
description: Tag name to use when downloading release artifacts (defaults to rust-v<version>).
|
||||
required: false
|
||||
type: string
|
||||
publish:
|
||||
description: Whether to publish to npm when the version is releasable.
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.compute.outputs.version }}
|
||||
release_tag: ${{ steps.compute.outputs.release_tag }}
|
||||
should_publish: ${{ steps.compute.outputs.should_publish }}
|
||||
npm_tag: ${{ steps.compute.outputs.npm_tag }}
|
||||
steps:
|
||||
- name: Compute version and tags
|
||||
id: compute
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
version="${{ inputs.release-version }}"
|
||||
release_tag="${{ inputs.release-tag }}"
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then
|
||||
version="${release_tag#rust-v}"
|
||||
elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then
|
||||
version="${GITHUB_REF_NAME#rust-v}"
|
||||
release_tag="${GITHUB_REF_NAME}"
|
||||
else
|
||||
echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$release_tag" ]]; then
|
||||
release_tag="rust-v${version}"
|
||||
fi
|
||||
|
||||
npm_tag=""
|
||||
should_publish="false"
|
||||
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
should_publish="true"
|
||||
elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then
|
||||
should_publish="true"
|
||||
npm_tag="alpha"
|
||||
fi
|
||||
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
rust-binaries:
|
||||
name: Build Rust - ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_LTO: ${{ contains(needs.metadata.outputs.version, '-alpha') && 'thin' || 'fat' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
- runner: macos-15-xlarge
|
||||
target: x86_64-apple-darwin
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
install_musl: true
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
install_musl: true
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install UBSan runtime (musl)
|
||||
if: ${{ matrix.install_musl }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1
|
||||
fi
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.93.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- if: ${{ matrix.install_musl }}
|
||||
name: Install Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.14.0
|
||||
|
||||
- if: ${{ matrix.install_musl }}
|
||||
name: Install musl build dependencies
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
|
||||
|
||||
- if: ${{ matrix.install_musl }}
|
||||
name: Configure rustc UBSan wrapper (musl host)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ubsan=""
|
||||
if command -v ldconfig >/dev/null 2>&1; then
|
||||
ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
|
||||
fi
|
||||
wrapper_root="${RUNNER_TEMP:-/tmp}"
|
||||
wrapper="${wrapper_root}/rustc-ubsan-wrapper"
|
||||
cat > "${wrapper}" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ -n "${ubsan}" ]]; then
|
||||
export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
|
||||
fi
|
||||
exec "\$1" "\${@:2}"
|
||||
EOF
|
||||
chmod +x "${wrapper}"
|
||||
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
|
||||
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
|
||||
|
||||
- if: ${{ matrix.install_musl }}
|
||||
name: Clear sanitizer flags (musl)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
|
||||
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
|
||||
# Override any runner-level Cargo config rustflags as well.
|
||||
echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
|
||||
|
||||
sanitize_flags() {
|
||||
local input="$1"
|
||||
input="${input//-fsanitize=undefined/}"
|
||||
input="${input//-fno-sanitize-recover=undefined/}"
|
||||
input="${input//-fno-sanitize-trap=undefined/}"
|
||||
echo "$input"
|
||||
}
|
||||
|
||||
cflags="$(sanitize_flags "${CFLAGS-}")"
|
||||
cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
|
||||
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
|
||||
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build exec server binaries
|
||||
run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper
|
||||
|
||||
- name: Stage exec server binaries
|
||||
run: |
|
||||
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}"
|
||||
mkdir -p "$dest"
|
||||
cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/"
|
||||
cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/"
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: shell-tool-mcp-rust-${{ matrix.target }}
|
||||
path: artifacts/**
|
||||
if-no-files-found: error
|
||||
|
||||
bash-linux:
|
||||
name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: ubuntu:24.04
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: ubuntu-22.04
|
||||
image: ubuntu:22.04
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: debian-12
|
||||
image: debian:12
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: debian-11
|
||||
image: debian:11
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: centos-9
|
||||
image: quay.io/centos/centos:stream9
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: arm64v8/ubuntu:24.04
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-22.04
|
||||
image: arm64v8/ubuntu:22.04
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-20.04
|
||||
image: arm64v8/ubuntu:20.04
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: debian-12
|
||||
image: arm64v8/debian:12
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: debian-11
|
||||
image: arm64v8/debian:11
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: centos-9
|
||||
image: quay.io/centos/centos:stream9
|
||||
steps:
|
||||
- name: Install build prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext libncursesw5-dev
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel
|
||||
else
|
||||
echo "Unsupported package manager in container"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build patched Bash
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash
|
||||
cd /tmp/bash
|
||||
git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
|
||||
./configure --without-bash-malloc
|
||||
cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)"
|
||||
make -j"${cores}"
|
||||
|
||||
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}"
|
||||
mkdir -p "$dest"
|
||||
cp bash "$dest/bash"
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }}
|
||||
path: artifacts/**
|
||||
if-no-files-found: error
|
||||
|
||||
bash-darwin:
|
||||
name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
variant: macos-15
|
||||
- runner: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
variant: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build patched Bash
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash
|
||||
cd /tmp/bash
|
||||
git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
|
||||
./configure --without-bash-malloc
|
||||
cores="$(getconf _NPROCESSORS_ONLN)"
|
||||
make -j"${cores}"
|
||||
|
||||
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}"
|
||||
mkdir -p "$dest"
|
||||
cp bash "$dest/bash"
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }}
|
||||
path: artifacts/**
|
||||
if-no-files-found: error
|
||||
|
||||
zsh-linux:
|
||||
name: Build zsh (Linux) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: ubuntu:24.04
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: ubuntu-22.04
|
||||
image: ubuntu:22.04
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: debian-12
|
||||
image: debian:12
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: debian-11
|
||||
image: debian:11
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: centos-9
|
||||
image: quay.io/centos/centos:stream9
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: arm64v8/ubuntu:24.04
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-22.04
|
||||
image: arm64v8/ubuntu:22.04
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-20.04
|
||||
image: arm64v8/ubuntu:20.04
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: debian-12
|
||||
image: arm64v8/debian:12
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: debian-11
|
||||
image: arm64v8/debian:11
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: centos-9
|
||||
image: quay.io/centos/centos:stream9
|
||||
steps:
|
||||
- name: Install build prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext libncursesw5-dev
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel
|
||||
else
|
||||
echo "Unsupported package manager in container"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build patched zsh
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone https://git.code.sf.net/p/zsh/code /tmp/zsh
|
||||
cd /tmp/zsh
|
||||
git checkout 77045ef899e53b9598bebc5a41db93a548a40ca6
|
||||
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/zsh-exec-wrapper.patch"
|
||||
./Util/preconfig
|
||||
./configure
|
||||
cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)"
|
||||
make -j"${cores}"
|
||||
|
||||
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/zsh/${{ matrix.variant }}"
|
||||
mkdir -p "$dest"
|
||||
cp Src/zsh "$dest/zsh"
|
||||
|
||||
- name: Smoke test zsh exec wrapper
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tmpdir="$(mktemp -d)"
|
||||
cat > "$tmpdir/exec-wrapper" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${CODEX_WRAPPER_LOG:?missing CODEX_WRAPPER_LOG}"
|
||||
printf '%s\n' "$@" > "$CODEX_WRAPPER_LOG"
|
||||
file="$1"
|
||||
shift
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
exec "$file"
|
||||
fi
|
||||
arg0="$1"
|
||||
shift
|
||||
exec -a "$arg0" "$file" "$@"
|
||||
EOF
|
||||
chmod +x "$tmpdir/exec-wrapper"
|
||||
|
||||
CODEX_WRAPPER_LOG="$tmpdir/wrapper.log" \
|
||||
EXEC_WRAPPER="$tmpdir/exec-wrapper" \
|
||||
/tmp/zsh/Src/zsh -fc '/bin/echo smoke-zsh' > "$tmpdir/stdout.txt"
|
||||
|
||||
grep -Fx "smoke-zsh" "$tmpdir/stdout.txt"
|
||||
grep -Fx "/bin/echo" "$tmpdir/wrapper.log"
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: shell-tool-mcp-zsh-${{ matrix.target }}-${{ matrix.variant }}
|
||||
path: artifacts/**
|
||||
if-no-files-found: error
|
||||
|
||||
zsh-darwin:
|
||||
name: Build zsh (macOS) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
variant: macos-15
|
||||
- runner: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
variant: macos-14
|
||||
steps:
|
||||
- name: Install build prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v autoconf >/dev/null 2>&1; then
|
||||
brew install autoconf
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build patched zsh
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone https://git.code.sf.net/p/zsh/code /tmp/zsh
|
||||
cd /tmp/zsh
|
||||
git checkout 77045ef899e53b9598bebc5a41db93a548a40ca6
|
||||
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/zsh-exec-wrapper.patch"
|
||||
./Util/preconfig
|
||||
./configure
|
||||
cores="$(getconf _NPROCESSORS_ONLN)"
|
||||
make -j"${cores}"
|
||||
|
||||
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/zsh/${{ matrix.variant }}"
|
||||
mkdir -p "$dest"
|
||||
cp Src/zsh "$dest/zsh"
|
||||
|
||||
- name: Smoke test zsh exec wrapper
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tmpdir="$(mktemp -d)"
|
||||
cat > "$tmpdir/exec-wrapper" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${CODEX_WRAPPER_LOG:?missing CODEX_WRAPPER_LOG}"
|
||||
printf '%s\n' "$@" > "$CODEX_WRAPPER_LOG"
|
||||
file="$1"
|
||||
shift
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
exec "$file"
|
||||
fi
|
||||
arg0="$1"
|
||||
shift
|
||||
exec -a "$arg0" "$file" "$@"
|
||||
EOF
|
||||
chmod +x "$tmpdir/exec-wrapper"
|
||||
|
||||
CODEX_WRAPPER_LOG="$tmpdir/wrapper.log" \
|
||||
EXEC_WRAPPER="$tmpdir/exec-wrapper" \
|
||||
/tmp/zsh/Src/zsh -fc '/bin/echo smoke-zsh' > "$tmpdir/stdout.txt"
|
||||
|
||||
grep -Fx "smoke-zsh" "$tmpdir/stdout.txt"
|
||||
grep -Fx "/bin/echo" "$tmpdir/wrapper.log"
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: shell-tool-mcp-zsh-${{ matrix.target }}-${{ matrix.variant }}
|
||||
path: artifacts/**
|
||||
if-no-files-found: error
|
||||
|
||||
package:
|
||||
name: Package npm module
|
||||
needs:
|
||||
- metadata
|
||||
- rust-binaries
|
||||
- bash-linux
|
||||
- bash-darwin
|
||||
- zsh-linux
|
||||
- zsh-darwin
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ needs.metadata.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install JavaScript dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build (shell-tool-mcp)
|
||||
run: pnpm --filter @openai/codex-shell-tool-mcp run build
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Assemble staging directory
|
||||
id: staging
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
staging="${STAGING_DIR}"
|
||||
mkdir -p "$staging" "$staging/vendor"
|
||||
cp shell-tool-mcp/README.md "$staging/"
|
||||
cp shell-tool-mcp/package.json "$staging/"
|
||||
cp -R shell-tool-mcp/bin "$staging/"
|
||||
|
||||
found_vendor="false"
|
||||
shopt -s nullglob
|
||||
for vendor_dir in artifacts/*/vendor; do
|
||||
rsync -av "$vendor_dir/" "$staging/vendor/"
|
||||
found_vendor="true"
|
||||
done
|
||||
if [[ "$found_vendor" == "false" ]]; then
|
||||
echo "No vendor payloads were downloaded."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node - <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const stagingDir = process.env.STAGING_DIR;
|
||||
const version = process.env.PACKAGE_VERSION;
|
||||
const pkgPath = path.join(stagingDir, "package.json");
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||
pkg.version = version;
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
||||
NODE
|
||||
|
||||
echo "dir=$staging" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp
|
||||
|
||||
- name: Ensure binaries are executable
|
||||
run: |
|
||||
set -euo pipefail
|
||||
staging="${{ steps.staging.outputs.dir }}"
|
||||
chmod +x \
|
||||
"$staging"/vendor/*/codex-exec-mcp-server \
|
||||
"$staging"/vendor/*/codex-execve-wrapper \
|
||||
"$staging"/vendor/*/bash/*/bash \
|
||||
"$staging"/vendor/*/zsh/*/zsh
|
||||
|
||||
- name: Create npm tarball
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist/npm
|
||||
staging="${{ steps.staging.outputs.dir }}"
|
||||
pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm")
|
||||
filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);')
|
||||
mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz"
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: codex-shell-tool-mcp-npm
|
||||
path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Publish npm package
|
||||
needs:
|
||||
- metadata
|
||||
- package
|
||||
if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: https://registry.npmjs.org
|
||||
scope: "@openai"
|
||||
|
||||
# Trusted publishing requires npm CLI version 11.5.1 or later.
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Download npm tarball
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: codex-shell-tool-mcp-npm
|
||||
path: dist/npm
|
||||
|
||||
- name: Publish to npm
|
||||
env:
|
||||
NPM_TAG: ${{ needs.metadata.outputs.npm_tag }}
|
||||
VERSION: ${{ needs.metadata.outputs.version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag_args=()
|
||||
if [[ -n "${NPM_TAG}" ]]; then
|
||||
tag_args+=(--tag "${NPM_TAG}")
|
||||
fi
|
||||
npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}"
|
||||
132
.github/workflows/v8-canary.yml
vendored
132
.github/workflows/v8-canary.yml
vendored
@@ -1,132 +0,0 @@
|
||||
name: v8-canary
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/scripts/rusty_v8_bazel.py"
|
||||
- ".github/workflows/rusty-v8-release.yml"
|
||||
- ".github/workflows/v8-canary.yml"
|
||||
- "MODULE.bazel"
|
||||
- "MODULE.bazel.lock"
|
||||
- "codex-rs/Cargo.toml"
|
||||
- "patches/BUILD.bazel"
|
||||
- "patches/v8_*.patch"
|
||||
- "third_party/v8/**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/scripts/rusty_v8_bazel.py"
|
||||
- ".github/workflows/rusty-v8-release.yml"
|
||||
- ".github/workflows/v8-canary.yml"
|
||||
- "MODULE.bazel"
|
||||
- "MODULE.bazel.lock"
|
||||
- "codex-rs/Cargo.toml"
|
||||
- "patches/BUILD.bazel"
|
||||
- "patches/v8_*.patch"
|
||||
- "third_party/v8/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
|
||||
cancel-in-progress: ${{ github.ref_name != 'main' }}
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
v8_version: ${{ steps.v8_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Resolve exact v8 crate version
|
||||
id: v8_version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)"
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux_amd64_musl
|
||||
target: x86_64-unknown-linux-musl
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux_arm64_musl
|
||||
target: aarch64-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: bazelbuild/setup-bazelisk@6ecf4fd8b7d1f9721785f1dd656a689acf9add47 # v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Build Bazel V8 release pair
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
target_suffix="${TARGET//-/_}"
|
||||
pair_target="//third_party/v8:rusty_v8_release_pair_${target_suffix}"
|
||||
extra_targets=(
|
||||
"@llvm//runtimes/libcxx:libcxx.static"
|
||||
"@llvm//runtimes/libcxx:libcxxabi.static"
|
||||
)
|
||||
|
||||
bazel_args=(
|
||||
build
|
||||
"--platforms=@llvm//platforms:${PLATFORM}"
|
||||
"${pair_target}"
|
||||
"${extra_targets[@]}"
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
)
|
||||
|
||||
bazel \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
"${bazel_args[@]}" \
|
||||
--config=ci-v8 \
|
||||
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
|
||||
|
||||
- name: Stage release pair
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
|
||||
--platform "${PLATFORM}" \
|
||||
--target "${TARGET}" \
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,7 +10,6 @@ node_modules
|
||||
# build
|
||||
dist/
|
||||
bazel-*
|
||||
user.bazelrc
|
||||
build/
|
||||
out/
|
||||
storybook-static/
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"rust-analyzer.checkOnSave": true,
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"rust-analyzer.check.extraArgs": ["--tests"],
|
||||
"rust-analyzer.check.extraArgs": ["--all-features", "--tests"],
|
||||
"rust-analyzer.rustfmt.extraArgs": ["--config", "imports_granularity=Item"],
|
||||
"rust-analyzer.cargo.targetDir": "${workspaceFolder}/codex-rs/target/rust-analyzer",
|
||||
"[rust]": {
|
||||
|
||||
38
AGENTS.md
38
AGENTS.md
@@ -11,11 +11,6 @@ In the codex-rs folder where the rust code lives:
|
||||
- Always collapse if statements per https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
|
||||
- Always inline format! args when possible per https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
|
||||
- Use method references over closures when possible per https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure_for_method_calls
|
||||
- Avoid bool or ambiguous `Option` parameters that force callers to write hard-to-read code such as `foo(false)` or `bar(None)`. Prefer enums, named methods, newtypes, or other idiomatic Rust API shapes when they keep the callsite self-documenting.
|
||||
- When you cannot make that API change and still need a small positional-literal callsite in Rust, follow the `argument_comment_lint` convention:
|
||||
- Use an exact `/*param_name*/` comment before opaque literal arguments such as `None`, booleans, and numeric literals when passing them by position.
|
||||
- Do not add these comments for string or char literals unless the comment adds real clarity; those literals are intentionally exempt from the lint.
|
||||
- If you add one of these comments, the parameter name must exactly match the callee signature.
|
||||
- When possible, make `match` statements exhaustive and avoid wildcard arms.
|
||||
- When writing tests, prefer comparing the equality of entire objects over fields one by one.
|
||||
- When making a change that adds or changes an API, ensure that the documentation in the `docs/` folder is up to date if applicable.
|
||||
@@ -24,46 +19,15 @@ In the codex-rs folder where the rust code lives:
|
||||
repo root to refresh `MODULE.bazel.lock`, and include that lockfile update in the same change.
|
||||
- After dependency changes, run `just bazel-lock-check` from the repo root so lockfile drift is caught
|
||||
locally before CI.
|
||||
- Bazel does not automatically make source-tree files available to compile-time Rust file access. If
|
||||
you add `include_str!`, `include_bytes!`, `sqlx::migrate!`, or similar build-time file or
|
||||
directory reads, update the crate's `BUILD.bazel` (`compile_data`, `build_script_data`, or test
|
||||
data) or Bazel may fail even when Cargo passes.
|
||||
- Do not create small helper methods that are referenced only once.
|
||||
- Avoid large modules:
|
||||
- Prefer adding new modules instead of growing existing ones.
|
||||
- Target Rust modules under 500 LoC, excluding tests.
|
||||
- If a file exceeds roughly 800 LoC, add new functionality in a new module instead of extending
|
||||
the existing file unless there is a strong documented reason not to.
|
||||
- This rule applies especially to high-touch files that already attract unrelated changes, such
|
||||
as `codex-rs/tui/src/app.rs`, `codex-rs/tui/src/bottom_pane/chat_composer.rs`,
|
||||
`codex-rs/tui/src/bottom_pane/footer.rs`, `codex-rs/tui/src/chatwidget.rs`,
|
||||
`codex-rs/tui/src/bottom_pane/mod.rs`, and similarly central orchestration modules.
|
||||
- When extracting code from a large module, move the related tests and module/type docs toward
|
||||
the new implementation so the invariants stay close to the code that owns them.
|
||||
- When running Rust commands (e.g. `just fix` or `cargo test`) be patient with the command and never try to kill them using the PID. Rust lock can make the execution slow, this is expected.
|
||||
|
||||
Run `just fmt` (in `codex-rs` directory) automatically after you have finished making Rust code changes; do not ask for approval to run it. Additionally, run the tests:
|
||||
|
||||
1. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `cargo test -p codex-tui`.
|
||||
2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test` (or `just test` if `cargo-nextest` is installed). Avoid `--all-features` for routine local runs because it expands the build matrix and can significantly increase `target/` disk usage; use it only when you specifically need full feature coverage. project-specific or individual tests can be run without asking the user, but do ask the user before running the complete test suite.
|
||||
2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test --all-features`. project-specific or individual tests can be run without asking the user, but do ask the user before running the complete test suite.
|
||||
|
||||
Before finalizing a large change to `codex-rs`, run `just fix -p <project>` (in `codex-rs` directory) to fix any linter issues in the code. Prefer scoping with `-p` to avoid slow workspace‑wide Clippy builds; only run `just fix` without `-p` if you changed shared crates. Do not re-run tests after running `fix` or `fmt`.
|
||||
|
||||
Also run `just argument-comment-lint` to ensure the codebase is clean of comment lint errors.
|
||||
|
||||
## The `codex-core` crate
|
||||
|
||||
Over time, the `codex-core` crate (defined in `codex-rs/core/`) has become bloated because it is the largest crate, so it is often easier to add something new to `codex-core` rather than refactor out the library code you need so your new code neither takes a dependency on, nor contributes to the size of, `codex-core`.
|
||||
|
||||
To that end: **resist adding code to codex-core**!
|
||||
|
||||
Particularly when introducing a new concept/feature/API, before adding to `codex-core`, consider whether:
|
||||
|
||||
- There is an existing crate other than `codex-core` that is an appropriate place for your new code to live.
|
||||
- It is time to introduce a new crate to the Cargo workspace for your new functionality. Refactor existing code as necessary to make this happen.
|
||||
|
||||
Likewise, when reviewing code, do not hesitate to push back on PRs that would unnecessarily add code to `codex-core`.
|
||||
|
||||
## TUI style conventions
|
||||
|
||||
See `codex-rs/tui/styles.md`.
|
||||
|
||||
@@ -9,7 +9,7 @@ platform(
|
||||
name = "local_linux",
|
||||
constraint_values = [
|
||||
# We mark the local platform as glibc-compatible because musl-built rust cannot dlopen proc macros.
|
||||
"@llvm//constraints/libc:gnu.2.28",
|
||||
"@toolchains_llvm_bootstrapped//constraints/libc:gnu.2.28",
|
||||
],
|
||||
parents = ["@platforms//host"],
|
||||
)
|
||||
@@ -28,8 +28,4 @@ alias(
|
||||
actual = "@rbe_platform",
|
||||
)
|
||||
|
||||
exports_files([
|
||||
"AGENTS.md",
|
||||
"workspace_root_test_launcher.bat.tpl",
|
||||
"workspace_root_test_launcher.sh.tpl",
|
||||
])
|
||||
exports_files(["AGENTS.md"])
|
||||
|
||||
329
MODULE.bazel
329
MODULE.bazel
@@ -1,112 +1,64 @@
|
||||
module(name = "codex")
|
||||
|
||||
bazel_dep(name = "bazel_skylib", version = "1.8.2")
|
||||
bazel_dep(name = "platforms", version = "1.0.0")
|
||||
bazel_dep(name = "llvm", version = "0.6.8")
|
||||
# The upstream LLVM archive contains a few unix-only symlink entries and is
|
||||
# missing a couple of MinGW compatibility archives that windows-gnullvm needs
|
||||
# during extraction and linking, so patch it until upstream grows native support.
|
||||
bazel_dep(name = "toolchains_llvm_bootstrapped", version = "0.5.3")
|
||||
single_version_override(
|
||||
module_name = "llvm",
|
||||
module_name = "toolchains_llvm_bootstrapped",
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:llvm_windows_symlink_extract.patch",
|
||||
],
|
||||
)
|
||||
# Abseil picks a MinGW pthread TLS path that does not match our hermetic
|
||||
# windows-gnullvm toolchain; force it onto the portable C++11 thread-local path.
|
||||
single_version_override(
|
||||
module_name = "abseil-cpp",
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:abseil_windows_gnullvm_thread_identity.patch",
|
||||
"//patches:toolchains_llvm_bootstrapped_resource_dir.patch",
|
||||
],
|
||||
)
|
||||
|
||||
register_toolchains("@llvm//toolchain:all")
|
||||
osx = use_extension("@toolchains_llvm_bootstrapped//extensions:osx.bzl", "osx")
|
||||
osx.framework(name = "ApplicationServices")
|
||||
osx.framework(name = "AppKit")
|
||||
osx.framework(name = "ColorSync")
|
||||
osx.framework(name = "CoreFoundation")
|
||||
osx.framework(name = "CoreGraphics")
|
||||
osx.framework(name = "CoreServices")
|
||||
osx.framework(name = "CoreText")
|
||||
osx.framework(name = "CFNetwork")
|
||||
osx.framework(name = "FontServices")
|
||||
osx.framework(name = "Foundation")
|
||||
osx.framework(name = "ImageIO")
|
||||
osx.framework(name = "IOKit")
|
||||
osx.framework(name = "Kernel")
|
||||
osx.framework(name = "OSLog")
|
||||
osx.framework(name = "Security")
|
||||
osx.framework(name = "SystemConfiguration")
|
||||
|
||||
osx = use_extension("@llvm//extensions:osx.bzl", "osx")
|
||||
osx.from_archive(
|
||||
sha256 = "6a4922f89487a96d7054ec6ca5065bfddd9f1d017c74d82f1d79cecf7feb8228",
|
||||
strip_prefix = "Payload/Library/Developer/CommandLineTools/SDKs/MacOSX26.2.sdk",
|
||||
type = "pkg",
|
||||
urls = [
|
||||
"https://swcdn.apple.com/content/downloads/26/44/047-81934-A_28TPKM5SD1/ps6pk6dk4x02vgfa5qsctq6tgf23t5f0w2/CLTools_macOSNMOS_SDK.pkg",
|
||||
],
|
||||
register_toolchains(
|
||||
"@toolchains_llvm_bootstrapped//toolchain:all",
|
||||
)
|
||||
osx.frameworks(names = [
|
||||
"ApplicationServices",
|
||||
"AppKit",
|
||||
"ColorSync",
|
||||
"CoreFoundation",
|
||||
"CoreGraphics",
|
||||
"CoreServices",
|
||||
"CoreText",
|
||||
"AudioToolbox",
|
||||
"CFNetwork",
|
||||
"FontServices",
|
||||
"AudioUnit",
|
||||
"CoreAudio",
|
||||
"CoreAudioTypes",
|
||||
"Foundation",
|
||||
"ImageIO",
|
||||
"IOKit",
|
||||
"Kernel",
|
||||
"OSLog",
|
||||
"Security",
|
||||
"SystemConfiguration",
|
||||
])
|
||||
use_repo(osx, "macos_sdk")
|
||||
|
||||
# Needed to disable xcode...
|
||||
bazel_dep(name = "apple_support", version = "2.1.0")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.16")
|
||||
bazel_dep(name = "rules_platform", version = "0.1.0")
|
||||
bazel_dep(name = "rules_rs", version = "0.0.43")
|
||||
# `rules_rs` 0.0.43 does not model `windows-gnullvm` as a distinct Windows exec
|
||||
# platform, so patch it until upstream grows that support for both x86_64 and
|
||||
# aarch64.
|
||||
single_version_override(
|
||||
bazel_dep(name = "rules_rs", version = "0.0.23")
|
||||
|
||||
# Special toolchains branch
|
||||
archive_override(
|
||||
module_name = "rules_rs",
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:rules_rs_windows_gnullvm_exec.patch",
|
||||
],
|
||||
version = "0.0.43",
|
||||
integrity = "sha256-YbDRjZos4UmfIPY98znK1BgBWRQ1/ui3CtL6RqxE30I=",
|
||||
strip_prefix = "rules_rs-6cf3d940fdc48baf3ebd6c37daf8e0be8fc73ecb",
|
||||
url = "https://github.com/dzbarsky/rules_rs/archive/6cf3d940fdc48baf3ebd6c37daf8e0be8fc73ecb.tar.gz",
|
||||
)
|
||||
|
||||
rules_rust = use_extension("@rules_rs//rs/experimental:rules_rust.bzl", "rules_rust")
|
||||
# Build-script probe binaries inherit CFLAGS/CXXFLAGS from Bazel's C++
|
||||
# toolchain. On `windows-gnullvm`, llvm-mingw does not ship
|
||||
# `libssp_nonshared`, so strip the forwarded stack-protector flags there.
|
||||
rules_rust.patch(
|
||||
patches = [
|
||||
"//patches:rules_rust_windows_gnullvm_build_script.patch",
|
||||
],
|
||||
strip = 1,
|
||||
)
|
||||
use_repo(rules_rust, "rules_rust")
|
||||
|
||||
nightly_rust = use_extension(
|
||||
"@rules_rs//rs/experimental:rules_rust_reexported_extensions.bzl",
|
||||
"rust",
|
||||
)
|
||||
nightly_rust.toolchain(
|
||||
versions = ["nightly/2025-09-18"],
|
||||
dev_components = True,
|
||||
edition = "2024",
|
||||
)
|
||||
use_repo(nightly_rust, "rust_toolchains")
|
||||
|
||||
toolchains = use_extension("@rules_rs//rs/experimental/toolchains:module_extension.bzl", "toolchains")
|
||||
toolchains.toolchain(
|
||||
edition = "2024",
|
||||
version = "1.93.0",
|
||||
)
|
||||
use_repo(toolchains, "default_rust_toolchains")
|
||||
use_repo(
|
||||
toolchains,
|
||||
"experimental_rust_toolchains_1_93_0",
|
||||
"rust_toolchain_artifacts_macos_aarch64_1_93_0",
|
||||
)
|
||||
|
||||
register_toolchains("@default_rust_toolchains//:all")
|
||||
register_toolchains("@rust_toolchains//:all")
|
||||
register_toolchains("@experimental_rust_toolchains_1_93_0//:all")
|
||||
|
||||
crate = use_extension("@rules_rs//rs:extensions.bzl", "crate")
|
||||
crate.from_cargo(
|
||||
@@ -116,36 +68,12 @@ crate.from_cargo(
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"aarch64-apple-darwin",
|
||||
# Keep both Windows ABIs in the generated Cargo metadata: the V8
|
||||
# experiment still consumes release assets that only exist under the
|
||||
# MSVC names while targeting the GNU toolchain.
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-pc-windows-gnullvm",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-pc-windows-gnullvm",
|
||||
],
|
||||
use_experimental_platforms = True,
|
||||
)
|
||||
crate.from_cargo(
|
||||
name = "argument_comment_lint_crates",
|
||||
cargo_lock = "//tools/argument-comment-lint:Cargo.lock",
|
||||
cargo_toml = "//tools/argument-comment-lint:Cargo.toml",
|
||||
platform_triples = [
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-pc-windows-gnullvm",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-pc-windows-gnullvm",
|
||||
],
|
||||
use_experimental_platforms = True,
|
||||
)
|
||||
|
||||
bazel_dep(name = "zstd", version = "1.5.7")
|
||||
@@ -166,16 +94,10 @@ crate.annotation(
|
||||
],
|
||||
)
|
||||
|
||||
crate.annotation(
|
||||
# The build script only validates embedded source/version metadata.
|
||||
crate = "rustc_apfloat",
|
||||
gen_build_script = "off",
|
||||
)
|
||||
|
||||
inject_repo(crate, "zstd")
|
||||
use_repo(crate, "argument_comment_lint_crates")
|
||||
|
||||
bazel_dep(name = "bzip2", version = "1.0.8.bcr.3")
|
||||
bazel_dep(name = "libcap", version = "2.27.bcr.1")
|
||||
|
||||
crate.annotation(
|
||||
crate = "bzip2-sys",
|
||||
@@ -214,7 +136,6 @@ crate.annotation(
|
||||
},
|
||||
crate = "openssl-sys",
|
||||
data = ["@openssl//:gen_dir"],
|
||||
gen_build_script = "on",
|
||||
)
|
||||
|
||||
inject_repo(crate, "openssl")
|
||||
@@ -224,58 +145,6 @@ crate.annotation(
|
||||
workspace_cargo_toml = "rust/runfiles/Cargo.toml",
|
||||
)
|
||||
|
||||
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
|
||||
new_local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "new_local_repository")
|
||||
|
||||
new_local_repository(
|
||||
name = "v8_targets",
|
||||
build_file = "//third_party/v8:BUILD.bazel",
|
||||
path = "third_party/v8",
|
||||
)
|
||||
|
||||
crate.annotation(
|
||||
build_script_data = [
|
||||
"@v8_targets//:rusty_v8_archive_for_target",
|
||||
"@v8_targets//:rusty_v8_binding_for_target",
|
||||
],
|
||||
build_script_env = {
|
||||
"RUSTY_V8_ARCHIVE": "$(execpath @v8_targets//:rusty_v8_archive_for_target)",
|
||||
"RUSTY_V8_SRC_BINDING_PATH": "$(execpath @v8_targets//:rusty_v8_binding_for_target)",
|
||||
},
|
||||
crate = "v8",
|
||||
gen_build_script = "on",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//patches:rusty_v8_prebuilt_out_dir.patch",
|
||||
],
|
||||
)
|
||||
|
||||
inject_repo(crate, "v8_targets")
|
||||
|
||||
llvm = use_extension("@llvm//extensions:llvm.bzl", "llvm")
|
||||
use_repo(llvm, "llvm-project")
|
||||
|
||||
crate.annotation(
|
||||
# Provide the hermetic SDK path so the build script doesn't try to invoke an unhermetic `xcrun --show-sdk-path`.
|
||||
build_script_data = [
|
||||
"@macos_sdk//sysroot",
|
||||
],
|
||||
build_script_env = {
|
||||
"BINDGEN_EXTRA_CLANG_ARGS": "-Xclang -internal-isystem -Xclang $(location @llvm//:builtin_resource_dir)/include",
|
||||
"COREAUDIO_SDK_PATH": "$(location @macos_sdk//sysroot)",
|
||||
"LIBCLANG_PATH": "$(location @llvm-project//clang:libclang_interface_output)",
|
||||
},
|
||||
build_script_tools = [
|
||||
"@llvm-project//clang:libclang_interface_output",
|
||||
"@llvm//:builtin_resource_dir",
|
||||
],
|
||||
crate = "coreaudio-sys",
|
||||
gen_build_script = "on",
|
||||
)
|
||||
|
||||
inject_repo(crate, "llvm", "llvm-project", "macos_sdk")
|
||||
|
||||
# Fix readme inclusions
|
||||
crate.annotation(
|
||||
crate = "windows-link",
|
||||
@@ -285,123 +154,29 @@ crate.annotation(
|
||||
],
|
||||
)
|
||||
|
||||
bazel_dep(name = "alsa_lib", version = "1.2.9.bcr.4")
|
||||
WINDOWS_IMPORT_LIB = """
|
||||
load("@rules_cc//cc:defs.bzl", "cc_import")
|
||||
|
||||
cc_import(
|
||||
name = "windows_import_lib",
|
||||
static_library = glob(["lib/*.a"])[0],
|
||||
)
|
||||
"""
|
||||
|
||||
crate.annotation(
|
||||
crate = "alsa-sys",
|
||||
additive_build_file_content = WINDOWS_IMPORT_LIB,
|
||||
crate = "windows_x86_64_gnullvm",
|
||||
gen_build_script = "off",
|
||||
deps = ["@alsa_lib"],
|
||||
deps = [":windows_import_lib"],
|
||||
)
|
||||
|
||||
inject_repo(crate, "alsa_lib")
|
||||
|
||||
bazel_dep(name = "v8", version = "14.6.202.9")
|
||||
archive_override(
|
||||
module_name = "v8",
|
||||
integrity = "sha256-JphDwLAzsd9KvgRZ7eQvNtPU6qGd3XjFt/a/1QITAJU=",
|
||||
patch_strip = 3,
|
||||
patches = [
|
||||
"//patches:v8_module_deps.patch",
|
||||
"//patches:v8_bazel_rules.patch",
|
||||
"//patches:v8_source_portability.patch",
|
||||
],
|
||||
strip_prefix = "v8-14.6.202.9",
|
||||
urls = ["https://github.com/v8/v8/archive/refs/tags/14.6.202.9.tar.gz"],
|
||||
crate.annotation(
|
||||
additive_build_file_content = WINDOWS_IMPORT_LIB,
|
||||
crate = "windows_aarch64_gnullvm",
|
||||
gen_build_script = "off",
|
||||
deps = [":windows_import_lib"],
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "v8_crate_146_4_0",
|
||||
build_file = "//third_party/v8:v8_crate.BUILD.bazel",
|
||||
sha256 = "d97bcac5cdc5a195a4813f1855a6bc658f240452aac36caa12fd6c6f16026ab1",
|
||||
strip_prefix = "v8-146.4.0",
|
||||
type = "tar.gz",
|
||||
urls = ["https://static.crates.io/crates/v8/v8-146.4.0.crate"],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_apple_darwin_archive",
|
||||
downloaded_file_path = "librusty_v8_release_aarch64-apple-darwin.a.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_aarch64-apple-darwin.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_unknown_linux_gnu_archive",
|
||||
downloaded_file_path = "librusty_v8_release_aarch64-unknown-linux-gnu.a.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_aarch64-unknown-linux-gnu.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_pc_windows_msvc_archive",
|
||||
downloaded_file_path = "rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_apple_darwin_archive",
|
||||
downloaded_file_path = "librusty_v8_release_x86_64-apple-darwin.a.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_x86_64-apple-darwin.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
|
||||
downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_pc_windows_msvc_archive",
|
||||
downloaded_file_path = "rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
|
||||
urls = [
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v146.4.0/rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_unknown_linux_musl_archive",
|
||||
downloaded_file_path = "librusty_v8_release_aarch64-unknown-linux-musl.a.gz",
|
||||
urls = [
|
||||
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/librusty_v8_release_aarch64-unknown-linux-musl.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_aarch64_unknown_linux_musl_binding",
|
||||
downloaded_file_path = "src_binding_release_aarch64-unknown-linux-musl.rs",
|
||||
urls = [
|
||||
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/src_binding_release_aarch64-unknown-linux-musl.rs",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_archive",
|
||||
downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-musl.a.gz",
|
||||
urls = [
|
||||
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/librusty_v8_release_x86_64-unknown-linux-musl.a.gz",
|
||||
],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_binding",
|
||||
downloaded_file_path = "src_binding_release_x86_64-unknown-linux-musl.rs",
|
||||
urls = [
|
||||
"https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/src_binding_release_x86_64-unknown-linux-musl.rs",
|
||||
],
|
||||
)
|
||||
|
||||
use_repo(crate, "crates")
|
||||
|
||||
bazel_dep(name = "libcap", version = "2.27.bcr.1")
|
||||
|
||||
rbe_platform_repository = use_repo_rule("//:rbe.bzl", "rbe_platform_repository")
|
||||
|
||||
rbe_platform_repository(
|
||||
|
||||
210
MODULE.bazel.lock
generated
210
MODULE.bazel.lock
generated
File diff suppressed because one or more lines are too long
@@ -1,11 +1,5 @@
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = ["-C", "link-arg=/STACK:8388608"]
|
||||
|
||||
# MSVC emits a warning about code that may trip "Cortex-A53 MPCore processor bug #843419" (see
|
||||
# https://developer.arm.com/documentation/epm048406/latest) which is sometimes emitted by LLVM.
|
||||
# Since Arm64 Windows 10+ isn't supported on that processor, it's safe to disable the warning.
|
||||
[target.aarch64-pc-windows-msvc]
|
||||
rustflags = ["-C", "link-arg=/STACK:8388608", "-C", "link-arg=/arm64hazardfree"]
|
||||
|
||||
[target.'cfg(all(windows, target_env = "gnu"))']
|
||||
rustflags = ["-C", "link-arg=-Wl,--stack,8388608"]
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
# Do not increase, fix your test instead
|
||||
slow-timeout = { period = "15s", terminate-after = 2 }
|
||||
|
||||
[test-groups.app_server_protocol_codegen]
|
||||
max-threads = 1
|
||||
|
||||
[test-groups.app_server_integration]
|
||||
max-threads = 1
|
||||
|
||||
|
||||
[[profile.default.overrides]]
|
||||
# Do not add new tests here
|
||||
@@ -17,13 +11,3 @@ slow-timeout = { period = "1m", terminate-after = 4 }
|
||||
[[profile.default.overrides]]
|
||||
filter = 'test(approval_matrix_covers_all_modes)'
|
||||
slow-timeout = { period = "30s", terminate-after = 2 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = 'package(codex-app-server-protocol) & (test(typescript_schema_fixtures_match_generated) | test(json_schema_fixtures_match_generated) | test(generate_ts_with_experimental_api_retains_experimental_entries) | test(generated_ts_optional_nullable_fields_only_in_params) | test(generate_json_filters_experimental_fields_and_methods))'
|
||||
test-group = 'app_server_protocol_codegen'
|
||||
|
||||
[[profile.default.overrides]]
|
||||
# These integration tests spawn a fresh app-server subprocess per case.
|
||||
# Keep the library unit tests parallel.
|
||||
filter = 'package(codex-app-server) & kind(test)'
|
||||
test-group = 'app_server_integration'
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
exports_files([
|
||||
"clippy.toml",
|
||||
"node-version.txt",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
name = "workspace-files",
|
||||
srcs = glob(
|
||||
[
|
||||
"*",
|
||||
".cargo/**",
|
||||
],
|
||||
exclude = [
|
||||
"BUILD.bazel",
|
||||
],
|
||||
),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
1170
codex-rs/Cargo.lock
generated
1170
codex-rs/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,24 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"analytics",
|
||||
"backend-client",
|
||||
"ansi-escape",
|
||||
"async-utils",
|
||||
"app-server",
|
||||
"app-server-client",
|
||||
"app-server-protocol",
|
||||
"app-server-test-client",
|
||||
"debug-client",
|
||||
"apply-patch",
|
||||
"arg0",
|
||||
"feedback",
|
||||
"features",
|
||||
"codex-backend-openapi-models",
|
||||
"code-mode",
|
||||
"cloud-requirements",
|
||||
"cloud-tasks",
|
||||
"cloud-tasks-client",
|
||||
"cli",
|
||||
"connectors",
|
||||
"config",
|
||||
"shell-command",
|
||||
"shell-escalation",
|
||||
"skills",
|
||||
"core",
|
||||
"core-skills",
|
||||
"hooks",
|
||||
"instructions",
|
||||
"secrets",
|
||||
"exec",
|
||||
"exec-server",
|
||||
@@ -43,18 +34,14 @@ members = [
|
||||
"ollama",
|
||||
"process-hardening",
|
||||
"protocol",
|
||||
"rollout",
|
||||
"rmcp-client",
|
||||
"responses-api-proxy",
|
||||
"sandboxing",
|
||||
"stdio-to-uds",
|
||||
"otel",
|
||||
"tui",
|
||||
"tools",
|
||||
"v8-poc",
|
||||
"utils/absolute-path",
|
||||
"utils/cargo-bin",
|
||||
"git-utils",
|
||||
"utils/git",
|
||||
"utils/cache",
|
||||
"utils/image",
|
||||
"utils/json-to-toml",
|
||||
@@ -66,21 +53,15 @@ members = [
|
||||
"utils/cli",
|
||||
"utils/elapsed",
|
||||
"utils/sandbox-summary",
|
||||
"utils/sanitizer",
|
||||
"utils/sleep-inhibitor",
|
||||
"utils/approval-presets",
|
||||
"utils/oss",
|
||||
"utils/output-truncation",
|
||||
"utils/path-utils",
|
||||
"utils/plugins",
|
||||
"utils/fuzzy-match",
|
||||
"utils/stream-parser",
|
||||
"utils/template",
|
||||
"codex-client",
|
||||
"codex-api",
|
||||
"state",
|
||||
"terminal-detection",
|
||||
"codex-experimental-api-macros",
|
||||
"plugin",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@@ -97,11 +78,8 @@ license = "Apache-2.0"
|
||||
# Internal
|
||||
app_test_support = { path = "app-server/tests/common" }
|
||||
codex-ansi-escape = { path = "ansi-escape" }
|
||||
codex-analytics = { path = "analytics" }
|
||||
codex-api = { path = "codex-api" }
|
||||
codex-code-mode = { path = "code-mode" }
|
||||
codex-app-server = { path = "app-server" }
|
||||
codex-app-server-client = { path = "app-server-client" }
|
||||
codex-app-server-protocol = { path = "app-server-protocol" }
|
||||
codex-app-server-test-client = { path = "app-server-test-client" }
|
||||
codex-apply-patch = { path = "apply-patch" }
|
||||
@@ -112,20 +90,15 @@ codex-chatgpt = { path = "chatgpt" }
|
||||
codex-cli = { path = "cli" }
|
||||
codex-client = { path = "codex-client" }
|
||||
codex-cloud-requirements = { path = "cloud-requirements" }
|
||||
codex-connectors = { path = "connectors" }
|
||||
codex-config = { path = "config" }
|
||||
codex-core = { path = "core" }
|
||||
codex-core-skills = { path = "core-skills" }
|
||||
codex-exec = { path = "exec" }
|
||||
codex-exec-server = { path = "exec-server" }
|
||||
codex-execpolicy = { path = "execpolicy" }
|
||||
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
|
||||
codex-feedback = { path = "feedback" }
|
||||
codex-features = { path = "features" }
|
||||
codex-file-search = { path = "file-search" }
|
||||
codex-git-utils = { path = "git-utils" }
|
||||
codex-git = { path = "utils/git" }
|
||||
codex-hooks = { path = "hooks" }
|
||||
codex-instructions = { path = "instructions" }
|
||||
codex-keyring-store = { path = "keyring-store" }
|
||||
codex-linux-sandbox = { path = "linux-sandbox" }
|
||||
codex-lmstudio = { path = "lmstudio" }
|
||||
@@ -134,23 +107,15 @@ codex-mcp-server = { path = "mcp-server" }
|
||||
codex-network-proxy = { path = "network-proxy" }
|
||||
codex-ollama = { path = "ollama" }
|
||||
codex-otel = { path = "otel" }
|
||||
codex-plugin = { path = "plugin" }
|
||||
codex-process-hardening = { path = "process-hardening" }
|
||||
codex-protocol = { path = "protocol" }
|
||||
codex-rollout = { path = "rollout" }
|
||||
codex-responses-api-proxy = { path = "responses-api-proxy" }
|
||||
codex-rmcp-client = { path = "rmcp-client" }
|
||||
codex-sandboxing = { path = "sandboxing" }
|
||||
codex-secrets = { path = "secrets" }
|
||||
codex-shell-command = { path = "shell-command" }
|
||||
codex-shell-escalation = { path = "shell-escalation" }
|
||||
codex-skills = { path = "skills" }
|
||||
codex-state = { path = "state" }
|
||||
codex-stdio-to-uds = { path = "stdio-to-uds" }
|
||||
codex-terminal-detection = { path = "terminal-detection" }
|
||||
codex-tools = { path = "tools" }
|
||||
codex-tui = { path = "tui" }
|
||||
codex-v8-poc = { path = "v8-poc" }
|
||||
codex-utils-absolute-path = { path = "utils/absolute-path" }
|
||||
codex-utils-approval-presets = { path = "utils/approval-presets" }
|
||||
codex-utils-cache = { path = "utils/cache" }
|
||||
@@ -162,19 +127,16 @@ codex-utils-home-dir = { path = "utils/home-dir" }
|
||||
codex-utils-image = { path = "utils/image" }
|
||||
codex-utils-json-to-toml = { path = "utils/json-to-toml" }
|
||||
codex-utils-oss = { path = "utils/oss" }
|
||||
codex-utils-output-truncation = { path = "utils/output-truncation" }
|
||||
codex-utils-path = { path = "utils/path-utils" }
|
||||
codex-utils-plugins = { path = "utils/plugins" }
|
||||
codex-utils-pty = { path = "utils/pty" }
|
||||
codex-utils-readiness = { path = "utils/readiness" }
|
||||
codex-utils-rustls-provider = { path = "utils/rustls-provider" }
|
||||
codex-utils-sandbox-summary = { path = "utils/sandbox-summary" }
|
||||
codex-utils-sanitizer = { path = "utils/sanitizer" }
|
||||
codex-utils-sleep-inhibitor = { path = "utils/sleep-inhibitor" }
|
||||
codex-utils-stream-parser = { path = "utils/stream-parser" }
|
||||
codex-utils-template = { path = "utils/template" }
|
||||
codex-utils-string = { path = "utils/string" }
|
||||
codex-windows-sandbox = { path = "windows-sandbox-rs" }
|
||||
core_test_support = { path = "core/tests/common" }
|
||||
exec_server_test_support = { path = "exec-server/tests/common" }
|
||||
mcp_test_support = { path = "mcp-server/tests/common" }
|
||||
|
||||
# External
|
||||
@@ -183,7 +145,6 @@ allocative = "0.3.3"
|
||||
ansi-to-tui = "7.0.0"
|
||||
anyhow = "1"
|
||||
arboard = { version = "3", features = ["wayland-data-control"] }
|
||||
arc-swap = "1.9.0"
|
||||
assert_cmd = "2"
|
||||
assert_matches = "1.5.0"
|
||||
async-channel = "2.3.1"
|
||||
@@ -198,10 +159,8 @@ chrono = "0.4.43"
|
||||
clap = "4"
|
||||
clap_complete = "4"
|
||||
color-eyre = "0.6.3"
|
||||
constant_time_eq = "0.3.1"
|
||||
crossbeam-channel = "0.5.15"
|
||||
crossterm = "0.28.1"
|
||||
csv = "1.3.1"
|
||||
ctor = "0.6.3"
|
||||
derive_more = "2"
|
||||
diffy = "0.4.2"
|
||||
@@ -213,26 +172,23 @@ env-flags = "0.1.1"
|
||||
env_logger = "0.11.9"
|
||||
eventsource-stream = "0.2.3"
|
||||
futures = { version = "0.3", default-features = false }
|
||||
gethostname = "1.1.0"
|
||||
globset = "0.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.3.1"
|
||||
icu_decimal = "2.1"
|
||||
icu_locale_core = "2.1"
|
||||
icu_provider = { version = "2.1", features = ["sync"] }
|
||||
ignore = "0.4.23"
|
||||
image = { version = "^0.25.9", default-features = false }
|
||||
iana-time-zone = "0.1.64"
|
||||
include_dir = "0.7.4"
|
||||
indexmap = "2.12.0"
|
||||
indoc = "2.0"
|
||||
insta = "1.46.3"
|
||||
inventory = "0.3.19"
|
||||
itertools = "0.14.0"
|
||||
jsonwebtoken = "9.3.1"
|
||||
keyring = { version = "3.6", default-features = false }
|
||||
landlock = "0.4.4"
|
||||
lazy_static = "1"
|
||||
libc = "0.2.182"
|
||||
libc = "0.2.177"
|
||||
log = "0.4"
|
||||
lru = "0.16.3"
|
||||
maplit = "1.0.2"
|
||||
@@ -248,14 +204,13 @@ opentelemetry-otlp = "0.31.0"
|
||||
opentelemetry-semantic-conventions = "0.31.0"
|
||||
opentelemetry_sdk = "0.31.0"
|
||||
os_info = "3.12.0"
|
||||
owo-colors = "4.3.0"
|
||||
owo-colors = "4.2.0"
|
||||
path-absolutize = "3.1.1"
|
||||
pathdiff = "0.2"
|
||||
portable-pty = "0.9.0"
|
||||
predicates = "3"
|
||||
pretty_assertions = "1.4.1"
|
||||
pulldown-cmark = "0.10"
|
||||
quick-xml = "0.38.4"
|
||||
rand = "0.9"
|
||||
ratatui = "0.29.0"
|
||||
ratatui-macros = "0.6.0"
|
||||
@@ -264,13 +219,10 @@ regex-lite = "0.1.8"
|
||||
reqwest = "0.12"
|
||||
rmcp = { version = "0.15.0", default-features = false }
|
||||
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
|
||||
v8 = "=146.4.0"
|
||||
rustls = { version = "0.23", default-features = false, features = [
|
||||
"ring",
|
||||
"std",
|
||||
] }
|
||||
rustls-native-certs = "0.8.3"
|
||||
rustls-pki-types = "1.14.0"
|
||||
schemars = "0.8.22"
|
||||
seccompiler = "0.5.0"
|
||||
semver = "1.0"
|
||||
@@ -278,7 +230,7 @@ sentry = "0.46.0"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
serde_path_to_error = "0.1.20"
|
||||
serde_with = "3.17"
|
||||
serde_with = "3.16"
|
||||
serde_yaml = "0.9"
|
||||
serial_test = "3.2.0"
|
||||
sha1 = "0.10.6"
|
||||
@@ -298,9 +250,8 @@ sqlx = { version = "0.8.6", default-features = false, features = [
|
||||
] }
|
||||
starlark = "0.13.0"
|
||||
strum = "0.27.2"
|
||||
strum_macros = "0.28.0"
|
||||
strum_macros = "0.27.2"
|
||||
supports-color = "3.0.2"
|
||||
syntect = "5"
|
||||
sys-locale = "0.3.2"
|
||||
tempfile = "3.23.0"
|
||||
test-log = "0.2.19"
|
||||
@@ -325,6 +276,7 @@ tracing-subscriber = "0.3.22"
|
||||
tracing-test = "0.2.5"
|
||||
tree-sitter = "0.25.10"
|
||||
tree-sitter-bash = "0.25"
|
||||
tree-sitter-highlight = "0.25.10"
|
||||
ts-rs = "11"
|
||||
tungstenite = { version = "0.27.0", features = ["deflate", "proxy"] }
|
||||
uds_windows = "1.1.0"
|
||||
@@ -389,13 +341,11 @@ ignored = [
|
||||
"icu_provider",
|
||||
"openssl-sys",
|
||||
"codex-utils-readiness",
|
||||
"codex-utils-template",
|
||||
"codex-v8-poc",
|
||||
"codex-secrets",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
split-debuginfo = "off"
|
||||
# Because we bundle some of these executables with the TypeScript CLI, we
|
||||
# remove everything to make the binary as small as possible.
|
||||
strip = "symbols"
|
||||
|
||||
@@ -50,7 +50,7 @@ You can enable notifications by configuring a script that is run whenever the ag
|
||||
|
||||
### `codex exec` to run Codex programmatically/non-interactively
|
||||
|
||||
To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the prompt via `stdin`) and Codex will work on your task until it decides that it is done and exits. If you provide both a prompt argument and piped stdin, Codex appends stdin as a `<stdin>` block after the prompt so patterns like `echo "my output" | codex exec "Summarize this concisely"` work naturally. Output is printed to the terminal directly. You can set the `RUST_LOG` environment variable to see more about what's going on.
|
||||
To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the prompt via `stdin`) and Codex will work on your task until it decides that it is done and exits. Output is printed to the terminal directly. You can set the `RUST_LOG` environment variable to see more about what's going on.
|
||||
Use `codex exec --ephemeral ...` to run without persisting session rollout files to disk.
|
||||
|
||||
### Experimenting with the Codex Sandbox
|
||||
@@ -88,7 +88,6 @@ codex --sandbox danger-full-access
|
||||
```
|
||||
|
||||
The same setting can be persisted in `~/.codex/config.toml` via the top-level `sandbox_mode = "MODE"` key, e.g. `sandbox_mode = "workspace-write"`.
|
||||
In `workspace-write`, Codex also includes `~/.codex/memories` in its writable roots so memory maintenance does not require an extra approval.
|
||||
|
||||
## Code Organization
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "analytics",
|
||||
crate_name = "codex_analytics",
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-analytics"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
name = "codex_analytics"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha1 = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -1,797 +0,0 @@
|
||||
use codex_git_utils::collect_git_info;
|
||||
use codex_git_utils::get_git_repo_root;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::default_client::create_client;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use serde::Serialize;
|
||||
use sha1::Digest;
|
||||
use sha1::Sha1;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TrackEventsContext {
|
||||
pub model_slug: String,
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
}
|
||||
|
||||
pub fn build_track_events_context(
|
||||
model_slug: String,
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
) -> TrackEventsContext {
|
||||
TrackEventsContext {
|
||||
model_slug,
|
||||
thread_id,
|
||||
turn_id,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SkillInvocation {
|
||||
pub skill_name: String,
|
||||
pub skill_scope: SkillScope,
|
||||
pub skill_path: PathBuf,
|
||||
pub invocation_type: InvocationType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum InvocationType {
|
||||
Explicit,
|
||||
Implicit,
|
||||
}
|
||||
|
||||
pub struct AppInvocation {
|
||||
pub connector_id: Option<String>,
|
||||
pub app_name: Option<String>,
|
||||
pub invocation_type: Option<InvocationType>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AnalyticsEventsQueue {
|
||||
sender: mpsc::Sender<TrackEventsJob>,
|
||||
app_used_emitted_keys: Arc<Mutex<HashSet<(String, String)>>>,
|
||||
plugin_used_emitted_keys: Arc<Mutex<HashSet<(String, String)>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnalyticsEventsClient {
|
||||
queue: AnalyticsEventsQueue,
|
||||
analytics_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl AnalyticsEventsQueue {
|
||||
pub(crate) fn new(auth_manager: Arc<AuthManager>, base_url: String) -> Self {
|
||||
let (sender, mut receiver) = mpsc::channel(ANALYTICS_EVENTS_QUEUE_SIZE);
|
||||
tokio::spawn(async move {
|
||||
while let Some(job) = receiver.recv().await {
|
||||
match job {
|
||||
TrackEventsJob::SkillInvocations(job) => {
|
||||
send_track_skill_invocations(&auth_manager, &base_url, job).await;
|
||||
}
|
||||
TrackEventsJob::AppMentioned(job) => {
|
||||
send_track_app_mentioned(&auth_manager, &base_url, job).await;
|
||||
}
|
||||
TrackEventsJob::AppUsed(job) => {
|
||||
send_track_app_used(&auth_manager, &base_url, job).await;
|
||||
}
|
||||
TrackEventsJob::PluginUsed(job) => {
|
||||
send_track_plugin_used(&auth_manager, &base_url, job).await;
|
||||
}
|
||||
TrackEventsJob::PluginInstalled(job) => {
|
||||
send_track_plugin_installed(&auth_manager, &base_url, job).await;
|
||||
}
|
||||
TrackEventsJob::PluginUninstalled(job) => {
|
||||
send_track_plugin_uninstalled(&auth_manager, &base_url, job).await;
|
||||
}
|
||||
TrackEventsJob::PluginEnabled(job) => {
|
||||
send_track_plugin_enabled(&auth_manager, &base_url, job).await;
|
||||
}
|
||||
TrackEventsJob::PluginDisabled(job) => {
|
||||
send_track_plugin_disabled(&auth_manager, &base_url, job).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Self {
|
||||
sender,
|
||||
app_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())),
|
||||
plugin_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_send(&self, job: TrackEventsJob) {
|
||||
if self.sender.try_send(job).is_err() {
|
||||
//TODO: add a metric for this
|
||||
tracing::warn!("dropping analytics events: queue is full");
|
||||
}
|
||||
}
|
||||
|
||||
fn should_enqueue_app_used(&self, tracking: &TrackEventsContext, app: &AppInvocation) -> bool {
|
||||
let Some(connector_id) = app.connector_id.as_ref() else {
|
||||
return true;
|
||||
};
|
||||
let mut emitted = self
|
||||
.app_used_emitted_keys
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if emitted.len() >= ANALYTICS_EVENT_DEDUPE_MAX_KEYS {
|
||||
emitted.clear();
|
||||
}
|
||||
emitted.insert((tracking.turn_id.clone(), connector_id.clone()))
|
||||
}
|
||||
|
||||
fn should_enqueue_plugin_used(
|
||||
&self,
|
||||
tracking: &TrackEventsContext,
|
||||
plugin: &PluginTelemetryMetadata,
|
||||
) -> bool {
|
||||
let mut emitted = self
|
||||
.plugin_used_emitted_keys
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if emitted.len() >= ANALYTICS_EVENT_DEDUPE_MAX_KEYS {
|
||||
emitted.clear();
|
||||
}
|
||||
emitted.insert((tracking.turn_id.clone(), plugin.plugin_id.as_key()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AnalyticsEventsClient {
|
||||
pub fn new(
|
||||
auth_manager: Arc<AuthManager>,
|
||||
base_url: String,
|
||||
analytics_enabled: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
queue: AnalyticsEventsQueue::new(Arc::clone(&auth_manager), base_url),
|
||||
analytics_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_skill_invocations(
|
||||
&self,
|
||||
tracking: TrackEventsContext,
|
||||
invocations: Vec<SkillInvocation>,
|
||||
) {
|
||||
track_skill_invocations(
|
||||
&self.queue,
|
||||
self.analytics_enabled,
|
||||
Some(tracking),
|
||||
invocations,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn track_app_mentioned(&self, tracking: TrackEventsContext, mentions: Vec<AppInvocation>) {
|
||||
track_app_mentioned(
|
||||
&self.queue,
|
||||
self.analytics_enabled,
|
||||
Some(tracking),
|
||||
mentions,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn track_app_used(&self, tracking: TrackEventsContext, app: AppInvocation) {
|
||||
track_app_used(&self.queue, self.analytics_enabled, Some(tracking), app);
|
||||
}
|
||||
|
||||
pub fn track_plugin_used(&self, tracking: TrackEventsContext, plugin: PluginTelemetryMetadata) {
|
||||
track_plugin_used(&self.queue, self.analytics_enabled, Some(tracking), plugin);
|
||||
}
|
||||
|
||||
pub fn track_plugin_installed(&self, plugin: PluginTelemetryMetadata) {
|
||||
track_plugin_management(
|
||||
&self.queue,
|
||||
self.analytics_enabled,
|
||||
PluginManagementEventType::Installed,
|
||||
plugin,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn track_plugin_uninstalled(&self, plugin: PluginTelemetryMetadata) {
|
||||
track_plugin_management(
|
||||
&self.queue,
|
||||
self.analytics_enabled,
|
||||
PluginManagementEventType::Uninstalled,
|
||||
plugin,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn track_plugin_enabled(&self, plugin: PluginTelemetryMetadata) {
|
||||
track_plugin_management(
|
||||
&self.queue,
|
||||
self.analytics_enabled,
|
||||
PluginManagementEventType::Enabled,
|
||||
plugin,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn track_plugin_disabled(&self, plugin: PluginTelemetryMetadata) {
|
||||
track_plugin_management(
|
||||
&self.queue,
|
||||
self.analytics_enabled,
|
||||
PluginManagementEventType::Disabled,
|
||||
plugin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum TrackEventsJob {
|
||||
SkillInvocations(TrackSkillInvocationsJob),
|
||||
AppMentioned(TrackAppMentionedJob),
|
||||
AppUsed(TrackAppUsedJob),
|
||||
PluginUsed(TrackPluginUsedJob),
|
||||
PluginInstalled(TrackPluginManagementJob),
|
||||
PluginUninstalled(TrackPluginManagementJob),
|
||||
PluginEnabled(TrackPluginManagementJob),
|
||||
PluginDisabled(TrackPluginManagementJob),
|
||||
}
|
||||
|
||||
struct TrackSkillInvocationsJob {
|
||||
analytics_enabled: Option<bool>,
|
||||
tracking: TrackEventsContext,
|
||||
invocations: Vec<SkillInvocation>,
|
||||
}
|
||||
|
||||
struct TrackAppMentionedJob {
|
||||
analytics_enabled: Option<bool>,
|
||||
tracking: TrackEventsContext,
|
||||
mentions: Vec<AppInvocation>,
|
||||
}
|
||||
|
||||
struct TrackAppUsedJob {
|
||||
analytics_enabled: Option<bool>,
|
||||
tracking: TrackEventsContext,
|
||||
app: AppInvocation,
|
||||
}
|
||||
|
||||
struct TrackPluginUsedJob {
|
||||
analytics_enabled: Option<bool>,
|
||||
tracking: TrackEventsContext,
|
||||
plugin: PluginTelemetryMetadata,
|
||||
}
|
||||
|
||||
struct TrackPluginManagementJob {
|
||||
analytics_enabled: Option<bool>,
|
||||
plugin: PluginTelemetryMetadata,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PluginManagementEventType {
|
||||
Installed,
|
||||
Uninstalled,
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
const ANALYTICS_EVENTS_QUEUE_SIZE: usize = 256;
|
||||
const ANALYTICS_EVENTS_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const ANALYTICS_EVENT_DEDUPE_MAX_KEYS: usize = 4096;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TrackEventsRequest {
|
||||
events: Vec<TrackEventRequest>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum TrackEventRequest {
|
||||
SkillInvocation(SkillInvocationEventRequest),
|
||||
AppMentioned(CodexAppMentionedEventRequest),
|
||||
AppUsed(CodexAppUsedEventRequest),
|
||||
PluginUsed(CodexPluginUsedEventRequest),
|
||||
PluginInstalled(CodexPluginEventRequest),
|
||||
PluginUninstalled(CodexPluginEventRequest),
|
||||
PluginEnabled(CodexPluginEventRequest),
|
||||
PluginDisabled(CodexPluginEventRequest),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SkillInvocationEventRequest {
|
||||
event_type: &'static str,
|
||||
skill_id: String,
|
||||
skill_name: String,
|
||||
event_params: SkillInvocationEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SkillInvocationEventParams {
|
||||
product_client_id: Option<String>,
|
||||
skill_scope: Option<String>,
|
||||
repo_url: Option<String>,
|
||||
thread_id: Option<String>,
|
||||
invoke_type: Option<InvocationType>,
|
||||
model_slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexAppMetadata {
|
||||
connector_id: Option<String>,
|
||||
thread_id: Option<String>,
|
||||
turn_id: Option<String>,
|
||||
app_name: Option<String>,
|
||||
product_client_id: Option<String>,
|
||||
invoke_type: Option<InvocationType>,
|
||||
model_slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexAppMentionedEventRequest {
|
||||
event_type: &'static str,
|
||||
event_params: CodexAppMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexAppUsedEventRequest {
|
||||
event_type: &'static str,
|
||||
event_params: CodexAppMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexPluginMetadata {
|
||||
plugin_id: Option<String>,
|
||||
plugin_name: Option<String>,
|
||||
marketplace_name: Option<String>,
|
||||
has_skills: Option<bool>,
|
||||
mcp_server_count: Option<usize>,
|
||||
connector_ids: Option<Vec<String>>,
|
||||
product_client_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexPluginUsedMetadata {
|
||||
#[serde(flatten)]
|
||||
plugin: CodexPluginMetadata,
|
||||
thread_id: Option<String>,
|
||||
turn_id: Option<String>,
|
||||
model_slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexPluginEventRequest {
|
||||
event_type: &'static str,
|
||||
event_params: CodexPluginMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexPluginUsedEventRequest {
|
||||
event_type: &'static str,
|
||||
event_params: CodexPluginUsedMetadata,
|
||||
}
|
||||
|
||||
pub(crate) fn track_skill_invocations(
|
||||
queue: &AnalyticsEventsQueue,
|
||||
analytics_enabled: Option<bool>,
|
||||
tracking: Option<TrackEventsContext>,
|
||||
invocations: Vec<SkillInvocation>,
|
||||
) {
|
||||
if analytics_enabled == Some(false) {
|
||||
return;
|
||||
}
|
||||
let Some(tracking) = tracking else {
|
||||
return;
|
||||
};
|
||||
if invocations.is_empty() {
|
||||
return;
|
||||
}
|
||||
let job = TrackEventsJob::SkillInvocations(TrackSkillInvocationsJob {
|
||||
analytics_enabled,
|
||||
tracking,
|
||||
invocations,
|
||||
});
|
||||
queue.try_send(job);
|
||||
}
|
||||
|
||||
pub(crate) fn track_app_mentioned(
|
||||
queue: &AnalyticsEventsQueue,
|
||||
analytics_enabled: Option<bool>,
|
||||
tracking: Option<TrackEventsContext>,
|
||||
mentions: Vec<AppInvocation>,
|
||||
) {
|
||||
if analytics_enabled == Some(false) {
|
||||
return;
|
||||
}
|
||||
let Some(tracking) = tracking else {
|
||||
return;
|
||||
};
|
||||
if mentions.is_empty() {
|
||||
return;
|
||||
}
|
||||
let job = TrackEventsJob::AppMentioned(TrackAppMentionedJob {
|
||||
analytics_enabled,
|
||||
tracking,
|
||||
mentions,
|
||||
});
|
||||
queue.try_send(job);
|
||||
}
|
||||
|
||||
pub(crate) fn track_app_used(
|
||||
queue: &AnalyticsEventsQueue,
|
||||
analytics_enabled: Option<bool>,
|
||||
tracking: Option<TrackEventsContext>,
|
||||
app: AppInvocation,
|
||||
) {
|
||||
if analytics_enabled == Some(false) {
|
||||
return;
|
||||
}
|
||||
let Some(tracking) = tracking else {
|
||||
return;
|
||||
};
|
||||
if !queue.should_enqueue_app_used(&tracking, &app) {
|
||||
return;
|
||||
}
|
||||
let job = TrackEventsJob::AppUsed(TrackAppUsedJob {
|
||||
analytics_enabled,
|
||||
tracking,
|
||||
app,
|
||||
});
|
||||
queue.try_send(job);
|
||||
}
|
||||
|
||||
pub(crate) fn track_plugin_used(
|
||||
queue: &AnalyticsEventsQueue,
|
||||
analytics_enabled: Option<bool>,
|
||||
tracking: Option<TrackEventsContext>,
|
||||
plugin: PluginTelemetryMetadata,
|
||||
) {
|
||||
if analytics_enabled == Some(false) {
|
||||
return;
|
||||
}
|
||||
let Some(tracking) = tracking else {
|
||||
return;
|
||||
};
|
||||
if !queue.should_enqueue_plugin_used(&tracking, &plugin) {
|
||||
return;
|
||||
}
|
||||
let job = TrackEventsJob::PluginUsed(TrackPluginUsedJob {
|
||||
analytics_enabled,
|
||||
tracking,
|
||||
plugin,
|
||||
});
|
||||
queue.try_send(job);
|
||||
}
|
||||
|
||||
fn track_plugin_management(
|
||||
queue: &AnalyticsEventsQueue,
|
||||
analytics_enabled: Option<bool>,
|
||||
event_type: PluginManagementEventType,
|
||||
plugin: PluginTelemetryMetadata,
|
||||
) {
|
||||
if analytics_enabled == Some(false) {
|
||||
return;
|
||||
}
|
||||
let job = TrackPluginManagementJob {
|
||||
analytics_enabled,
|
||||
plugin,
|
||||
};
|
||||
let job = match event_type {
|
||||
PluginManagementEventType::Installed => TrackEventsJob::PluginInstalled(job),
|
||||
PluginManagementEventType::Uninstalled => TrackEventsJob::PluginUninstalled(job),
|
||||
PluginManagementEventType::Enabled => TrackEventsJob::PluginEnabled(job),
|
||||
PluginManagementEventType::Disabled => TrackEventsJob::PluginDisabled(job),
|
||||
};
|
||||
queue.try_send(job);
|
||||
}
|
||||
|
||||
async fn send_track_skill_invocations(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
job: TrackSkillInvocationsJob,
|
||||
) {
|
||||
let TrackSkillInvocationsJob {
|
||||
analytics_enabled,
|
||||
tracking,
|
||||
invocations,
|
||||
} = job;
|
||||
let mut events = Vec::with_capacity(invocations.len());
|
||||
for invocation in invocations {
|
||||
let skill_scope = match invocation.skill_scope {
|
||||
SkillScope::User => "user",
|
||||
SkillScope::Repo => "repo",
|
||||
SkillScope::System => "system",
|
||||
SkillScope::Admin => "admin",
|
||||
};
|
||||
let repo_root = get_git_repo_root(invocation.skill_path.as_path());
|
||||
let repo_url = if let Some(root) = repo_root.as_ref() {
|
||||
collect_git_info(root)
|
||||
.await
|
||||
.and_then(|info| info.repository_url)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let skill_id = skill_id_for_local_skill(
|
||||
repo_url.as_deref(),
|
||||
repo_root.as_deref(),
|
||||
invocation.skill_path.as_path(),
|
||||
invocation.skill_name.as_str(),
|
||||
);
|
||||
events.push(TrackEventRequest::SkillInvocation(
|
||||
SkillInvocationEventRequest {
|
||||
event_type: "skill_invocation",
|
||||
skill_id,
|
||||
skill_name: invocation.skill_name.clone(),
|
||||
event_params: SkillInvocationEventParams {
|
||||
thread_id: Some(tracking.thread_id.clone()),
|
||||
invoke_type: Some(invocation.invocation_type),
|
||||
model_slug: Some(tracking.model_slug.clone()),
|
||||
product_client_id: Some(originator().value),
|
||||
repo_url,
|
||||
skill_scope: Some(skill_scope.to_string()),
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
send_track_events(auth_manager, analytics_enabled, base_url, events).await;
|
||||
}
|
||||
|
||||
async fn send_track_app_mentioned(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
job: TrackAppMentionedJob,
|
||||
) {
|
||||
let TrackAppMentionedJob {
|
||||
analytics_enabled,
|
||||
tracking,
|
||||
mentions,
|
||||
} = job;
|
||||
let events = mentions
|
||||
.into_iter()
|
||||
.map(|mention| {
|
||||
let event_params = codex_app_metadata(&tracking, mention);
|
||||
TrackEventRequest::AppMentioned(CodexAppMentionedEventRequest {
|
||||
event_type: "codex_app_mentioned",
|
||||
event_params,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
send_track_events(auth_manager, analytics_enabled, base_url, events).await;
|
||||
}
|
||||
|
||||
async fn send_track_app_used(auth_manager: &AuthManager, base_url: &str, job: TrackAppUsedJob) {
|
||||
let TrackAppUsedJob {
|
||||
analytics_enabled,
|
||||
tracking,
|
||||
app,
|
||||
} = job;
|
||||
let event_params = codex_app_metadata(&tracking, app);
|
||||
let events = vec![TrackEventRequest::AppUsed(CodexAppUsedEventRequest {
|
||||
event_type: "codex_app_used",
|
||||
event_params,
|
||||
})];
|
||||
|
||||
send_track_events(auth_manager, analytics_enabled, base_url, events).await;
|
||||
}
|
||||
|
||||
async fn send_track_plugin_used(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
job: TrackPluginUsedJob,
|
||||
) {
|
||||
let TrackPluginUsedJob {
|
||||
analytics_enabled,
|
||||
tracking,
|
||||
plugin,
|
||||
} = job;
|
||||
let events = vec![TrackEventRequest::PluginUsed(CodexPluginUsedEventRequest {
|
||||
event_type: "codex_plugin_used",
|
||||
event_params: codex_plugin_used_metadata(&tracking, plugin),
|
||||
})];
|
||||
|
||||
send_track_events(auth_manager, analytics_enabled, base_url, events).await;
|
||||
}
|
||||
|
||||
async fn send_track_plugin_installed(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
job: TrackPluginManagementJob,
|
||||
) {
|
||||
send_track_plugin_management_event(auth_manager, base_url, job, "codex_plugin_installed").await;
|
||||
}
|
||||
|
||||
async fn send_track_plugin_uninstalled(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
job: TrackPluginManagementJob,
|
||||
) {
|
||||
send_track_plugin_management_event(auth_manager, base_url, job, "codex_plugin_uninstalled")
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send_track_plugin_enabled(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
job: TrackPluginManagementJob,
|
||||
) {
|
||||
send_track_plugin_management_event(auth_manager, base_url, job, "codex_plugin_enabled").await;
|
||||
}
|
||||
|
||||
async fn send_track_plugin_disabled(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
job: TrackPluginManagementJob,
|
||||
) {
|
||||
send_track_plugin_management_event(auth_manager, base_url, job, "codex_plugin_disabled").await;
|
||||
}
|
||||
|
||||
async fn send_track_plugin_management_event(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
job: TrackPluginManagementJob,
|
||||
event_type: &'static str,
|
||||
) {
|
||||
let TrackPluginManagementJob {
|
||||
analytics_enabled,
|
||||
plugin,
|
||||
} = job;
|
||||
let event_params = codex_plugin_metadata(plugin);
|
||||
let event = CodexPluginEventRequest {
|
||||
event_type,
|
||||
event_params,
|
||||
};
|
||||
let events = vec![match event_type {
|
||||
"codex_plugin_installed" => TrackEventRequest::PluginInstalled(event),
|
||||
"codex_plugin_uninstalled" => TrackEventRequest::PluginUninstalled(event),
|
||||
"codex_plugin_enabled" => TrackEventRequest::PluginEnabled(event),
|
||||
"codex_plugin_disabled" => TrackEventRequest::PluginDisabled(event),
|
||||
_ => unreachable!("unknown plugin management event type"),
|
||||
}];
|
||||
|
||||
send_track_events(auth_manager, analytics_enabled, base_url, events).await;
|
||||
}
|
||||
|
||||
fn codex_app_metadata(tracking: &TrackEventsContext, app: AppInvocation) -> CodexAppMetadata {
|
||||
CodexAppMetadata {
|
||||
connector_id: app.connector_id,
|
||||
thread_id: Some(tracking.thread_id.clone()),
|
||||
turn_id: Some(tracking.turn_id.clone()),
|
||||
app_name: app.app_name,
|
||||
product_client_id: Some(originator().value),
|
||||
invoke_type: app.invocation_type,
|
||||
model_slug: Some(tracking.model_slug.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPluginMetadata {
|
||||
let capability_summary = plugin.capability_summary;
|
||||
CodexPluginMetadata {
|
||||
plugin_id: Some(plugin.plugin_id.as_key()),
|
||||
plugin_name: Some(plugin.plugin_id.plugin_name),
|
||||
marketplace_name: Some(plugin.plugin_id.marketplace_name),
|
||||
has_skills: capability_summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.has_skills),
|
||||
mcp_server_count: capability_summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.mcp_server_names.len()),
|
||||
connector_ids: capability_summary.map(|summary| {
|
||||
summary
|
||||
.app_connector_ids
|
||||
.into_iter()
|
||||
.map(|connector_id| connector_id.0)
|
||||
.collect()
|
||||
}),
|
||||
product_client_id: Some(originator().value),
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_plugin_used_metadata(
|
||||
tracking: &TrackEventsContext,
|
||||
plugin: PluginTelemetryMetadata,
|
||||
) -> CodexPluginUsedMetadata {
|
||||
CodexPluginUsedMetadata {
|
||||
plugin: codex_plugin_metadata(plugin),
|
||||
thread_id: Some(tracking.thread_id.clone()),
|
||||
turn_id: Some(tracking.turn_id.clone()),
|
||||
model_slug: Some(tracking.model_slug.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_track_events(
|
||||
auth_manager: &AuthManager,
|
||||
analytics_enabled: Option<bool>,
|
||||
base_url: &str,
|
||||
events: Vec<TrackEventRequest>,
|
||||
) {
|
||||
if analytics_enabled == Some(false) {
|
||||
return;
|
||||
}
|
||||
if events.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(auth) = auth_manager.auth().await else {
|
||||
return;
|
||||
};
|
||||
if !auth.is_chatgpt_auth() {
|
||||
return;
|
||||
}
|
||||
let access_token = match auth.get_token() {
|
||||
Ok(token) => token,
|
||||
Err(_) => return,
|
||||
};
|
||||
let Some(account_id) = auth.get_account_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let base_url = base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/codex/analytics-events/events");
|
||||
let payload = TrackEventsRequest { events };
|
||||
|
||||
let response = create_client()
|
||||
.post(&url)
|
||||
.timeout(ANALYTICS_EVENTS_TIMEOUT)
|
||||
.bearer_auth(&access_token)
|
||||
.header("chatgpt-account-id", &account_id)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) if response.status().is_success() => {}
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
tracing::warn!("events failed with status {status}: {body}");
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to send events request: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn skill_id_for_local_skill(
|
||||
repo_url: Option<&str>,
|
||||
repo_root: Option<&Path>,
|
||||
skill_path: &Path,
|
||||
skill_name: &str,
|
||||
) -> String {
|
||||
let path = normalize_path_for_skill_id(repo_url, repo_root, skill_path);
|
||||
let prefix = if let Some(url) = repo_url {
|
||||
format!("repo_{url}")
|
||||
} else {
|
||||
"personal".to_string()
|
||||
};
|
||||
let raw_id = format!("{prefix}_{path}_{skill_name}");
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(raw_id.as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
/// Returns a normalized path for skill ID construction.
|
||||
///
|
||||
/// - Repo-scoped skills use a path relative to the repo root.
|
||||
/// - User/admin/system skills use an absolute path.
|
||||
fn normalize_path_for_skill_id(
|
||||
repo_url: Option<&str>,
|
||||
repo_root: Option<&Path>,
|
||||
skill_path: &Path,
|
||||
) -> String {
|
||||
let resolved_path =
|
||||
std::fs::canonicalize(skill_path).unwrap_or_else(|_| skill_path.to_path_buf());
|
||||
match (repo_url, repo_root) {
|
||||
(Some(_), Some(root)) => {
|
||||
let resolved_root = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
|
||||
resolved_path
|
||||
.strip_prefix(&resolved_root)
|
||||
.unwrap_or(resolved_path.as_path())
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
}
|
||||
_ => resolved_path.to_string_lossy().replace('\\', "/"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "analytics_client_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,298 +0,0 @@
|
||||
use super::AnalyticsEventsQueue;
|
||||
use super::AppInvocation;
|
||||
use super::CodexAppMentionedEventRequest;
|
||||
use super::CodexAppUsedEventRequest;
|
||||
use super::CodexPluginEventRequest;
|
||||
use super::CodexPluginUsedEventRequest;
|
||||
use super::InvocationType;
|
||||
use super::TrackEventRequest;
|
||||
use super::TrackEventsContext;
|
||||
use super::codex_app_metadata;
|
||||
use super::codex_plugin_metadata;
|
||||
use super::codex_plugin_used_metadata;
|
||||
use super::normalize_path_for_skill_id;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
fn expected_absolute_path(path: &PathBuf) -> String {
|
||||
std::fs::canonicalize(path)
|
||||
.unwrap_or_else(|_| path.to_path_buf())
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_path_for_skill_id_repo_scoped_uses_relative_path() {
|
||||
let repo_root = PathBuf::from("/repo/root");
|
||||
let skill_path = PathBuf::from("/repo/root/.codex/skills/doc/SKILL.md");
|
||||
|
||||
let path = normalize_path_for_skill_id(
|
||||
Some("https://example.com/repo.git"),
|
||||
Some(repo_root.as_path()),
|
||||
skill_path.as_path(),
|
||||
);
|
||||
|
||||
assert_eq!(path, ".codex/skills/doc/SKILL.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_path_for_skill_id_user_scoped_uses_absolute_path() {
|
||||
let skill_path = PathBuf::from("/Users/abc/.codex/skills/doc/SKILL.md");
|
||||
|
||||
let path = normalize_path_for_skill_id(
|
||||
/*repo_url*/ None,
|
||||
/*repo_root*/ None,
|
||||
skill_path.as_path(),
|
||||
);
|
||||
let expected = expected_absolute_path(&skill_path);
|
||||
|
||||
assert_eq!(path, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_path_for_skill_id_admin_scoped_uses_absolute_path() {
|
||||
let skill_path = PathBuf::from("/etc/codex/skills/doc/SKILL.md");
|
||||
|
||||
let path = normalize_path_for_skill_id(
|
||||
/*repo_url*/ None,
|
||||
/*repo_root*/ None,
|
||||
skill_path.as_path(),
|
||||
);
|
||||
let expected = expected_absolute_path(&skill_path);
|
||||
|
||||
assert_eq!(path, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_path_for_skill_id_repo_root_not_in_skill_path_uses_absolute_path() {
|
||||
let repo_root = PathBuf::from("/repo/root");
|
||||
let skill_path = PathBuf::from("/other/path/.codex/skills/doc/SKILL.md");
|
||||
|
||||
let path = normalize_path_for_skill_id(
|
||||
Some("https://example.com/repo.git"),
|
||||
Some(repo_root.as_path()),
|
||||
skill_path.as_path(),
|
||||
);
|
||||
let expected = expected_absolute_path(&skill_path);
|
||||
|
||||
assert_eq!(path, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_mentioned_event_serializes_expected_shape() {
|
||||
let tracking = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
};
|
||||
let event = TrackEventRequest::AppMentioned(CodexAppMentionedEventRequest {
|
||||
event_type: "codex_app_mentioned",
|
||||
event_params: codex_app_metadata(
|
||||
&tracking,
|
||||
AppInvocation {
|
||||
connector_id: Some("calendar".to_string()),
|
||||
app_name: Some("Calendar".to_string()),
|
||||
invocation_type: Some(InvocationType::Explicit),
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize app mentioned event");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_app_mentioned",
|
||||
"event_params": {
|
||||
"connector_id": "calendar",
|
||||
"thread_id": "thread-1",
|
||||
"turn_id": "turn-1",
|
||||
"app_name": "Calendar",
|
||||
"product_client_id": originator().value,
|
||||
"invoke_type": "explicit",
|
||||
"model_slug": "gpt-5"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_used_event_serializes_expected_shape() {
|
||||
let tracking = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
};
|
||||
let event = TrackEventRequest::AppUsed(CodexAppUsedEventRequest {
|
||||
event_type: "codex_app_used",
|
||||
event_params: codex_app_metadata(
|
||||
&tracking,
|
||||
AppInvocation {
|
||||
connector_id: Some("drive".to_string()),
|
||||
app_name: Some("Google Drive".to_string()),
|
||||
invocation_type: Some(InvocationType::Implicit),
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize app used event");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_app_used",
|
||||
"event_params": {
|
||||
"connector_id": "drive",
|
||||
"thread_id": "thread-2",
|
||||
"turn_id": "turn-2",
|
||||
"app_name": "Google Drive",
|
||||
"product_client_id": originator().value,
|
||||
"invoke_type": "implicit",
|
||||
"model_slug": "gpt-5"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_used_dedupe_is_keyed_by_turn_and_connector() {
|
||||
let (sender, _receiver) = mpsc::channel(1);
|
||||
let queue = AnalyticsEventsQueue {
|
||||
sender,
|
||||
app_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())),
|
||||
plugin_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())),
|
||||
};
|
||||
let app = AppInvocation {
|
||||
connector_id: Some("calendar".to_string()),
|
||||
app_name: Some("Calendar".to_string()),
|
||||
invocation_type: Some(InvocationType::Implicit),
|
||||
};
|
||||
|
||||
let turn_1 = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
};
|
||||
let turn_2 = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(queue.should_enqueue_app_used(&turn_1, &app), true);
|
||||
assert_eq!(queue.should_enqueue_app_used(&turn_1, &app), false);
|
||||
assert_eq!(queue.should_enqueue_app_used(&turn_2, &app), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_used_event_serializes_expected_shape() {
|
||||
let tracking = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-3".to_string(),
|
||||
turn_id: "turn-3".to_string(),
|
||||
};
|
||||
let event = TrackEventRequest::PluginUsed(CodexPluginUsedEventRequest {
|
||||
event_type: "codex_plugin_used",
|
||||
event_params: codex_plugin_used_metadata(&tracking, sample_plugin_metadata()),
|
||||
});
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize plugin used event");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_plugin_used",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 2,
|
||||
"connector_ids": ["calendar", "drive"],
|
||||
"product_client_id": originator().value,
|
||||
"thread_id": "thread-3",
|
||||
"turn_id": "turn-3",
|
||||
"model_slug": "gpt-5"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_management_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::PluginInstalled(CodexPluginEventRequest {
|
||||
event_type: "codex_plugin_installed",
|
||||
event_params: codex_plugin_metadata(sample_plugin_metadata()),
|
||||
});
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize plugin installed event");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 2,
|
||||
"connector_ids": ["calendar", "drive"],
|
||||
"product_client_id": originator().value
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_used_dedupe_is_keyed_by_turn_and_plugin() {
|
||||
let (sender, _receiver) = mpsc::channel(1);
|
||||
let queue = AnalyticsEventsQueue {
|
||||
sender,
|
||||
app_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())),
|
||||
plugin_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())),
|
||||
};
|
||||
let plugin = sample_plugin_metadata();
|
||||
|
||||
let turn_1 = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
};
|
||||
let turn_2 = TrackEventsContext {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(queue.should_enqueue_plugin_used(&turn_1, &plugin), true);
|
||||
assert_eq!(queue.should_enqueue_plugin_used(&turn_1, &plugin), false);
|
||||
assert_eq!(queue.should_enqueue_plugin_used(&turn_2, &plugin), true);
|
||||
}
|
||||
|
||||
fn sample_plugin_metadata() -> PluginTelemetryMetadata {
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("sample@test").expect("valid plugin id"),
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "sample@test".to_string(),
|
||||
display_name: "sample".to_string(),
|
||||
description: None,
|
||||
has_skills: true,
|
||||
mcp_server_names: vec!["mcp-1".to_string(), "mcp-2".to_string()],
|
||||
app_connector_ids: vec![
|
||||
AppConnectorId("calendar".to_string()),
|
||||
AppConnectorId("drive".to_string()),
|
||||
],
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
mod analytics_client;
|
||||
|
||||
pub use analytics_client::AnalyticsEventsClient;
|
||||
pub use analytics_client::AppInvocation;
|
||||
pub use analytics_client::InvocationType;
|
||||
pub use analytics_client::SkillInvocation;
|
||||
pub use analytics_client::TrackEventsContext;
|
||||
pub use analytics_client::build_track_events_context;
|
||||
@@ -1,6 +0,0 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "app-server-client",
|
||||
crate_name = "codex_app_server_client",
|
||||
)
|
||||
@@ -1,33 +0,0 @@
|
||||
[package]
|
||||
name = "codex-app-server-client"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_app_server_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-app-server = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
@@ -1,67 +0,0 @@
|
||||
# codex-app-server-client
|
||||
|
||||
Shared in-process app-server client used by conversational CLI surfaces:
|
||||
|
||||
- `codex-exec`
|
||||
- `codex-tui`
|
||||
|
||||
## Purpose
|
||||
|
||||
This crate centralizes startup and lifecycle management for an in-process
|
||||
`codex-app-server` runtime, so CLI clients do not need to duplicate:
|
||||
|
||||
- app-server bootstrap and initialize handshake
|
||||
- in-memory request/event transport wiring
|
||||
- lifecycle orchestration around caller-provided startup identity
|
||||
- graceful shutdown behavior
|
||||
|
||||
## Startup identity
|
||||
|
||||
Callers pass both the app-server `SessionSource` and the initialize
|
||||
`client_info.name` explicitly when starting the facade.
|
||||
|
||||
That keeps thread metadata (for example in `thread/list` and `thread/read`)
|
||||
aligned with the originating runtime without baking TUI/exec-specific policy
|
||||
into the shared client layer.
|
||||
|
||||
## Transport model
|
||||
|
||||
The in-process path uses typed channels:
|
||||
|
||||
- client -> server: `ClientRequest` / `ClientNotification`
|
||||
- server -> client: `InProcessServerEvent`
|
||||
- `ServerRequest`
|
||||
- `ServerNotification`
|
||||
- `LegacyNotification`
|
||||
|
||||
JSON serialization is still used at external transport boundaries
|
||||
(stdio/websocket), but the in-process hot path is typed.
|
||||
|
||||
Typed requests still receive app-server responses through the JSON-RPC
|
||||
result envelope internally. That is intentional: the in-process path is
|
||||
meant to preserve app-server semantics while removing the process
|
||||
boundary, not to introduce a second response contract.
|
||||
|
||||
## Bootstrap behavior
|
||||
|
||||
The client facade starts an already-initialized in-process runtime, but
|
||||
thread bootstrap still follows normal app-server flow:
|
||||
|
||||
- caller sends `thread/start` or `thread/resume`
|
||||
- app-server returns the immediate typed response
|
||||
- richer session metadata may arrive later as a `SessionConfigured`
|
||||
legacy event
|
||||
|
||||
Surfaces such as TUI and exec may therefore need a short bootstrap
|
||||
phase where they reconcile startup response data with later events.
|
||||
|
||||
## Backpressure and shutdown
|
||||
|
||||
- Queues are bounded and use `DEFAULT_IN_PROCESS_CHANNEL_CAPACITY` by default.
|
||||
- Full queues return explicit overload behavior instead of unbounded growth.
|
||||
- `shutdown()` performs a bounded graceful shutdown and then aborts if timeout
|
||||
is exceeded.
|
||||
|
||||
If the client falls behind on event consumption, the worker emits
|
||||
`InProcessServerEvent::Lagged` and may reject pending server requests so
|
||||
approval flows do not hang indefinitely behind a saturated queue.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,982 +0,0 @@
|
||||
/*
|
||||
This module implements the websocket-backed app-server client transport.
|
||||
|
||||
It owns the remote connection lifecycle, including the initialize/initialized
|
||||
handshake, JSON-RPC request/response routing, server-request resolution, and
|
||||
notification streaming. The rest of the crate uses the same `AppServerEvent`
|
||||
surface for both in-process and remote transports, so callers such as the TUI
|
||||
can switch between them without changing their higher-level session logic.
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Error as IoError;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Result as IoResult;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::AppServerEvent;
|
||||
use crate::RequestResult;
|
||||
use crate::SHUTDOWN_TIMEOUT;
|
||||
use crate::TypedRequestError;
|
||||
use crate::request_method_name;
|
||||
use crate::server_notification_requires_delivery;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientNotification;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::Result as JsonRpcResult;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::MaybeTlsStream;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||
use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RemoteAppServerConnectArgs {
|
||||
pub websocket_url: String,
|
||||
pub auth_token: Option<String>,
|
||||
pub client_name: String,
|
||||
pub client_version: String,
|
||||
pub experimental_api: bool,
|
||||
pub opt_out_notification_methods: Vec<String>,
|
||||
pub channel_capacity: usize,
|
||||
}
|
||||
|
||||
impl RemoteAppServerConnectArgs {
|
||||
fn initialize_params(&self) -> InitializeParams {
|
||||
let capabilities = InitializeCapabilities {
|
||||
experimental_api: self.experimental_api,
|
||||
opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.opt_out_notification_methods.clone())
|
||||
},
|
||||
};
|
||||
|
||||
InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: self.client_name.clone(),
|
||||
title: None,
|
||||
version: self.client_version.clone(),
|
||||
},
|
||||
capabilities: Some(capabilities),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn websocket_url_supports_auth_token(url: &Url) -> bool {
|
||||
match (url.scheme(), url.host()) {
|
||||
("wss", Some(_)) => true,
|
||||
("ws", Some(url::Host::Domain(domain))) => domain.eq_ignore_ascii_case("localhost"),
|
||||
("ws", Some(url::Host::Ipv4(addr))) => addr.is_loopback(),
|
||||
("ws", Some(url::Host::Ipv6(addr))) => addr.is_loopback(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
enum RemoteClientCommand {
|
||||
Request {
|
||||
request: Box<ClientRequest>,
|
||||
response_tx: oneshot::Sender<IoResult<RequestResult>>,
|
||||
},
|
||||
Notify {
|
||||
notification: ClientNotification,
|
||||
response_tx: oneshot::Sender<IoResult<()>>,
|
||||
},
|
||||
ResolveServerRequest {
|
||||
request_id: RequestId,
|
||||
result: JsonRpcResult,
|
||||
response_tx: oneshot::Sender<IoResult<()>>,
|
||||
},
|
||||
RejectServerRequest {
|
||||
request_id: RequestId,
|
||||
error: JSONRPCErrorError,
|
||||
response_tx: oneshot::Sender<IoResult<()>>,
|
||||
},
|
||||
Shutdown {
|
||||
response_tx: oneshot::Sender<IoResult<()>>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct RemoteAppServerClient {
|
||||
command_tx: mpsc::Sender<RemoteClientCommand>,
|
||||
event_rx: mpsc::Receiver<AppServerEvent>,
|
||||
pending_events: VecDeque<AppServerEvent>,
|
||||
worker_handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RemoteAppServerRequestHandle {
|
||||
command_tx: mpsc::Sender<RemoteClientCommand>,
|
||||
}
|
||||
|
||||
impl RemoteAppServerClient {
|
||||
pub async fn connect(args: RemoteAppServerConnectArgs) -> IoResult<Self> {
|
||||
let channel_capacity = args.channel_capacity.max(1);
|
||||
let websocket_url = args.websocket_url.clone();
|
||||
let url = Url::parse(&websocket_url).map_err(|err| {
|
||||
IoError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("invalid websocket URL `{websocket_url}`: {err}"),
|
||||
)
|
||||
})?;
|
||||
if args.auth_token.is_some() && !websocket_url_supports_auth_token(&url) {
|
||||
return Err(IoError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"remote auth tokens require `wss://` or loopback `ws://` URLs; got `{websocket_url}`"
|
||||
),
|
||||
));
|
||||
}
|
||||
let mut request = url.as_str().into_client_request().map_err(|err| {
|
||||
IoError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("invalid websocket URL `{websocket_url}`: {err}"),
|
||||
)
|
||||
})?;
|
||||
if let Some(auth_token) = args.auth_token.as_deref() {
|
||||
let header_value =
|
||||
HeaderValue::from_str(&format!("Bearer {auth_token}")).map_err(|err| {
|
||||
IoError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("invalid remote authorization header value: {err}"),
|
||||
)
|
||||
})?;
|
||||
request.headers_mut().insert(AUTHORIZATION, header_value);
|
||||
}
|
||||
let stream = timeout(CONNECT_TIMEOUT, connect_async(request))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::TimedOut,
|
||||
format!("timed out connecting to remote app server at `{websocket_url}`"),
|
||||
)
|
||||
})?
|
||||
.map(|(stream, _response)| stream)
|
||||
.map_err(|err| {
|
||||
IoError::other(format!(
|
||||
"failed to connect to remote app server at `{websocket_url}`: {err}"
|
||||
))
|
||||
})?;
|
||||
let mut stream = stream;
|
||||
let pending_events = initialize_remote_connection(
|
||||
&mut stream,
|
||||
&websocket_url,
|
||||
args.initialize_params(),
|
||||
INITIALIZE_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (command_tx, mut command_rx) = mpsc::channel::<RemoteClientCommand>(channel_capacity);
|
||||
let (event_tx, event_rx) = mpsc::channel::<AppServerEvent>(channel_capacity);
|
||||
let worker_handle = tokio::spawn(async move {
|
||||
let mut pending_requests =
|
||||
HashMap::<RequestId, oneshot::Sender<IoResult<RequestResult>>>::new();
|
||||
let mut skipped_events = 0usize;
|
||||
loop {
|
||||
tokio::select! {
|
||||
command = command_rx.recv() => {
|
||||
let Some(command) = command else {
|
||||
let _ = stream.close(None).await;
|
||||
break;
|
||||
};
|
||||
match command {
|
||||
RemoteClientCommand::Request { request, response_tx } => {
|
||||
let request_id = request_id_from_client_request(&request);
|
||||
if pending_requests.contains_key(&request_id) {
|
||||
let _ = response_tx.send(Err(IoError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("duplicate remote app-server request id `{request_id}`"),
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
pending_requests.insert(request_id.clone(), response_tx);
|
||||
if let Err(err) = write_jsonrpc_message(
|
||||
&mut stream,
|
||||
JSONRPCMessage::Request(jsonrpc_request_from_client_request(*request)),
|
||||
&websocket_url,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let err_message = err.to_string();
|
||||
if let Some(response_tx) = pending_requests.remove(&request_id) {
|
||||
let _ = response_tx.send(Err(err));
|
||||
}
|
||||
let _ = deliver_event(
|
||||
&event_tx,
|
||||
&mut skipped_events,
|
||||
AppServerEvent::Disconnected {
|
||||
message: format!(
|
||||
"remote app server at `{websocket_url}` write failed: {err_message}"
|
||||
),
|
||||
},
|
||||
&mut stream,
|
||||
)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
RemoteClientCommand::Notify { notification, response_tx } => {
|
||||
let result = write_jsonrpc_message(
|
||||
&mut stream,
|
||||
JSONRPCMessage::Notification(
|
||||
jsonrpc_notification_from_client_notification(notification),
|
||||
),
|
||||
&websocket_url,
|
||||
)
|
||||
.await;
|
||||
let _ = response_tx.send(result);
|
||||
}
|
||||
RemoteClientCommand::ResolveServerRequest {
|
||||
request_id,
|
||||
result,
|
||||
response_tx,
|
||||
} => {
|
||||
let result = write_jsonrpc_message(
|
||||
&mut stream,
|
||||
JSONRPCMessage::Response(JSONRPCResponse {
|
||||
id: request_id,
|
||||
result,
|
||||
}),
|
||||
&websocket_url,
|
||||
)
|
||||
.await;
|
||||
let _ = response_tx.send(result);
|
||||
}
|
||||
RemoteClientCommand::RejectServerRequest {
|
||||
request_id,
|
||||
error,
|
||||
response_tx,
|
||||
} => {
|
||||
let result = write_jsonrpc_message(
|
||||
&mut stream,
|
||||
JSONRPCMessage::Error(JSONRPCError {
|
||||
error,
|
||||
id: request_id,
|
||||
}),
|
||||
&websocket_url,
|
||||
)
|
||||
.await;
|
||||
let _ = response_tx.send(result);
|
||||
}
|
||||
RemoteClientCommand::Shutdown { response_tx } => {
|
||||
let close_result = stream.close(None).await.map_err(|err| {
|
||||
IoError::other(format!(
|
||||
"failed to close websocket app server `{websocket_url}`: {err}"
|
||||
))
|
||||
});
|
||||
let _ = response_tx.send(close_result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
message = stream.next() => {
|
||||
match message {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
match serde_json::from_str::<JSONRPCMessage>(&text) {
|
||||
Ok(JSONRPCMessage::Response(response)) => {
|
||||
if let Some(response_tx) = pending_requests.remove(&response.id) {
|
||||
let _ = response_tx.send(Ok(Ok(response.result)));
|
||||
}
|
||||
}
|
||||
Ok(JSONRPCMessage::Error(error)) => {
|
||||
if let Some(response_tx) = pending_requests.remove(&error.id) {
|
||||
let _ = response_tx.send(Ok(Err(error.error)));
|
||||
}
|
||||
}
|
||||
Ok(JSONRPCMessage::Notification(notification)) => {
|
||||
if let Some(event) =
|
||||
app_server_event_from_notification(notification)
|
||||
&& let Err(err) = deliver_event(
|
||||
&event_tx,
|
||||
&mut skipped_events,
|
||||
event,
|
||||
&mut stream,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(%err, "failed to deliver remote app-server event");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(JSONRPCMessage::Request(request)) => {
|
||||
let request_id = request.id.clone();
|
||||
let method = request.method.clone();
|
||||
match ServerRequest::try_from(request) {
|
||||
Ok(request) => {
|
||||
if let Err(err) = deliver_event(
|
||||
&event_tx,
|
||||
&mut skipped_events,
|
||||
AppServerEvent::ServerRequest(request),
|
||||
&mut stream,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(%err, "failed to deliver remote app-server server request");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(%err, method, "rejecting unknown remote app-server request");
|
||||
if let Err(reject_err) = write_jsonrpc_message(
|
||||
&mut stream,
|
||||
JSONRPCMessage::Error(JSONRPCError {
|
||||
error: JSONRPCErrorError {
|
||||
code: -32601,
|
||||
message: format!(
|
||||
"unsupported remote app-server request `{method}`"
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
id: request_id,
|
||||
}),
|
||||
&websocket_url,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let err_message = reject_err.to_string();
|
||||
let _ = deliver_event(
|
||||
&event_tx,
|
||||
&mut skipped_events,
|
||||
AppServerEvent::Disconnected {
|
||||
message: format!(
|
||||
"remote app server at `{websocket_url}` write failed: {err_message}"
|
||||
),
|
||||
},
|
||||
&mut stream,
|
||||
)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = deliver_event(
|
||||
&event_tx,
|
||||
&mut skipped_events,
|
||||
AppServerEvent::Disconnected {
|
||||
message: format!(
|
||||
"remote app server at `{websocket_url}` sent invalid JSON-RPC: {err}"
|
||||
),
|
||||
},
|
||||
&mut stream,
|
||||
)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Close(frame))) => {
|
||||
let reason = frame
|
||||
.as_ref()
|
||||
.map(|frame| frame.reason.to_string())
|
||||
.filter(|reason| !reason.is_empty())
|
||||
.unwrap_or_else(|| "connection closed".to_string());
|
||||
let _ = deliver_event(
|
||||
&event_tx,
|
||||
&mut skipped_events,
|
||||
AppServerEvent::Disconnected {
|
||||
message: format!(
|
||||
"remote app server at `{websocket_url}` disconnected: {reason}"
|
||||
),
|
||||
},
|
||||
&mut stream,
|
||||
)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
Some(Ok(Message::Binary(_)))
|
||||
| Some(Ok(Message::Ping(_)))
|
||||
| Some(Ok(Message::Pong(_)))
|
||||
| Some(Ok(Message::Frame(_))) => {}
|
||||
Some(Err(err)) => {
|
||||
let _ = deliver_event(
|
||||
&event_tx,
|
||||
&mut skipped_events,
|
||||
AppServerEvent::Disconnected {
|
||||
message: format!(
|
||||
"remote app server at `{websocket_url}` transport failed: {err}"
|
||||
),
|
||||
},
|
||||
&mut stream,
|
||||
)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
let _ = deliver_event(
|
||||
&event_tx,
|
||||
&mut skipped_events,
|
||||
AppServerEvent::Disconnected {
|
||||
message: format!(
|
||||
"remote app server at `{websocket_url}` closed the connection"
|
||||
),
|
||||
},
|
||||
&mut stream,
|
||||
)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let err = IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server worker channel is closed",
|
||||
);
|
||||
for (_, response_tx) in pending_requests {
|
||||
let _ = response_tx.send(Err(IoError::new(err.kind(), err.to_string())));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
command_tx,
|
||||
event_rx,
|
||||
pending_events: pending_events.into(),
|
||||
worker_handle,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn request_handle(&self) -> RemoteAppServerRequestHandle {
|
||||
RemoteAppServerRequestHandle {
|
||||
command_tx: self.command_tx.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
self.command_tx
|
||||
.send(RemoteClientCommand::Request {
|
||||
request: Box::new(request),
|
||||
response_tx,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server worker channel is closed",
|
||||
)
|
||||
})?;
|
||||
response_rx.await.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server request channel is closed",
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let method = request_method_name(&request);
|
||||
let response =
|
||||
self.request(request)
|
||||
.await
|
||||
.map_err(|source| TypedRequestError::Transport {
|
||||
method: method.clone(),
|
||||
source,
|
||||
})?;
|
||||
let result = response.map_err(|source| TypedRequestError::Server {
|
||||
method: method.clone(),
|
||||
source,
|
||||
})?;
|
||||
serde_json::from_value(result)
|
||||
.map_err(|source| TypedRequestError::Deserialize { method, source })
|
||||
}
|
||||
|
||||
pub async fn notify(&self, notification: ClientNotification) -> IoResult<()> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
self.command_tx
|
||||
.send(RemoteClientCommand::Notify {
|
||||
notification,
|
||||
response_tx,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server worker channel is closed",
|
||||
)
|
||||
})?;
|
||||
response_rx.await.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server notify channel is closed",
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
pub async fn resolve_server_request(
|
||||
&self,
|
||||
request_id: RequestId,
|
||||
result: JsonRpcResult,
|
||||
) -> IoResult<()> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
self.command_tx
|
||||
.send(RemoteClientCommand::ResolveServerRequest {
|
||||
request_id,
|
||||
result,
|
||||
response_tx,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server worker channel is closed",
|
||||
)
|
||||
})?;
|
||||
response_rx.await.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server resolve channel is closed",
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
pub async fn reject_server_request(
|
||||
&self,
|
||||
request_id: RequestId,
|
||||
error: JSONRPCErrorError,
|
||||
) -> IoResult<()> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
self.command_tx
|
||||
.send(RemoteClientCommand::RejectServerRequest {
|
||||
request_id,
|
||||
error,
|
||||
response_tx,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server worker channel is closed",
|
||||
)
|
||||
})?;
|
||||
response_rx.await.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server reject channel is closed",
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
pub async fn next_event(&mut self) -> Option<AppServerEvent> {
|
||||
if let Some(event) = self.pending_events.pop_front() {
|
||||
return Some(event);
|
||||
}
|
||||
self.event_rx.recv().await
|
||||
}
|
||||
|
||||
pub async fn shutdown(self) -> IoResult<()> {
|
||||
let Self {
|
||||
command_tx,
|
||||
event_rx,
|
||||
pending_events: _pending_events,
|
||||
worker_handle,
|
||||
} = self;
|
||||
let mut worker_handle = worker_handle;
|
||||
drop(event_rx);
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
if command_tx
|
||||
.send(RemoteClientCommand::Shutdown { response_tx })
|
||||
.await
|
||||
.is_ok()
|
||||
&& let Ok(command_result) = timeout(SHUTDOWN_TIMEOUT, response_rx).await
|
||||
{
|
||||
command_result.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server shutdown channel is closed",
|
||||
)
|
||||
})??;
|
||||
}
|
||||
|
||||
if let Err(_elapsed) = timeout(SHUTDOWN_TIMEOUT, &mut worker_handle).await {
|
||||
worker_handle.abort();
|
||||
let _ = worker_handle.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteAppServerRequestHandle {
|
||||
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
self.command_tx
|
||||
.send(RemoteClientCommand::Request {
|
||||
request: Box::new(request),
|
||||
response_tx,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server worker channel is closed",
|
||||
)
|
||||
})?;
|
||||
response_rx.await.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server request channel is closed",
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let method = request_method_name(&request);
|
||||
let response =
|
||||
self.request(request)
|
||||
.await
|
||||
.map_err(|source| TypedRequestError::Transport {
|
||||
method: method.clone(),
|
||||
source,
|
||||
})?;
|
||||
let result = response.map_err(|source| TypedRequestError::Server {
|
||||
method: method.clone(),
|
||||
source,
|
||||
})?;
|
||||
serde_json::from_value(result)
|
||||
.map_err(|source| TypedRequestError::Deserialize { method, source })
|
||||
}
|
||||
}
|
||||
|
||||
async fn initialize_remote_connection(
|
||||
stream: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
websocket_url: &str,
|
||||
params: InitializeParams,
|
||||
initialize_timeout: Duration,
|
||||
) -> IoResult<Vec<AppServerEvent>> {
|
||||
let initialize_request_id = RequestId::String("initialize".to_string());
|
||||
let mut pending_events = Vec::new();
|
||||
write_jsonrpc_message(
|
||||
stream,
|
||||
JSONRPCMessage::Request(jsonrpc_request_from_client_request(
|
||||
ClientRequest::Initialize {
|
||||
request_id: initialize_request_id.clone(),
|
||||
params,
|
||||
},
|
||||
)),
|
||||
websocket_url,
|
||||
)
|
||||
.await?;
|
||||
|
||||
timeout(initialize_timeout, async {
|
||||
loop {
|
||||
match stream.next().await {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
let message = serde_json::from_str::<JSONRPCMessage>(&text).map_err(|err| {
|
||||
IoError::other(format!(
|
||||
"remote app server at `{websocket_url}` sent invalid initialize response: {err}"
|
||||
))
|
||||
})?;
|
||||
match message {
|
||||
JSONRPCMessage::Response(response) if response.id == initialize_request_id => {
|
||||
break Ok(());
|
||||
}
|
||||
JSONRPCMessage::Error(error) if error.id == initialize_request_id => {
|
||||
break Err(IoError::other(format!(
|
||||
"remote app server at `{websocket_url}` rejected initialize: {}",
|
||||
error.error.message
|
||||
)));
|
||||
}
|
||||
JSONRPCMessage::Notification(notification) => {
|
||||
if let Some(event) = app_server_event_from_notification(notification) {
|
||||
pending_events.push(event);
|
||||
}
|
||||
}
|
||||
JSONRPCMessage::Request(request) => {
|
||||
let request_id = request.id.clone();
|
||||
let method = request.method.clone();
|
||||
match ServerRequest::try_from(request) {
|
||||
Ok(request) => {
|
||||
pending_events.push(AppServerEvent::ServerRequest(request));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(%err, method, "rejecting unknown remote app-server request during initialize");
|
||||
write_jsonrpc_message(
|
||||
stream,
|
||||
JSONRPCMessage::Error(JSONRPCError {
|
||||
error: JSONRPCErrorError {
|
||||
code: -32601,
|
||||
message: format!(
|
||||
"unsupported remote app-server request `{method}`"
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
id: request_id,
|
||||
}),
|
||||
websocket_url,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
JSONRPCMessage::Response(_) | JSONRPCMessage::Error(_) => {}
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Binary(_)))
|
||||
| Some(Ok(Message::Ping(_)))
|
||||
| Some(Ok(Message::Pong(_)))
|
||||
| Some(Ok(Message::Frame(_))) => {}
|
||||
Some(Ok(Message::Close(frame))) => {
|
||||
let reason = frame
|
||||
.as_ref()
|
||||
.map(|frame| frame.reason.to_string())
|
||||
.filter(|reason| !reason.is_empty())
|
||||
.unwrap_or_else(|| "connection closed during initialize".to_string());
|
||||
break Err(IoError::new(
|
||||
ErrorKind::ConnectionAborted,
|
||||
format!(
|
||||
"remote app server at `{websocket_url}` closed during initialize: {reason}"
|
||||
),
|
||||
));
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
break Err(IoError::other(format!(
|
||||
"remote app server at `{websocket_url}` transport failed during initialize: {err}"
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
break Err(IoError::new(
|
||||
ErrorKind::UnexpectedEof,
|
||||
format!("remote app server at `{websocket_url}` closed during initialize"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::TimedOut,
|
||||
format!("timed out waiting for initialize response from `{websocket_url}`"),
|
||||
)
|
||||
})??;
|
||||
|
||||
write_jsonrpc_message(
|
||||
stream,
|
||||
JSONRPCMessage::Notification(jsonrpc_notification_from_client_notification(
|
||||
ClientNotification::Initialized,
|
||||
)),
|
||||
websocket_url,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(pending_events)
|
||||
}
|
||||
|
||||
fn app_server_event_from_notification(notification: JSONRPCNotification) -> Option<AppServerEvent> {
|
||||
match ServerNotification::try_from(notification) {
|
||||
Ok(notification) => Some(AppServerEvent::ServerNotification(notification)),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn deliver_event(
|
||||
event_tx: &mpsc::Sender<AppServerEvent>,
|
||||
skipped_events: &mut usize,
|
||||
event: AppServerEvent,
|
||||
stream: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
) -> IoResult<()> {
|
||||
if *skipped_events > 0 {
|
||||
if event_requires_delivery(&event) {
|
||||
if event_tx
|
||||
.send(AppServerEvent::Lagged {
|
||||
skipped: *skipped_events,
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err(IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server event consumer channel is closed",
|
||||
));
|
||||
}
|
||||
*skipped_events = 0;
|
||||
} else {
|
||||
match event_tx.try_send(AppServerEvent::Lagged {
|
||||
skipped: *skipped_events,
|
||||
}) {
|
||||
Ok(()) => *skipped_events = 0,
|
||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||
*skipped_events = (*skipped_events).saturating_add(1);
|
||||
reject_if_server_request_dropped(stream, &event).await?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Closed(_)) => {
|
||||
return Err(IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server event consumer channel is closed",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if event_requires_delivery(&event) {
|
||||
event_tx.send(event).await.map_err(|_| {
|
||||
IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server event consumer channel is closed",
|
||||
)
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match event_tx.try_send(event) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(mpsc::error::TrySendError::Full(event)) => {
|
||||
*skipped_events = (*skipped_events).saturating_add(1);
|
||||
reject_if_server_request_dropped(stream, &event).await
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Closed(_)) => Err(IoError::new(
|
||||
ErrorKind::BrokenPipe,
|
||||
"remote app-server event consumer channel is closed",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn reject_if_server_request_dropped(
|
||||
stream: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
event: &AppServerEvent,
|
||||
) -> IoResult<()> {
|
||||
let AppServerEvent::ServerRequest(request) = event else {
|
||||
return Ok(());
|
||||
};
|
||||
write_jsonrpc_message(
|
||||
stream,
|
||||
JSONRPCMessage::Error(JSONRPCError {
|
||||
error: JSONRPCErrorError {
|
||||
code: -32001,
|
||||
message: "remote app-server event queue is full".to_string(),
|
||||
data: None,
|
||||
},
|
||||
id: request.id().clone(),
|
||||
}),
|
||||
"<remote-app-server>",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn event_requires_delivery(event: &AppServerEvent) -> bool {
|
||||
match event {
|
||||
AppServerEvent::ServerNotification(notification) => {
|
||||
server_notification_requires_delivery(notification)
|
||||
}
|
||||
AppServerEvent::Disconnected { .. } => true,
|
||||
AppServerEvent::Lagged { .. } | AppServerEvent::ServerRequest(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn request_id_from_client_request(request: &ClientRequest) -> RequestId {
|
||||
jsonrpc_request_from_client_request(request.clone()).id
|
||||
}
|
||||
|
||||
fn jsonrpc_request_from_client_request(request: ClientRequest) -> JSONRPCRequest {
|
||||
let value = match serde_json::to_value(request) {
|
||||
Ok(value) => value,
|
||||
Err(err) => panic!("client request should serialize: {err}"),
|
||||
};
|
||||
match serde_json::from_value(value) {
|
||||
Ok(request) => request,
|
||||
Err(err) => panic!("client request should encode as JSON-RPC request: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn jsonrpc_notification_from_client_notification(
|
||||
notification: ClientNotification,
|
||||
) -> JSONRPCNotification {
|
||||
let value = match serde_json::to_value(notification) {
|
||||
Ok(value) => value,
|
||||
Err(err) => panic!("client notification should serialize: {err}"),
|
||||
};
|
||||
match serde_json::from_value(value) {
|
||||
Ok(notification) => notification,
|
||||
Err(err) => panic!("client notification should encode as JSON-RPC notification: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_jsonrpc_message(
|
||||
stream: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
message: JSONRPCMessage,
|
||||
websocket_url: &str,
|
||||
) -> IoResult<()> {
|
||||
let payload = serde_json::to_string(&message).map_err(IoError::other)?;
|
||||
stream
|
||||
.send(Message::Text(payload.into()))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
IoError::other(format!(
|
||||
"failed to write websocket message to `{websocket_url}`: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn event_requires_delivery_marks_transcript_and_disconnect_events() {
|
||||
assert!(event_requires_delivery(
|
||||
&AppServerEvent::ServerNotification(ServerNotification::AgentMessageDelta(
|
||||
codex_app_server_protocol::AgentMessageDeltaNotification {
|
||||
thread_id: "thread".to_string(),
|
||||
turn_id: "turn".to_string(),
|
||||
item_id: "item".to_string(),
|
||||
delta: "hello".to_string(),
|
||||
},
|
||||
),)
|
||||
));
|
||||
assert!(event_requires_delivery(
|
||||
&AppServerEvent::ServerNotification(ServerNotification::ItemCompleted(
|
||||
codex_app_server_protocol::ItemCompletedNotification {
|
||||
thread_id: "thread".to_string(),
|
||||
turn_id: "turn".to_string(),
|
||||
item: codex_app_server_protocol::ThreadItem::Plan {
|
||||
id: "item".to_string(),
|
||||
text: "step".to_string(),
|
||||
},
|
||||
}
|
||||
),)
|
||||
));
|
||||
assert!(event_requires_delivery(&AppServerEvent::Disconnected {
|
||||
message: "closed".to_string(),
|
||||
}));
|
||||
assert!(!event_requires_delivery(&AppServerEvent::Lagged {
|
||||
skipped: 1
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -14,23 +14,15 @@ workspace = true
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-experimental-api-macros = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-experimental-api-macros = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"base64",
|
||||
"macros",
|
||||
"schemars",
|
||||
"server",
|
||||
] }
|
||||
ts-rs = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"FileChange": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"add"
|
||||
],
|
||||
"title": "AddFileChangeType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"type"
|
||||
],
|
||||
"title": "AddFileChange",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"delete"
|
||||
],
|
||||
"title": "DeleteFileChangeType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"type"
|
||||
],
|
||||
"title": "DeleteFileChange",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"move_path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"update"
|
||||
],
|
||||
"title": "UpdateFileChangeType",
|
||||
"type": "string"
|
||||
},
|
||||
"unified_diff": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"unified_diff"
|
||||
],
|
||||
"title": "UpdateFileChange",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"callId": {
|
||||
"description": "Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent] and [codex_protocol::protocol::PatchApplyEndEvent].",
|
||||
"type": "string"
|
||||
},
|
||||
"conversationId": {
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
},
|
||||
"fileChanges": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/FileChange"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"grantRoot": {
|
||||
"description": "When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"reason": {
|
||||
"description": "Optional explanatory reason (e.g. request for extra write access).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"callId",
|
||||
"conversationId",
|
||||
"fileChanges"
|
||||
],
|
||||
"title": "ApplyPatchApprovalParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"NetworkPolicyAmendment": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/NetworkPolicyRuleAction"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"host"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkPolicyRuleAction": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReviewDecision": {
|
||||
"description": "User's decision in response to an ExecApprovalRequest.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "User has approved this command and the agent should execute it.",
|
||||
"enum": [
|
||||
"approved"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.",
|
||||
"properties": {
|
||||
"approved_execpolicy_amendment": {
|
||||
"properties": {
|
||||
"proposed_execpolicy_amendment": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"proposed_execpolicy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"approved_execpolicy_amendment"
|
||||
],
|
||||
"title": "ApprovedExecpolicyAmendmentReviewDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.",
|
||||
"enum": [
|
||||
"approved_for_session"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.",
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"$ref": "#/definitions/NetworkPolicyAmendment"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"title": "NetworkPolicyAmendmentReviewDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
|
||||
"enum": [
|
||||
"denied"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User has denied this command and the agent should not do anything until the user's next command.",
|
||||
"enum": [
|
||||
"abort"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"decision": {
|
||||
"$ref": "#/definitions/ReviewDecision"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"decision"
|
||||
],
|
||||
"title": "ApplyPatchApprovalResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ChatgptAuthTokensRefreshReason": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex attempted a backend request and received `401 Unauthorized`.",
|
||||
"enum": [
|
||||
"unauthorized"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"previousAccountId": {
|
||||
"description": "Workspace/account identifier that Codex was previously using.\n\nClients that manage multiple accounts/workspaces can use this as a hint to refresh the token for the correct workspace.\n\nThis may be `null` when the prior auth state did not include a workspace identifier (`chatgpt_account_id`).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/ChatgptAuthTokensRefreshReason"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reason"
|
||||
],
|
||||
"title": "ChatgptAuthTokensRefreshParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"accessToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"chatgptAccountId": {
|
||||
"type": "string"
|
||||
},
|
||||
"chatgptPlanType": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessToken",
|
||||
"chatgptAccountId"
|
||||
],
|
||||
"title": "ChatgptAuthTokensRefreshResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"initialized"
|
||||
],
|
||||
"title": "InitializedNotificationMethod",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
],
|
||||
"title": "InitializedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"title": "ClientNotification"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,390 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalPermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AdditionalFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AdditionalNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"CommandAction": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"read"
|
||||
],
|
||||
"title": "ReadCommandActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "ReadCommandAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"listFiles"
|
||||
],
|
||||
"title": "ListFilesCommandActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"type"
|
||||
],
|
||||
"title": "ListFilesCommandAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"query": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"search"
|
||||
],
|
||||
"title": "SearchCommandActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"type"
|
||||
],
|
||||
"title": "SearchCommandAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"title": "UnknownCommandActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"type"
|
||||
],
|
||||
"title": "UnknownCommandAction",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionApprovalDecision": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "User approved the command.",
|
||||
"enum": [
|
||||
"accept"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.",
|
||||
"enum": [
|
||||
"acceptForSession"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.",
|
||||
"properties": {
|
||||
"acceptWithExecpolicyAmendment": {
|
||||
"properties": {
|
||||
"execpolicy_amendment": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"execpolicy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"acceptWithExecpolicyAmendment"
|
||||
],
|
||||
"title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User chose a persistent network policy rule (allow/deny) for this host.",
|
||||
"properties": {
|
||||
"applyNetworkPolicyAmendment": {
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"$ref": "#/definitions/NetworkPolicyAmendment"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"applyNetworkPolicyAmendment"
|
||||
],
|
||||
"title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User denied the command. The agent will continue the turn.",
|
||||
"enum": [
|
||||
"decline"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User denied the command. The turn will also be immediately interrupted.",
|
||||
"enum": [
|
||||
"cancel"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkApprovalContext": {
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"protocol": {
|
||||
"$ref": "#/definitions/NetworkApprovalProtocol"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"protocol"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"socks5Tcp",
|
||||
"socks5Udp"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkPolicyAmendment": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/NetworkPolicyRuleAction"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"host"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkPolicyRuleAction": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"approvalId": {
|
||||
"description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is null.\n\nFor zsh-exec-bridge subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"command": {
|
||||
"description": "The command to be executed.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"commandActions": {
|
||||
"description": "Best-effort parsed command actions for friendly display.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CommandAction"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cwd": {
|
||||
"description": "The command's working directory.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
"networkApprovalContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NetworkApprovalContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional context for a managed-network approval prompt."
|
||||
},
|
||||
"proposedExecpolicyAmendment": {
|
||||
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"proposedNetworkPolicyAmendments": {
|
||||
"description": "Optional proposed network policy amendments (allow/deny host) for future requests.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NetworkPolicyAmendment"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"reason": {
|
||||
"description": "Optional explanatory reason (e.g. request for network access).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"itemId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"title": "CommandExecutionRequestApprovalParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"CommandExecutionApprovalDecision": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "User approved the command.",
|
||||
"enum": [
|
||||
"accept"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.",
|
||||
"enum": [
|
||||
"acceptForSession"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.",
|
||||
"properties": {
|
||||
"acceptWithExecpolicyAmendment": {
|
||||
"properties": {
|
||||
"execpolicy_amendment": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"execpolicy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"acceptWithExecpolicyAmendment"
|
||||
],
|
||||
"title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User chose a persistent network policy rule (allow/deny) for this host.",
|
||||
"properties": {
|
||||
"applyNetworkPolicyAmendment": {
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"$ref": "#/definitions/NetworkPolicyAmendment"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"applyNetworkPolicyAmendment"
|
||||
],
|
||||
"title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User denied the command. The agent will continue the turn.",
|
||||
"enum": [
|
||||
"decline"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User denied the command. The turn will also be immediately interrupted.",
|
||||
"enum": [
|
||||
"cancel"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkPolicyAmendment": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/NetworkPolicyRuleAction"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"host"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkPolicyRuleAction": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"decision": {
|
||||
"$ref": "#/definitions/CommandExecutionApprovalDecision"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"decision"
|
||||
],
|
||||
"title": "CommandExecutionRequestApprovalResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"arguments": true,
|
||||
"callId": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tool": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"arguments",
|
||||
"callId",
|
||||
"threadId",
|
||||
"tool",
|
||||
"turnId"
|
||||
],
|
||||
"title": "DynamicToolCallParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"DynamicToolCallOutputContentItem": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"inputText"
|
||||
],
|
||||
"title": "InputTextDynamicToolCallOutputContentItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text",
|
||||
"type"
|
||||
],
|
||||
"title": "InputTextDynamicToolCallOutputContentItem",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"imageUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"inputImage"
|
||||
],
|
||||
"title": "InputImageDynamicToolCallOutputContentItemType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"imageUrl",
|
||||
"type"
|
||||
],
|
||||
"title": "InputImageDynamicToolCallOutputContentItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"contentItems": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/DynamicToolCallOutputContentItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"contentItems",
|
||||
"success"
|
||||
],
|
||||
"title": "DynamicToolCallResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ParsedCommand": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"cmd": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "(Best effort) Path to the file being read by the command. When possible, this is an absolute path, though when relative, it should be resolved against the `cwd`` that will be used to run the command to derive the absolute path.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"read"
|
||||
],
|
||||
"title": "ReadParsedCommandType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cmd",
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "ReadParsedCommand",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"cmd": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"list_files"
|
||||
],
|
||||
"title": "ListFilesParsedCommandType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cmd",
|
||||
"type"
|
||||
],
|
||||
"title": "ListFilesParsedCommand",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"cmd": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"query": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"search"
|
||||
],
|
||||
"title": "SearchParsedCommandType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cmd",
|
||||
"type"
|
||||
],
|
||||
"title": "SearchParsedCommand",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"cmd": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"title": "UnknownParsedCommandType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cmd",
|
||||
"type"
|
||||
],
|
||||
"title": "UnknownParsedCommand",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"approvalId": {
|
||||
"description": "Identifier for this specific approval callback.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"callId": {
|
||||
"description": "Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent] and [codex_protocol::protocol::ExecCommandEndEvent].",
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"conversationId": {
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"parsedCmd": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ParsedCommand"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"reason": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"callId",
|
||||
"command",
|
||||
"conversationId",
|
||||
"cwd",
|
||||
"parsedCmd"
|
||||
],
|
||||
"title": "ExecCommandApprovalParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"NetworkPolicyAmendment": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/NetworkPolicyRuleAction"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"host"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkPolicyRuleAction": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReviewDecision": {
|
||||
"description": "User's decision in response to an ExecApprovalRequest.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "User has approved this command and the agent should execute it.",
|
||||
"enum": [
|
||||
"approved"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.",
|
||||
"properties": {
|
||||
"approved_execpolicy_amendment": {
|
||||
"properties": {
|
||||
"proposed_execpolicy_amendment": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"proposed_execpolicy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"approved_execpolicy_amendment"
|
||||
],
|
||||
"title": "ApprovedExecpolicyAmendmentReviewDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.",
|
||||
"enum": [
|
||||
"approved_for_session"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.",
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"properties": {
|
||||
"network_policy_amendment": {
|
||||
"$ref": "#/definitions/NetworkPolicyAmendment"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network_policy_amendment"
|
||||
],
|
||||
"title": "NetworkPolicyAmendmentReviewDecision",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
|
||||
"enum": [
|
||||
"denied"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User has denied this command and the agent should not do anything until the user's next command.",
|
||||
"enum": [
|
||||
"abort"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"decision": {
|
||||
"$ref": "#/definitions/ReviewDecision"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"decision"
|
||||
],
|
||||
"title": "ExecCommandApprovalResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"grantRoot": {
|
||||
"description": "[UNSTABLE] When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"description": "Optional explanatory reason (e.g. request for extra write access).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"itemId",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"title": "FileChangeRequestApprovalParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"FileChangeApprovalDecision": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "User approved the file changes.",
|
||||
"enum": [
|
||||
"accept"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User approved the file changes and future changes to the same files should run without prompting.",
|
||||
"enum": [
|
||||
"acceptForSession"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User denied the file changes. The agent will continue the turn.",
|
||||
"enum": [
|
||||
"decline"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "User denied the file changes. The turn will also be immediately interrupted.",
|
||||
"enum": [
|
||||
"cancel"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"decision": {
|
||||
"$ref": "#/definitions/FileChangeApprovalDecision"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"decision"
|
||||
],
|
||||
"title": "FileChangeRequestApprovalResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cancellationToken": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"roots": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query",
|
||||
"roots"
|
||||
],
|
||||
"title": "FuzzyFileSearchParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchResult": {
|
||||
"description": "Superset of [`codex_file_search::FileMatch`]",
|
||||
"properties": {
|
||||
"file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"indices": {
|
||||
"items": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"root": {
|
||||
"type": "string"
|
||||
},
|
||||
"score": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"files": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchResult"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"files"
|
||||
],
|
||||
"title": "FuzzyFileSearchResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchResult": {
|
||||
"description": "Superset of [`codex_file_search::FileMatch`]",
|
||||
"properties": {
|
||||
"file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"indices": {
|
||||
"items": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"root": {
|
||||
"type": "string"
|
||||
},
|
||||
"score": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"files": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchResult"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"files",
|
||||
"query",
|
||||
"sessionId"
|
||||
],
|
||||
"title": "FuzzyFileSearchSessionUpdatedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"JSONRPCErrorError": {
|
||||
"properties": {
|
||||
"code": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"data": true,
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code",
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"description": "A response to a request that indicates an error occurred.",
|
||||
"properties": {
|
||||
"error": {
|
||||
"$ref": "#/definitions/JSONRPCErrorError"
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error",
|
||||
"id"
|
||||
],
|
||||
"title": "JSONRPCError",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"code": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"data": true,
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code",
|
||||
"message"
|
||||
],
|
||||
"title": "JSONRPCErrorError",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/JSONRPCRequest"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/JSONRPCNotification"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/JSONRPCResponse"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/JSONRPCError"
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"JSONRPCError": {
|
||||
"description": "A response to a request that indicates an error occurred.",
|
||||
"properties": {
|
||||
"error": {
|
||||
"$ref": "#/definitions/JSONRPCErrorError"
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error",
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"JSONRPCErrorError": {
|
||||
"properties": {
|
||||
"code": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"data": true,
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code",
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"JSONRPCNotification": {
|
||||
"description": "A notification which does not expect a response.",
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"params": true
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"JSONRPCRequest": {
|
||||
"description": "A request that expects a response.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"params": true,
|
||||
"trace": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/W3cTraceContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional W3C Trace Context for distributed tracing."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"JSONRPCResponse": {
|
||||
"description": "A successful (non-error) response to a request.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"result": true
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"result"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"W3cTraceContext": {
|
||||
"properties": {
|
||||
"traceparent": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tracestate": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent.",
|
||||
"title": "JSONRPCMessage"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "A notification which does not expect a response.",
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"params": true
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
],
|
||||
"title": "JSONRPCNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"W3cTraceContext": {
|
||||
"properties": {
|
||||
"traceparent": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tracestate": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "A request that expects a response.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"params": true,
|
||||
"trace": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/W3cTraceContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional W3C Trace Context for distributed tracing."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method"
|
||||
],
|
||||
"title": "JSONRPCRequest",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"description": "A successful (non-error) response to a request.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"result": true
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"result"
|
||||
],
|
||||
"title": "JSONRPCResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,609 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"McpElicitationArrayType": {
|
||||
"enum": [
|
||||
"array"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationBooleanSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationBooleanType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationBooleanType": {
|
||||
"enum": [
|
||||
"boolean"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationConstOption": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"const": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"const",
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationEnumSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationSingleSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationMultiSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationLegacyTitledEnumSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationLegacyTitledEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"enumNames": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enum",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationMultiSelectEnumSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationUntitledMultiSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationTitledMultiSelectEnumSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationNumberSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"format": "double",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"maximum": {
|
||||
"format": "double",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minimum": {
|
||||
"format": "double",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationNumberType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationNumberType": {
|
||||
"enum": [
|
||||
"number",
|
||||
"integer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationObjectType": {
|
||||
"enum": [
|
||||
"object"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationPrimitiveSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationStringSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationNumberSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationBooleanSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationSchema": {
|
||||
"additionalProperties": false,
|
||||
"description": "Typed form schema for MCP `elicitation/create` requests.\n\nThis matches the `requestedSchema` shape from the MCP 2025-11-25 `ElicitRequestFormParams` schema.",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/McpElicitationPrimitiveSchema"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"required": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationObjectType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"properties",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationSingleSelectEnumSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationUntitledSingleSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationTitledSingleSelectEnumSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationStringFormat": {
|
||||
"enum": [
|
||||
"email",
|
||||
"uri",
|
||||
"date",
|
||||
"date-time"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationStringSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationStringFormat"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"maxLength": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minLength": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationStringType": {
|
||||
"enum": [
|
||||
"string"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationTitledEnumItems": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"anyOf": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationConstOption"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"anyOf"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationTitledMultiSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationTitledEnumItems"
|
||||
},
|
||||
"maxItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationArrayType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"items",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationTitledSingleSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"oneOf": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationConstOption"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"oneOf",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationUntitledEnumItems": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enum": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enum",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationUntitledMultiSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationUntitledEnumItems"
|
||||
},
|
||||
"maxItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationArrayType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"items",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationUntitledSingleSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enum",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"form"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"requestedSchema": {
|
||||
"$ref": "#/definitions/McpElicitationSchema"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"mode",
|
||||
"requestedSchema"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"elicitationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"url"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"elicitationId",
|
||||
"message",
|
||||
"mode",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"serverName": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"description": "Active Codex turn when this elicitation was observed, if app-server could correlate one.\n\nThis is nullable because MCP models elicitation as a standalone server-to-client request identified by the MCP server request id. It may be triggered during a turn, but turn context is app-server correlation rather than part of the protocol identity of the elicitation itself.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"serverName",
|
||||
"threadId"
|
||||
],
|
||||
"title": "McpServerElicitationRequestParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"McpServerElicitationAction": {
|
||||
"enum": [
|
||||
"accept",
|
||||
"decline",
|
||||
"cancel"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional client metadata for form-mode action handling."
|
||||
},
|
||||
"action": {
|
||||
"$ref": "#/definitions/McpServerElicitationAction"
|
||||
},
|
||||
"content": {
|
||||
"description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"title": "McpServerElicitationRequestResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"RequestPermissionProfile": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AdditionalFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AdditionalNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/RequestPermissionProfile"
|
||||
},
|
||||
"reason": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"itemId",
|
||||
"permissions",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"title": "PermissionsRequestApprovalParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"GrantedPermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AdditionalFileSystemPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AdditionalNetworkPermissions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionGrantScope": {
|
||||
"enum": [
|
||||
"turn",
|
||||
"session"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/GrantedPermissionProfile"
|
||||
},
|
||||
"scope": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PermissionGrantScope"
|
||||
}
|
||||
],
|
||||
"default": "turn"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"title": "PermissionsRequestApprovalResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
],
|
||||
"title": "RequestId"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ToolRequestUserInputOption": {
|
||||
"description": "EXPERIMENTAL. Defines a single selectable option for request_user_input.",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"label"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ToolRequestUserInputQuestion": {
|
||||
"description": "EXPERIMENTAL. Represents one request_user_input question and its required options.",
|
||||
"properties": {
|
||||
"header": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isOther": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"isSecret": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"options": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ToolRequestUserInputOption"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"question": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"header",
|
||||
"id",
|
||||
"question"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "EXPERIMENTAL. Params sent with a request_user_input event.",
|
||||
"properties": {
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
"questions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ToolRequestUserInputQuestion"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"itemId",
|
||||
"questions",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"title": "ToolRequestUserInputParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ToolRequestUserInputAnswer": {
|
||||
"description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.",
|
||||
"properties": {
|
||||
"answers": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"answers"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "EXPERIMENTAL. Response payload mapping question ids to answers.",
|
||||
"properties": {
|
||||
"answers": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/ToolRequestUserInputAnswer"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"answers"
|
||||
],
|
||||
"title": "ToolRequestUserInputResponse",
|
||||
"type": "object"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ClientInfo": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"InitializeCapabilities": {
|
||||
"description": "Client-declared capabilities negotiated during initialize.",
|
||||
"properties": {
|
||||
"experimentalApi": {
|
||||
"default": false,
|
||||
"description": "Opt into receiving experimental API methods and fields.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"optOutNotificationMethods": {
|
||||
"description": "Exact notification method names that should be suppressed for this connection (for example `thread/started`).",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"capabilities": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InitializeCapabilities"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"clientInfo": {
|
||||
"$ref": "#/definitions/ClientInfo"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"clientInfo"
|
||||
],
|
||||
"title": "InitializeParams",
|
||||
"type": "object"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user