mirror of
https://github.com/openai/codex.git
synced 2026-04-08 23:04:47 +00:00
Compare commits
238 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32a0c52acd | ||
|
|
6ccf770583 | ||
|
|
600c3e49e0 | ||
|
|
08797193aa | ||
|
|
fb3dcfde1d | ||
|
|
6c36e7d688 | ||
|
|
f480b98984 | ||
|
|
e9702411ab | ||
|
|
d45513ce5a | ||
|
|
8614f92fc4 | ||
|
|
59af4a730c | ||
|
|
470b3592e6 | ||
|
|
8a13f82204 | ||
|
|
252d79f5eb | ||
|
|
365154d5da | ||
|
|
b525b5a3a7 | ||
|
|
f1a2b920f9 | ||
|
|
0b9e42f6f7 | ||
|
|
5d1671ca70 | ||
|
|
2b9bf5d3d4 | ||
|
|
cd591dc457 | ||
|
|
feb4f0051a | ||
|
|
82506527f1 | ||
|
|
3b32de4fab | ||
|
|
4cc6818996 | ||
|
|
413c1e1fdf | ||
|
|
89f1a44afa | ||
|
|
99f167e6bf | ||
|
|
68e16baabe | ||
|
|
2a8c3a2a52 | ||
|
|
e2bb45bb24 | ||
|
|
51f75e2f56 | ||
|
|
741e2fdeb8 | ||
|
|
90320fc51a | ||
|
|
24c598e8a9 | ||
|
|
4bb507d2c4 | ||
|
|
232db0613a | ||
|
|
9d13d29acd | ||
|
|
806e5f7c69 | ||
|
|
5fe9ef06ce | ||
|
|
ee12772e80 | ||
|
|
b34a3a6e92 | ||
|
|
0de7662dab | ||
|
|
1f2411629f | ||
|
|
d2df7c54b2 | ||
|
|
a504d8f0fa | ||
|
|
9f737c28dd | ||
|
|
756c45ec61 | ||
|
|
e88c2cf4d7 | ||
|
|
54faa76960 | ||
|
|
e62d645e67 | ||
|
|
03edd4fbee | ||
|
|
36cd163504 | ||
|
|
73dab2046f | ||
|
|
03c07956cf | ||
|
|
756ba8baae | ||
|
|
1525bbdb9a | ||
|
|
46b7e4fb2c | ||
|
|
9bb813353e | ||
|
|
bd30bad96f | ||
|
|
4eabc3dcb1 | ||
|
|
0225479f0d | ||
|
|
2b4cc221df | ||
|
|
ded559680d | ||
|
|
4ce97cef02 | ||
|
|
54dbbb839e | ||
|
|
f44eb29181 | ||
|
|
4294031a93 | ||
|
|
fb41a79f37 | ||
|
|
ab58141e22 | ||
|
|
82b061afb2 | ||
|
|
624c69e840 | ||
|
|
e65ee38579 | ||
|
|
d9b899309d | ||
|
|
b5edeb98a0 | ||
|
|
152b676597 | ||
|
|
4fd5c35c4f | ||
|
|
cca36c5681 | ||
|
|
9e19004bc2 | ||
|
|
39097ab65d | ||
|
|
3a22e10172 | ||
|
|
c9e706f8b6 | ||
|
|
8a19dbb177 | ||
|
|
6edb865cc6 | ||
|
|
13d828d236 | ||
|
|
e4f1b3a65e | ||
|
|
91ca49e53c | ||
|
|
8d19646861 | ||
|
|
f263607c60 | ||
|
|
eaf12beacf | ||
|
|
4b8bab6ad3 | ||
|
|
a70aee1a1e | ||
|
|
567d2603b8 | ||
|
|
3d8cdac797 | ||
|
|
1d4b5f130c | ||
|
|
dc07108af8 | ||
|
|
faab4d39e1 | ||
|
|
0ab8eda375 | ||
|
|
a71fc47cf8 | ||
|
|
1cc87019b4 | ||
|
|
0f7394883e | ||
|
|
a3b3e7a6cc | ||
|
|
cc8fd0ff65 | ||
|
|
af8a9d2d2b | ||
|
|
b15c918836 | ||
|
|
14f95db57b | ||
|
|
b4787bf4c0 | ||
|
|
6fff9955f1 | ||
|
|
8cd7f20b48 | ||
|
|
6db6de031a | ||
|
|
beb3978a3b | ||
|
|
862158b9e9 | ||
|
|
cb9fb562a4 | ||
|
|
95e809c135 | ||
|
|
7a3eec6fdb | ||
|
|
3c7f013f97 | ||
|
|
0bd31dc382 | ||
|
|
9bb7f0a694 | ||
|
|
93380a6fac | ||
|
|
57b98bc4cd | ||
|
|
f5d39a88ce | ||
|
|
c0f2fed67e | ||
|
|
30ee9e769e | ||
|
|
cb8dc18a64 | ||
|
|
5d64e58a38 | ||
|
|
f894c3f687 | ||
|
|
2146e1b82d | ||
|
|
1d8a22e9dd | ||
|
|
95b0b5a204 | ||
|
|
a098834148 | ||
|
|
7fc36249b5 | ||
|
|
ea27d861b2 | ||
|
|
ab6cce62b8 | ||
|
|
e47ed5e57f | ||
|
|
bd50496411 | ||
|
|
627299c551 | ||
|
|
97df35c74f | ||
|
|
c1d18ceb6f | ||
|
|
e846fed2b1 | ||
|
|
f32a5e84bf | ||
|
|
5131e0de45 | ||
|
|
828b837235 | ||
|
|
52e779d35d | ||
|
|
aa2403e2eb | ||
|
|
9f71d57a65 | ||
|
|
cc97982bbb | ||
|
|
1b5a16f05e | ||
|
|
7c1c633f3f | ||
|
|
e19b351364 | ||
|
|
cb9ef06ecc | ||
|
|
d1068e057a | ||
|
|
c2699c666c | ||
|
|
0b856a4757 | ||
|
|
74d7149130 | ||
|
|
5a2f3a8102 | ||
|
|
d7e5bc6a3a | ||
|
|
d4464125c5 | ||
|
|
59b68f5519 | ||
|
|
6cf832fc63 | ||
|
|
30f6786d62 | ||
|
|
f83f3fa2a6 | ||
|
|
1b711a5501 | ||
|
|
148dbb25f0 | ||
|
|
e6f5451a2c | ||
|
|
323aa968c3 | ||
|
|
a99d4845e3 | ||
|
|
4d4767f797 | ||
|
|
d1043ef90e | ||
|
|
9f0be146db | ||
|
|
dc263f5926 | ||
|
|
e8d5c6b446 | ||
|
|
75365bf718 | ||
|
|
5cca5c0093 | ||
|
|
d3b99ef110 | ||
|
|
f839f3ff2e | ||
|
|
c846a57d03 | ||
|
|
5bbfee69b6 | ||
|
|
609ac0c7ab | ||
|
|
df5f79da36 | ||
|
|
0c776c433b | ||
|
|
3152d1a557 | ||
|
|
23d638a573 | ||
|
|
d0474f2bc1 | ||
|
|
dedd1c386a | ||
|
|
2e942ce830 | ||
|
|
ae057e0bb9 | ||
|
|
424e532a6b | ||
|
|
9a8730f31e | ||
|
|
04ec9ef8af | ||
|
|
103acdfb06 | ||
|
|
0fe873ad5f | ||
|
|
e8de4ea953 | ||
|
|
868ac158d7 | ||
|
|
f396454097 | ||
|
|
03b2465591 | ||
|
|
b09b58ce2d | ||
|
|
285f4ea817 | ||
|
|
4c72e62d0b | ||
|
|
1fc8aa0e16 | ||
|
|
2b8d29ac0d | ||
|
|
ec21e1fd01 | ||
|
|
25fbd7e40e | ||
|
|
873e466549 | ||
|
|
20f43c1e05 | ||
|
|
0071968829 | ||
|
|
ea650a91b3 | ||
|
|
19f0d196d1 | ||
|
|
390b644b21 | ||
|
|
28a9807f84 | ||
|
|
9313c49e4c | ||
|
|
258ba436f1 | ||
|
|
716f7b0428 | ||
|
|
c74190a622 | ||
|
|
213756c9ab | ||
|
|
bb95ec3ec6 | ||
|
|
af568afdd5 | ||
|
|
38e648ca67 | ||
|
|
54d3ad1ede | ||
|
|
7880414a27 | ||
|
|
3807807f91 | ||
|
|
3bbc1ce003 | ||
|
|
4e119a3b38 | ||
|
|
46b653e73c | ||
|
|
f7ef9599ed | ||
|
|
a16a9109d7 | ||
|
|
2238c16a91 | ||
|
|
c25c0d6e9e | ||
|
|
313fb95989 | ||
|
|
4e27a87ec6 | ||
|
|
ae8a3be958 | ||
|
|
bc53d42fd9 | ||
|
|
178d2b00b1 | ||
|
|
48144a7fa4 | ||
|
|
fce0f76d57 | ||
|
|
65f631c3d6 | ||
|
|
61429a6c10 | ||
|
|
3d1abf3f3d | ||
|
|
bede1d9e23 |
45
.bazelrc
45
.bazelrc
@@ -44,6 +44,7 @@ common --remote_timeout=3600
|
||||
common --noexperimental_throttle_remote_action_building
|
||||
common --experimental_remote_execution_keepalive
|
||||
common --grpc_keepalive_time=30s
|
||||
common --experimental_remote_downloader=grpcs://remote.buildbuddy.io
|
||||
|
||||
# This limits both in-flight executions and concurrent downloads. Even with high number
|
||||
# of jobs execution will still be limited by CPU cores, so this just pays a bit of
|
||||
@@ -76,12 +77,54 @@ common:ci-bazel --build_metadata=TAG_workflow=bazel
|
||||
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
|
||||
# Keep this deny-list in sync with `codex-rs/Cargo.toml` `[workspace.lints.clippy]`.
|
||||
# Cargo applies those lint levels to member crates that opt into `[lints] workspace = true`
|
||||
# in their own `Cargo.toml`, but `rules_rust` Bazel clippy does not read Cargo lint levels.
|
||||
# `clippy.toml` can configure lint behavior, but it cannot set allow/warn/deny/forbid levels.
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=-Dwarnings
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::expect_used
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::identity_op
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_clamp
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_filter
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_find
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_flatten
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_map
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_memcpy
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_non_exhaustive
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_ok_or
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_range_contains
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_retain
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_strip
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_try_fold
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::manual_unwrap_or
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::needless_borrow
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::needless_borrowed_reference
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::needless_collect
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::needless_late_init
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::needless_option_as_deref
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::needless_question_mark
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::needless_update
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::redundant_clone
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::redundant_closure
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::redundant_closure_for_method_calls
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::redundant_static_lifetimes
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::trivially_copy_pass_by_ref
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::uninlined_format_args
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::unnecessary_filter_map
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::unnecessary_lazy_evaluations
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::unnecessary_sort_by
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::unnecessary_to_owned
|
||||
build:clippy --@rules_rust//rust/settings:clippy_flag=--deny=clippy::unwrap_used
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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.
|
||||
description: Babysit a GitHub pull request after creation by continuously polling review comments, CI checks/workflow runs, and mergeability state until the PR is merged/closed or user help is required. Diagnose failures, retry likely flaky failures up to 3 times, auto-fix/push branch-related issues when appropriate, and keep watching open PRs so fresh review feedback is surfaced promptly. 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
|
||||
@@ -9,8 +9,8 @@ description: Babysit a GitHub pull request after creation by continuously pollin
|
||||
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).
|
||||
- Optional handoff milestone: the PR is currently green + mergeable + review-clean. Treat this as a progress state, not a watcher stop, so late-arriving review comments are still surfaced promptly while the PR remains open.
|
||||
|
||||
Do not stop merely because a single snapshot returns `idle` while checks are still pending.
|
||||
|
||||
@@ -24,19 +24,20 @@ Accept any of the following:
|
||||
## 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`).
|
||||
2. Run the watcher script to snapshot PR/review/CI 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, and push.
|
||||
8. If the failure is likely flaky/unrelated and `retry_failed_checks` is present, rerun failed jobs with `--retry-failed-now`.
|
||||
9. 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.
|
||||
10. On every loop, verify mergeability / merge-conflict status (for example via `gh pr view`) in addition to CI and review state.
|
||||
11. After any push or rerun action, immediately return to step 1 and continue polling on the updated SHA/state.
|
||||
12. 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).
|
||||
13. Repeat polling until the PR is green + review-clean + mergeable, `stop_pr_closed` appears, or a user-help-required blocker is reached.
|
||||
14. 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.
|
||||
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, look for newly surfaced review feedback before acting on CI failures or mergeability state, then verify mergeability / merge-conflict status (for example via `gh pr view`) alongside CI.
|
||||
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 `stop_pr_closed` appears or a user-help-required blocker is reached. A green + review-clean + mergeable PR is a progress milestone, not a reason to stop the watcher while the PR is still open.
|
||||
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
|
||||
|
||||
@@ -94,10 +95,11 @@ 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. Resume watching on the new SHA immediately (do not stop after reporting the push).
|
||||
5. 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.
|
||||
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, record it as handled by continuing the watcher loop (the script de-duplicates surfaced items via state after surfacing them).
|
||||
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
|
||||
@@ -124,13 +126,14 @@ Use this loop in a live Codex session:
|
||||
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. 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.
|
||||
7. 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.
|
||||
8. If you pushed a commit or triggered a rerun, report the action briefly and continue polling (do not stop).
|
||||
9. After a review-fix push, proactively restart continuous monitoring (`--watch`) in the same turn unless a strict stop condition has already been reached.
|
||||
10. If everything is passing, mergeable, not blocked on required review approval, and there are no unaddressed review items, report success and stop.
|
||||
11. If blocked on a user-help-required issue (infra outage, exhausted flaky retries, unclear reviewer request, permissions), report the blocker and stop.
|
||||
12. Otherwise sleep according to the polling cadence below and repeat.
|
||||
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 that the PR is currently ready to merge but keep the watcher running so new review comments are surfaced quickly while the PR remains open.
|
||||
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.
|
||||
@@ -138,19 +141,18 @@ Do not hand control back to the user after a review-fix push just because a new
|
||||
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:
|
||||
Keep review polling aggressive 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.
|
||||
- After CI turns green: keep polling at the base cadence while the PR remains open so newly posted review comments are surfaced promptly instead of waiting on a long green-state backoff.
|
||||
- Reset the cadence immediately 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): stay on the base polling cadence.
|
||||
- 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:
|
||||
@@ -159,14 +161,14 @@ Keep polling when:
|
||||
- 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.
|
||||
- CI is green and mergeable, but the PR is still open and you are waiting for possible new review comments or merge-conflict changes.
|
||||
- The PR is green but blocked on review approval (`REVIEW_REQUIRED` / similar); continue polling at the base 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.
|
||||
- Treat push confirmations, intermediate CI snapshots, ready-to-merge 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.`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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."
|
||||
short_description: "Watch PR review comments, CI, and merge conflicts"
|
||||
default_prompt: "Babysit the current PR: monitor reviewer comments, CI, and merge-conflict status (prefer the watcher’s --watch mode for live monitoring); surface new review feedback before acting on CI or mergeability work, 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. Do not treat a green + mergeable PR as a terminal stop while it is still open; continue polling autonomously after any push/rerun so newly posted review comments are surfaced until a strict terminal stop condition is reached or the user interrupts."
|
||||
|
||||
@@ -45,7 +45,6 @@ MERGE_CONFLICT_OR_BLOCKING_STATES = {
|
||||
"DRAFT",
|
||||
"UNKNOWN",
|
||||
}
|
||||
GREEN_STATE_MAX_POLL_SECONDS = 60 * 60
|
||||
|
||||
|
||||
class GhCommandError(RuntimeError):
|
||||
@@ -578,7 +577,7 @@ def recommend_actions(pr, checks_summary, failed_runs, new_review_items, retries
|
||||
return unique_actions(actions)
|
||||
|
||||
if is_pr_ready_to_merge(pr, checks_summary, new_review_items):
|
||||
actions.append("stop_ready_to_merge")
|
||||
actions.append("ready_to_merge")
|
||||
return unique_actions(actions)
|
||||
|
||||
if new_review_items:
|
||||
@@ -606,12 +605,6 @@ def collect_snapshot(args):
|
||||
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,
|
||||
@@ -619,6 +612,15 @@ def collect_snapshot(args):
|
||||
fresh_state=fresh_state,
|
||||
authenticated_login=authenticated_login,
|
||||
)
|
||||
# Surface review feedback before drilling into CI and mergeability details.
|
||||
# That keeps the babysitter responsive to new comments even when other
|
||||
# actions are also available.
|
||||
# `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"])
|
||||
|
||||
retries_used = current_retry_count(state, pr["head_sha"])
|
||||
actions = recommend_actions(
|
||||
@@ -761,7 +763,6 @@ def run_watch(args):
|
||||
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
|
||||
@@ -769,13 +770,13 @@ def run_watch(args):
|
||||
current_change_key = snapshot_change_key(snapshot)
|
||||
changed = current_change_key != last_change_key
|
||||
green = is_ci_green(snapshot)
|
||||
pr = snapshot.get("pr") or {}
|
||||
pr_open = not bool(pr.get("closed")) and not bool(pr.get("merged"))
|
||||
|
||||
if not green:
|
||||
if not green or pr_open:
|
||||
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)
|
||||
|
||||
155
.codex/skills/babysit-pr/scripts/test_gh_pr_watch.py
Normal file
155
.codex/skills/babysit-pr/scripts/test_gh_pr_watch.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import argparse
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).with_name("gh_pr_watch.py")
|
||||
MODULE_SPEC = importlib.util.spec_from_file_location("gh_pr_watch", MODULE_PATH)
|
||||
gh_pr_watch = importlib.util.module_from_spec(MODULE_SPEC)
|
||||
assert MODULE_SPEC.loader is not None
|
||||
MODULE_SPEC.loader.exec_module(gh_pr_watch)
|
||||
|
||||
|
||||
def sample_pr():
|
||||
return {
|
||||
"number": 123,
|
||||
"url": "https://github.com/openai/codex/pull/123",
|
||||
"repo": "openai/codex",
|
||||
"head_sha": "abc123",
|
||||
"head_branch": "feature",
|
||||
"state": "OPEN",
|
||||
"merged": False,
|
||||
"closed": False,
|
||||
"mergeable": "MERGEABLE",
|
||||
"merge_state_status": "CLEAN",
|
||||
"review_decision": "",
|
||||
}
|
||||
|
||||
|
||||
def sample_checks(**overrides):
|
||||
checks = {
|
||||
"pending_count": 0,
|
||||
"failed_count": 0,
|
||||
"passed_count": 12,
|
||||
"all_terminal": True,
|
||||
}
|
||||
checks.update(overrides)
|
||||
return checks
|
||||
|
||||
|
||||
def test_collect_snapshot_fetches_review_items_before_ci(monkeypatch, tmp_path):
|
||||
call_order = []
|
||||
pr = sample_pr()
|
||||
|
||||
monkeypatch.setattr(gh_pr_watch, "resolve_pr", lambda *args, **kwargs: pr)
|
||||
monkeypatch.setattr(gh_pr_watch, "load_state", lambda path: ({}, True))
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"get_authenticated_login",
|
||||
lambda: call_order.append("auth") or "octocat",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"fetch_new_review_items",
|
||||
lambda *args, **kwargs: call_order.append("review") or [],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"get_pr_checks",
|
||||
lambda *args, **kwargs: call_order.append("checks") or [],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"summarize_checks",
|
||||
lambda checks: call_order.append("summarize") or sample_checks(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"get_workflow_runs_for_sha",
|
||||
lambda *args, **kwargs: call_order.append("workflow") or [],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"failed_runs_from_workflow_runs",
|
||||
lambda *args, **kwargs: call_order.append("failed_runs") or [],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"recommend_actions",
|
||||
lambda *args, **kwargs: call_order.append("recommend") or ["idle"],
|
||||
)
|
||||
monkeypatch.setattr(gh_pr_watch, "save_state", lambda *args, **kwargs: None)
|
||||
|
||||
args = argparse.Namespace(
|
||||
pr="123",
|
||||
repo=None,
|
||||
state_file=str(tmp_path / "watcher-state.json"),
|
||||
max_flaky_retries=3,
|
||||
)
|
||||
|
||||
gh_pr_watch.collect_snapshot(args)
|
||||
|
||||
assert call_order.index("review") < call_order.index("checks")
|
||||
assert call_order.index("review") < call_order.index("workflow")
|
||||
|
||||
|
||||
def test_recommend_actions_prioritizes_review_comments():
|
||||
actions = gh_pr_watch.recommend_actions(
|
||||
sample_pr(),
|
||||
sample_checks(failed_count=1),
|
||||
[{"run_id": 99}],
|
||||
[{"kind": "review_comment", "id": "1"}],
|
||||
0,
|
||||
3,
|
||||
)
|
||||
|
||||
assert actions == [
|
||||
"process_review_comment",
|
||||
"diagnose_ci_failure",
|
||||
"retry_failed_checks",
|
||||
]
|
||||
|
||||
|
||||
def test_run_watch_keeps_polling_open_ready_to_merge_pr(monkeypatch):
|
||||
sleeps = []
|
||||
events = []
|
||||
snapshot = {
|
||||
"pr": sample_pr(),
|
||||
"checks": sample_checks(),
|
||||
"failed_runs": [],
|
||||
"new_review_items": [],
|
||||
"actions": ["ready_to_merge"],
|
||||
"retry_state": {
|
||||
"current_sha_retries_used": 0,
|
||||
"max_flaky_retries": 3,
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"collect_snapshot",
|
||||
lambda args: (snapshot, Path("/tmp/codex-babysit-pr-state.json")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"print_event",
|
||||
lambda event, payload: events.append((event, payload)),
|
||||
)
|
||||
|
||||
class StopWatch(Exception):
|
||||
pass
|
||||
|
||||
def fake_sleep(seconds):
|
||||
sleeps.append(seconds)
|
||||
if len(sleeps) >= 2:
|
||||
raise StopWatch
|
||||
|
||||
monkeypatch.setattr(gh_pr_watch.time, "sleep", fake_sleep)
|
||||
|
||||
with pytest.raises(StopWatch):
|
||||
gh_pr_watch.run_watch(argparse.Namespace(poll_seconds=30))
|
||||
|
||||
assert sleeps == [30, 30]
|
||||
assert [event for event, _ in events] == ["snapshot", "snapshot"]
|
||||
95
.github/actions/setup-bazel-ci/action.yml
vendored
95
.github/actions/setup-bazel-ci/action.yml
vendored
@@ -9,9 +9,9 @@ inputs:
|
||||
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 }}
|
||||
repository-cache-path:
|
||||
description: Filesystem path used for the Bazel repository cache.
|
||||
value: ${{ steps.configure_bazel_repository_cache.outputs.repository-cache-path }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -41,25 +41,86 @@ runs:
|
||||
- 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 repository cache
|
||||
id: configure_bazel_repository_cache
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Keep the repository cache under HOME on all runners. Windows `D:\a`
|
||||
# cache paths match `.bazelrc`, but `actions/cache/restore` currently
|
||||
# returns HTTP 400 for that path in the Windows clippy job.
|
||||
$repositoryCachePath = Join-Path $HOME '.cache/bazel-repo-cache'
|
||||
"repository-cache-path=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"BAZEL_REPOSITORY_CACHE=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
- name: Configure Bazel output root (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Use a very short path to reduce argv/path length issues, but avoid the
|
||||
# drive root because some Windows test launchers mis-handle MANIFEST paths there.
|
||||
"BAZEL_OUTPUT_USER_ROOT=D:\b" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
# 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.
|
||||
$hasDDrive = Test-Path 'D:\'
|
||||
$bazelOutputUserRoot = if ($hasDDrive) { 'D:\b' } else { 'C:\b' }
|
||||
$repoContentsCache = Join-Path $env:RUNNER_TEMP "bazel-repo-contents-cache-$env:GITHUB_RUN_ID-$env:GITHUB_JOB"
|
||||
"BAZEL_OUTPUT_USER_ROOT=$bazelOutputUserRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"BAZEL_REPO_CONTENTS_CACHE=$repoContentsCache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
- name: Expose MSVC SDK environment (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Bazel exec-side Rust build scripts do not reliably inherit the MSVC developer
|
||||
# shell on GitHub-hosted Windows runners, so discover the latest VS install and
|
||||
# ask `VsDevCmd.bat` to materialize the x64/x64 compiler + SDK environment.
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (-not (Test-Path $vswhere)) {
|
||||
throw "vswhere.exe not found"
|
||||
}
|
||||
|
||||
$installPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null
|
||||
if (-not $installPath) {
|
||||
throw "Could not locate a Visual Studio installation with VC tools"
|
||||
}
|
||||
|
||||
$vsDevCmd = Join-Path $installPath 'Common7\Tools\VsDevCmd.bat'
|
||||
if (-not (Test-Path $vsDevCmd)) {
|
||||
throw "VsDevCmd.bat not found at $vsDevCmd"
|
||||
}
|
||||
|
||||
# Keep the export surface explicit: these are the paths and SDK roots that the
|
||||
# MSVC toolchain probes need later when Bazel runs Windows exec-platform build
|
||||
# scripts such as `aws-lc-sys`.
|
||||
$varsToExport = @(
|
||||
'INCLUDE',
|
||||
'LIB',
|
||||
'LIBPATH',
|
||||
'PATH',
|
||||
'UCRTVersion',
|
||||
'UniversalCRTSdkDir',
|
||||
'VCINSTALLDIR',
|
||||
'VCToolsInstallDir',
|
||||
'WindowsLibPath',
|
||||
'WindowsSdkBinPath',
|
||||
'WindowsSdkDir',
|
||||
'WindowsSDKLibVersion',
|
||||
'WindowsSDKVersion'
|
||||
)
|
||||
|
||||
# `VsDevCmd.bat` is a batch file, so invoke it under `cmd.exe`, suppress its
|
||||
# banner, then dump the resulting environment with `set`. Re-export only the
|
||||
# approved keys into `GITHUB_ENV` so later steps inherit the same MSVC context.
|
||||
$envLines = & cmd.exe /c ('"{0}" -no_logo -arch=x64 -host_arch=x64 >nul && set' -f $vsDevCmd)
|
||||
foreach ($line in $envLines) {
|
||||
if ($line -notmatch '^(.*?)=(.*)$') {
|
||||
continue
|
||||
}
|
||||
|
||||
$name = $matches[1]
|
||||
$value = $matches[2]
|
||||
if ($varsToExport -contains $name) {
|
||||
"$name=$value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
}
|
||||
}
|
||||
|
||||
- name: Enable Git long paths (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
115
.github/scripts/run-argument-comment-lint-bazel.sh
vendored
Executable file
115
.github/scripts/run-argument-comment-lint-bazel.sh
vendored
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ci_config=ci-linux
|
||||
case "${RUNNER_OS:-}" in
|
||||
macOS)
|
||||
ci_config=ci-macos
|
||||
;;
|
||||
Windows)
|
||||
ci_config=ci-windows
|
||||
;;
|
||||
esac
|
||||
|
||||
bazel_lint_args=("$@")
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
has_host_platform_override=0
|
||||
for arg in "${bazel_lint_args[@]}"; do
|
||||
if [[ "$arg" == --host_platform=* ]]; then
|
||||
has_host_platform_override=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $has_host_platform_override -eq 0 ]]; then
|
||||
# The nightly Windows lint toolchain is registered with an MSVC exec
|
||||
# platform even though the lint target platform stays on `windows-gnullvm`.
|
||||
# Override the host platform here so the exec-side helper binaries actually
|
||||
# match the registered toolchain set.
|
||||
bazel_lint_args+=("--host_platform=//:local_windows_msvc")
|
||||
fi
|
||||
|
||||
# Native Windows lint runs need exec-side Rust helper binaries and proc-macros
|
||||
# to use rust-lld instead of the C++ linker path. The default `none`
|
||||
# preference resolves to `cc` when a cc_toolchain is present, which currently
|
||||
# routes these exec actions through clang++ with an argument shape it cannot
|
||||
# consume.
|
||||
bazel_lint_args+=("--@rules_rust//rust/settings:toolchain_linker_preference=rust")
|
||||
|
||||
# Some Rust top-level targets are still intentionally incompatible with the
|
||||
# local Windows MSVC exec platform. Skip those explicit targets so the native
|
||||
# lint aspect can run across the compatible crate graph instead of failing the
|
||||
# whole build after analysis.
|
||||
bazel_lint_args+=("--skip_incompatible_explicit_targets")
|
||||
fi
|
||||
|
||||
bazel_startup_args=()
|
||||
if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
|
||||
bazel_startup_args+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
|
||||
run_bazel() {
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
MSYS2_ARG_CONV_EXCL='*' bazel "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
bazel "$@"
|
||||
}
|
||||
|
||||
run_bazel_with_startup_args() {
|
||||
if [[ ${#bazel_startup_args[@]} -gt 0 ]]; then
|
||||
run_bazel "${bazel_startup_args[@]}" "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
run_bazel "$@"
|
||||
}
|
||||
|
||||
read_query_labels() {
|
||||
local query="$1"
|
||||
local query_stdout
|
||||
local query_stderr
|
||||
query_stdout="$(mktemp)"
|
||||
query_stderr="$(mktemp)"
|
||||
|
||||
if ! run_bazel_with_startup_args \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
query \
|
||||
--keep_going \
|
||||
--output=label \
|
||||
"$query" >"$query_stdout" 2>"$query_stderr"; then
|
||||
cat "$query_stderr" >&2
|
||||
rm -f "$query_stdout" "$query_stderr"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat "$query_stdout"
|
||||
rm -f "$query_stdout" "$query_stderr"
|
||||
}
|
||||
|
||||
final_build_targets=(//codex-rs/...)
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
# Bazel's local Windows platform currently lacks a default test toolchain for
|
||||
# `rust_test`, so target the concrete Rust crate rules directly. The lint
|
||||
# aspect still walks their crate graph, which preserves incremental reuse for
|
||||
# non-test code while avoiding non-Rust wrapper targets such as platform_data.
|
||||
final_build_targets=()
|
||||
while IFS= read -r label; do
|
||||
[[ -n "$label" ]] || continue
|
||||
final_build_targets+=("$label")
|
||||
done < <(read_query_labels 'kind("rust_(library|binary|proc_macro) rule", //codex-rs/...)')
|
||||
|
||||
if [[ ${#final_build_targets[@]} -eq 0 ]]; then
|
||||
echo "Failed to discover Windows Bazel lint targets." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
build \
|
||||
"${bazel_lint_args[@]}" \
|
||||
-- \
|
||||
"${final_build_targets[@]}"
|
||||
86
.github/scripts/run-bazel-ci.sh
vendored
86
.github/scripts/run-bazel-ci.sh
vendored
@@ -5,6 +5,7 @@ set -euo pipefail
|
||||
print_failed_bazel_test_logs=0
|
||||
use_node_test_env=0
|
||||
remote_download_toplevel=0
|
||||
windows_msvc_host_platform=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -20,6 +21,10 @@ while [[ $# -gt 0 ]]; do
|
||||
remote_download_toplevel=1
|
||||
shift
|
||||
;;
|
||||
--windows-msvc-host-platform)
|
||||
windows_msvc_host_platform=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
@@ -32,7 +37,7 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: $0 [--print-failed-test-logs] [--use-node-test-env] [--remote-download-toplevel] -- <bazel args> -- <targets>" >&2
|
||||
echo "Usage: $0 [--print-failed-test-logs] [--use-node-test-env] [--remote-download-toplevel] [--windows-msvc-host-platform] -- <bazel args> -- <targets>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -41,6 +46,15 @@ if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
|
||||
bazel_startup_args+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
|
||||
run_bazel() {
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
MSYS2_ARG_CONV_EXCL='*' bazel "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
bazel "$@"
|
||||
}
|
||||
|
||||
ci_config=ci-linux
|
||||
case "${RUNNER_OS:-}" in
|
||||
macOS)
|
||||
@@ -60,7 +74,7 @@ print_bazel_test_log_tails() {
|
||||
bazel_info_cmd+=("${bazel_startup_args[@]}")
|
||||
fi
|
||||
|
||||
testlogs_dir="$("${bazel_info_cmd[@]}" info bazel-testlogs 2>/dev/null || echo bazel-testlogs)"
|
||||
testlogs_dir="$(run_bazel "${bazel_info_cmd[@]:1}" info bazel-testlogs 2>/dev/null || echo bazel-testlogs)"
|
||||
|
||||
local failed_targets=()
|
||||
while IFS= read -r target; do
|
||||
@@ -112,20 +126,82 @@ if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $use_node_test_env -eq 1 && "${RUNNER_OS:-}" != "Windows" ]]; then
|
||||
if [[ $use_node_test_env -eq 1 ]]; then
|
||||
# Bazel test sandboxes on macOS may resolve an older Homebrew `node`
|
||||
# before the `actions/setup-node` runtime on PATH.
|
||||
node_bin="$(which node)"
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
node_bin="$(cygpath -w "${node_bin}")"
|
||||
fi
|
||||
bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}")
|
||||
fi
|
||||
|
||||
post_config_bazel_args=()
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_msvc_host_platform -eq 1 ]]; then
|
||||
has_host_platform_override=0
|
||||
for arg in "${bazel_args[@]}"; do
|
||||
if [[ "$arg" == --host_platform=* ]]; then
|
||||
has_host_platform_override=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $has_host_platform_override -eq 0 ]]; then
|
||||
# Keep Windows Bazel targets on `windows-gnullvm` for cfg coverage, but opt
|
||||
# specific jobs into an MSVC exec platform when they need helper binaries
|
||||
# like Rust test wrappers and V8 generators to resolve a compatible host
|
||||
# toolchain.
|
||||
post_config_bazel_args+=("--host_platform=//:local_windows_msvc")
|
||||
fi
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
if [[ -n "${BAZEL_REPO_CONTENTS_CACHE:-}" ]]; then
|
||||
# Windows self-hosted runners can run multiple Bazel jobs concurrently. Give
|
||||
# each job its own repo contents cache so they do not fight over the shared
|
||||
# path configured in `ci-windows`.
|
||||
post_config_bazel_args+=("--repo_contents_cache=${BAZEL_REPO_CONTENTS_CACHE}")
|
||||
fi
|
||||
|
||||
if [[ -n "${BAZEL_REPOSITORY_CACHE:-}" ]]; then
|
||||
post_config_bazel_args+=("--repository_cache=${BAZEL_REPOSITORY_CACHE}")
|
||||
fi
|
||||
|
||||
if [[ -n "${CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR:-}" ]]; then
|
||||
post_config_bazel_args+=(
|
||||
"--execution_log_compact_file=${CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR}/execution-log-${bazel_args[0]}-${GITHUB_JOB:-local}-$$.zst"
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
windows_action_env_vars=(
|
||||
INCLUDE
|
||||
LIB
|
||||
LIBPATH
|
||||
PATH
|
||||
UCRTVersion
|
||||
UniversalCRTSdkDir
|
||||
VCINSTALLDIR
|
||||
VCToolsInstallDir
|
||||
WindowsLibPath
|
||||
WindowsSdkBinPath
|
||||
WindowsSdkDir
|
||||
WindowsSDKLibVersion
|
||||
WindowsSDKVersion
|
||||
)
|
||||
|
||||
for env_var in "${windows_action_env_vars[@]}"; do
|
||||
if [[ -n "${!env_var:-}" ]]; then
|
||||
post_config_bazel_args+=("--action_env=${env_var}" "--host_action_env=${env_var}")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
bazel_console_log="$(mktemp)"
|
||||
trap 'rm -f "$bazel_console_log"' EXIT
|
||||
|
||||
@@ -149,7 +225,7 @@ if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
bazel_run_args+=("${post_config_bazel_args[@]}")
|
||||
fi
|
||||
set +e
|
||||
"${bazel_cmd[@]}" \
|
||||
run_bazel "${bazel_cmd[@]:1}" \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
"${bazel_run_args[@]}" \
|
||||
-- \
|
||||
@@ -184,7 +260,7 @@ else
|
||||
bazel_run_args+=("${post_config_bazel_args[@]}")
|
||||
fi
|
||||
set +e
|
||||
"${bazel_cmd[@]}" \
|
||||
run_bazel "${bazel_cmd[@]:1}" \
|
||||
--noexperimental_remote_repo_contents_cache \
|
||||
"${bazel_run_args[@]}" \
|
||||
-- \
|
||||
|
||||
234
.github/scripts/verify_bazel_clippy_lints.py
vendored
Normal file
234
.github/scripts/verify_bazel_clippy_lints.py
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_CARGO_TOML = ROOT / "codex-rs" / "Cargo.toml"
|
||||
DEFAULT_BAZELRC = ROOT / ".bazelrc"
|
||||
BAZEL_CLIPPY_FLAG_PREFIX = "build:clippy --@rules_rust//rust/settings:clippy_flag="
|
||||
BAZEL_SPECIAL_FLAGS = {"-Dwarnings"}
|
||||
VALID_LEVELS = {"allow", "warn", "deny", "forbid"}
|
||||
LONG_FLAG_RE = re.compile(
|
||||
r"^--(?P<level>allow|warn|deny|forbid)=clippy::(?P<lint>[a-z0-9_]+)$"
|
||||
)
|
||||
SHORT_FLAG_RE = re.compile(r"^-(?P<level>[AWDF])clippy::(?P<lint>[a-z0-9_]+)$")
|
||||
SHORT_LEVEL_NAMES = {
|
||||
"A": "allow",
|
||||
"W": "warn",
|
||||
"D": "deny",
|
||||
"F": "forbid",
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Verify that Bazel clippy flags in .bazelrc stay in sync with "
|
||||
"codex-rs/Cargo.toml [workspace.lints.clippy]."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cargo-toml",
|
||||
type=Path,
|
||||
default=DEFAULT_CARGO_TOML,
|
||||
help="Path to the workspace Cargo.toml to inspect.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bazelrc",
|
||||
type=Path,
|
||||
default=DEFAULT_BAZELRC,
|
||||
help="Path to the .bazelrc file to inspect.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
cargo_toml = args.cargo_toml.resolve()
|
||||
bazelrc = args.bazelrc.resolve()
|
||||
|
||||
cargo_lints = load_workspace_clippy_lints(cargo_toml)
|
||||
bazel_lints = load_bazel_clippy_lints(bazelrc)
|
||||
|
||||
missing = sorted(cargo_lints.keys() - bazel_lints.keys())
|
||||
extra = sorted(bazel_lints.keys() - cargo_lints.keys())
|
||||
mismatched = sorted(
|
||||
lint
|
||||
for lint in cargo_lints.keys() & bazel_lints.keys()
|
||||
if cargo_lints[lint] != bazel_lints[lint]
|
||||
)
|
||||
|
||||
if missing or extra or mismatched:
|
||||
print_sync_error(
|
||||
cargo_toml=cargo_toml,
|
||||
bazelrc=bazelrc,
|
||||
cargo_lints=cargo_lints,
|
||||
bazel_lints=bazel_lints,
|
||||
missing=missing,
|
||||
extra=extra,
|
||||
mismatched=mismatched,
|
||||
)
|
||||
return 1
|
||||
|
||||
print(
|
||||
"Bazel clippy flags in "
|
||||
f"{display_path(bazelrc)} match "
|
||||
f"{display_path(cargo_toml)} [workspace.lints.clippy]."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def load_workspace_clippy_lints(cargo_toml: Path) -> dict[str, str]:
|
||||
workspace = tomllib.loads(cargo_toml.read_text())["workspace"]
|
||||
clippy_lints = workspace["lints"]["clippy"]
|
||||
parsed: dict[str, str] = {}
|
||||
for lint, level in clippy_lints.items():
|
||||
if not isinstance(level, str):
|
||||
raise SystemExit(
|
||||
f"expected string lint level for clippy::{lint} in {cargo_toml}, got {level!r}"
|
||||
)
|
||||
normalized = level.strip().lower()
|
||||
if normalized not in VALID_LEVELS:
|
||||
raise SystemExit(
|
||||
f"unsupported lint level {level!r} for clippy::{lint} in {cargo_toml}"
|
||||
)
|
||||
parsed[lint] = normalized
|
||||
return parsed
|
||||
|
||||
|
||||
def load_bazel_clippy_lints(bazelrc: Path) -> dict[str, str]:
|
||||
parsed: dict[str, str] = {}
|
||||
line_numbers: dict[str, int] = {}
|
||||
|
||||
for lineno, line in enumerate(bazelrc.read_text().splitlines(), start=1):
|
||||
if not line.startswith(BAZEL_CLIPPY_FLAG_PREFIX):
|
||||
continue
|
||||
|
||||
flag = line.removeprefix(BAZEL_CLIPPY_FLAG_PREFIX).strip()
|
||||
if flag in BAZEL_SPECIAL_FLAGS:
|
||||
continue
|
||||
|
||||
parsed_flag = parse_bazel_lint_flag(flag)
|
||||
if parsed_flag is None:
|
||||
continue
|
||||
|
||||
lint, level = parsed_flag
|
||||
if lint in parsed:
|
||||
raise SystemExit(
|
||||
f"duplicate Bazel clippy entry for clippy::{lint} at "
|
||||
f"{bazelrc}:{line_numbers[lint]} and {bazelrc}:{lineno}"
|
||||
)
|
||||
parsed[lint] = level
|
||||
line_numbers[lint] = lineno
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_bazel_lint_flag(flag: str) -> tuple[str, str] | None:
|
||||
long_match = LONG_FLAG_RE.match(flag)
|
||||
if long_match:
|
||||
return long_match["lint"], long_match["level"]
|
||||
|
||||
short_match = SHORT_FLAG_RE.match(flag)
|
||||
if short_match:
|
||||
return short_match["lint"], SHORT_LEVEL_NAMES[short_match["level"]]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def print_sync_error(
|
||||
*,
|
||||
cargo_toml: Path,
|
||||
bazelrc: Path,
|
||||
cargo_lints: dict[str, str],
|
||||
bazel_lints: dict[str, str],
|
||||
missing: list[str],
|
||||
extra: list[str],
|
||||
mismatched: list[str],
|
||||
) -> None:
|
||||
cargo_toml_display = display_path(cargo_toml)
|
||||
bazelrc_display = display_path(bazelrc)
|
||||
example_manifest = find_workspace_lints_example_manifest()
|
||||
|
||||
print(
|
||||
"ERROR: Bazel clippy flags are out of sync with Cargo workspace clippy lints.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(file=sys.stderr)
|
||||
print(
|
||||
f"Cargo defines the source of truth in {cargo_toml_display} "
|
||||
"[workspace.lints.clippy].",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if example_manifest is not None:
|
||||
print(
|
||||
"Cargo applies those lint levels to member crates that opt into "
|
||||
f"`[lints] workspace = true`, for example {example_manifest}.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"Bazel clippy does not ingest Cargo lint levels automatically, and "
|
||||
"`clippy.toml` can configure lint behavior but cannot set allow/warn/deny/forbid.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f"Update {bazelrc_display} so its `build:clippy` "
|
||||
"`clippy_flag` entries match Cargo.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if missing:
|
||||
print(file=sys.stderr)
|
||||
print("Missing Bazel entries:", file=sys.stderr)
|
||||
for lint in missing:
|
||||
print(f" {render_bazelrc_line(lint, cargo_lints[lint])}", file=sys.stderr)
|
||||
|
||||
if mismatched:
|
||||
print(file=sys.stderr)
|
||||
print("Mismatched lint levels:", file=sys.stderr)
|
||||
for lint in mismatched:
|
||||
cargo_level = cargo_lints[lint]
|
||||
bazel_level = bazel_lints[lint]
|
||||
print(
|
||||
f" clippy::{lint}: Cargo has {cargo_level}, Bazel has {bazel_level}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f" expected: {render_bazelrc_line(lint, cargo_level)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if extra:
|
||||
print(file=sys.stderr)
|
||||
print("Extra Bazel entries with no Cargo counterpart:", file=sys.stderr)
|
||||
for lint in extra:
|
||||
print(f" {render_bazelrc_line(lint, bazel_lints[lint])}", file=sys.stderr)
|
||||
|
||||
|
||||
def render_bazelrc_line(lint: str, level: str) -> str:
|
||||
return f"{BAZEL_CLIPPY_FLAG_PREFIX}--{level}=clippy::{lint}"
|
||||
|
||||
|
||||
def display_path(path: Path) -> str:
|
||||
try:
|
||||
return str(path.relative_to(ROOT))
|
||||
except ValueError:
|
||||
return str(path)
|
||||
|
||||
|
||||
def find_workspace_lints_example_manifest() -> str | None:
|
||||
for cargo_toml in sorted((ROOT / "codex-rs").glob("**/Cargo.toml")):
|
||||
if cargo_toml == DEFAULT_CARGO_TOML:
|
||||
continue
|
||||
data = tomllib.loads(cargo_toml.read_text())
|
||||
if data.get("lints", {}).get("workspace") is True:
|
||||
return str(cargo_toml.relative_to(ROOT))
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
388
.github/scripts/verify_cargo_workspace_manifests.py
vendored
Normal file
388
.github/scripts/verify_cargo_workspace_manifests.py
vendored
Normal file
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Verify that codex-rs Cargo manifests follow workspace manifest policy.
|
||||
|
||||
Checks:
|
||||
- Crates inherit `[workspace.package]` metadata.
|
||||
- Crates opt into `[lints] workspace = true`.
|
||||
- Crate names follow the codex-rs directory naming conventions.
|
||||
- Workspace manifests do not introduce workspace crate feature toggles.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CARGO_RS_ROOT = ROOT / "codex-rs"
|
||||
WORKSPACE_PACKAGE_FIELDS = ("version", "edition", "license")
|
||||
TOP_LEVEL_NAME_EXCEPTIONS = {
|
||||
"windows-sandbox-rs": "codex-windows-sandbox",
|
||||
}
|
||||
UTILITY_NAME_EXCEPTIONS = {
|
||||
"path-utils": "codex-utils-path",
|
||||
}
|
||||
MANIFEST_FEATURE_EXCEPTIONS = {}
|
||||
OPTIONAL_DEPENDENCY_EXCEPTIONS = set()
|
||||
INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS = {}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
internal_package_names = workspace_package_names()
|
||||
used_manifest_feature_exceptions: set[str] = set()
|
||||
used_optional_dependency_exceptions: set[tuple[str, str, str]] = set()
|
||||
used_internal_dependency_feature_exceptions: set[tuple[str, str, str]] = set()
|
||||
failures_by_path: dict[str, list[str]] = {}
|
||||
|
||||
for path in manifests_to_verify():
|
||||
if errors := manifest_errors(
|
||||
path,
|
||||
internal_package_names,
|
||||
used_manifest_feature_exceptions,
|
||||
used_optional_dependency_exceptions,
|
||||
used_internal_dependency_feature_exceptions,
|
||||
):
|
||||
failures_by_path[manifest_key(path)] = errors
|
||||
|
||||
add_unused_exception_errors(
|
||||
failures_by_path,
|
||||
used_manifest_feature_exceptions,
|
||||
used_optional_dependency_exceptions,
|
||||
used_internal_dependency_feature_exceptions,
|
||||
)
|
||||
|
||||
if not failures_by_path:
|
||||
return 0
|
||||
|
||||
print(
|
||||
"Cargo manifests under codex-rs must inherit workspace package metadata, "
|
||||
"opt into workspace lints, and avoid introducing new workspace crate "
|
||||
"features."
|
||||
)
|
||||
print(
|
||||
"Workspace crate features are disallowed because our Bazel build setup "
|
||||
"does not honor them today, which can let issues hidden behind feature "
|
||||
"gates go unnoticed, and because they add extra crate build "
|
||||
"permutations we want to avoid."
|
||||
)
|
||||
print(
|
||||
"Cargo only applies `codex-rs/Cargo.toml` `[workspace.lints.clippy]` "
|
||||
"entries to a crate when that crate declares:"
|
||||
)
|
||||
print()
|
||||
print("[lints]")
|
||||
print("workspace = true")
|
||||
print()
|
||||
print(
|
||||
"Without that opt-in, `cargo clippy` can miss violations that Bazel clippy "
|
||||
"catches."
|
||||
)
|
||||
print()
|
||||
print(
|
||||
"Package-name checks apply to `codex-rs/<crate>/Cargo.toml` and "
|
||||
"`codex-rs/utils/<crate>/Cargo.toml`."
|
||||
)
|
||||
print(
|
||||
"Workspace crate features are forbidden; add a targeted exception here "
|
||||
"only if there is a deliberate temporary migration in flight."
|
||||
)
|
||||
print()
|
||||
for path in sorted(failures_by_path):
|
||||
errors = failures_by_path[path]
|
||||
print(f"{path}:")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
def manifest_errors(
|
||||
path: Path,
|
||||
internal_package_names: set[str],
|
||||
used_manifest_feature_exceptions: set[str],
|
||||
used_optional_dependency_exceptions: set[tuple[str, str, str]],
|
||||
used_internal_dependency_feature_exceptions: set[tuple[str, str, str]],
|
||||
) -> list[str]:
|
||||
manifest = load_manifest(path)
|
||||
package = manifest.get("package")
|
||||
if not isinstance(package, dict) and path != CARGO_RS_ROOT / "Cargo.toml":
|
||||
return []
|
||||
|
||||
errors = []
|
||||
if isinstance(package, dict):
|
||||
for field in WORKSPACE_PACKAGE_FIELDS:
|
||||
if not is_workspace_reference(package.get(field)):
|
||||
errors.append(f"set `{field}.workspace = true` in `[package]`")
|
||||
|
||||
lints = manifest.get("lints")
|
||||
if not (isinstance(lints, dict) and lints.get("workspace") is True):
|
||||
errors.append("add `[lints]` with `workspace = true`")
|
||||
|
||||
expected_name = expected_package_name(path)
|
||||
if expected_name is not None:
|
||||
actual_name = package.get("name")
|
||||
if actual_name != expected_name:
|
||||
errors.append(
|
||||
f"set `[package].name` to `{expected_name}` (found `{actual_name}`)"
|
||||
)
|
||||
|
||||
path_key = manifest_key(path)
|
||||
features = manifest.get("features")
|
||||
if features is not None:
|
||||
normalized_features = normalize_feature_mapping(features)
|
||||
expected_features = MANIFEST_FEATURE_EXCEPTIONS.get(path_key)
|
||||
if expected_features is None:
|
||||
errors.append(
|
||||
"remove `[features]`; new workspace crate features are not allowed"
|
||||
)
|
||||
else:
|
||||
used_manifest_feature_exceptions.add(path_key)
|
||||
if normalized_features != expected_features:
|
||||
errors.append(
|
||||
"limit `[features]` to the existing exception list while "
|
||||
"workspace crate features are being removed "
|
||||
f"(expected {render_feature_mapping(expected_features)})"
|
||||
)
|
||||
|
||||
for section_name, dependencies in dependency_sections(manifest):
|
||||
for dependency_name, dependency in dependencies.items():
|
||||
if not isinstance(dependency, dict):
|
||||
continue
|
||||
|
||||
if dependency.get("optional") is True:
|
||||
exception_key = (path_key, section_name, dependency_name)
|
||||
if exception_key in OPTIONAL_DEPENDENCY_EXCEPTIONS:
|
||||
used_optional_dependency_exceptions.add(exception_key)
|
||||
else:
|
||||
errors.append(
|
||||
"remove `optional = true` from "
|
||||
f"`{dependency_entry_label(section_name, dependency_name)}`; "
|
||||
"new optional dependencies are not allowed because they "
|
||||
"create crate features"
|
||||
)
|
||||
|
||||
if not is_internal_dependency(path, dependency_name, dependency, internal_package_names):
|
||||
continue
|
||||
|
||||
dependency_features = dependency.get("features")
|
||||
if dependency_features is not None:
|
||||
normalized_dependency_features = normalize_string_list(
|
||||
dependency_features
|
||||
)
|
||||
exception_key = (path_key, section_name, dependency_name)
|
||||
expected_dependency_features = (
|
||||
INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS.get(exception_key)
|
||||
)
|
||||
if expected_dependency_features is None:
|
||||
errors.append(
|
||||
"remove `features = [...]` from workspace dependency "
|
||||
f"`{dependency_entry_label(section_name, dependency_name)}`; "
|
||||
"new workspace crate feature activations are not allowed"
|
||||
)
|
||||
else:
|
||||
used_internal_dependency_feature_exceptions.add(exception_key)
|
||||
if normalized_dependency_features != expected_dependency_features:
|
||||
errors.append(
|
||||
"limit workspace dependency features on "
|
||||
f"`{dependency_entry_label(section_name, dependency_name)}` "
|
||||
"to the existing exception list while workspace crate "
|
||||
"features are being removed "
|
||||
f"(expected {render_string_list(expected_dependency_features)})"
|
||||
)
|
||||
|
||||
if dependency.get("default-features") is False:
|
||||
errors.append(
|
||||
"remove `default-features = false` from workspace dependency "
|
||||
f"`{dependency_entry_label(section_name, dependency_name)}`; "
|
||||
"new workspace crate feature toggles are not allowed"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def expected_package_name(path: Path) -> str | None:
|
||||
parts = path.relative_to(CARGO_RS_ROOT).parts
|
||||
if len(parts) == 2 and parts[1] == "Cargo.toml":
|
||||
directory = parts[0]
|
||||
return TOP_LEVEL_NAME_EXCEPTIONS.get(
|
||||
directory,
|
||||
directory if directory.startswith("codex-") else f"codex-{directory}",
|
||||
)
|
||||
if len(parts) == 3 and parts[0] == "utils" and parts[2] == "Cargo.toml":
|
||||
directory = parts[1]
|
||||
return UTILITY_NAME_EXCEPTIONS.get(directory, f"codex-utils-{directory}")
|
||||
return None
|
||||
|
||||
|
||||
def is_workspace_reference(value: object) -> bool:
|
||||
return isinstance(value, dict) and value.get("workspace") is True
|
||||
|
||||
|
||||
def manifest_key(path: Path) -> str:
|
||||
return str(path.relative_to(ROOT))
|
||||
|
||||
|
||||
def normalize_feature_mapping(value: object) -> dict[str, tuple[str, ...]] | None:
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
|
||||
normalized = {}
|
||||
for key, features in value.items():
|
||||
if not isinstance(key, str):
|
||||
return None
|
||||
normalized_features = normalize_string_list(features)
|
||||
if normalized_features is None:
|
||||
return None
|
||||
normalized[key] = normalized_features
|
||||
return normalized
|
||||
|
||||
|
||||
def normalize_string_list(value: object) -> tuple[str, ...] | None:
|
||||
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
||||
return None
|
||||
return tuple(value)
|
||||
|
||||
|
||||
def render_feature_mapping(features: dict[str, tuple[str, ...]]) -> str:
|
||||
entries = [
|
||||
f"{name} = {render_string_list(items)}" for name, items in features.items()
|
||||
]
|
||||
return ", ".join(entries)
|
||||
|
||||
|
||||
def render_string_list(items: tuple[str, ...]) -> str:
|
||||
return "[" + ", ".join(f'"{item}"' for item in items) + "]"
|
||||
|
||||
|
||||
def dependency_sections(manifest: dict) -> list[tuple[str, dict]]:
|
||||
sections = []
|
||||
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
|
||||
dependencies = manifest.get(section_name)
|
||||
if isinstance(dependencies, dict):
|
||||
sections.append((section_name, dependencies))
|
||||
|
||||
workspace = manifest.get("workspace")
|
||||
if isinstance(workspace, dict):
|
||||
workspace_dependencies = workspace.get("dependencies")
|
||||
if isinstance(workspace_dependencies, dict):
|
||||
sections.append(("workspace.dependencies", workspace_dependencies))
|
||||
|
||||
target = manifest.get("target")
|
||||
if not isinstance(target, dict):
|
||||
return sections
|
||||
|
||||
for target_name, tables in target.items():
|
||||
if not isinstance(tables, dict):
|
||||
continue
|
||||
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
|
||||
dependencies = tables.get(section_name)
|
||||
if isinstance(dependencies, dict):
|
||||
sections.append((f"target.{target_name}.{section_name}", dependencies))
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def dependency_entry_label(section_name: str, dependency_name: str) -> str:
|
||||
return f"[{section_name}].{dependency_name}"
|
||||
|
||||
|
||||
def is_internal_dependency(
|
||||
manifest_path: Path,
|
||||
dependency_name: str,
|
||||
dependency: dict,
|
||||
internal_package_names: set[str],
|
||||
) -> bool:
|
||||
package_name = dependency.get("package", dependency_name)
|
||||
if isinstance(package_name, str) and package_name in internal_package_names:
|
||||
return True
|
||||
|
||||
dependency_path = dependency.get("path")
|
||||
if not isinstance(dependency_path, str):
|
||||
return False
|
||||
|
||||
resolved_dependency_path = (manifest_path.parent / dependency_path).resolve()
|
||||
try:
|
||||
resolved_dependency_path.relative_to(CARGO_RS_ROOT)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def add_unused_exception_errors(
|
||||
failures_by_path: dict[str, list[str]],
|
||||
used_manifest_feature_exceptions: set[str],
|
||||
used_optional_dependency_exceptions: set[tuple[str, str, str]],
|
||||
used_internal_dependency_feature_exceptions: set[tuple[str, str, str]],
|
||||
) -> None:
|
||||
for path_key in sorted(
|
||||
set(MANIFEST_FEATURE_EXCEPTIONS) - used_manifest_feature_exceptions
|
||||
):
|
||||
add_failure(
|
||||
failures_by_path,
|
||||
path_key,
|
||||
"remove the stale `[features]` exception from "
|
||||
"`MANIFEST_FEATURE_EXCEPTIONS`",
|
||||
)
|
||||
|
||||
for path_key, section_name, dependency_name in sorted(
|
||||
OPTIONAL_DEPENDENCY_EXCEPTIONS - used_optional_dependency_exceptions
|
||||
):
|
||||
add_failure(
|
||||
failures_by_path,
|
||||
path_key,
|
||||
"remove the stale optional-dependency exception for "
|
||||
f"`{dependency_entry_label(section_name, dependency_name)}` from "
|
||||
"`OPTIONAL_DEPENDENCY_EXCEPTIONS`",
|
||||
)
|
||||
|
||||
for path_key, section_name, dependency_name in sorted(
|
||||
set(INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS)
|
||||
- used_internal_dependency_feature_exceptions
|
||||
):
|
||||
add_failure(
|
||||
failures_by_path,
|
||||
path_key,
|
||||
"remove the stale internal dependency feature exception for "
|
||||
f"`{dependency_entry_label(section_name, dependency_name)}` from "
|
||||
"`INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS`",
|
||||
)
|
||||
|
||||
|
||||
def add_failure(failures_by_path: dict[str, list[str]], path_key: str, error: str) -> None:
|
||||
failures_by_path.setdefault(path_key, []).append(error)
|
||||
|
||||
|
||||
def workspace_package_names() -> set[str]:
|
||||
package_names = set()
|
||||
for path in cargo_manifests():
|
||||
manifest = load_manifest(path)
|
||||
package = manifest.get("package")
|
||||
if not isinstance(package, dict):
|
||||
continue
|
||||
package_name = package.get("name")
|
||||
if isinstance(package_name, str):
|
||||
package_names.add(package_name)
|
||||
return package_names
|
||||
|
||||
|
||||
def load_manifest(path: Path) -> dict:
|
||||
return tomllib.loads(path.read_text())
|
||||
|
||||
|
||||
def cargo_manifests() -> list[Path]:
|
||||
return sorted(
|
||||
path
|
||||
for path in CARGO_RS_ROOT.rglob("Cargo.toml")
|
||||
if path != CARGO_RS_ROOT / "Cargo.toml"
|
||||
)
|
||||
|
||||
|
||||
def manifests_to_verify() -> list[Path]:
|
||||
return [CARGO_RS_ROOT / "Cargo.toml", *cargo_manifests()]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
6
.github/workflows/README.md
vendored
6
.github/workflows/README.md
vendored
@@ -5,15 +5,15 @@ The workflows in this directory are split so that pull requests get fast, review
|
||||
## 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.
|
||||
It runs Bazel `test` and Bazel `clippy` on the supported Bazel targets,
|
||||
including the generated Rust test binaries needed to lint inline `#[cfg(test)]`
|
||||
code.
|
||||
- `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`.
|
||||
|
||||
110
.github/workflows/bazel.yml
vendored
110
.github/workflows/bazel.yml
vendored
@@ -58,11 +58,31 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
install-test-prereqs: "true"
|
||||
|
||||
# Restore the Bazel repository cache explicitly so external dependencies
|
||||
# do not need to be re-downloaded on every CI run. Keep restore failures
|
||||
# non-fatal so transient cache-service errors degrade to a cold build
|
||||
# instead of failing the job.
|
||||
- name: Restore bazel repository cache
|
||||
id: cache_bazel_repository_restore
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
|
||||
key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
|
||||
restore-keys: |
|
||||
bazel-cache-${{ matrix.target }}
|
||||
|
||||
- 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
|
||||
|
||||
- name: Set up Bazel execution logs
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p "${RUNNER_TEMP}/bazel-execution-logs"
|
||||
echo "CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR=${RUNNER_TEMP}/bazel-execution-logs" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: bazel test //...
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
@@ -76,28 +96,45 @@ jobs:
|
||||
-//third_party/v8:all
|
||||
)
|
||||
|
||||
bazel_wrapper_args=(
|
||||
--print-failed-test-logs
|
||||
--use-node-test-env
|
||||
)
|
||||
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
bazel_wrapper_args+=(--windows-msvc-host-platform)
|
||||
fi
|
||||
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
--print-failed-test-logs \
|
||||
--use-node-test-env \
|
||||
"${bazel_wrapper_args[@]}" \
|
||||
-- \
|
||||
test \
|
||||
--test_tag_filters=-argument-comment-lint \
|
||||
--test_verbose_timeout_warnings \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
-- \
|
||||
"${bazel_targets[@]}"
|
||||
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: bazel-execution-logs-test-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
if-no-files-found: ignore
|
||||
|
||||
# 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'
|
||||
if: always() && !cancelled() && steps.cache_bazel_repository_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
|
||||
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:
|
||||
@@ -124,30 +161,71 @@ jobs:
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: bazel build --config=clippy //codex-rs/...
|
||||
# Restore the Bazel repository cache explicitly so external dependencies
|
||||
# do not need to be re-downloaded on every CI run. Keep restore failures
|
||||
# non-fatal so transient cache-service errors degrade to a cold build
|
||||
# instead of failing the job.
|
||||
- name: Restore bazel repository cache
|
||||
id: cache_bazel_repository_restore
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
|
||||
key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
|
||||
restore-keys: |
|
||||
bazel-cache-${{ matrix.target }}
|
||||
|
||||
- name: Set up Bazel execution logs
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p "${RUNNER_TEMP}/bazel-execution-logs"
|
||||
echo "CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR=${RUNNER_TEMP}/bazel-execution-logs" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: bazel build --config=clippy lint targets
|
||||
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.
|
||||
bazel_clippy_args=(
|
||||
--config=clippy
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA}
|
||||
--build_metadata=TAG_job=clippy
|
||||
)
|
||||
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
# Some explicit targets pulled in through //codex-rs/... are
|
||||
# intentionally incompatible with `//:local_windows`, but the lint
|
||||
# aspect still traverses their compatible Rust deps.
|
||||
bazel_clippy_args+=(--skip_incompatible_explicit_targets)
|
||||
fi
|
||||
|
||||
bazel_target_lines="$(./scripts/list-bazel-clippy-targets.sh)"
|
||||
bazel_targets=()
|
||||
while IFS= read -r target; do
|
||||
bazel_targets+=("${target}")
|
||||
done <<< "${bazel_target_lines}"
|
||||
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
build \
|
||||
--config=clippy \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
--build_metadata=TAG_job=clippy \
|
||||
"${bazel_clippy_args[@]}" \
|
||||
-- \
|
||||
//codex-rs/... \
|
||||
-//codex-rs/v8-poc:all
|
||||
"${bazel_targets[@]}"
|
||||
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: bazel-execution-logs-clippy-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
if-no-files-found: ignore
|
||||
|
||||
# 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'
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
|
||||
key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -14,6 +14,12 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Verify codex-rs Cargo manifests inherit workspace settings
|
||||
run: python3 .github/scripts/verify_cargo_workspace_manifests.py
|
||||
|
||||
- name: Verify Bazel clippy flags match Cargo workspace lints
|
||||
run: python3 .github/scripts/verify_bazel_clippy_lints.py
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
||||
with:
|
||||
|
||||
16
.github/workflows/issue-labeler.yml
vendored
16
.github/workflows/issue-labeler.yml
vendored
@@ -52,13 +52,15 @@ jobs:
|
||||
6. code-review — Issues related to the code review feature or functionality.
|
||||
7. safety-check - Issues related to cyber risk detection or trusted access verification.
|
||||
8. auth - Problems related to authentication, login, or access tokens.
|
||||
9. codex-exec - Problems related to the "codex exec" command or functionality.
|
||||
10. context-management - Problems related to compaction, context windows, or available context reporting.
|
||||
11. custom-model - Problems that involve using custom model providers, local models, or OSS models.
|
||||
12. rate-limits - Problems related to token limits, rate limits, or token usage reporting.
|
||||
13. sandbox - Issues related to local sandbox environments or tool call approvals to override sandbox restrictions.
|
||||
14. tool-calls - Problems related to specific tool call invocations including unexpected errors, failures, or hangs.
|
||||
15. TUI - Problems with the terminal user interface (TUI) including keyboard shortcuts, copy & pasting, menus, or screen update issues.
|
||||
9. exec - Problems related to the "codex exec" command or functionality.
|
||||
10. hooks - Problems related to event hooks
|
||||
11. context - Problems related to compaction, context windows, or available context reporting.
|
||||
12. skills - Problems related to skills or plugins
|
||||
13. custom-model - Problems that involve using custom model providers, local models, or OSS models.
|
||||
14. rate-limits - Problems related to token limits, rate limits, or token usage reporting.
|
||||
15. sandbox - Issues related to local sandbox environments or tool call approvals to override sandbox restrictions.
|
||||
16. tool-calls - Problems related to specific tool call invocations including unexpected errors, failures, or hangs.
|
||||
17. TUI - Problems with the terminal user interface (TUI) including keyboard shortcuts, copy & pasting, menus, or screen update issues.
|
||||
|
||||
Issue number: ${{ github.event.issue.number }}
|
||||
|
||||
|
||||
49
.github/workflows/rust-ci-full.yml
vendored
49
.github/workflows/rust-ci-full.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "**full-ci**"
|
||||
workflow_dispatch:
|
||||
|
||||
# CI builds in debug (dev) for faster signal.
|
||||
@@ -73,6 +74,7 @@ jobs:
|
||||
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:
|
||||
@@ -88,29 +90,42 @@ jobs:
|
||||
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
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
components: llvm-tools-preview, rustc-dev, rust-src
|
||||
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
- name: Run argument comment lint on codex-rs
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
- name: Run argument comment lint on codex-rs via Bazel
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py
|
||||
- name: Run argument comment lint on codex-rs (default targets only)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py -- --lib --bins
|
||||
- name: Run argument comment lint on codex-rs
|
||||
run: |
|
||||
bazel_targets="$(./tools/argument-comment-lint/list-bazel-targets.sh)"
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
build \
|
||||
--config=argument-comment-lint \
|
||||
--keep_going \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
-- \
|
||||
${bazel_targets}
|
||||
- name: Run argument comment lint on codex-rs via Bazel
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: python ./tools/argument-comment-lint/run-prebuilt-linter.py
|
||||
run: |
|
||||
./.github/scripts/run-argument-comment-lint-bazel.sh \
|
||||
--config=argument-comment-lint \
|
||||
--platforms=//:local_windows \
|
||||
--keep_going \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA}
|
||||
|
||||
# --- CI to validate on different os/targets --------------------------------
|
||||
lint_build:
|
||||
@@ -431,10 +446,10 @@ jobs:
|
||||
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
|
||||
cargo chef cook --recipe-path "$RECIPE" --target ${{ matrix.target }} --release
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo clippy --target ${{ matrix.target }} --all-features --tests --profile ${{ matrix.profile }} --timings -- -D warnings
|
||||
run: cargo clippy --target ${{ matrix.target }} --tests --profile ${{ matrix.profile }} --timings -- -D warnings
|
||||
|
||||
- name: Upload Cargo timings (clippy)
|
||||
if: always()
|
||||
@@ -658,7 +673,7 @@ jobs:
|
||||
|
||||
- name: tests
|
||||
id: test
|
||||
run: cargo nextest run --all-features --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
|
||||
run: cargo nextest run --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
NEXTEST_STATUS_LEVEL: leak
|
||||
|
||||
60
.github/workflows/rust-ci.yml
vendored
60
.github/workflows/rust-ci.yml
vendored
@@ -93,9 +93,16 @@ jobs:
|
||||
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: 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
|
||||
@@ -121,6 +128,7 @@ jobs:
|
||||
argument_comment_lint_prebuilt:
|
||||
name: Argument comment lint - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
needs: changed
|
||||
if: ${{ needs.changed.outputs.argument_comment_lint == 'true' || needs.changed.outputs.workflows == 'true' }}
|
||||
strategy:
|
||||
@@ -129,40 +137,54 @@ jobs:
|
||||
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
|
||||
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
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
components: llvm-tools-preview, rustc-dev, rust-src
|
||||
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
|
||||
- name: Run argument comment lint on codex-rs
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
- name: Run argument comment lint on codex-rs via Bazel
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py
|
||||
# Linux still uses the default-targets-only form for now, but PRs run the
|
||||
# released linter on all three platforms so wrapper regressions surface pre-merge.
|
||||
- name: Run argument comment lint on codex-rs (default targets only)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py -- --lib --bins
|
||||
- name: Run argument comment lint on codex-rs
|
||||
run: |
|
||||
bazel_targets="$(./tools/argument-comment-lint/list-bazel-targets.sh)"
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
build \
|
||||
--config=argument-comment-lint \
|
||||
--keep_going \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
-- \
|
||||
${bazel_targets}
|
||||
- name: Run argument comment lint on codex-rs via Bazel
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: python ./tools/argument-comment-lint/run-prebuilt-linter.py
|
||||
run: |
|
||||
./.github/scripts/run-argument-comment-lint-bazel.sh \
|
||||
--config=argument-comment-lint \
|
||||
--platforms=//:local_windows \
|
||||
--keep_going \
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA}
|
||||
|
||||
# --- Gatherer job that you mark as the ONLY required status -----------------
|
||||
results:
|
||||
|
||||
7
.github/workflows/rust-release.yml
vendored
7
.github/workflows/rust-release.yml
vendored
@@ -584,14 +584,11 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version: 22
|
||||
# Node 24 bundles npm >= 11.5.1, which trusted publishing requires.
|
||||
node-version: 24
|
||||
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 tarballs from release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
17
AGENTS.md
17
AGENTS.md
@@ -15,10 +15,13 @@ In the codex-rs folder where the rust code lives:
|
||||
- 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.
|
||||
- The parameter name in the comment must exactly match the callee signature.
|
||||
- You can run `just argument-comment-lint` to run the lint check locally. This is powered by Bazel, so running it the first time can be slow if Bazel is not warmed up, though incremental invocations should take <15s. Most of the time, it is best to update the PR and let CI take responsibility for checking this (or run it asynchronously in the background after submitting the PR). Note CI checks all three platforms, which the local run does not.
|
||||
- When possible, make `match` statements exhaustive and avoid wildcard arms.
|
||||
- Newly added traits should include doc comments that explain their role and how implementations are expected to use them.
|
||||
- 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.
|
||||
- Prefer private modules and explicitly exported public crate API.
|
||||
- If you change `ConfigToml` or nested config types, run `just write-config-schema` to update `codex-rs/core/config.schema.json`.
|
||||
- If you change Rust dependencies (`Cargo.toml` or `Cargo.lock`), run `just bazel-lock-update` from the
|
||||
repo root to refresh `MODULE.bazel.lock`, and include that lockfile update in the same change.
|
||||
@@ -35,9 +38,9 @@ In the codex-rs folder where the rust code lives:
|
||||
- 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_app_server/src/app.rs`, `codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs`,
|
||||
`codex-rs/tui_app_server/src/bottom_pane/footer.rs`, `codex-rs/tui_app_server/src/chatwidget.rs`,
|
||||
`codex-rs/tui_app_server/src/bottom_pane/mod.rs`, and similarly central orchestration modules.
|
||||
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.
|
||||
@@ -49,8 +52,6 @@ Run `just fmt` (in `codex-rs` directory) automatically after you have finished m
|
||||
|
||||
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`.
|
||||
@@ -66,12 +67,10 @@ Likewise, when reviewing code, do not hesitate to push back on PRs that would un
|
||||
|
||||
## TUI style conventions
|
||||
|
||||
See `codex-rs/tui_app_server/styles.md`.
|
||||
See `codex-rs/tui/styles.md`.
|
||||
|
||||
## TUI code conventions
|
||||
|
||||
- When a change lands in `codex-rs/tui` and `codex-rs/tui_app_server` has a parallel implementation of the same behavior, reflect the change in `codex-rs/tui_app_server` too unless there is a documented reason not to.
|
||||
|
||||
- Use concise styling helpers from ratatui’s Stylize trait.
|
||||
- Basic spans: use "text".into()
|
||||
- Styled spans: use "text".red(), "text".green(), "text".magenta(), "text".dim(), etc.
|
||||
|
||||
@@ -17,12 +17,19 @@ platform(
|
||||
platform(
|
||||
name = "local_windows",
|
||||
constraint_values = [
|
||||
# We just need to pick one of the ABIs. Do the same one we target.
|
||||
"@rules_rs//rs/experimental/platforms/constraints:windows_gnullvm",
|
||||
],
|
||||
parents = ["@platforms//host"],
|
||||
)
|
||||
|
||||
platform(
|
||||
name = "local_windows_msvc",
|
||||
constraint_values = [
|
||||
"@rules_rs//rs/experimental/platforms/constraints:windows_msvc",
|
||||
],
|
||||
parents = ["@platforms//host"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "rbe",
|
||||
actual = "@rbe_platform",
|
||||
|
||||
124
MODULE.bazel
124
MODULE.bazel
@@ -27,11 +27,11 @@ register_toolchains("@llvm//toolchain:all")
|
||||
|
||||
osx = use_extension("@llvm//extensions:osx.bzl", "osx")
|
||||
osx.from_archive(
|
||||
sha256 = "6a4922f89487a96d7054ec6ca5065bfddd9f1d017c74d82f1d79cecf7feb8228",
|
||||
strip_prefix = "Payload/Library/Developer/CommandLineTools/SDKs/MacOSX26.2.sdk",
|
||||
sha256 = "1bde70c0b1c2ab89ff454acbebf6741390d7b7eb149ca2a3ca24cc9203a408b7",
|
||||
strip_prefix = "Payload/Library/Developer/CommandLineTools/SDKs/MacOSX26.4.sdk",
|
||||
type = "pkg",
|
||||
urls = [
|
||||
"https://swcdn.apple.com/content/downloads/26/44/047-81934-A_28TPKM5SD1/ps6pk6dk4x02vgfa5qsctq6tgf23t5f0w2/CLTools_macOSNMOS_SDK.pkg",
|
||||
"https://swcdn.apple.com/content/downloads/32/53/047-96692-A_OAHIHT53YB/ybtshxmrcju8m2qvw3w5elr4rajtg1x3y3/CLTools_macOSNMOS_SDK.pkg",
|
||||
],
|
||||
)
|
||||
osx.frameworks(names = [
|
||||
@@ -71,6 +71,8 @@ single_version_override(
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:rules_rs_windows_gnullvm_exec.patch",
|
||||
"//patches:rules_rs_delete_git_worktree_pointer.patch",
|
||||
"//patches:rules_rs_windows_exec_linker.patch",
|
||||
],
|
||||
version = "0.0.43",
|
||||
)
|
||||
@@ -82,11 +84,60 @@ rules_rust = use_extension("@rules_rs//rs/experimental:rules_rust.bzl", "rules_r
|
||||
rules_rust.patch(
|
||||
patches = [
|
||||
"//patches:rules_rust_windows_gnullvm_build_script.patch",
|
||||
"//patches:rules_rust_windows_exec_msvc_build_script_env.patch",
|
||||
"//patches:rules_rust_windows_bootstrap_process_wrapper_linker.patch",
|
||||
"//patches:rules_rust_windows_build_script_runner_paths.patch",
|
||||
"//patches:rules_rust_windows_msvc_direct_link_args.patch",
|
||||
"//patches:rules_rust_windows_process_wrapper_skip_temp_outputs.patch",
|
||||
"//patches:rules_rust_windows_exec_bin_target.patch",
|
||||
"//patches:rules_rust_windows_exec_std.patch",
|
||||
"//patches:rules_rust_windows_exec_rustc_dev_rlib.patch",
|
||||
"//patches:rules_rust_repository_set_exec_constraints.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",
|
||||
)
|
||||
# Keep Windows exec tools on MSVC so Bazel helper binaries link correctly, but
|
||||
# lint crate targets as `windows-gnullvm` to preserve the repo's actual cfgs.
|
||||
nightly_rust.repository_set(
|
||||
name = "rust_windows_x86_64",
|
||||
dev_components = True,
|
||||
edition = "2024",
|
||||
exec_triple = "x86_64-pc-windows-msvc",
|
||||
exec_compatible_with = [
|
||||
"@platforms//cpu:x86_64",
|
||||
"@platforms//os:windows",
|
||||
"@rules_rs//rs/experimental/platforms/constraints:windows_msvc",
|
||||
],
|
||||
target_compatible_with = [
|
||||
"@platforms//cpu:x86_64",
|
||||
"@platforms//os:windows",
|
||||
"@rules_rs//rs/experimental/platforms/constraints:windows_msvc",
|
||||
],
|
||||
target_triple = "x86_64-pc-windows-msvc",
|
||||
versions = ["nightly/2025-09-18"],
|
||||
)
|
||||
nightly_rust.repository_set(
|
||||
name = "rust_windows_x86_64",
|
||||
target_compatible_with = [
|
||||
"@platforms//cpu:x86_64",
|
||||
"@platforms//os:windows",
|
||||
"@rules_rs//rs/experimental/platforms/constraints:windows_gnullvm",
|
||||
],
|
||||
target_triple = "x86_64-pc-windows-gnullvm",
|
||||
)
|
||||
use_repo(nightly_rust, "rust_toolchains")
|
||||
|
||||
toolchains = use_extension("@rules_rs//rs/experimental/toolchains:module_extension.bzl", "toolchains")
|
||||
toolchains.toolchain(
|
||||
edition = "2024",
|
||||
@@ -95,6 +146,7 @@ toolchains.toolchain(
|
||||
use_repo(toolchains, "default_rust_toolchains")
|
||||
|
||||
register_toolchains("@default_rust_toolchains//:all")
|
||||
register_toolchains("@rust_toolchains//:all")
|
||||
|
||||
crate = use_extension("@rules_rs//rs:extensions.bzl", "crate")
|
||||
crate.from_cargo(
|
||||
@@ -117,13 +169,41 @@ crate.from_cargo(
|
||||
],
|
||||
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")
|
||||
|
||||
crate.annotation(
|
||||
crate = "zstd-sys",
|
||||
gen_build_script = "off",
|
||||
deps = ["@zstd"],
|
||||
gen_build_script = "on",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//patches:zstd-sys_windows_msvc_include_dirs.patch",
|
||||
],
|
||||
)
|
||||
crate.annotation(
|
||||
crate = "ring",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//patches:ring_windows_msvc_include_dirs.patch",
|
||||
],
|
||||
)
|
||||
crate.annotation(
|
||||
build_script_env = {
|
||||
@@ -133,12 +213,28 @@ crate.annotation(
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//patches:aws-lc-sys_memcmp_check.patch",
|
||||
"//patches:aws-lc-sys_windows_msvc_prebuilt_nasm.patch",
|
||||
"//patches:aws-lc-sys_windows_msvc_memcmp_probe.patch",
|
||||
],
|
||||
)
|
||||
|
||||
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")
|
||||
single_version_override(
|
||||
module_name = "bzip2",
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:bzip2_windows_stack_args.patch",
|
||||
],
|
||||
)
|
||||
|
||||
crate.annotation(
|
||||
crate = "bzip2-sys",
|
||||
@@ -152,20 +248,30 @@ bazel_dep(name = "zlib", version = "1.3.1.bcr.8")
|
||||
|
||||
crate.annotation(
|
||||
crate = "libz-sys",
|
||||
gen_build_script = "off",
|
||||
deps = ["@zlib"],
|
||||
gen_build_script = "on",
|
||||
)
|
||||
|
||||
inject_repo(crate, "zlib")
|
||||
|
||||
# TODO(zbarsky): Enable annotation after fixing windows arm64 builds.
|
||||
bazel_dep(name = "xz", version = "5.4.5.bcr.8")
|
||||
single_version_override(
|
||||
module_name = "xz",
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:xz_windows_stack_args.patch",
|
||||
],
|
||||
)
|
||||
|
||||
crate.annotation(
|
||||
crate = "lzma-sys",
|
||||
gen_build_script = "on",
|
||||
gen_build_script = "off",
|
||||
deps = ["@xz//:lzma"],
|
||||
)
|
||||
|
||||
bazel_dep(name = "openssl", version = "3.5.4.bcr.0")
|
||||
|
||||
inject_repo(crate, "xz")
|
||||
|
||||
crate.annotation(
|
||||
build_script_data = [
|
||||
"@openssl//:gen_dir",
|
||||
|
||||
71
MODULE.bazel.lock
generated
71
MODULE.bazel.lock
generated
File diff suppressed because one or more lines are too long
@@ -46,7 +46,7 @@ Each archive contains a single entry with the platform baked into the name (e.g.
|
||||
|
||||
### Using Codex with your ChatGPT plan
|
||||
|
||||
Run `codex` and select **Sign in with ChatGPT**. We recommend signing into your ChatGPT account to use Codex as part of your Plus, Pro, Team, Edu, or Enterprise plan. [Learn more about what's included in your ChatGPT plan](https://help.openai.com/en/articles/11369540-codex-in-chatgpt).
|
||||
Run `codex` and select **Sign in with ChatGPT**. We recommend signing into your ChatGPT account to use Codex as part of your Plus, Pro, Business, Edu, or Enterprise plan. [Learn more about what's included in your ChatGPT plan](https://help.openai.com/en/articles/11369540-codex-in-chatgpt).
|
||||
|
||||
You can also use Codex with an API key, but this requires [additional setup](https://developers.openai.com/codex/auth#sign-in-with-an-api-key).
|
||||
|
||||
|
||||
@@ -2,3 +2,17 @@ exports_files([
|
||||
"clippy.toml",
|
||||
"node-version.txt",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
name = "workspace-files",
|
||||
srcs = glob(
|
||||
[
|
||||
"*",
|
||||
".cargo/**",
|
||||
],
|
||||
exclude = [
|
||||
"BUILD.bazel",
|
||||
],
|
||||
),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
246
codex-rs/Cargo.lock
generated
246
codex-rs/Cargo.lock
generated
@@ -408,9 +408,11 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-app-server-protocol",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-models-manager",
|
||||
"codex-protocol",
|
||||
"codex-utils-cargo-bin",
|
||||
"core_test_support",
|
||||
@@ -447,7 +449,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.52.0",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
@@ -1337,10 +1339,12 @@ checksum = "e9b18233253483ce2f65329a24072ec414db782531bdbb7d0bbc4bd2ce6b7e21"
|
||||
name = "codex-analytics"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
"os_info",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1365,7 +1369,9 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"codex-client",
|
||||
"codex-protocol",
|
||||
"codex-utils-rustls-provider",
|
||||
@@ -1399,11 +1405,13 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-analytics",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-backend-client",
|
||||
"codex-chatgpt",
|
||||
"codex-cloud-requirements",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
@@ -1411,9 +1419,12 @@ dependencies = [
|
||||
"codex-file-search",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-rmcp-client",
|
||||
"codex-rollout",
|
||||
"codex-sandboxing",
|
||||
"codex-shell-command",
|
||||
"codex-state",
|
||||
@@ -1423,9 +1434,11 @@ dependencies = [
|
||||
"codex-utils-cli",
|
||||
"codex-utils-json-to-toml",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-rustls-provider",
|
||||
"constant_time_eq",
|
||||
"core_test_support",
|
||||
"futures",
|
||||
"gethostname",
|
||||
"hmac",
|
||||
"jsonwebtoken",
|
||||
"opentelemetry",
|
||||
@@ -1448,6 +1461,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid",
|
||||
"wiremock",
|
||||
]
|
||||
@@ -1482,6 +1496,7 @@ dependencies = [
|
||||
"codex-experimental-api-macros",
|
||||
"codex-git-utils",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"inventory",
|
||||
@@ -1491,7 +1506,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"shlex",
|
||||
"similar",
|
||||
"strum_macros 0.28.0",
|
||||
"tempfile",
|
||||
@@ -1529,11 +1543,14 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"assert_matches",
|
||||
"codex-exec-server",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"pretty_assertions",
|
||||
"similar",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
]
|
||||
@@ -1544,9 +1561,11 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-apply-patch",
|
||||
"codex-exec-server",
|
||||
"codex-linux-sandbox",
|
||||
"codex-sandboxing",
|
||||
"codex-shell-escalation",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-home-dir",
|
||||
"dotenvy",
|
||||
"tempfile",
|
||||
@@ -1570,7 +1589,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"codex-backend-openapi-models",
|
||||
"codex-client",
|
||||
"codex-core",
|
||||
"codex-login",
|
||||
"codex-protocol",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
@@ -1593,6 +1612,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-config",
|
||||
"codex-connectors",
|
||||
"codex-core",
|
||||
"codex-git-utils",
|
||||
@@ -1627,6 +1647,7 @@ dependencies = [
|
||||
"codex-execpolicy",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-mcp-server",
|
||||
"codex-protocol",
|
||||
"codex-responses-api-proxy",
|
||||
@@ -1638,6 +1659,7 @@ dependencies = [
|
||||
"codex-tui",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
"codex-utils-path",
|
||||
"codex-windows-sandbox",
|
||||
"libc",
|
||||
"owo-colors",
|
||||
@@ -1693,7 +1715,9 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-backend-client",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-login",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"hmac",
|
||||
@@ -1719,6 +1743,7 @@ dependencies = [
|
||||
"clap",
|
||||
"codex-client",
|
||||
"codex-cloud-tasks-client",
|
||||
"codex-cloud-tasks-mock-client",
|
||||
"codex-core",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
@@ -1748,12 +1773,21 @@ dependencies = [
|
||||
"chrono",
|
||||
"codex-backend-client",
|
||||
"codex-git-utils",
|
||||
"diffy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-cloud-tasks-mock-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"codex-cloud-tasks-client",
|
||||
"diffy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-code-mode"
|
||||
version = "0.0.0"
|
||||
@@ -1768,6 +1802,10 @@ dependencies = [
|
||||
"v8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-collaboration-mode-templates"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "codex-config"
|
||||
version = "0.0.0"
|
||||
@@ -1775,8 +1813,13 @@ dependencies = [
|
||||
"anyhow",
|
||||
"codex-app-server-protocol",
|
||||
"codex-execpolicy",
|
||||
"codex-features",
|
||||
"codex-git-utils",
|
||||
"codex-model-provider-info",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"dunce",
|
||||
"futures",
|
||||
"multimap",
|
||||
"pretty_assertions",
|
||||
@@ -1785,11 +1828,13 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"toml_edit 0.24.0+spec-1.1.0",
|
||||
"tracing",
|
||||
"wildmatch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1816,7 +1861,6 @@ dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bm25",
|
||||
"chardetng",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-analytics",
|
||||
@@ -1832,14 +1876,19 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-git-utils",
|
||||
"codex-hooks",
|
||||
"codex-instructions",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-network-proxy",
|
||||
"codex-otel",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
"codex-response-debug-context",
|
||||
"codex-rmcp-client",
|
||||
"codex-rollout",
|
||||
"codex-sandboxing",
|
||||
@@ -1869,7 +1918,6 @@ dependencies = [
|
||||
"ctor 0.6.3",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"encoding_rs",
|
||||
"env-flags",
|
||||
"eventsource-stream",
|
||||
"futures",
|
||||
@@ -1878,7 +1926,6 @@ dependencies = [
|
||||
"image",
|
||||
"indexmap 2.13.0",
|
||||
"insta",
|
||||
"landlock",
|
||||
"libc",
|
||||
"maplit",
|
||||
"notify",
|
||||
@@ -1892,8 +1939,6 @@ dependencies = [
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"rmcp",
|
||||
"schemars 0.8.22",
|
||||
"seccompiler",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
@@ -1917,7 +1962,6 @@ dependencies = [
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"which 8.0.0",
|
||||
"wildmatch",
|
||||
"windows-sys 0.52.0",
|
||||
"wiremock",
|
||||
"zip",
|
||||
@@ -1980,6 +2024,8 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-feedback",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider-info",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -2082,7 +2128,6 @@ dependencies = [
|
||||
name = "codex-features"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-login",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"pretty_assertions",
|
||||
@@ -2097,6 +2142,7 @@ name = "codex-feedback"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-login",
|
||||
"codex-protocol",
|
||||
"pretty_assertions",
|
||||
"sentry",
|
||||
@@ -2180,6 +2226,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"clap",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
@@ -2201,6 +2248,7 @@ name = "codex-lmstudio"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-core",
|
||||
"codex-model-provider-info",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2217,10 +2265,13 @@ dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-keyring-store",
|
||||
"codex-model-provider-info",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-terminal-detection",
|
||||
"codex-utils-template",
|
||||
@@ -2232,7 +2283,6 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
@@ -2248,15 +2298,47 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-mcp"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
"codex-async-utils",
|
||||
"codex-config",
|
||||
"codex-login",
|
||||
"codex-otel",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
"codex-rmcp-client",
|
||||
"codex-utils-plugins",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
"regex-lite",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-mcp-server"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-arg0",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-models-manager",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-utils-cli",
|
||||
@@ -2277,6 +2359,54 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-model-provider-info"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"http 1.4.0",
|
||||
"maplit",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-models-manager"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-collaboration-mode-templates",
|
||||
"codex-config",
|
||||
"codex-feedback",
|
||||
"codex-login",
|
||||
"codex-model-provider-info",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-response-debug-context",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-output-truncation",
|
||||
"codex-utils-template",
|
||||
"core_test_support",
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-network-proxy"
|
||||
version = "0.0.0"
|
||||
@@ -2316,6 +2446,7 @@ dependencies = [
|
||||
"async-stream",
|
||||
"bytes",
|
||||
"codex-core",
|
||||
"codex-model-provider-info",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
@@ -2380,18 +2511,27 @@ name = "codex-protocol"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chardetng",
|
||||
"chrono",
|
||||
"codex-async-utils",
|
||||
"codex-execpolicy",
|
||||
"codex-git-utils",
|
||||
"codex-network-proxy",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-image",
|
||||
"codex-utils-string",
|
||||
"codex-utils-template",
|
||||
"encoding_rs",
|
||||
"http 1.4.0",
|
||||
"icu_decimal",
|
||||
"icu_locale_core",
|
||||
"icu_provider",
|
||||
"landlock",
|
||||
"pretty_assertions",
|
||||
"quick-xml",
|
||||
"reqwest",
|
||||
"schemars 0.8.22",
|
||||
"seccompiler",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
@@ -2399,11 +2539,24 @@ dependencies = [
|
||||
"strum_macros 0.28.0",
|
||||
"sys-locale",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"ts-rs",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-response-debug-context"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"codex-api",
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-responses-api-proxy"
|
||||
version = "0.0.0"
|
||||
@@ -2413,6 +2566,7 @@ dependencies = [
|
||||
"codex-process-hardening",
|
||||
"ctor 0.6.3",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2427,6 +2581,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-keyring-store",
|
||||
"codex-protocol",
|
||||
"codex-utils-cargo-bin",
|
||||
@@ -2438,7 +2593,6 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"rmcp",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
@@ -2613,11 +2767,17 @@ dependencies = [
|
||||
name = "codex-tools"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-code-mode",
|
||||
"codex-features",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-pty",
|
||||
"pretty_assertions",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2636,16 +2796,21 @@ dependencies = [
|
||||
"codex-arg0",
|
||||
"codex-chatgpt",
|
||||
"codex-cli",
|
||||
"codex-client",
|
||||
"codex-cloud-requirements",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-rollout",
|
||||
"codex-shell-command",
|
||||
"codex-state",
|
||||
"codex-terminal-detection",
|
||||
@@ -2668,7 +2833,6 @@ dependencies = [
|
||||
"diffy",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"hound",
|
||||
"image",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
@@ -2705,6 +2869,7 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.1",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"vt100",
|
||||
"webbrowser",
|
||||
@@ -2718,7 +2883,6 @@ name = "codex-utils-absolute-path"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"path-absolutize",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
@@ -2807,6 +2971,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-core",
|
||||
"codex-lmstudio",
|
||||
"codex-model-provider-info",
|
||||
"codex-ollama",
|
||||
]
|
||||
|
||||
@@ -2833,6 +2998,7 @@ dependencies = [
|
||||
name = "codex-utils-plugins"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-login",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
@@ -2877,6 +3043,7 @@ name = "codex-utils-sandbox-summary"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-core",
|
||||
"codex-model-provider-info",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
@@ -3147,6 +3314,9 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
@@ -3801,7 +3971,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4046,7 +4216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4818,12 +4988,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hound"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@@ -4958,14 +5122,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.19"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.4.0",
|
||||
"http-body",
|
||||
@@ -4974,7 +5137,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.2",
|
||||
"socket2 0.5.10",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -5480,7 +5643,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5967,7 +6130,7 @@ name = "mcp_test_support"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-core",
|
||||
"codex-login",
|
||||
"codex-mcp-server",
|
||||
"codex-terminal-detection",
|
||||
"codex-utils-cargo-bin",
|
||||
@@ -6258,7 +6421,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6434,7 +6597,7 @@ version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64 0.21.7",
|
||||
"chrono",
|
||||
"getrandom 0.2.17",
|
||||
"http 1.4.0",
|
||||
@@ -6902,7 +7065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7448,7 +7611,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls",
|
||||
"socket2 0.6.2",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -7485,9 +7648,9 @@ dependencies = [
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.2",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8090,7 +8253,6 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
@@ -8324,7 +8486,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9723,9 +9885,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -9758,7 +9920,7 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.3",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11204,7 +11366,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -18,7 +18,9 @@ members = [
|
||||
"cloud-requirements",
|
||||
"cloud-tasks",
|
||||
"cloud-tasks-client",
|
||||
"cloud-tasks-mock-client",
|
||||
"cli",
|
||||
"collaboration-mode-templates",
|
||||
"connectors",
|
||||
"config",
|
||||
"shell-command",
|
||||
@@ -38,7 +40,10 @@ members = [
|
||||
"linux-sandbox",
|
||||
"lmstudio",
|
||||
"login",
|
||||
"codex-mcp",
|
||||
"mcp-server",
|
||||
"model-provider-info",
|
||||
"models-manager",
|
||||
"network-proxy",
|
||||
"ollama",
|
||||
"process-hardening",
|
||||
@@ -46,10 +51,11 @@ members = [
|
||||
"rollout",
|
||||
"rmcp-client",
|
||||
"responses-api-proxy",
|
||||
"response-debug-context",
|
||||
"sandboxing",
|
||||
"stdio-to-uds",
|
||||
"otel",
|
||||
"tui_app_server",
|
||||
"tui",
|
||||
"tools",
|
||||
"v8-poc",
|
||||
"utils/absolute-path",
|
||||
@@ -96,10 +102,9 @@ license = "Apache-2.0"
|
||||
[workspace.dependencies]
|
||||
# Internal
|
||||
app_test_support = { path = "app-server/tests/common" }
|
||||
codex-ansi-escape = { path = "ansi-escape" }
|
||||
codex-analytics = { path = "analytics" }
|
||||
codex-ansi-escape = { path = "ansi-escape" }
|
||||
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" }
|
||||
@@ -111,17 +116,21 @@ codex-backend-client = { path = "backend-client" }
|
||||
codex-chatgpt = { path = "chatgpt" }
|
||||
codex-cli = { path = "cli" }
|
||||
codex-client = { path = "codex-client" }
|
||||
codex-collaboration-mode-templates = { path = "collaboration-mode-templates" }
|
||||
codex-cloud-requirements = { path = "cloud-requirements" }
|
||||
codex-connectors = { path = "connectors" }
|
||||
codex-cloud-tasks-client = { path = "cloud-tasks-client" }
|
||||
codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" }
|
||||
codex-code-mode = { path = "code-mode" }
|
||||
codex-config = { path = "config" }
|
||||
codex-connectors = { path = "connectors" }
|
||||
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-feedback = { path = "feedback" }
|
||||
codex-file-search = { path = "file-search" }
|
||||
codex-git-utils = { path = "git-utils" }
|
||||
codex-hooks = { path = "hooks" }
|
||||
@@ -130,16 +139,20 @@ codex-keyring-store = { path = "keyring-store" }
|
||||
codex-linux-sandbox = { path = "linux-sandbox" }
|
||||
codex-lmstudio = { path = "lmstudio" }
|
||||
codex-login = { path = "login" }
|
||||
codex-mcp = { path = "codex-mcp" }
|
||||
codex-mcp-server = { path = "mcp-server" }
|
||||
codex-model-provider-info = { path = "model-provider-info" }
|
||||
codex-models-manager = { path = "models-manager" }
|
||||
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-response-debug-context = { path = "response-debug-context" }
|
||||
codex-rmcp-client = { path = "rmcp-client" }
|
||||
codex-rollout = { path = "rollout" }
|
||||
codex-sandboxing = { path = "sandboxing" }
|
||||
codex-secrets = { path = "secrets" }
|
||||
codex-shell-command = { path = "shell-command" }
|
||||
@@ -149,8 +162,7 @@ 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_app_server" }
|
||||
codex-v8-poc = { path = "v8-poc" }
|
||||
codex-tui = { path = "tui" }
|
||||
codex-utils-absolute-path = { path = "utils/absolute-path" }
|
||||
codex-utils-approval-presets = { path = "utils/approval-presets" }
|
||||
codex-utils-cache = { path = "utils/cache" }
|
||||
@@ -171,8 +183,9 @@ codex-utils-rustls-provider = { path = "utils/rustls-provider" }
|
||||
codex-utils-sandbox-summary = { path = "utils/sandbox-summary" }
|
||||
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-utils-template = { path = "utils/template" }
|
||||
codex-v8-poc = { path = "v8-poc" }
|
||||
codex-windows-sandbox = { path = "windows-sandbox-rs" }
|
||||
core_test_support = { path = "core/tests/common" }
|
||||
mcp_test_support = { path = "mcp-server/tests/common" }
|
||||
@@ -217,12 +230,12 @@ gethostname = "1.1.0"
|
||||
globset = "0.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.3.1"
|
||||
iana-time-zone = "0.1.64"
|
||||
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"
|
||||
insta = "1.46.3"
|
||||
@@ -264,7 +277,6 @@ 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",
|
||||
@@ -333,6 +345,7 @@ unicode-width = "0.2"
|
||||
url = "2"
|
||||
urlencoding = "2.1"
|
||||
uuid = "1"
|
||||
v8 = "=146.4.0"
|
||||
vt100 = "0.16.2"
|
||||
walkdir = "2.5.0"
|
||||
webbrowser = "1.0"
|
||||
|
||||
@@ -13,10 +13,12 @@ path = "src/lib.rs"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
os_info = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha1 = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
|
||||
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
308
codex-rs/analytics/src/client.rs
Normal file
308
codex-rs/analytics/src/client.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
use crate::events::AppServerRpcTransport;
|
||||
use crate::events::TrackEventRequest;
|
||||
use crate::events::TrackEventsRequest;
|
||||
use crate::events::current_runtime_metadata;
|
||||
use crate::facts::AnalyticsFact;
|
||||
use crate::facts::AppInvocation;
|
||||
use crate::facts::AppMentionedInput;
|
||||
use crate::facts::AppUsedInput;
|
||||
use crate::facts::CustomAnalyticsFact;
|
||||
use crate::facts::PluginState;
|
||||
use crate::facts::PluginStateChangedInput;
|
||||
use crate::facts::SkillInvocation;
|
||||
use crate::facts::SkillInvokedInput;
|
||||
use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::TrackEventsContext;
|
||||
use crate::facts::TurnResolvedConfigFact;
|
||||
use crate::facts::TurnTokenUsageFact;
|
||||
use crate::reducer::AnalyticsReducer;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponse;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::default_client::create_client;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
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(Clone)]
|
||||
pub(crate) struct AnalyticsEventsQueue {
|
||||
pub(crate) sender: mpsc::Sender<AnalyticsFact>,
|
||||
pub(crate) app_used_emitted_keys: Arc<Mutex<HashSet<(String, String)>>>,
|
||||
pub(crate) 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 {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
while let Some(input) = receiver.recv().await {
|
||||
let mut events = Vec::new();
|
||||
reducer.ingest(input, &mut events).await;
|
||||
send_track_events(&auth_manager, &base_url, events).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, input: AnalyticsFact) {
|
||||
if self.sender.try_send(input).is_err() {
|
||||
//TODO: add a metric for this
|
||||
tracing::warn!("dropping analytics events: queue is full");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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()))
|
||||
}
|
||||
|
||||
pub(crate) 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>,
|
||||
) {
|
||||
if invocations.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::SkillInvoked(
|
||||
SkillInvokedInput {
|
||||
tracking,
|
||||
invocations,
|
||||
},
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_initialize(
|
||||
&self,
|
||||
connection_id: u64,
|
||||
params: InitializeParams,
|
||||
product_client_id: String,
|
||||
rpc_transport: AppServerRpcTransport,
|
||||
) {
|
||||
self.record_fact(AnalyticsFact::Initialize {
|
||||
connection_id,
|
||||
params,
|
||||
product_client_id,
|
||||
runtime: current_runtime_metadata(),
|
||||
rpc_transport,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn track_subagent_thread_started(&self, input: SubAgentThreadStartedInput) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::SubAgentThreadStarted(input),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_app_mentioned(&self, tracking: TrackEventsContext, mentions: Vec<AppInvocation>) {
|
||||
if mentions.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::AppMentioned(
|
||||
AppMentionedInput { tracking, mentions },
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_request(&self, connection_id: u64, request_id: RequestId, request: ClientRequest) {
|
||||
self.record_fact(AnalyticsFact::Request {
|
||||
connection_id,
|
||||
request_id,
|
||||
request: Box::new(request),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn track_app_used(&self, tracking: TrackEventsContext, app: AppInvocation) {
|
||||
if !self.queue.should_enqueue_app_used(&tracking, &app) {
|
||||
return;
|
||||
}
|
||||
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::AppUsed(
|
||||
AppUsedInput { tracking, app },
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_plugin_used(&self, tracking: TrackEventsContext, plugin: PluginTelemetryMetadata) {
|
||||
if !self.queue.should_enqueue_plugin_used(&tracking, &plugin) {
|
||||
return;
|
||||
}
|
||||
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::PluginUsed(
|
||||
crate::facts::PluginUsedInput { tracking, plugin },
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_turn_resolved_config(&self, fact: TurnResolvedConfigFact) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::TurnResolvedConfig(Box::new(fact)),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_turn_token_usage(&self, fact: TurnTokenUsageFact) {
|
||||
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::TurnTokenUsage(
|
||||
Box::new(fact),
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_plugin_installed(&self, plugin: PluginTelemetryMetadata) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput {
|
||||
plugin,
|
||||
state: PluginState::Installed,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_plugin_uninstalled(&self, plugin: PluginTelemetryMetadata) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput {
|
||||
plugin,
|
||||
state: PluginState::Uninstalled,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_plugin_enabled(&self, plugin: PluginTelemetryMetadata) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput {
|
||||
plugin,
|
||||
state: PluginState::Enabled,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_plugin_disabled(&self, plugin: PluginTelemetryMetadata) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput {
|
||||
plugin,
|
||||
state: PluginState::Disabled,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn record_fact(&self, input: AnalyticsFact) {
|
||||
if self.analytics_enabled == Some(false) {
|
||||
return;
|
||||
}
|
||||
self.queue.try_send(input);
|
||||
}
|
||||
|
||||
pub fn track_response(&self, connection_id: u64, response: ClientResponse) {
|
||||
self.record_fact(AnalyticsFact::Response {
|
||||
connection_id,
|
||||
response: Box::new(response),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn track_notification(&self, notification: ServerNotification) {
|
||||
self.record_fact(AnalyticsFact::Notification(Box::new(notification)));
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_track_events(
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
events: Vec<TrackEventRequest>,
|
||||
) {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
328
codex-rs/analytics/src/events.rs
Normal file
328
codex-rs/analytics/src/events.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
use crate::facts::AppInvocation;
|
||||
use crate::facts::InvocationType;
|
||||
use crate::facts::PluginState;
|
||||
use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::TrackEventsContext;
|
||||
use crate::facts::TurnStatus;
|
||||
use crate::facts::TurnSubmissionType;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AppServerRpcTransport {
|
||||
Stdio,
|
||||
Websocket,
|
||||
InProcess,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum ThreadInitializationMode {
|
||||
New,
|
||||
Forked,
|
||||
Resumed,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct TrackEventsRequest {
|
||||
pub(crate) events: Vec<TrackEventRequest>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum TrackEventRequest {
|
||||
SkillInvocation(SkillInvocationEventRequest),
|
||||
ThreadInitialized(ThreadInitializedEvent),
|
||||
AppMentioned(CodexAppMentionedEventRequest),
|
||||
AppUsed(CodexAppUsedEventRequest),
|
||||
TurnEvent(Box<CodexTurnEventRequest>),
|
||||
PluginUsed(CodexPluginUsedEventRequest),
|
||||
PluginInstalled(CodexPluginEventRequest),
|
||||
PluginUninstalled(CodexPluginEventRequest),
|
||||
PluginEnabled(CodexPluginEventRequest),
|
||||
PluginDisabled(CodexPluginEventRequest),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct SkillInvocationEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) skill_id: String,
|
||||
pub(crate) skill_name: String,
|
||||
pub(crate) event_params: SkillInvocationEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct SkillInvocationEventParams {
|
||||
pub(crate) product_client_id: Option<String>,
|
||||
pub(crate) skill_scope: Option<String>,
|
||||
pub(crate) repo_url: Option<String>,
|
||||
pub(crate) thread_id: Option<String>,
|
||||
pub(crate) invoke_type: Option<InvocationType>,
|
||||
pub(crate) model_slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub(crate) struct CodexAppServerClientMetadata {
|
||||
pub(crate) product_client_id: String,
|
||||
pub(crate) client_name: Option<String>,
|
||||
pub(crate) client_version: Option<String>,
|
||||
pub(crate) rpc_transport: AppServerRpcTransport,
|
||||
pub(crate) experimental_api_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub(crate) struct CodexRuntimeMetadata {
|
||||
pub(crate) codex_rs_version: String,
|
||||
pub(crate) runtime_os: String,
|
||||
pub(crate) runtime_os_version: String,
|
||||
pub(crate) runtime_arch: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct ThreadInitializedEventParams {
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
||||
pub(crate) runtime: CodexRuntimeMetadata,
|
||||
pub(crate) model: String,
|
||||
pub(crate) ephemeral: bool,
|
||||
pub(crate) thread_source: Option<&'static str>,
|
||||
pub(crate) initialization_mode: ThreadInitializationMode,
|
||||
pub(crate) subagent_source: Option<String>,
|
||||
pub(crate) parent_thread_id: Option<String>,
|
||||
pub(crate) created_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct ThreadInitializedEvent {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: ThreadInitializedEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexAppMetadata {
|
||||
pub(crate) connector_id: Option<String>,
|
||||
pub(crate) thread_id: Option<String>,
|
||||
pub(crate) turn_id: Option<String>,
|
||||
pub(crate) app_name: Option<String>,
|
||||
pub(crate) product_client_id: Option<String>,
|
||||
pub(crate) invoke_type: Option<InvocationType>,
|
||||
pub(crate) model_slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexAppMentionedEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexAppMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexAppUsedEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexAppMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexTurnEventParams {
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) product_client_id: String,
|
||||
pub(crate) submission_type: Option<TurnSubmissionType>,
|
||||
pub(crate) model: Option<String>,
|
||||
pub(crate) model_provider: String,
|
||||
pub(crate) sandbox_policy: Option<&'static str>,
|
||||
pub(crate) reasoning_effort: Option<String>,
|
||||
pub(crate) reasoning_summary: Option<String>,
|
||||
pub(crate) service_tier: String,
|
||||
pub(crate) approval_policy: String,
|
||||
pub(crate) approvals_reviewer: String,
|
||||
pub(crate) sandbox_network_access: bool,
|
||||
pub(crate) collaboration_mode: Option<&'static str>,
|
||||
pub(crate) personality: Option<String>,
|
||||
pub(crate) num_input_images: usize,
|
||||
pub(crate) is_first_turn: bool,
|
||||
pub(crate) status: Option<TurnStatus>,
|
||||
pub(crate) turn_error: Option<CodexErrorInfo>,
|
||||
pub(crate) steer_count: Option<usize>,
|
||||
pub(crate) total_tool_call_count: Option<usize>,
|
||||
pub(crate) shell_command_count: Option<usize>,
|
||||
pub(crate) file_change_count: Option<usize>,
|
||||
pub(crate) mcp_tool_call_count: Option<usize>,
|
||||
pub(crate) dynamic_tool_call_count: Option<usize>,
|
||||
pub(crate) subagent_tool_call_count: Option<usize>,
|
||||
pub(crate) web_search_count: Option<usize>,
|
||||
pub(crate) image_generation_count: Option<usize>,
|
||||
pub(crate) input_tokens: Option<i64>,
|
||||
pub(crate) cached_input_tokens: Option<i64>,
|
||||
pub(crate) output_tokens: Option<i64>,
|
||||
pub(crate) reasoning_output_tokens: Option<i64>,
|
||||
pub(crate) total_tokens: Option<i64>,
|
||||
pub(crate) duration_ms: Option<u64>,
|
||||
pub(crate) started_at: Option<u64>,
|
||||
pub(crate) completed_at: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexTurnEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexTurnEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginMetadata {
|
||||
pub(crate) plugin_id: Option<String>,
|
||||
pub(crate) plugin_name: Option<String>,
|
||||
pub(crate) marketplace_name: Option<String>,
|
||||
pub(crate) has_skills: Option<bool>,
|
||||
pub(crate) mcp_server_count: Option<usize>,
|
||||
pub(crate) connector_ids: Option<Vec<String>>,
|
||||
pub(crate) product_client_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginUsedMetadata {
|
||||
#[serde(flatten)]
|
||||
pub(crate) plugin: CodexPluginMetadata,
|
||||
pub(crate) thread_id: Option<String>,
|
||||
pub(crate) turn_id: Option<String>,
|
||||
pub(crate) model_slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexPluginMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginUsedEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexPluginUsedMetadata,
|
||||
}
|
||||
|
||||
pub(crate) fn plugin_state_event_type(state: PluginState) -> &'static str {
|
||||
match state {
|
||||
PluginState::Installed => "codex_plugin_installed",
|
||||
PluginState::Uninstalled => "codex_plugin_uninstalled",
|
||||
PluginState::Enabled => "codex_plugin_enabled",
|
||||
PluginState::Disabled => "codex_plugin_disabled",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn thread_source_name(thread_source: &SessionSource) -> Option<&'static str> {
|
||||
match thread_source {
|
||||
SessionSource::Cli | SessionSource::VSCode | SessionSource::Exec => Some("user"),
|
||||
SessionSource::SubAgent(_) => Some("subagent"),
|
||||
SessionSource::Mcp | SessionSource::Custom(_) | SessionSource::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_runtime_metadata() -> CodexRuntimeMetadata {
|
||||
let os_info = os_info::get();
|
||||
CodexRuntimeMetadata {
|
||||
codex_rs_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
runtime_os: std::env::consts::OS.to_string(),
|
||||
runtime_os_version: os_info.version().to_string(),
|
||||
runtime_arch: std::env::consts::ARCH.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn subagent_thread_started_event_request(
|
||||
input: SubAgentThreadStartedInput,
|
||||
) -> ThreadInitializedEvent {
|
||||
let event_params = ThreadInitializedEventParams {
|
||||
thread_id: input.thread_id,
|
||||
app_server_client: CodexAppServerClientMetadata {
|
||||
product_client_id: input.product_client_id,
|
||||
client_name: Some(input.client_name),
|
||||
client_version: Some(input.client_version),
|
||||
rpc_transport: AppServerRpcTransport::InProcess,
|
||||
experimental_api_enabled: None,
|
||||
},
|
||||
runtime: current_runtime_metadata(),
|
||||
model: input.model,
|
||||
ephemeral: input.ephemeral,
|
||||
thread_source: Some("subagent"),
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
subagent_source: Some(subagent_source_name(&input.subagent_source)),
|
||||
parent_thread_id: subagent_parent_thread_id(&input.subagent_source),
|
||||
created_at: input.created_at,
|
||||
};
|
||||
ThreadInitializedEvent {
|
||||
event_type: "codex_thread_initialized",
|
||||
event_params,
|
||||
}
|
||||
}
|
||||
|
||||
fn subagent_source_name(subagent_source: &SubAgentSource) -> String {
|
||||
match subagent_source {
|
||||
SubAgentSource::Review => "review".to_string(),
|
||||
SubAgentSource::Compact => "compact".to_string(),
|
||||
SubAgentSource::ThreadSpawn { .. } => "thread_spawn".to_string(),
|
||||
SubAgentSource::MemoryConsolidation => "memory_consolidation".to_string(),
|
||||
SubAgentSource::Other(other) => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Option<String> {
|
||||
match subagent_source {
|
||||
SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id, ..
|
||||
} => Some(parent_thread_id.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
183
codex-rs/analytics/src/facts.rs
Normal file
183
codex-rs/analytics/src/facts.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use crate::events::AppServerRpcTransport;
|
||||
use crate::events::CodexRuntimeMetadata;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponse;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[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, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TurnSubmissionType {
|
||||
Default,
|
||||
Queued,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TurnResolvedConfigFact {
|
||||
pub turn_id: String,
|
||||
pub thread_id: String,
|
||||
pub num_input_images: usize,
|
||||
pub submission_type: Option<TurnSubmissionType>,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
pub reasoning_summary: Option<ReasoningSummary>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub approval_policy: AskForApproval,
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
pub sandbox_network_access: bool,
|
||||
pub collaboration_mode: ModeKind,
|
||||
pub personality: Option<Personality>,
|
||||
pub is_first_turn: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TurnTokenUsageFact {
|
||||
pub turn_id: String,
|
||||
pub thread_id: String,
|
||||
pub token_usage: TokenUsage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TurnStatus {
|
||||
Completed,
|
||||
Failed,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
#[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 struct SubAgentThreadStartedInput {
|
||||
pub thread_id: String,
|
||||
pub product_client_id: String,
|
||||
pub client_name: String,
|
||||
pub client_version: String,
|
||||
pub model: String,
|
||||
pub ephemeral: bool,
|
||||
pub subagent_source: SubAgentSource,
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum AnalyticsFact {
|
||||
Initialize {
|
||||
connection_id: u64,
|
||||
params: InitializeParams,
|
||||
product_client_id: String,
|
||||
runtime: CodexRuntimeMetadata,
|
||||
rpc_transport: AppServerRpcTransport,
|
||||
},
|
||||
Request {
|
||||
connection_id: u64,
|
||||
request_id: RequestId,
|
||||
request: Box<ClientRequest>,
|
||||
},
|
||||
Response {
|
||||
connection_id: u64,
|
||||
response: Box<ClientResponse>,
|
||||
},
|
||||
Notification(Box<ServerNotification>),
|
||||
// Facts that do not naturally exist on the app-server protocol surface, or
|
||||
// would require non-trivial protocol reshaping on this branch.
|
||||
Custom(CustomAnalyticsFact),
|
||||
}
|
||||
|
||||
pub(crate) enum CustomAnalyticsFact {
|
||||
SubAgentThreadStarted(SubAgentThreadStartedInput),
|
||||
TurnResolvedConfig(Box<TurnResolvedConfigFact>),
|
||||
TurnTokenUsage(Box<TurnTokenUsageFact>),
|
||||
SkillInvoked(SkillInvokedInput),
|
||||
AppMentioned(AppMentionedInput),
|
||||
AppUsed(AppUsedInput),
|
||||
PluginUsed(PluginUsedInput),
|
||||
PluginStateChanged(PluginStateChangedInput),
|
||||
}
|
||||
|
||||
pub(crate) struct SkillInvokedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub invocations: Vec<SkillInvocation>,
|
||||
}
|
||||
|
||||
pub(crate) struct AppMentionedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub mentions: Vec<AppInvocation>,
|
||||
}
|
||||
|
||||
pub(crate) struct AppUsedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub app: AppInvocation,
|
||||
}
|
||||
|
||||
pub(crate) struct PluginUsedInput {
|
||||
pub tracking: TrackEventsContext,
|
||||
pub plugin: PluginTelemetryMetadata,
|
||||
}
|
||||
|
||||
pub(crate) struct PluginStateChangedInput {
|
||||
pub plugin: PluginTelemetryMetadata,
|
||||
pub state: PluginState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum PluginState {
|
||||
Installed,
|
||||
Uninstalled,
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
@@ -1,8 +1,19 @@
|
||||
mod analytics_client;
|
||||
mod client;
|
||||
mod events;
|
||||
mod facts;
|
||||
mod reducer;
|
||||
|
||||
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;
|
||||
pub use client::AnalyticsEventsClient;
|
||||
pub use events::AppServerRpcTransport;
|
||||
pub use facts::AppInvocation;
|
||||
pub use facts::InvocationType;
|
||||
pub use facts::SkillInvocation;
|
||||
pub use facts::SubAgentThreadStartedInput;
|
||||
pub use facts::TrackEventsContext;
|
||||
pub use facts::TurnResolvedConfigFact;
|
||||
pub use facts::TurnStatus;
|
||||
pub use facts::TurnTokenUsageFact;
|
||||
pub use facts::build_track_events_context;
|
||||
|
||||
#[cfg(test)]
|
||||
mod analytics_client_tests;
|
||||
|
||||
697
codex-rs/analytics/src/reducer.rs
Normal file
697
codex-rs/analytics/src/reducer.rs
Normal file
@@ -0,0 +1,697 @@
|
||||
use crate::events::AppServerRpcTransport;
|
||||
use crate::events::CodexAppMentionedEventRequest;
|
||||
use crate::events::CodexAppServerClientMetadata;
|
||||
use crate::events::CodexAppUsedEventRequest;
|
||||
use crate::events::CodexPluginEventRequest;
|
||||
use crate::events::CodexPluginUsedEventRequest;
|
||||
use crate::events::CodexRuntimeMetadata;
|
||||
use crate::events::CodexTurnEventParams;
|
||||
use crate::events::CodexTurnEventRequest;
|
||||
use crate::events::SkillInvocationEventParams;
|
||||
use crate::events::SkillInvocationEventRequest;
|
||||
use crate::events::ThreadInitializationMode;
|
||||
use crate::events::ThreadInitializedEvent;
|
||||
use crate::events::ThreadInitializedEventParams;
|
||||
use crate::events::TrackEventRequest;
|
||||
use crate::events::codex_app_metadata;
|
||||
use crate::events::codex_plugin_metadata;
|
||||
use crate::events::codex_plugin_used_metadata;
|
||||
use crate::events::plugin_state_event_type;
|
||||
use crate::events::subagent_thread_started_event_request;
|
||||
use crate::events::thread_source_name;
|
||||
use crate::facts::AnalyticsFact;
|
||||
use crate::facts::AppMentionedInput;
|
||||
use crate::facts::AppUsedInput;
|
||||
use crate::facts::CustomAnalyticsFact;
|
||||
use crate::facts::PluginState;
|
||||
use crate::facts::PluginStateChangedInput;
|
||||
use crate::facts::PluginUsedInput;
|
||||
use crate::facts::SkillInvokedInput;
|
||||
use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::TurnResolvedConfigFact;
|
||||
use crate::facts::TurnStatus;
|
||||
use crate::facts::TurnTokenUsageFact;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponse;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_git_utils::collect_git_info;
|
||||
use codex_git_utils::get_git_repo_root;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use sha1::Digest;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AnalyticsReducer {
|
||||
requests: HashMap<(u64, RequestId), RequestState>,
|
||||
turns: HashMap<String, TurnState>,
|
||||
connections: HashMap<u64, ConnectionState>,
|
||||
}
|
||||
|
||||
struct ConnectionState {
|
||||
app_server_client: CodexAppServerClientMetadata,
|
||||
runtime: CodexRuntimeMetadata,
|
||||
}
|
||||
|
||||
enum RequestState {
|
||||
TurnStart(PendingTurnStartState),
|
||||
}
|
||||
|
||||
struct PendingTurnStartState {
|
||||
thread_id: String,
|
||||
num_input_images: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CompletedTurnState {
|
||||
status: Option<TurnStatus>,
|
||||
turn_error: Option<CodexErrorInfo>,
|
||||
completed_at: u64,
|
||||
duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
struct TurnState {
|
||||
connection_id: Option<u64>,
|
||||
thread_id: Option<String>,
|
||||
num_input_images: Option<usize>,
|
||||
resolved_config: Option<TurnResolvedConfigFact>,
|
||||
started_at: Option<u64>,
|
||||
token_usage: Option<TokenUsage>,
|
||||
completed: Option<CompletedTurnState>,
|
||||
}
|
||||
|
||||
impl AnalyticsReducer {
|
||||
pub(crate) async fn ingest(&mut self, input: AnalyticsFact, out: &mut Vec<TrackEventRequest>) {
|
||||
match input {
|
||||
AnalyticsFact::Initialize {
|
||||
connection_id,
|
||||
params,
|
||||
product_client_id,
|
||||
runtime,
|
||||
rpc_transport,
|
||||
} => {
|
||||
self.ingest_initialize(
|
||||
connection_id,
|
||||
params,
|
||||
product_client_id,
|
||||
runtime,
|
||||
rpc_transport,
|
||||
);
|
||||
}
|
||||
AnalyticsFact::Request {
|
||||
connection_id,
|
||||
request_id,
|
||||
request,
|
||||
} => {
|
||||
self.ingest_request(connection_id, request_id, *request);
|
||||
}
|
||||
AnalyticsFact::Response {
|
||||
connection_id,
|
||||
response,
|
||||
} => {
|
||||
self.ingest_response(connection_id, *response, out);
|
||||
}
|
||||
AnalyticsFact::Notification(notification) => {
|
||||
self.ingest_notification(*notification, out);
|
||||
}
|
||||
AnalyticsFact::Custom(input) => match input {
|
||||
CustomAnalyticsFact::SubAgentThreadStarted(input) => {
|
||||
self.ingest_subagent_thread_started(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::TurnResolvedConfig(input) => {
|
||||
self.ingest_turn_resolved_config(*input, out);
|
||||
}
|
||||
CustomAnalyticsFact::TurnTokenUsage(input) => {
|
||||
self.ingest_turn_token_usage(*input, out);
|
||||
}
|
||||
CustomAnalyticsFact::SkillInvoked(input) => {
|
||||
self.ingest_skill_invoked(input, out).await;
|
||||
}
|
||||
CustomAnalyticsFact::AppMentioned(input) => {
|
||||
self.ingest_app_mentioned(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::AppUsed(input) => {
|
||||
self.ingest_app_used(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::PluginUsed(input) => {
|
||||
self.ingest_plugin_used(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::PluginStateChanged(input) => {
|
||||
self.ingest_plugin_state_changed(input, out);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest_initialize(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
params: InitializeParams,
|
||||
product_client_id: String,
|
||||
runtime: CodexRuntimeMetadata,
|
||||
rpc_transport: AppServerRpcTransport,
|
||||
) {
|
||||
self.connections.insert(
|
||||
connection_id,
|
||||
ConnectionState {
|
||||
app_server_client: CodexAppServerClientMetadata {
|
||||
product_client_id,
|
||||
client_name: Some(params.client_info.name),
|
||||
client_version: Some(params.client_info.version),
|
||||
rpc_transport,
|
||||
experimental_api_enabled: params
|
||||
.capabilities
|
||||
.map(|capabilities| capabilities.experimental_api),
|
||||
},
|
||||
runtime,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn ingest_subagent_thread_started(
|
||||
&mut self,
|
||||
input: SubAgentThreadStartedInput,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
out.push(TrackEventRequest::ThreadInitialized(
|
||||
subagent_thread_started_event_request(input),
|
||||
));
|
||||
}
|
||||
|
||||
fn ingest_request(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
request_id: RequestId,
|
||||
request: ClientRequest,
|
||||
) {
|
||||
let ClientRequest::TurnStart { params, .. } = request else {
|
||||
return;
|
||||
};
|
||||
self.requests.insert(
|
||||
(connection_id, request_id),
|
||||
RequestState::TurnStart(PendingTurnStartState {
|
||||
thread_id: params.thread_id,
|
||||
num_input_images: params
|
||||
.input
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
matches!(item, UserInput::Image { .. } | UserInput::LocalImage { .. })
|
||||
})
|
||||
.count(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn ingest_turn_resolved_config(
|
||||
&mut self,
|
||||
input: TurnResolvedConfigFact,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let turn_id = input.turn_id.clone();
|
||||
let thread_id = input.thread_id.clone();
|
||||
let num_input_images = input.num_input_images;
|
||||
let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState {
|
||||
connection_id: None,
|
||||
thread_id: None,
|
||||
num_input_images: None,
|
||||
resolved_config: None,
|
||||
started_at: None,
|
||||
token_usage: None,
|
||||
completed: None,
|
||||
});
|
||||
turn_state.thread_id = Some(thread_id);
|
||||
turn_state.num_input_images = Some(num_input_images);
|
||||
turn_state.resolved_config = Some(input);
|
||||
self.maybe_emit_turn_event(&turn_id, out);
|
||||
}
|
||||
|
||||
fn ingest_turn_token_usage(
|
||||
&mut self,
|
||||
input: TurnTokenUsageFact,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let turn_id = input.turn_id.clone();
|
||||
let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState {
|
||||
connection_id: None,
|
||||
thread_id: None,
|
||||
num_input_images: None,
|
||||
resolved_config: None,
|
||||
started_at: None,
|
||||
token_usage: None,
|
||||
completed: None,
|
||||
});
|
||||
turn_state.thread_id = Some(input.thread_id);
|
||||
turn_state.token_usage = Some(input.token_usage);
|
||||
self.maybe_emit_turn_event(&turn_id, out);
|
||||
}
|
||||
|
||||
async fn ingest_skill_invoked(
|
||||
&mut self,
|
||||
input: SkillInvokedInput,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let SkillInvokedInput {
|
||||
tracking,
|
||||
invocations,
|
||||
} = input;
|
||||
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(),
|
||||
);
|
||||
out.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()),
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest_app_mentioned(&mut self, input: AppMentionedInput, out: &mut Vec<TrackEventRequest>) {
|
||||
let AppMentionedInput { tracking, mentions } = input;
|
||||
out.extend(mentions.into_iter().map(|mention| {
|
||||
let event_params = codex_app_metadata(&tracking, mention);
|
||||
TrackEventRequest::AppMentioned(CodexAppMentionedEventRequest {
|
||||
event_type: "codex_app_mentioned",
|
||||
event_params,
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
fn ingest_app_used(&mut self, input: AppUsedInput, out: &mut Vec<TrackEventRequest>) {
|
||||
let AppUsedInput { tracking, app } = input;
|
||||
let event_params = codex_app_metadata(&tracking, app);
|
||||
out.push(TrackEventRequest::AppUsed(CodexAppUsedEventRequest {
|
||||
event_type: "codex_app_used",
|
||||
event_params,
|
||||
}));
|
||||
}
|
||||
|
||||
fn ingest_plugin_used(&mut self, input: PluginUsedInput, out: &mut Vec<TrackEventRequest>) {
|
||||
let PluginUsedInput { tracking, plugin } = input;
|
||||
out.push(TrackEventRequest::PluginUsed(CodexPluginUsedEventRequest {
|
||||
event_type: "codex_plugin_used",
|
||||
event_params: codex_plugin_used_metadata(&tracking, plugin),
|
||||
}));
|
||||
}
|
||||
|
||||
fn ingest_plugin_state_changed(
|
||||
&mut self,
|
||||
input: PluginStateChangedInput,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let PluginStateChangedInput { plugin, state } = input;
|
||||
let event = CodexPluginEventRequest {
|
||||
event_type: plugin_state_event_type(state),
|
||||
event_params: codex_plugin_metadata(plugin),
|
||||
};
|
||||
out.push(match state {
|
||||
PluginState::Installed => TrackEventRequest::PluginInstalled(event),
|
||||
PluginState::Uninstalled => TrackEventRequest::PluginUninstalled(event),
|
||||
PluginState::Enabled => TrackEventRequest::PluginEnabled(event),
|
||||
PluginState::Disabled => TrackEventRequest::PluginDisabled(event),
|
||||
});
|
||||
}
|
||||
|
||||
fn ingest_response(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
response: ClientResponse,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
match response {
|
||||
ClientResponse::ThreadStart { response, .. } => {
|
||||
self.emit_thread_initialized(
|
||||
connection_id,
|
||||
response.thread,
|
||||
response.model,
|
||||
ThreadInitializationMode::New,
|
||||
out,
|
||||
);
|
||||
}
|
||||
ClientResponse::ThreadResume { response, .. } => {
|
||||
self.emit_thread_initialized(
|
||||
connection_id,
|
||||
response.thread,
|
||||
response.model,
|
||||
ThreadInitializationMode::Resumed,
|
||||
out,
|
||||
);
|
||||
}
|
||||
ClientResponse::ThreadFork { response, .. } => {
|
||||
self.emit_thread_initialized(
|
||||
connection_id,
|
||||
response.thread,
|
||||
response.model,
|
||||
ThreadInitializationMode::Forked,
|
||||
out,
|
||||
);
|
||||
}
|
||||
ClientResponse::TurnStart {
|
||||
request_id,
|
||||
response,
|
||||
} => {
|
||||
let turn_id = response.turn.id;
|
||||
let Some(RequestState::TurnStart(pending_request)) =
|
||||
self.requests.remove(&(connection_id, request_id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState {
|
||||
connection_id: None,
|
||||
thread_id: None,
|
||||
num_input_images: None,
|
||||
resolved_config: None,
|
||||
started_at: None,
|
||||
token_usage: None,
|
||||
completed: None,
|
||||
});
|
||||
turn_state.connection_id = Some(connection_id);
|
||||
turn_state.thread_id = Some(pending_request.thread_id);
|
||||
turn_state.num_input_images = Some(pending_request.num_input_images);
|
||||
self.maybe_emit_turn_event(&turn_id, out);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest_notification(
|
||||
&mut self,
|
||||
notification: ServerNotification,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
match notification {
|
||||
ServerNotification::TurnStarted(notification) => {
|
||||
let turn_state = self.turns.entry(notification.turn.id).or_insert(TurnState {
|
||||
connection_id: None,
|
||||
thread_id: None,
|
||||
num_input_images: None,
|
||||
resolved_config: None,
|
||||
started_at: None,
|
||||
token_usage: None,
|
||||
completed: None,
|
||||
});
|
||||
turn_state.started_at = notification
|
||||
.turn
|
||||
.started_at
|
||||
.and_then(|started_at| u64::try_from(started_at).ok());
|
||||
}
|
||||
ServerNotification::TurnCompleted(notification) => {
|
||||
let turn_state =
|
||||
self.turns
|
||||
.entry(notification.turn.id.clone())
|
||||
.or_insert(TurnState {
|
||||
connection_id: None,
|
||||
thread_id: None,
|
||||
num_input_images: None,
|
||||
resolved_config: None,
|
||||
started_at: None,
|
||||
token_usage: None,
|
||||
completed: None,
|
||||
});
|
||||
turn_state.completed = Some(CompletedTurnState {
|
||||
status: analytics_turn_status(notification.turn.status),
|
||||
turn_error: notification
|
||||
.turn
|
||||
.error
|
||||
.and_then(|error| error.codex_error_info),
|
||||
completed_at: notification
|
||||
.turn
|
||||
.completed_at
|
||||
.and_then(|completed_at| u64::try_from(completed_at).ok())
|
||||
.unwrap_or_default(),
|
||||
duration_ms: notification
|
||||
.turn
|
||||
.duration_ms
|
||||
.and_then(|duration_ms| u64::try_from(duration_ms).ok()),
|
||||
});
|
||||
let turn_id = notification.turn.id;
|
||||
self.maybe_emit_turn_event(&turn_id, out);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_thread_initialized(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
thread: codex_app_server_protocol::Thread,
|
||||
model: String,
|
||||
initialization_mode: ThreadInitializationMode,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let thread_source: SessionSource = thread.source.into();
|
||||
let Some(connection_state) = self.connections.get(&connection_id) else {
|
||||
return;
|
||||
};
|
||||
out.push(TrackEventRequest::ThreadInitialized(
|
||||
ThreadInitializedEvent {
|
||||
event_type: "codex_thread_initialized",
|
||||
event_params: ThreadInitializedEventParams {
|
||||
thread_id: thread.id,
|
||||
app_server_client: connection_state.app_server_client.clone(),
|
||||
runtime: connection_state.runtime.clone(),
|
||||
model,
|
||||
ephemeral: thread.ephemeral,
|
||||
thread_source: thread_source_name(&thread_source),
|
||||
initialization_mode,
|
||||
subagent_source: None,
|
||||
parent_thread_id: None,
|
||||
created_at: u64::try_from(thread.created_at).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn maybe_emit_turn_event(&mut self, turn_id: &str, out: &mut Vec<TrackEventRequest>) {
|
||||
let Some(turn_state) = self.turns.get(turn_id) else {
|
||||
return;
|
||||
};
|
||||
if turn_state.thread_id.is_none()
|
||||
|| turn_state.num_input_images.is_none()
|
||||
|| turn_state.resolved_config.is_none()
|
||||
|| turn_state.completed.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let product_client_id = turn_state
|
||||
.connection_id
|
||||
.and_then(|connection_id| self.connections.get(&connection_id))
|
||||
.map(|connection_state| connection_state.app_server_client.product_client_id.clone())
|
||||
.unwrap_or_else(|| originator().value);
|
||||
out.push(TrackEventRequest::TurnEvent(Box::new(
|
||||
CodexTurnEventRequest {
|
||||
event_type: "codex_turn_event",
|
||||
event_params: codex_turn_event_params(
|
||||
product_client_id,
|
||||
turn_id.to_string(),
|
||||
turn_state,
|
||||
),
|
||||
},
|
||||
)));
|
||||
self.turns.remove(turn_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_turn_event_params(
|
||||
product_client_id: String,
|
||||
turn_id: String,
|
||||
turn_state: &TurnState,
|
||||
) -> CodexTurnEventParams {
|
||||
let (Some(thread_id), Some(num_input_images), Some(resolved_config), Some(completed)) = (
|
||||
turn_state.thread_id.clone(),
|
||||
turn_state.num_input_images,
|
||||
turn_state.resolved_config.clone(),
|
||||
turn_state.completed.clone(),
|
||||
) else {
|
||||
unreachable!("turn event params require a fully populated turn state");
|
||||
};
|
||||
let started_at = turn_state.started_at;
|
||||
let TurnResolvedConfigFact {
|
||||
turn_id: _resolved_turn_id,
|
||||
thread_id: _resolved_thread_id,
|
||||
num_input_images: _resolved_num_input_images,
|
||||
submission_type,
|
||||
model,
|
||||
model_provider,
|
||||
sandbox_policy,
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
service_tier,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_network_access,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
is_first_turn,
|
||||
} = resolved_config;
|
||||
let token_usage = turn_state.token_usage.clone();
|
||||
CodexTurnEventParams {
|
||||
thread_id,
|
||||
turn_id,
|
||||
product_client_id,
|
||||
submission_type,
|
||||
model: Some(model),
|
||||
model_provider,
|
||||
sandbox_policy: Some(sandbox_policy_mode(&sandbox_policy)),
|
||||
reasoning_effort: reasoning_effort.map(|value| value.to_string()),
|
||||
reasoning_summary: reasoning_summary_mode(reasoning_summary),
|
||||
service_tier: service_tier
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
approval_policy: approval_policy.to_string(),
|
||||
approvals_reviewer: approvals_reviewer.to_string(),
|
||||
sandbox_network_access,
|
||||
collaboration_mode: Some(collaboration_mode_mode(collaboration_mode)),
|
||||
personality: personality_mode(personality),
|
||||
num_input_images,
|
||||
is_first_turn,
|
||||
status: completed.status,
|
||||
turn_error: completed.turn_error,
|
||||
steer_count: None,
|
||||
total_tool_call_count: None,
|
||||
shell_command_count: None,
|
||||
file_change_count: None,
|
||||
mcp_tool_call_count: None,
|
||||
dynamic_tool_call_count: None,
|
||||
subagent_tool_call_count: None,
|
||||
web_search_count: None,
|
||||
image_generation_count: None,
|
||||
input_tokens: token_usage
|
||||
.as_ref()
|
||||
.map(|token_usage| token_usage.input_tokens),
|
||||
cached_input_tokens: token_usage
|
||||
.as_ref()
|
||||
.map(|token_usage| token_usage.cached_input_tokens),
|
||||
output_tokens: token_usage
|
||||
.as_ref()
|
||||
.map(|token_usage| token_usage.output_tokens),
|
||||
reasoning_output_tokens: token_usage
|
||||
.as_ref()
|
||||
.map(|token_usage| token_usage.reasoning_output_tokens),
|
||||
total_tokens: token_usage
|
||||
.as_ref()
|
||||
.map(|token_usage| token_usage.total_tokens),
|
||||
duration_ms: completed.duration_ms,
|
||||
started_at,
|
||||
completed_at: Some(completed.completed_at),
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_policy_mode(sandbox_policy: &SandboxPolicy) -> &'static str {
|
||||
match sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess => "full_access",
|
||||
SandboxPolicy::ReadOnly { .. } => "read_only",
|
||||
SandboxPolicy::WorkspaceWrite { .. } => "workspace_write",
|
||||
SandboxPolicy::ExternalSandbox { .. } => "external_sandbox",
|
||||
}
|
||||
}
|
||||
|
||||
fn collaboration_mode_mode(mode: ModeKind) -> &'static str {
|
||||
match mode {
|
||||
ModeKind::Plan => "plan",
|
||||
ModeKind::Default | ModeKind::PairProgramming | ModeKind::Execute => "default",
|
||||
}
|
||||
}
|
||||
|
||||
fn reasoning_summary_mode(summary: Option<ReasoningSummary>) -> Option<String> {
|
||||
match summary {
|
||||
Some(ReasoningSummary::None) | None => None,
|
||||
Some(summary) => Some(summary.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn personality_mode(personality: Option<Personality>) -> Option<String> {
|
||||
match personality {
|
||||
Some(Personality::None) | None => None,
|
||||
Some(personality) => Some(personality.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn analytics_turn_status(status: codex_app_server_protocol::TurnStatus) -> Option<TurnStatus> {
|
||||
match status {
|
||||
codex_app_server_protocol::TurnStatus::Completed => Some(TurnStatus::Completed),
|
||||
codex_app_server_protocol::TurnStatus::Failed => Some(TurnStatus::Failed),
|
||||
codex_app_server_protocol::TurnStatus::Interrupted => Some(TurnStatus::Interrupted),
|
||||
codex_app_server_protocol::TurnStatus::InProgress => None,
|
||||
}
|
||||
}
|
||||
|
||||
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::Sha1::new();
|
||||
sha1::Digest::update(&mut hasher, raw_id.as_bytes());
|
||||
format!("{:x}", sha1::Digest::finalize(hasher))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub(crate) 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('\\', "/"),
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ license.workspace = true
|
||||
name = "codex_ansi_escape"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
ansi-to-tui = { workspace = true }
|
||||
ratatui = { workspace = true, features = [
|
||||
|
||||
@@ -1060,6 +1060,9 @@ mod tests {
|
||||
items: Vec::new(),
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
completed_at: Some(0),
|
||||
duration_ms: Some(1),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1834,6 +1837,9 @@ mod tests {
|
||||
items: Vec::new(),
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
completed_at: Some(0),
|
||||
duration_ms: None,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -17,12 +17,12 @@ clap = { workspace = true, features = ["derive"] }
|
||||
codex-experimental-api-macros = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-shell-command = { 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 = [
|
||||
|
||||
@@ -800,7 +800,7 @@
|
||||
"description": "Stop filesystem watch notifications for a prior `fs/watch`.",
|
||||
"properties": {
|
||||
"watchId": {
|
||||
"description": "Watch identifier returned by `fs/watch`.",
|
||||
"description": "Watch identifier previously provided to `fs/watch`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -819,10 +819,15 @@
|
||||
}
|
||||
],
|
||||
"description": "Absolute file or directory path to watch."
|
||||
},
|
||||
"watchId": {
|
||||
"description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
"path",
|
||||
"watchId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -1042,6 +1047,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"detail": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerStatusDetail"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted."
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional page size; defaults to a server-defined value.",
|
||||
"format": "uint32",
|
||||
@@ -1208,6 +1224,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpResourceReadParams": {
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"uri": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server",
|
||||
"threadId",
|
||||
"uri"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerOauthLoginParams": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -1235,6 +1270,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerStatusDetail": {
|
||||
"enum": [
|
||||
"full",
|
||||
"toolsAndAuthOnly"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MergeStrategy": {
|
||||
"enum": [
|
||||
"replace",
|
||||
@@ -2815,6 +2857,48 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeStartTransport": {
|
||||
"description": "EXPERIMENTAL - transport used by thread realtime.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"websocket"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"description": "SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the realtime events data channel.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webrtc"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"type"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ThreadResumeParams": {
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
@@ -4420,6 +4504,30 @@
|
||||
"title": "McpServerStatus/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"mcpServer/resource/read"
|
||||
],
|
||||
"title": "McpServer/resource/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/McpResourceReadParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "McpServer/resource/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -1013,7 +1013,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"watchId": {
|
||||
"description": "Watch identifier returned by `fs/watch`.",
|
||||
"description": "Watch identifier previously provided to `fs/watch`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -1163,6 +1163,176 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"GuardianApprovalReviewAction": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"command"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"cwd",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"argv": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"program": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"execve"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"argv",
|
||||
"cwd",
|
||||
"program",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"applyPatch"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"files",
|
||||
"type"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"protocol": {
|
||||
"$ref": "#/definitions/NetworkApprovalProtocol"
|
||||
},
|
||||
"target": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"networkAccess"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"protocol",
|
||||
"target",
|
||||
"type"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"connectorName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolName": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolTitle": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"mcpToolCall"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server",
|
||||
"toolName",
|
||||
"type"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GuardianApprovalReviewStatus": {
|
||||
"description": "[UNSTABLE] Lifecycle state for a guardian approval review.",
|
||||
"enum": [
|
||||
@@ -1173,6 +1343,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianCommandSource": {
|
||||
"enum": [
|
||||
"shell",
|
||||
"unifiedExec"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianRiskLevel": {
|
||||
"description": "[UNSTABLE] Risk level assigned by guardian approval review.",
|
||||
"enum": [
|
||||
@@ -1400,7 +1577,9 @@
|
||||
"ItemGuardianApprovalReviewCompletedNotification": {
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"properties": {
|
||||
"action": true,
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
@@ -1415,6 +1594,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"threadId",
|
||||
@@ -1425,7 +1605,9 @@
|
||||
"ItemGuardianApprovalReviewStartedNotification": {
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"properties": {
|
||||
"action": true,
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
@@ -1440,6 +1622,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"threadId",
|
||||
@@ -1553,6 +1736,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -1672,6 +1856,15 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"socks5Tcp",
|
||||
"socks5Udp"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
@@ -2233,6 +2426,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -3104,6 +3304,22 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeSdpNotification": {
|
||||
"description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.",
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeStartedNotification": {
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
"properties": {
|
||||
@@ -3333,6 +3549,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -3354,6 +3586,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
@@ -4703,6 +4943,26 @@
|
||||
"title": "Thread/realtime/outputAudio/deltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/realtime/sdp"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadRealtimeSdpNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
@@ -1201,6 +1201,30 @@
|
||||
"title": "McpServerStatus/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"mcpServer/resource/read"
|
||||
],
|
||||
"title": "McpServer/resource/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/McpResourceReadParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "McpServer/resource/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -3063,7 +3087,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"protocol": {
|
||||
"$ref": "#/definitions/NetworkApprovalProtocol"
|
||||
"$ref": "#/definitions/v2/NetworkApprovalProtocol"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -3072,15 +3096,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"socks5Tcp",
|
||||
"socks5Udp"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkPolicyAmendment": {
|
||||
"properties": {
|
||||
"action": {
|
||||
@@ -4320,6 +4335,26 @@
|
||||
"title": "Thread/realtime/outputAudio/deltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/realtime/sdp"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadRealtimeSdpNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -7496,7 +7531,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"watchId": {
|
||||
"description": "Watch identifier returned by `fs/watch`.",
|
||||
"description": "Watch identifier previously provided to `fs/watch`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -7766,7 +7801,7 @@
|
||||
"description": "Stop filesystem watch notifications for a prior `fs/watch`.",
|
||||
"properties": {
|
||||
"watchId": {
|
||||
"description": "Watch identifier returned by `fs/watch`.",
|
||||
"description": "Watch identifier previously provided to `fs/watch`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -7793,25 +7828,6 @@
|
||||
}
|
||||
],
|
||||
"description": "Absolute file or directory path to watch."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FsWatchParams",
|
||||
"type": "object"
|
||||
},
|
||||
"FsWatchResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Created watch handle returned by `fs/watch`.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Canonicalized path associated with the watch."
|
||||
},
|
||||
"watchId": {
|
||||
"description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.",
|
||||
@@ -7822,6 +7838,25 @@
|
||||
"path",
|
||||
"watchId"
|
||||
],
|
||||
"title": "FsWatchParams",
|
||||
"type": "object"
|
||||
},
|
||||
"FsWatchResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Successful response for `fs/watch`.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Canonicalized path associated with the watch."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FsWatchResponse",
|
||||
"type": "object"
|
||||
},
|
||||
@@ -8077,6 +8112,176 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"GuardianApprovalReviewAction": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/v2/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"command"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"cwd",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"argv": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"program": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/v2/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"execve"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"argv",
|
||||
"cwd",
|
||||
"program",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"applyPatch"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"files",
|
||||
"type"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"protocol": {
|
||||
"$ref": "#/definitions/v2/NetworkApprovalProtocol"
|
||||
},
|
||||
"target": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"networkAccess"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"protocol",
|
||||
"target",
|
||||
"type"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"connectorName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolName": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolTitle": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"mcpToolCall"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server",
|
||||
"toolName",
|
||||
"type"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GuardianApprovalReviewStatus": {
|
||||
"description": "[UNSTABLE] Lifecycle state for a guardian approval review.",
|
||||
"enum": [
|
||||
@@ -8087,6 +8292,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianCommandSource": {
|
||||
"enum": [
|
||||
"shell",
|
||||
"unifiedExec"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianRiskLevel": {
|
||||
"description": "[UNSTABLE] Risk level assigned by guardian approval review.",
|
||||
"enum": [
|
||||
@@ -8349,7 +8561,9 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"properties": {
|
||||
"action": true,
|
||||
"action": {
|
||||
"$ref": "#/definitions/v2/GuardianApprovalReviewAction"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/v2/GuardianApprovalReview"
|
||||
},
|
||||
@@ -8364,6 +8578,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"threadId",
|
||||
@@ -8376,7 +8591,9 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"properties": {
|
||||
"action": true,
|
||||
"action": {
|
||||
"$ref": "#/definitions/v2/GuardianApprovalReviewAction"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/v2/GuardianApprovalReview"
|
||||
},
|
||||
@@ -8391,6 +8608,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"threadId",
|
||||
@@ -8430,6 +8648,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"detail": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/McpServerStatusDetail"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted."
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional page size; defaults to a server-defined value.",
|
||||
"format": "uint32",
|
||||
@@ -8755,6 +8984,43 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpResourceReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"uri": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server",
|
||||
"threadId",
|
||||
"uri"
|
||||
],
|
||||
"title": "McpResourceReadParams",
|
||||
"type": "object"
|
||||
},
|
||||
"McpResourceReadResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"contents": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ResourceContent"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"contents"
|
||||
],
|
||||
"title": "McpResourceReadResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerOauthLoginCompletedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -8870,6 +9136,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerStatusDetail": {
|
||||
"enum": [
|
||||
"full",
|
||||
"toolsAndAuthOnly"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpServerStatusUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -8931,6 +9204,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -9249,6 +9523,15 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"socks5Tcp",
|
||||
"socks5Udp"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkDomainPermission": {
|
||||
"enum": [
|
||||
"allow",
|
||||
@@ -9290,6 +9573,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerFullAccessDenylistOnly": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowAllUnixSockets": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -10460,6 +10749,57 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ResourceContent": {
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"mimeType": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"uri": {
|
||||
"description": "The URI of this resource.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text",
|
||||
"uri"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"blob": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"uri": {
|
||||
"description": "The URI of this resource.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"blob",
|
||||
"uri"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "Contents returned when reading a resource from an MCP server."
|
||||
},
|
||||
"ResourceTemplate": {
|
||||
"description": "A template description for resources available on the server.",
|
||||
"properties": {
|
||||
@@ -12003,6 +12343,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -13328,6 +13675,66 @@
|
||||
"title": "ThreadRealtimeOutputAudioDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeSdpNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.",
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadRealtimeSdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeStartTransport": {
|
||||
"description": "EXPERIMENTAL - transport used by thread realtime.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"websocket"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"description": "SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the realtime events data channel.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webrtc"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"type"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ThreadRealtimeStartedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
@@ -14139,6 +14546,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -14160,6 +14583,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/v2/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -1776,6 +1776,30 @@
|
||||
"title": "McpServerStatus/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"mcpServer/resource/read"
|
||||
],
|
||||
"title": "McpServer/resource/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/McpResourceReadParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "McpServer/resource/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -4155,7 +4179,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"watchId": {
|
||||
"description": "Watch identifier returned by `fs/watch`.",
|
||||
"description": "Watch identifier previously provided to `fs/watch`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -4425,7 +4449,7 @@
|
||||
"description": "Stop filesystem watch notifications for a prior `fs/watch`.",
|
||||
"properties": {
|
||||
"watchId": {
|
||||
"description": "Watch identifier returned by `fs/watch`.",
|
||||
"description": "Watch identifier previously provided to `fs/watch`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -4452,25 +4476,6 @@
|
||||
}
|
||||
],
|
||||
"description": "Absolute file or directory path to watch."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FsWatchParams",
|
||||
"type": "object"
|
||||
},
|
||||
"FsWatchResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Created watch handle returned by `fs/watch`.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Canonicalized path associated with the watch."
|
||||
},
|
||||
"watchId": {
|
||||
"description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.",
|
||||
@@ -4481,6 +4486,25 @@
|
||||
"path",
|
||||
"watchId"
|
||||
],
|
||||
"title": "FsWatchParams",
|
||||
"type": "object"
|
||||
},
|
||||
"FsWatchResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Successful response for `fs/watch`.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Canonicalized path associated with the watch."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FsWatchResponse",
|
||||
"type": "object"
|
||||
},
|
||||
@@ -4847,6 +4871,176 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"GuardianApprovalReviewAction": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"command"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"cwd",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"argv": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"program": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"execve"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"argv",
|
||||
"cwd",
|
||||
"program",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"applyPatch"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"files",
|
||||
"type"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"protocol": {
|
||||
"$ref": "#/definitions/NetworkApprovalProtocol"
|
||||
},
|
||||
"target": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"networkAccess"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"protocol",
|
||||
"target",
|
||||
"type"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"connectorName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolName": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolTitle": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"mcpToolCall"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server",
|
||||
"toolName",
|
||||
"type"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GuardianApprovalReviewStatus": {
|
||||
"description": "[UNSTABLE] Lifecycle state for a guardian approval review.",
|
||||
"enum": [
|
||||
@@ -4857,6 +5051,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianCommandSource": {
|
||||
"enum": [
|
||||
"shell",
|
||||
"unifiedExec"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianRiskLevel": {
|
||||
"description": "[UNSTABLE] Risk level assigned by guardian approval review.",
|
||||
"enum": [
|
||||
@@ -5163,7 +5364,9 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"properties": {
|
||||
"action": true,
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
@@ -5178,6 +5381,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"threadId",
|
||||
@@ -5190,7 +5394,9 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"properties": {
|
||||
"action": true,
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
@@ -5205,6 +5411,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"threadId",
|
||||
@@ -5244,6 +5451,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"detail": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerStatusDetail"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted."
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional page size; defaults to a server-defined value.",
|
||||
"format": "uint32",
|
||||
@@ -5569,6 +5787,43 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpResourceReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"uri": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server",
|
||||
"threadId",
|
||||
"uri"
|
||||
],
|
||||
"title": "McpResourceReadParams",
|
||||
"type": "object"
|
||||
},
|
||||
"McpResourceReadResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"contents": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ResourceContent"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"contents"
|
||||
],
|
||||
"title": "McpResourceReadResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerOauthLoginCompletedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -5684,6 +5939,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerStatusDetail": {
|
||||
"enum": [
|
||||
"full",
|
||||
"toolsAndAuthOnly"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpServerStatusUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -5745,6 +6007,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -6063,6 +6326,15 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"socks5Tcp",
|
||||
"socks5Udp"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkDomainPermission": {
|
||||
"enum": [
|
||||
"allow",
|
||||
@@ -6104,6 +6376,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerFullAccessDenylistOnly": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowAllUnixSockets": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -7274,6 +7552,57 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ResourceContent": {
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"mimeType": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"uri": {
|
||||
"description": "The URI of this resource.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text",
|
||||
"uri"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"blob": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"uri": {
|
||||
"description": "The URI of this resource.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"blob",
|
||||
"uri"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "Contents returned when reading a resource from an MCP server."
|
||||
},
|
||||
"ResourceTemplate": {
|
||||
"description": "A template description for resources available on the server.",
|
||||
"properties": {
|
||||
@@ -9107,6 +9436,26 @@
|
||||
"title": "Thread/realtime/outputAudio/deltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/realtime/sdp"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadRealtimeSdpNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -9849,6 +10198,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -11174,6 +11530,66 @@
|
||||
"title": "ThreadRealtimeOutputAudioDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeSdpNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.",
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadRealtimeSdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeStartTransport": {
|
||||
"description": "EXPERIMENTAL - transport used by thread realtime.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"websocket"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"description": "SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the realtime events data channel.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webrtc"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"type"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ThreadRealtimeStartedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
@@ -11985,6 +12401,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -12006,6 +12438,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ApprovalsReviewer": {
|
||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.",
|
||||
"enum": [
|
||||
"user",
|
||||
"guardian_subagent"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AskForApproval": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -143,6 +151,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerFullAccessDenylistOnly": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowAllUnixSockets": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"watchId": {
|
||||
"description": "Watch identifier returned by `fs/watch`.",
|
||||
"description": "Watch identifier previously provided to `fs/watch`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Stop filesystem watch notifications for a prior `fs/watch`.",
|
||||
"properties": {
|
||||
"watchId": {
|
||||
"description": "Watch identifier returned by `fs/watch`.",
|
||||
"description": "Watch identifier previously provided to `fs/watch`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,10 +15,15 @@
|
||||
}
|
||||
],
|
||||
"description": "Absolute file or directory path to watch."
|
||||
},
|
||||
"watchId": {
|
||||
"description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
"path",
|
||||
"watchId"
|
||||
],
|
||||
"title": "FsWatchParams",
|
||||
"type": "object"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Created watch handle returned by `fs/watch`.",
|
||||
"description": "Successful response for `fs/watch`.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"allOf": [
|
||||
@@ -15,15 +15,10 @@
|
||||
}
|
||||
],
|
||||
"description": "Canonicalized path associated with the watch."
|
||||
},
|
||||
"watchId": {
|
||||
"description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"watchId"
|
||||
"path"
|
||||
],
|
||||
"title": "FsWatchResponse",
|
||||
"type": "object"
|
||||
|
||||
@@ -294,6 +294,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -37,6 +37,176 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"GuardianApprovalReviewAction": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"command"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"cwd",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"argv": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"program": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"execve"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"argv",
|
||||
"cwd",
|
||||
"program",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"applyPatch"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"files",
|
||||
"type"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"protocol": {
|
||||
"$ref": "#/definitions/NetworkApprovalProtocol"
|
||||
},
|
||||
"target": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"networkAccess"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"protocol",
|
||||
"target",
|
||||
"type"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"connectorName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolName": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolTitle": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"mcpToolCall"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server",
|
||||
"toolName",
|
||||
"type"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GuardianApprovalReviewStatus": {
|
||||
"description": "[UNSTABLE] Lifecycle state for a guardian approval review.",
|
||||
"enum": [
|
||||
@@ -47,6 +217,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianCommandSource": {
|
||||
"enum": [
|
||||
"shell",
|
||||
"unifiedExec"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianRiskLevel": {
|
||||
"description": "[UNSTABLE] Risk level assigned by guardian approval review.",
|
||||
"enum": [
|
||||
@@ -55,11 +232,22 @@
|
||||
"high"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"socks5Tcp",
|
||||
"socks5Udp"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"properties": {
|
||||
"action": true,
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
@@ -74,6 +262,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"threadId",
|
||||
|
||||
@@ -37,6 +37,176 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"GuardianApprovalReviewAction": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"command"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"cwd",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "CommandGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"argv": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"program": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/GuardianCommandSource"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"execve"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"argv",
|
||||
"cwd",
|
||||
"program",
|
||||
"source",
|
||||
"type"
|
||||
],
|
||||
"title": "ExecveGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"applyPatch"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"files",
|
||||
"type"
|
||||
],
|
||||
"title": "ApplyPatchGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"protocol": {
|
||||
"$ref": "#/definitions/NetworkApprovalProtocol"
|
||||
},
|
||||
"target": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"networkAccess"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"protocol",
|
||||
"target",
|
||||
"type"
|
||||
],
|
||||
"title": "NetworkAccessGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"connectorName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolName": {
|
||||
"type": "string"
|
||||
},
|
||||
"toolTitle": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"mcpToolCall"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewActionType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server",
|
||||
"toolName",
|
||||
"type"
|
||||
],
|
||||
"title": "McpToolCallGuardianApprovalReviewAction",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GuardianApprovalReviewStatus": {
|
||||
"description": "[UNSTABLE] Lifecycle state for a guardian approval review.",
|
||||
"enum": [
|
||||
@@ -47,6 +217,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianCommandSource": {
|
||||
"enum": [
|
||||
"shell",
|
||||
"unifiedExec"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"GuardianRiskLevel": {
|
||||
"description": "[UNSTABLE] Risk level assigned by guardian approval review.",
|
||||
"enum": [
|
||||
@@ -55,11 +232,22 @@
|
||||
"high"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"socks5Tcp",
|
||||
"socks5Udp"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "[UNSTABLE] Temporary notification payload for guardian automatic approval review. This shape is expected to change soon.\n\nTODO(ccunningham): Attach guardian review state to the reviewed tool item's lifecycle instead of sending separate standalone review notifications so the app-server API can persist and replay review state via `thread/read`.",
|
||||
"properties": {
|
||||
"action": true,
|
||||
"action": {
|
||||
"$ref": "#/definitions/GuardianApprovalReviewAction"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/GuardianApprovalReview"
|
||||
},
|
||||
@@ -74,6 +262,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"review",
|
||||
"targetItemId",
|
||||
"threadId",
|
||||
|
||||
@@ -294,6 +294,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"McpServerStatusDetail": {
|
||||
"enum": [
|
||||
"full",
|
||||
"toolsAndAuthOnly"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"description": "Opaque pagination cursor returned by a previous call.",
|
||||
@@ -8,6 +17,17 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"detail": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerStatusDetail"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted."
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional page size; defaults to a server-defined value.",
|
||||
"format": "uint32",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"uri": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server",
|
||||
"threadId",
|
||||
"uri"
|
||||
],
|
||||
"title": "McpResourceReadParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ResourceContent": {
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"mimeType": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"uri": {
|
||||
"description": "The URI of this resource.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text",
|
||||
"uri"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"blob": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"uri": {
|
||||
"description": "The URI of this resource.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"blob",
|
||||
"uri"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "Contents returned when reading a resource from an MCP server."
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"contents": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ResourceContent"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"contents"
|
||||
],
|
||||
"title": "McpResourceReadResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -430,6 +430,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -1267,6 +1268,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1288,6 +1305,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -518,6 +518,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -1042,6 +1043,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1849,6 +1857,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1870,6 +1894,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -800,6 +801,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1607,6 +1615,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1628,6 +1652,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -800,6 +801,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1607,6 +1615,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1628,6 +1652,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -800,6 +801,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1607,6 +1615,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1628,6 +1652,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.",
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadRealtimeSdpNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -518,6 +518,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -1042,6 +1043,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1849,6 +1857,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1870,6 +1894,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -800,6 +801,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1607,6 +1615,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1628,6 +1652,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -518,6 +518,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -1042,6 +1043,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1849,6 +1857,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1870,6 +1894,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -800,6 +801,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1607,6 +1615,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1628,6 +1652,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -800,6 +801,13 @@
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forkedFromId": {
|
||||
"description": "Source thread id when this thread was created by forking another thread.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1607,6 +1615,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1628,6 +1652,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -430,6 +430,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -1267,6 +1268,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1288,6 +1305,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -430,6 +430,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -1267,6 +1268,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1288,6 +1305,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
@@ -430,6 +430,7 @@
|
||||
},
|
||||
"McpToolCallResult": {
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"content": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
@@ -1267,6 +1268,22 @@
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
"completedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn completed.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"durationMs": {
|
||||
"description": "Duration between turn start and completion in milliseconds, if known.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1288,6 +1305,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/TurnStatus"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
/**
|
||||
* Contents returned when reading a resource from an MCP server.
|
||||
*/
|
||||
export type ResourceContent = {
|
||||
/**
|
||||
* The URI of this resource.
|
||||
*/
|
||||
uri: string, mimeType?: string, text: string, _meta?: JsonValue, } | {
|
||||
/**
|
||||
* The URI of this resource.
|
||||
*/
|
||||
uri: string, mimeType?: string, blob: string, _meta?: JsonValue, };
|
||||
@@ -41,6 +41,7 @@ import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosed
|
||||
import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification";
|
||||
import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification";
|
||||
import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification";
|
||||
import type { ThreadRealtimeSdpNotification } from "./v2/ThreadRealtimeSdpNotification";
|
||||
import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification";
|
||||
import type { ThreadRealtimeTranscriptUpdatedNotification } from "./v2/ThreadRealtimeTranscriptUpdatedNotification";
|
||||
import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification";
|
||||
@@ -57,4 +58,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
|
||||
/**
|
||||
* Notification sent from the server to the client.
|
||||
*/
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
|
||||
@@ -55,6 +55,7 @@ export type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSumm
|
||||
export type { ReasoningSummary } from "./ReasoningSummary";
|
||||
export type { RequestId } from "./RequestId";
|
||||
export type { Resource } from "./Resource";
|
||||
export type { ResourceContent } from "./ResourceContent";
|
||||
export type { ResourceTemplate } from "./ResourceTemplate";
|
||||
export type { ResponseItem } from "./ResponseItem";
|
||||
export type { ReviewDecision } from "./ReviewDecision";
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
*/
|
||||
export type FsChangedNotification = {
|
||||
/**
|
||||
* Watch identifier returned by `fs/watch`.
|
||||
* Watch identifier previously provided to `fs/watch`.
|
||||
*/
|
||||
watchId: string,
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
*/
|
||||
export type FsUnwatchParams = {
|
||||
/**
|
||||
* Watch identifier returned by `fs/watch`.
|
||||
* Watch identifier previously provided to `fs/watch`.
|
||||
*/
|
||||
watchId: string, };
|
||||
|
||||
@@ -7,6 +7,10 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
* Start filesystem watch notifications for an absolute path.
|
||||
*/
|
||||
export type FsWatchParams = {
|
||||
/**
|
||||
* Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.
|
||||
*/
|
||||
watchId: string,
|
||||
/**
|
||||
* Absolute file or directory path to watch.
|
||||
*/
|
||||
|
||||
@@ -4,13 +4,9 @@
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
/**
|
||||
* Created watch handle returned by `fs/watch`.
|
||||
* Successful response for `fs/watch`.
|
||||
*/
|
||||
export type FsWatchResponse = {
|
||||
/**
|
||||
* Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.
|
||||
*/
|
||||
watchId: string,
|
||||
/**
|
||||
* Canonicalized path associated with the watch.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GuardianCommandSource } from "./GuardianCommandSource";
|
||||
import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
|
||||
|
||||
export type GuardianApprovalReviewAction = { "type": "command", source: GuardianCommandSource, command: string, cwd: string, } | { "type": "execve", source: GuardianCommandSource, program: string, argv: Array<string>, cwd: string, } | { "type": "applyPatch", cwd: string, files: Array<string>, } | { "type": "networkAccess", target: string, host: string, protocol: NetworkApprovalProtocol, port: number, } | { "type": "mcpToolCall", server: string, toolName: string, connectorId: string | null, connectorName: string | null, toolTitle: string | null, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type GuardianCommandSource = "shell" | "unifiedExec";
|
||||
@@ -1,8 +1,8 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { GuardianApprovalReview } from "./GuardianApprovalReview";
|
||||
import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction";
|
||||
|
||||
/**
|
||||
* [UNSTABLE] Temporary notification payload for guardian automatic approval
|
||||
@@ -12,4 +12,4 @@ import type { GuardianApprovalReview } from "./GuardianApprovalReview";
|
||||
* lifecycle instead of sending separate standalone review notifications so the
|
||||
* app-server API can persist and replay review state via `thread/read`.
|
||||
*/
|
||||
export type ItemGuardianApprovalReviewCompletedNotification = { threadId: string, turnId: string, targetItemId: string, review: GuardianApprovalReview, action: JsonValue | null, };
|
||||
export type ItemGuardianApprovalReviewCompletedNotification = { threadId: string, turnId: string, targetItemId: string, review: GuardianApprovalReview, action: GuardianApprovalReviewAction, };
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { GuardianApprovalReview } from "./GuardianApprovalReview";
|
||||
import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction";
|
||||
|
||||
/**
|
||||
* [UNSTABLE] Temporary notification payload for guardian automatic approval
|
||||
@@ -12,4 +12,4 @@ import type { GuardianApprovalReview } from "./GuardianApprovalReview";
|
||||
* lifecycle instead of sending separate standalone review notifications so the
|
||||
* app-server API can persist and replay review state via `thread/read`.
|
||||
*/
|
||||
export type ItemGuardianApprovalReviewStartedNotification = { threadId: string, turnId: string, targetItemId: string, review: GuardianApprovalReview, action: JsonValue | null, };
|
||||
export type ItemGuardianApprovalReviewStartedNotification = { threadId: string, turnId: string, targetItemId: string, review: GuardianApprovalReview, action: GuardianApprovalReviewAction, };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { McpServerStatusDetail } from "./McpServerStatusDetail";
|
||||
|
||||
export type ListMcpServerStatusParams = {
|
||||
/**
|
||||
@@ -10,4 +11,9 @@ cursor?: string | null,
|
||||
/**
|
||||
* Optional page size; defaults to a server-defined value.
|
||||
*/
|
||||
limit?: number | null, };
|
||||
limit?: number | null,
|
||||
/**
|
||||
* Controls how much MCP inventory data to fetch for each server.
|
||||
* Defaults to `Full` when omitted.
|
||||
*/
|
||||
detail?: McpServerStatusDetail | null, };
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type McpResourceReadParams = { threadId: string, server: string, uri: string, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ResourceContent } from "../ResourceContent";
|
||||
|
||||
export type McpResourceReadResponse = { contents: Array<ResourceContent>, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type McpServerStatusDetail = "full" | "toolsAndAuthOnly";
|
||||
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
|
||||
export type McpToolCallResult = { content: Array<JsonValue>, structuredContent: JsonValue | null, };
|
||||
export type McpToolCallResult = { content: Array<JsonValue>, structuredContent: JsonValue | null, _meta: JsonValue | null, };
|
||||
|
||||
@@ -29,4 +29,4 @@ unixSockets: { [key in string]?: NetworkUnixSocketPermission } | null,
|
||||
/**
|
||||
* Legacy compatibility view derived from `unix_sockets`.
|
||||
*/
|
||||
allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
|
||||
allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, dangerFullAccessDenylistOnly: boolean | null, };
|
||||
|
||||
@@ -7,6 +7,10 @@ import type { ThreadStatus } from "./ThreadStatus";
|
||||
import type { Turn } from "./Turn";
|
||||
|
||||
export type Thread = { id: string,
|
||||
/**
|
||||
* Source thread id when this thread was created by forking another thread.
|
||||
*/
|
||||
forkedFromId: string | null,
|
||||
/**
|
||||
* Usually the first user message in the thread, if available.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.
|
||||
*/
|
||||
export type ThreadRealtimeSdpNotification = { threadId: string, sdp: string, };
|
||||
@@ -0,0 +1,13 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL - transport used by thread realtime.
|
||||
*/
|
||||
export type ThreadRealtimeStartTransport = { "type": "websocket" } | { "type": "webrtc",
|
||||
/**
|
||||
* SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the
|
||||
* realtime events data channel.
|
||||
*/
|
||||
sdp: string, };
|
||||
@@ -15,4 +15,16 @@ items: Array<ThreadItem>, status: TurnStatus,
|
||||
/**
|
||||
* Only populated when the Turn's status is failed.
|
||||
*/
|
||||
error: TurnError | null, };
|
||||
error: TurnError | null,
|
||||
/**
|
||||
* Unix timestamp (in seconds) when the turn started.
|
||||
*/
|
||||
startedAt: number | null,
|
||||
/**
|
||||
* Unix timestamp (in seconds) when the turn completed.
|
||||
*/
|
||||
completedAt: number | null,
|
||||
/**
|
||||
* Duration between turn start and completion in milliseconds, if known.
|
||||
*/
|
||||
durationMs: number | null, };
|
||||
|
||||
@@ -123,7 +123,9 @@ export type { GetAccountResponse } from "./GetAccountResponse";
|
||||
export type { GitInfo } from "./GitInfo";
|
||||
export type { GrantedPermissionProfile } from "./GrantedPermissionProfile";
|
||||
export type { GuardianApprovalReview } from "./GuardianApprovalReview";
|
||||
export type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction";
|
||||
export type { GuardianApprovalReviewStatus } from "./GuardianApprovalReviewStatus";
|
||||
export type { GuardianCommandSource } from "./GuardianCommandSource";
|
||||
export type { GuardianRiskLevel } from "./GuardianRiskLevel";
|
||||
export type { HookCompletedNotification } from "./HookCompletedNotification";
|
||||
export type { HookEventName } from "./HookEventName";
|
||||
@@ -170,6 +172,8 @@ export type { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitatio
|
||||
export type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems";
|
||||
export type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema";
|
||||
export type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema";
|
||||
export type { McpResourceReadParams } from "./McpResourceReadParams";
|
||||
export type { McpResourceReadResponse } from "./McpResourceReadResponse";
|
||||
export type { McpServerElicitationAction } from "./McpServerElicitationAction";
|
||||
export type { McpServerElicitationRequestParams } from "./McpServerElicitationRequestParams";
|
||||
export type { McpServerElicitationRequestResponse } from "./McpServerElicitationRequestResponse";
|
||||
@@ -179,6 +183,7 @@ export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse"
|
||||
export type { McpServerRefreshResponse } from "./McpServerRefreshResponse";
|
||||
export type { McpServerStartupState } from "./McpServerStartupState";
|
||||
export type { McpServerStatus } from "./McpServerStatus";
|
||||
export type { McpServerStatusDetail } from "./McpServerStatusDetail";
|
||||
export type { McpServerStatusUpdatedNotification } from "./McpServerStatusUpdatedNotification";
|
||||
export type { McpToolCallError } from "./McpToolCallError";
|
||||
export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification";
|
||||
@@ -289,6 +294,8 @@ export type { ThreadRealtimeClosedNotification } from "./ThreadRealtimeClosedNot
|
||||
export type { ThreadRealtimeErrorNotification } from "./ThreadRealtimeErrorNotification";
|
||||
export type { ThreadRealtimeItemAddedNotification } from "./ThreadRealtimeItemAddedNotification";
|
||||
export type { ThreadRealtimeOutputAudioDeltaNotification } from "./ThreadRealtimeOutputAudioDeltaNotification";
|
||||
export type { ThreadRealtimeSdpNotification } from "./ThreadRealtimeSdpNotification";
|
||||
export type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTransport";
|
||||
export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification";
|
||||
export type { ThreadRealtimeTranscriptUpdatedNotification } from "./ThreadRealtimeTranscriptUpdatedNotification";
|
||||
export type { ThreadResumeParams } from "./ThreadResumeParams";
|
||||
|
||||
@@ -15,6 +15,7 @@ pub use export::generate_ts_with_options;
|
||||
pub use export::generate_types;
|
||||
pub use jsonrpc_lite::*;
|
||||
pub use protocol::common::*;
|
||||
pub use protocol::item_builders::*;
|
||||
pub use protocol::thread_history::*;
|
||||
pub use protocol::v1::ApplyPatchApprovalParams;
|
||||
pub use protocol::v1::ApplyPatchApprovalResponse;
|
||||
|
||||
@@ -459,6 +459,11 @@ client_request_definitions! {
|
||||
response: v2::ListMcpServerStatusResponse,
|
||||
},
|
||||
|
||||
McpResourceRead => "mcpServer/resource/read" {
|
||||
params: v2::McpResourceReadParams,
|
||||
response: v2::McpResourceReadResponse,
|
||||
},
|
||||
|
||||
WindowsSandboxSetupStart => "windowsSandbox/setupStart" {
|
||||
params: v2::WindowsSandboxSetupStartParams,
|
||||
response: v2::WindowsSandboxSetupStartResponse,
|
||||
@@ -615,6 +620,41 @@ macro_rules! server_request_definitions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed response from the client to the server.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "method", rename_all = "camelCase")]
|
||||
pub enum ServerResponse {
|
||||
$(
|
||||
$(#[$variant_meta])*
|
||||
$(#[serde(rename = $wire)])?
|
||||
$variant {
|
||||
#[serde(rename = "id")]
|
||||
request_id: RequestId,
|
||||
response: $response,
|
||||
},
|
||||
)*
|
||||
}
|
||||
|
||||
impl ServerResponse {
|
||||
pub fn id(&self) -> &RequestId {
|
||||
match self {
|
||||
$(Self::$variant { request_id, .. } => request_id,)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method(&self) -> String {
|
||||
serde_json::to_value(self)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("method")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.unwrap_or_else(|| "<unknown>".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, JsonSchema)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ServerRequestPayload {
|
||||
@@ -965,6 +1005,8 @@ server_notification_definitions! {
|
||||
ThreadRealtimeTranscriptUpdated => "thread/realtime/transcriptUpdated" (v2::ThreadRealtimeTranscriptUpdatedNotification),
|
||||
#[experimental("thread/realtime/outputAudio/delta")]
|
||||
ThreadRealtimeOutputAudioDelta => "thread/realtime/outputAudio/delta" (v2::ThreadRealtimeOutputAudioDeltaNotification),
|
||||
#[experimental("thread/realtime/sdp")]
|
||||
ThreadRealtimeSdp => "thread/realtime/sdp" (v2::ThreadRealtimeSdpNotification),
|
||||
#[experimental("thread/realtime/error")]
|
||||
ThreadRealtimeError => "thread/realtime/error" (v2::ThreadRealtimeErrorNotification),
|
||||
#[experimental("thread/realtime/closed")]
|
||||
@@ -1225,6 +1267,30 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_server_response() -> Result<()> {
|
||||
let response = ServerResponse::CommandExecutionRequestApproval {
|
||||
request_id: RequestId::Integer(8),
|
||||
response: v2::CommandExecutionRequestApprovalResponse {
|
||||
decision: v2::CommandExecutionApprovalDecision::AcceptForSession,
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(response.id(), &RequestId::Integer(8));
|
||||
assert_eq!(response.method(), "item/commandExecution/requestApproval");
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "item/commandExecution/requestApproval",
|
||||
"id": 8,
|
||||
"response": {
|
||||
"decision": "acceptForSession"
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&response)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_mcp_server_elicitation_request() -> Result<()> {
|
||||
let requested_schema: v2::McpElicitationSchema = serde_json::from_value(json!({
|
||||
@@ -1307,6 +1373,7 @@ mod tests {
|
||||
response: v2::ThreadStartResponse {
|
||||
thread: v2::Thread {
|
||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||
forked_from_id: None,
|
||||
preview: "first prompt".to_string(),
|
||||
ephemeral: true,
|
||||
model_provider: "openai".to_string(),
|
||||
@@ -1343,6 +1410,7 @@ mod tests {
|
||||
"response": {
|
||||
"thread": {
|
||||
"id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
||||
"forkedFromId": null,
|
||||
"preview": "first prompt",
|
||||
"ephemeral": true,
|
||||
"modelProvider": "openai",
|
||||
@@ -1628,6 +1696,7 @@ mod tests {
|
||||
let request = ClientRequest::FsWatch {
|
||||
request_id: RequestId::Integer(10),
|
||||
params: v2::FsWatchParams {
|
||||
watch_id: "watch-git".to_string(),
|
||||
path: absolute_path("tmp/repo/.git"),
|
||||
},
|
||||
};
|
||||
@@ -1636,6 +1705,7 @@ mod tests {
|
||||
"method": "fs/watch",
|
||||
"id": 10,
|
||||
"params": {
|
||||
"watchId": "watch-git",
|
||||
"path": absolute_path_string("tmp/repo/.git")
|
||||
}
|
||||
}),
|
||||
@@ -1693,6 +1763,7 @@ mod tests {
|
||||
thread_id: "thr_123".to_string(),
|
||||
prompt: "You are on a call".to_string(),
|
||||
session_id: Some("sess_456".to_string()),
|
||||
transport: None,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -1702,7 +1773,8 @@ mod tests {
|
||||
"params": {
|
||||
"threadId": "thr_123",
|
||||
"prompt": "You are on a call",
|
||||
"sessionId": "sess_456"
|
||||
"sessionId": "sess_456",
|
||||
"transport": null
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
@@ -1782,6 +1854,7 @@ mod tests {
|
||||
thread_id: "thr_123".to_string(),
|
||||
prompt: "You are on a call".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
},
|
||||
};
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request);
|
||||
|
||||
299
codex-rs/app-server-protocol/src/protocol/item_builders.rs
Normal file
299
codex-rs/app-server-protocol/src/protocol/item_builders.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
//! Shared builders for synthetic [`ThreadItem`] values emitted by the app-server layer.
|
||||
//!
|
||||
//! These items do not come from first-class core `ItemStarted` / `ItemCompleted` events.
|
||||
//! Instead, the app-server synthesizes them so clients can render a coherent lifecycle for
|
||||
//! approvals and other pre-execution flows before the underlying tool has started or when the
|
||||
//! tool never starts at all.
|
||||
//!
|
||||
//! Keeping these builders in one place is useful for two reasons:
|
||||
//! - Live notifications and rebuilt `thread/read` history both need to construct the same
|
||||
//! synthetic items, so sharing the logic avoids drift between those paths.
|
||||
//! - The projection is presentation-specific. Core protocol events stay generic, while the
|
||||
//! app-server protocol decides how to surface those events as `ThreadItem`s for clients.
|
||||
use crate::protocol::common::ServerNotification;
|
||||
use crate::protocol::v2::CommandAction;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use crate::protocol::v2::CommandExecutionStatus;
|
||||
use crate::protocol::v2::FileUpdateChange;
|
||||
use crate::protocol::v2::GuardianApprovalReview;
|
||||
use crate::protocol::v2::GuardianApprovalReviewStatus;
|
||||
use crate::protocol::v2::ItemGuardianApprovalReviewCompletedNotification;
|
||||
use crate::protocol::v2::ItemGuardianApprovalReviewStartedNotification;
|
||||
use crate::protocol::v2::PatchApplyStatus;
|
||||
use crate::protocol::v2::PatchChangeKind;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::ExecCommandBeginEvent;
|
||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::GuardianAssessmentAction;
|
||||
use codex_protocol::protocol::GuardianAssessmentEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::PatchApplyEndEvent;
|
||||
use codex_shell_command::parse_command::parse_command;
|
||||
use codex_shell_command::parse_command::shlex_join;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn build_file_change_approval_request_item(
|
||||
payload: &ApplyPatchApprovalRequestEvent,
|
||||
) -> ThreadItem {
|
||||
ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_file_change_begin_item(payload: &PatchApplyBeginEvent) -> ThreadItem {
|
||||
ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_file_change_end_item(payload: &PatchApplyEndEvent) -> ThreadItem {
|
||||
ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: (&payload.status).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_command_execution_approval_request_item(
|
||||
payload: &ExecApprovalRequestEvent,
|
||||
) -> ThreadItem {
|
||||
ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command: shlex_join(&payload.command),
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions: payload
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(CommandAction::from)
|
||||
.collect(),
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem {
|
||||
ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command: shlex_join(&payload.command),
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions: payload
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(CommandAction::from)
|
||||
.collect(),
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> ThreadItem {
|
||||
let aggregated_output = if payload.aggregated_output.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(payload.aggregated_output.clone())
|
||||
};
|
||||
let duration_ms = i64::try_from(payload.duration.as_millis()).unwrap_or(i64::MAX);
|
||||
|
||||
ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command: shlex_join(&payload.command),
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status: (&payload.status).into(),
|
||||
command_actions: payload
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(CommandAction::from)
|
||||
.collect(),
|
||||
aggregated_output,
|
||||
exit_code: Some(payload.exit_code),
|
||||
duration_ms: Some(duration_ms),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a guardian-derived [`ThreadItem`].
|
||||
///
|
||||
/// Currently this only synthesizes [`ThreadItem::CommandExecution`] for
|
||||
/// [`GuardianAssessmentAction::Command`] and [`GuardianAssessmentAction::Execve`].
|
||||
pub fn build_item_from_guardian_event(
|
||||
assessment: &GuardianAssessmentEvent,
|
||||
status: CommandExecutionStatus,
|
||||
) -> Option<ThreadItem> {
|
||||
match &assessment.action {
|
||||
GuardianAssessmentAction::Command { command, cwd, .. } => {
|
||||
let command = command.clone();
|
||||
let command_actions = vec![CommandAction::Unknown {
|
||||
command: command.clone(),
|
||||
}];
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: assessment.id.clone(),
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
})
|
||||
}
|
||||
GuardianAssessmentAction::Execve {
|
||||
program, argv, cwd, ..
|
||||
} => {
|
||||
let argv = if argv.is_empty() {
|
||||
vec![program.clone()]
|
||||
} else {
|
||||
std::iter::once(program.clone())
|
||||
.chain(argv.iter().skip(1).cloned())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let command = shlex_join(&argv);
|
||||
let parsed_cmd = parse_command(&argv);
|
||||
let command_actions = if parsed_cmd.is_empty() {
|
||||
vec![CommandAction::Unknown {
|
||||
command: command.clone(),
|
||||
}]
|
||||
} else {
|
||||
parsed_cmd.into_iter().map(CommandAction::from).collect()
|
||||
};
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: assessment.id.clone(),
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
})
|
||||
}
|
||||
GuardianAssessmentAction::ApplyPatch { .. }
|
||||
| GuardianAssessmentAction::NetworkAccess { .. }
|
||||
| GuardianAssessmentAction::McpToolCall { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn guardian_auto_approval_review_notification(
|
||||
conversation_id: &ThreadId,
|
||||
event_turn_id: &str,
|
||||
assessment: &GuardianAssessmentEvent,
|
||||
) -> ServerNotification {
|
||||
// TODO(ccunningham): Attach guardian review state to the reviewed tool
|
||||
// item's lifecycle instead of sending standalone review notifications so
|
||||
// the app-server API can persist and replay review state via `thread/read`.
|
||||
let turn_id = if assessment.turn_id.is_empty() {
|
||||
event_turn_id.to_string()
|
||||
} else {
|
||||
assessment.turn_id.clone()
|
||||
};
|
||||
let review = GuardianApprovalReview {
|
||||
status: match assessment.status {
|
||||
codex_protocol::protocol::GuardianAssessmentStatus::InProgress => {
|
||||
GuardianApprovalReviewStatus::InProgress
|
||||
}
|
||||
codex_protocol::protocol::GuardianAssessmentStatus::Approved => {
|
||||
GuardianApprovalReviewStatus::Approved
|
||||
}
|
||||
codex_protocol::protocol::GuardianAssessmentStatus::Denied => {
|
||||
GuardianApprovalReviewStatus::Denied
|
||||
}
|
||||
codex_protocol::protocol::GuardianAssessmentStatus::Aborted => {
|
||||
GuardianApprovalReviewStatus::Aborted
|
||||
}
|
||||
},
|
||||
risk_score: assessment.risk_score,
|
||||
risk_level: assessment.risk_level.map(Into::into),
|
||||
rationale: assessment.rationale.clone(),
|
||||
};
|
||||
let action = assessment.action.clone().into();
|
||||
match assessment.status {
|
||||
codex_protocol::protocol::GuardianAssessmentStatus::InProgress => {
|
||||
ServerNotification::ItemGuardianApprovalReviewStarted(
|
||||
ItemGuardianApprovalReviewStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id,
|
||||
target_item_id: assessment.id.clone(),
|
||||
review,
|
||||
action,
|
||||
},
|
||||
)
|
||||
}
|
||||
codex_protocol::protocol::GuardianAssessmentStatus::Approved
|
||||
| codex_protocol::protocol::GuardianAssessmentStatus::Denied
|
||||
| codex_protocol::protocol::GuardianAssessmentStatus::Aborted => {
|
||||
ServerNotification::ItemGuardianApprovalReviewCompleted(
|
||||
ItemGuardianApprovalReviewCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id,
|
||||
target_item_id: assessment.id.clone(),
|
||||
review,
|
||||
action,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_patch_changes(changes: &HashMap<PathBuf, FileChange>) -> Vec<FileUpdateChange> {
|
||||
let mut converted: Vec<FileUpdateChange> = changes
|
||||
.iter()
|
||||
.map(|(path, change)| FileUpdateChange {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
kind: map_patch_change_kind(change),
|
||||
diff: format_file_change_diff(change),
|
||||
})
|
||||
.collect();
|
||||
converted.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
converted
|
||||
}
|
||||
|
||||
fn map_patch_change_kind(change: &FileChange) -> PatchChangeKind {
|
||||
match change {
|
||||
FileChange::Add { .. } => PatchChangeKind::Add,
|
||||
FileChange::Delete { .. } => PatchChangeKind::Delete,
|
||||
FileChange::Update { move_path, .. } => PatchChangeKind::Update {
|
||||
move_path: move_path.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn format_file_change_diff(change: &FileChange) -> String {
|
||||
match change {
|
||||
FileChange::Add { content } => content.clone(),
|
||||
FileChange::Delete { content } => content.clone(),
|
||||
FileChange::Update {
|
||||
unified_diff,
|
||||
move_path,
|
||||
} => {
|
||||
if let Some(path) = move_path {
|
||||
format!("{unified_diff}\n\nMoved to: {}", path.display())
|
||||
} else {
|
||||
unified_diff.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Exposes protocol pieces used by `lib.rs` via `pub use protocol::common::*;`.
|
||||
|
||||
pub mod common;
|
||||
pub mod item_builders;
|
||||
mod mappers;
|
||||
mod serde_helpers;
|
||||
pub mod thread_history;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use crate::protocol::item_builders::build_command_execution_begin_item;
|
||||
use crate::protocol::item_builders::build_command_execution_end_item;
|
||||
use crate::protocol::item_builders::build_file_change_approval_request_item;
|
||||
use crate::protocol::item_builders::build_file_change_begin_item;
|
||||
use crate::protocol::item_builders::build_file_change_end_item;
|
||||
use crate::protocol::item_builders::build_item_from_guardian_event;
|
||||
use crate::protocol::v2::CollabAgentState;
|
||||
use crate::protocol::v2::CollabAgentTool;
|
||||
use crate::protocol::v2::CollabAgentToolCallStatus;
|
||||
use crate::protocol::v2::CommandAction;
|
||||
use crate::protocol::v2::CommandExecutionStatus;
|
||||
use crate::protocol::v2::DynamicToolCallOutputContentItem;
|
||||
use crate::protocol::v2::DynamicToolCallStatus;
|
||||
use crate::protocol::v2::FileUpdateChange;
|
||||
use crate::protocol::v2::McpToolCallError;
|
||||
use crate::protocol::v2::McpToolCallResult;
|
||||
use crate::protocol::v2::McpToolCallStatus;
|
||||
use crate::protocol::v2::PatchApplyStatus;
|
||||
use crate::protocol::v2::PatchChangeKind;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use crate::protocol::v2::Turn;
|
||||
use crate::protocol::v2::TurnError as V2TurnError;
|
||||
@@ -31,6 +33,8 @@ use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandBeginEvent;
|
||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||
use codex_protocol::protocol::GuardianAssessmentEvent;
|
||||
use codex_protocol::protocol::GuardianAssessmentStatus;
|
||||
use codex_protocol::protocol::ImageGenerationBeginEvent;
|
||||
use codex_protocol::protocol::ImageGenerationEndEvent;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
@@ -53,6 +57,14 @@ use std::collections::HashMap;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::CommandAction;
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::FileUpdateChange;
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::PatchApplyStatus;
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::PatchChangeKind;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus;
|
||||
#[cfg(test)]
|
||||
@@ -149,6 +161,7 @@ impl ThreadHistoryBuilder {
|
||||
EventMsg::WebSearchEnd(payload) => self.handle_web_search_end(payload),
|
||||
EventMsg::ExecCommandBegin(payload) => self.handle_exec_command_begin(payload),
|
||||
EventMsg::ExecCommandEnd(payload) => self.handle_exec_command_end(payload),
|
||||
EventMsg::GuardianAssessment(payload) => self.handle_guardian_assessment(payload),
|
||||
EventMsg::ApplyPatchApprovalRequest(payload) => {
|
||||
self.handle_apply_patch_approval_request(payload)
|
||||
}
|
||||
@@ -375,57 +388,12 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
|
||||
fn handle_exec_command_begin(&mut self, payload: &ExecCommandBeginEvent) {
|
||||
let command = shlex::try_join(payload.command.iter().map(String::as_str))
|
||||
.unwrap_or_else(|_| payload.command.join(" "));
|
||||
let command_actions = payload
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(CommandAction::from)
|
||||
.collect();
|
||||
let item = ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command,
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
};
|
||||
let item = build_command_execution_begin_item(payload);
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
|
||||
fn handle_exec_command_end(&mut self, payload: &ExecCommandEndEvent) {
|
||||
let status: CommandExecutionStatus = (&payload.status).into();
|
||||
let duration_ms = i64::try_from(payload.duration.as_millis()).unwrap_or(i64::MAX);
|
||||
let aggregated_output = if payload.aggregated_output.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(payload.aggregated_output.clone())
|
||||
};
|
||||
let command = shlex::try_join(payload.command.iter().map(String::as_str))
|
||||
.unwrap_or_else(|_| payload.command.join(" "));
|
||||
let command_actions = payload
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(CommandAction::from)
|
||||
.collect();
|
||||
let item = ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command,
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output,
|
||||
exit_code: Some(payload.exit_code),
|
||||
duration_ms: Some(duration_ms),
|
||||
};
|
||||
let item = build_command_execution_end_item(payload);
|
||||
// Command completions can arrive out of order. Unified exec may return
|
||||
// while a PTY is still running, then emit ExecCommandEnd later from a
|
||||
// background exit watcher when that process finally exits. By then, a
|
||||
@@ -434,12 +402,26 @@ impl ThreadHistoryBuilder {
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
|
||||
fn handle_apply_patch_approval_request(&mut self, payload: &ApplyPatchApprovalRequestEvent) {
|
||||
let item = ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
fn handle_guardian_assessment(&mut self, payload: &GuardianAssessmentEvent) {
|
||||
let status = match payload.status {
|
||||
GuardianAssessmentStatus::InProgress => CommandExecutionStatus::InProgress,
|
||||
GuardianAssessmentStatus::Denied | GuardianAssessmentStatus::Aborted => {
|
||||
CommandExecutionStatus::Declined
|
||||
}
|
||||
GuardianAssessmentStatus::Approved => return,
|
||||
};
|
||||
let Some(item) = build_item_from_guardian_event(payload, status) else {
|
||||
return;
|
||||
};
|
||||
if payload.turn_id.is_empty() {
|
||||
self.upsert_item_in_current_turn(item);
|
||||
} else {
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_apply_patch_approval_request(&mut self, payload: &ApplyPatchApprovalRequestEvent) {
|
||||
let item = build_file_change_approval_request_item(payload);
|
||||
if payload.turn_id.is_empty() {
|
||||
self.upsert_item_in_current_turn(item);
|
||||
} else {
|
||||
@@ -448,11 +430,7 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
|
||||
fn handle_patch_apply_begin(&mut self, payload: &PatchApplyBeginEvent) {
|
||||
let item = ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
};
|
||||
let item = build_file_change_begin_item(payload);
|
||||
if payload.turn_id.is_empty() {
|
||||
self.upsert_item_in_current_turn(item);
|
||||
} else {
|
||||
@@ -461,12 +439,7 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
|
||||
fn handle_patch_apply_end(&mut self, payload: &PatchApplyEndEvent) {
|
||||
let status: PatchApplyStatus = (&payload.status).into();
|
||||
let item = ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status,
|
||||
};
|
||||
let item = build_file_change_end_item(payload);
|
||||
if payload.turn_id.is_empty() {
|
||||
self.upsert_item_in_current_turn(item);
|
||||
} else {
|
||||
@@ -547,6 +520,7 @@ impl ThreadHistoryBuilder {
|
||||
Some(McpToolCallResult {
|
||||
content: value.content.clone(),
|
||||
structured_content: value.structured_content.clone(),
|
||||
meta: value.meta.clone(),
|
||||
}),
|
||||
None,
|
||||
),
|
||||
@@ -891,22 +865,29 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
|
||||
fn handle_turn_aborted(&mut self, payload: &TurnAbortedEvent) {
|
||||
let apply_abort = |turn: &mut PendingTurn| {
|
||||
turn.status = TurnStatus::Interrupted;
|
||||
turn.completed_at = payload.completed_at;
|
||||
turn.duration_ms = payload.duration_ms;
|
||||
};
|
||||
if let Some(turn_id) = payload.turn_id.as_deref() {
|
||||
// Prefer an exact ID match so we interrupt the turn explicitly targeted by the event.
|
||||
if let Some(turn) = self.current_turn.as_mut().filter(|turn| turn.id == turn_id) {
|
||||
turn.status = TurnStatus::Interrupted;
|
||||
apply_abort(turn);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(turn) = self.turns.iter_mut().find(|turn| turn.id == turn_id) {
|
||||
turn.status = TurnStatus::Interrupted;
|
||||
turn.completed_at = payload.completed_at;
|
||||
turn.duration_ms = payload.duration_ms;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the event has no ID (or refers to an unknown turn), fall back to the active turn.
|
||||
if let Some(turn) = self.current_turn.as_mut() {
|
||||
turn.status = TurnStatus::Interrupted;
|
||||
apply_abort(turn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,15 +896,18 @@ impl ThreadHistoryBuilder {
|
||||
self.current_turn = Some(
|
||||
self.new_turn(Some(payload.turn_id.clone()))
|
||||
.with_status(TurnStatus::InProgress)
|
||||
.with_started_at(payload.started_at)
|
||||
.opened_explicitly(),
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_turn_complete(&mut self, payload: &TurnCompleteEvent) {
|
||||
let mark_completed = |status: &mut TurnStatus| {
|
||||
if matches!(*status, TurnStatus::Completed | TurnStatus::InProgress) {
|
||||
*status = TurnStatus::Completed;
|
||||
let mark_completed = |turn: &mut PendingTurn| {
|
||||
if matches!(turn.status, TurnStatus::Completed | TurnStatus::InProgress) {
|
||||
turn.status = TurnStatus::Completed;
|
||||
}
|
||||
turn.completed_at = payload.completed_at;
|
||||
turn.duration_ms = payload.duration_ms;
|
||||
};
|
||||
|
||||
// Prefer an exact ID match from the active turn and then close it.
|
||||
@@ -932,7 +916,7 @@ impl ThreadHistoryBuilder {
|
||||
.as_mut()
|
||||
.filter(|turn| turn.id == payload.turn_id)
|
||||
{
|
||||
mark_completed(&mut current_turn.status);
|
||||
mark_completed(current_turn);
|
||||
self.finish_current_turn();
|
||||
return;
|
||||
}
|
||||
@@ -942,13 +926,17 @@ impl ThreadHistoryBuilder {
|
||||
.iter_mut()
|
||||
.find(|turn| turn.id == payload.turn_id)
|
||||
{
|
||||
mark_completed(&mut turn.status);
|
||||
if matches!(turn.status, TurnStatus::Completed | TurnStatus::InProgress) {
|
||||
turn.status = TurnStatus::Completed;
|
||||
}
|
||||
turn.completed_at = payload.completed_at;
|
||||
turn.duration_ms = payload.duration_ms;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the completion event cannot be matched, apply it to the active turn.
|
||||
if let Some(current_turn) = self.current_turn.as_mut() {
|
||||
mark_completed(&mut current_turn.status);
|
||||
mark_completed(current_turn);
|
||||
self.finish_current_turn();
|
||||
}
|
||||
}
|
||||
@@ -981,7 +969,7 @@ impl ThreadHistoryBuilder {
|
||||
if turn.items.is_empty() && !turn.opened_explicitly && !turn.saw_compaction {
|
||||
return;
|
||||
}
|
||||
self.turns.push(turn.into());
|
||||
self.turns.push(Turn::from(turn));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -991,6 +979,9 @@ impl ThreadHistoryBuilder {
|
||||
items: Vec::new(),
|
||||
error: None,
|
||||
status: TurnStatus::Completed,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
opened_explicitly: false,
|
||||
saw_compaction: false,
|
||||
rollout_start_index: self.current_rollout_index,
|
||||
@@ -1076,21 +1067,6 @@ fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_patch_changes(
|
||||
changes: &HashMap<std::path::PathBuf, codex_protocol::protocol::FileChange>,
|
||||
) -> Vec<FileUpdateChange> {
|
||||
let mut converted: Vec<FileUpdateChange> = changes
|
||||
.iter()
|
||||
.map(|(path, change)| FileUpdateChange {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
kind: map_patch_change_kind(change),
|
||||
diff: format_file_change_diff(change),
|
||||
})
|
||||
.collect();
|
||||
converted.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
converted
|
||||
}
|
||||
|
||||
fn convert_dynamic_tool_content_items(
|
||||
items: &[codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem],
|
||||
) -> Vec<DynamicToolCallOutputContentItem> {
|
||||
@@ -1108,33 +1084,6 @@ fn convert_dynamic_tool_content_items(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn map_patch_change_kind(change: &codex_protocol::protocol::FileChange) -> PatchChangeKind {
|
||||
match change {
|
||||
codex_protocol::protocol::FileChange::Add { .. } => PatchChangeKind::Add,
|
||||
codex_protocol::protocol::FileChange::Delete { .. } => PatchChangeKind::Delete,
|
||||
codex_protocol::protocol::FileChange::Update { move_path, .. } => PatchChangeKind::Update {
|
||||
move_path: move_path.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn format_file_change_diff(change: &codex_protocol::protocol::FileChange) -> String {
|
||||
match change {
|
||||
codex_protocol::protocol::FileChange::Add { content } => content.clone(),
|
||||
codex_protocol::protocol::FileChange::Delete { content } => content.clone(),
|
||||
codex_protocol::protocol::FileChange::Update {
|
||||
unified_diff,
|
||||
move_path,
|
||||
} => {
|
||||
if let Some(path) = move_path {
|
||||
format!("{unified_diff}\n\nMoved to: {}", path.display())
|
||||
} else {
|
||||
unified_diff.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn upsert_turn_item(items: &mut Vec<ThreadItem>, item: ThreadItem) {
|
||||
if let Some(existing_item) = items
|
||||
.iter_mut()
|
||||
@@ -1151,6 +1100,9 @@ struct PendingTurn {
|
||||
items: Vec<ThreadItem>,
|
||||
error: Option<TurnError>,
|
||||
status: TurnStatus,
|
||||
started_at: Option<i64>,
|
||||
completed_at: Option<i64>,
|
||||
duration_ms: Option<i64>,
|
||||
/// True when this turn originated from an explicit `turn_started`/`turn_complete`
|
||||
/// boundary, so we preserve it even if it has no renderable items.
|
||||
opened_explicitly: bool,
|
||||
@@ -1171,6 +1123,11 @@ impl PendingTurn {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
fn with_started_at(mut self, started_at: Option<i64>) -> Self {
|
||||
self.started_at = started_at;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PendingTurn> for Turn {
|
||||
@@ -1180,6 +1137,9 @@ impl From<PendingTurn> for Turn {
|
||||
items: value.items,
|
||||
error: value.error,
|
||||
status: value.status,
|
||||
started_at: value.started_at,
|
||||
completed_at: value.completed_at,
|
||||
duration_ms: value.duration_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1191,6 +1151,9 @@ impl From<&PendingTurn> for Turn {
|
||||
items: value.items.clone(),
|
||||
error: value.error.clone(),
|
||||
status: value.status.clone(),
|
||||
started_at: value.started_at,
|
||||
completed_at: value.completed_at,
|
||||
duration_ms: value.duration_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1205,6 +1168,7 @@ mod tests {
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::MessagePhase as CoreMessagePhase;
|
||||
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
@@ -1342,6 +1306,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -1362,6 +1327,8 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1414,6 +1381,7 @@ mod tests {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-image".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
@@ -1433,6 +1401,8 @@ mod tests {
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-image".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -1444,6 +1414,9 @@ mod tests {
|
||||
id: "turn-image".into(),
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
items: vec![
|
||||
ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
@@ -1533,6 +1506,8 @@ mod tests {
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".into()),
|
||||
reason: TurnAbortReason::Replaced,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "Let's try again".into(),
|
||||
@@ -1730,6 +1705,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -1748,6 +1724,8 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1784,6 +1762,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -1884,11 +1863,73 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstructs_mcp_tool_result_meta_from_persisted_completion_events() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::McpToolCallEnd(McpToolCallEndEvent {
|
||||
call_id: "mcp-1".into(),
|
||||
invocation: McpInvocation {
|
||||
server: "docs".into(),
|
||||
tool: "lookup".into(),
|
||||
arguments: Some(serde_json::json!({"id":"123"})),
|
||||
},
|
||||
duration: Duration::from_millis(8),
|
||||
result: Ok(CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": "result"
|
||||
})],
|
||||
structured_content: Some(serde_json::json!({"id":"123"})),
|
||||
is_error: Some(false),
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/lookup.html"
|
||||
})),
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
let items = events
|
||||
.into_iter()
|
||||
.map(RolloutItem::EventMsg)
|
||||
.collect::<Vec<_>>();
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(
|
||||
turns[0].items[0],
|
||||
ThreadItem::McpToolCall {
|
||||
id: "mcp-1".into(),
|
||||
server: "docs".into(),
|
||||
tool: "lookup".into(),
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: serde_json::json!({"id":"123"}),
|
||||
result: Some(McpToolCallResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": "result"
|
||||
})],
|
||||
structured_content: Some(serde_json::json!({"id":"123"})),
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/lookup.html"
|
||||
})),
|
||||
}),
|
||||
error: None,
|
||||
duration_ms: Some(8),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstructs_dynamic_tool_items_from_request_and_response_events() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -1948,6 +1989,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2030,11 +2072,144 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstructs_declined_guardian_command_item() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "review this command".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-exec".into(),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_score: None,
|
||||
risk_level: None,
|
||||
rationale: None,
|
||||
action: serde_json::from_value(serde_json::json!({
|
||||
"type": "command",
|
||||
"source": "shell",
|
||||
"command": "rm -rf /tmp/guardian",
|
||||
"cwd": "/tmp",
|
||||
}))
|
||||
.expect("guardian action"),
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-exec".into(),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::Denied,
|
||||
risk_score: Some(97),
|
||||
risk_level: Some(codex_protocol::protocol::GuardianRiskLevel::High),
|
||||
rationale: Some("Would delete user data.".into()),
|
||||
action: serde_json::from_value(serde_json::json!({
|
||||
"type": "command",
|
||||
"source": "shell",
|
||||
"command": "rm -rf /tmp/guardian",
|
||||
"cwd": "/tmp",
|
||||
}))
|
||||
.expect("guardian action"),
|
||||
}),
|
||||
];
|
||||
|
||||
let items = events
|
||||
.into_iter()
|
||||
.map(RolloutItem::EventMsg)
|
||||
.collect::<Vec<_>>();
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(turns[0].items.len(), 2);
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::CommandExecution {
|
||||
id: "guardian-exec".into(),
|
||||
command: "rm -rf /tmp/guardian".into(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Declined,
|
||||
command_actions: vec![CommandAction::Unknown {
|
||||
command: "rm -rf /tmp/guardian".into(),
|
||||
}],
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstructs_in_progress_guardian_execve_item() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "run a subcommand".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-execve".into(),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_score: None,
|
||||
risk_level: None,
|
||||
rationale: None,
|
||||
action: serde_json::from_value(serde_json::json!({
|
||||
"type": "execve",
|
||||
"source": "shell",
|
||||
"program": "/bin/rm",
|
||||
"argv": ["/usr/bin/rm", "-f", "/tmp/file.sqlite"],
|
||||
"cwd": "/tmp",
|
||||
}))
|
||||
.expect("guardian action"),
|
||||
}),
|
||||
];
|
||||
|
||||
let items = events
|
||||
.into_iter()
|
||||
.map(RolloutItem::EventMsg)
|
||||
.collect::<Vec<_>>();
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(turns[0].items.len(), 2);
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::CommandExecution {
|
||||
id: "guardian-execve".into(),
|
||||
command: "/bin/rm -f /tmp/file.sqlite".into(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions: vec![CommandAction::Unknown {
|
||||
command: "/bin/rm -f /tmp/file.sqlite".into(),
|
||||
}],
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assigns_late_exec_completion_to_original_turn() {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2047,9 +2222,12 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2081,6 +2259,8 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2118,6 +2298,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2130,9 +2311,12 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2164,6 +2348,8 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2196,6 +2382,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2259,6 +2446,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2321,6 +2509,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2333,9 +2522,12 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2348,6 +2540,8 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "still in b".into(),
|
||||
@@ -2357,6 +2551,8 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2376,6 +2572,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2388,9 +2585,12 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2403,6 +2603,8 @@ mod tests {
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
turn_id: Some("turn-a".into()),
|
||||
reason: TurnAbortReason::Replaced,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "still in b".into(),
|
||||
@@ -2428,6 +2630,7 @@ mod tests {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-compact".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
@@ -2438,6 +2641,8 @@ mod tests {
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-compact".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -2448,6 +2653,9 @@ mod tests {
|
||||
id: "turn-compact".into(),
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
items: Vec::new(),
|
||||
}]
|
||||
);
|
||||
@@ -2665,6 +2873,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2677,6 +2886,8 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
EventMsg::Error(ErrorEvent {
|
||||
message: "request-level failure".into(),
|
||||
@@ -2696,6 +2907,9 @@ mod tests {
|
||||
id: "turn-a".into(),
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
items: vec![ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
content: vec![UserInput::Text {
|
||||
@@ -2712,6 +2926,7 @@ mod tests {
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
@@ -2730,6 +2945,8 @@ mod tests {
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2765,6 +2982,7 @@ mod tests {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
@@ -2778,6 +2996,8 @@ mod tests {
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -2808,6 +3028,7 @@ mod tests {
|
||||
let items = vec![
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
@@ -2823,6 +3044,8 @@ mod tests {
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
})),
|
||||
];
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
|
||||
use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction;
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol;
|
||||
use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment;
|
||||
@@ -27,6 +29,7 @@ use codex_protocol::config_types::WebSearchToolConfig;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::mcp::Resource as McpResource;
|
||||
pub use codex_protocol::mcp::ResourceContent as McpResourceContent;
|
||||
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
|
||||
use codex_protocol::mcp::Tool as McpTool;
|
||||
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
|
||||
@@ -848,6 +851,8 @@ pub struct ConfigReadResponse {
|
||||
pub struct ConfigRequirements {
|
||||
#[experimental(nested)]
|
||||
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
|
||||
#[experimental("configRequirements/read.allowedApprovalsReviewers")]
|
||||
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
|
||||
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
||||
pub feature_requirements: Option<BTreeMap<String, bool>>,
|
||||
@@ -880,6 +885,7 @@ pub struct NetworkRequirements {
|
||||
/// Legacy compatibility view derived from `unix_sockets`.
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
pub danger_full_access_denylist_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -1945,6 +1951,18 @@ pub struct ListMcpServerStatusParams {
|
||||
/// Optional page size; defaults to a server-defined value.
|
||||
#[ts(optional = nullable)]
|
||||
pub limit: Option<u32>,
|
||||
/// Controls how much MCP inventory data to fetch for each server.
|
||||
/// Defaults to `Full` when omitted.
|
||||
#[ts(optional = nullable)]
|
||||
pub detail: Option<McpServerStatusDetail>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub enum McpServerStatusDetail {
|
||||
Full,
|
||||
ToolsAndAuthOnly,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -1968,6 +1986,22 @@ pub struct ListMcpServerStatusResponse {
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpResourceReadParams {
|
||||
pub thread_id: String,
|
||||
pub server: String,
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpResourceReadResponse {
|
||||
pub contents: Vec<McpResourceContent>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -2315,17 +2349,17 @@ pub struct FsCopyResponse {}
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct FsWatchParams {
|
||||
/// Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.
|
||||
pub watch_id: String,
|
||||
/// Absolute file or directory path to watch.
|
||||
pub path: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
/// Created watch handle returned by `fs/watch`.
|
||||
/// Successful response for `fs/watch`.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct FsWatchResponse {
|
||||
/// Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.
|
||||
pub watch_id: String,
|
||||
/// Canonicalized path associated with the watch.
|
||||
pub path: AbsolutePathBuf,
|
||||
}
|
||||
@@ -2335,7 +2369,7 @@ pub struct FsWatchResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct FsUnwatchParams {
|
||||
/// Watch identifier returned by `fs/watch`.
|
||||
/// Watch identifier previously provided to `fs/watch`.
|
||||
pub watch_id: String,
|
||||
}
|
||||
|
||||
@@ -2350,7 +2384,7 @@ pub struct FsUnwatchResponse {}
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct FsChangedNotification {
|
||||
/// Watch identifier returned by `fs/watch`.
|
||||
/// Watch identifier previously provided to `fs/watch`.
|
||||
pub watch_id: String,
|
||||
/// File or directory paths associated with this event.
|
||||
pub changed_paths: Vec<AbsolutePathBuf>,
|
||||
@@ -3572,6 +3606,8 @@ impl From<CoreSkillErrorInfo> for SkillErrorInfo {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct Thread {
|
||||
pub id: String,
|
||||
/// Source thread id when this thread was created by forking another thread.
|
||||
pub forked_from_id: Option<String>,
|
||||
/// Usually the first user message in the thread, if available.
|
||||
pub preview: String,
|
||||
/// Whether the thread is ephemeral and should not be materialized on disk.
|
||||
@@ -3687,6 +3723,15 @@ pub struct Turn {
|
||||
pub status: TurnStatus,
|
||||
/// Only populated when the Turn's status is failed.
|
||||
pub error: Option<TurnError>,
|
||||
/// Unix timestamp (in seconds) when the turn started.
|
||||
#[ts(type = "number | null")]
|
||||
pub started_at: Option<i64>,
|
||||
/// Unix timestamp (in seconds) when the turn completed.
|
||||
#[ts(type = "number | null")]
|
||||
pub completed_at: Option<i64>,
|
||||
/// Duration between turn start and completion in milliseconds, if known.
|
||||
#[ts(type = "number | null")]
|
||||
pub duration_ms: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -3809,6 +3854,21 @@ pub struct ThreadRealtimeStartParams {
|
||||
pub prompt: String,
|
||||
#[ts(optional = nullable)]
|
||||
pub session_id: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub transport: Option<ThreadRealtimeStartTransport>,
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL - transport used by thread realtime.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/", tag = "type")]
|
||||
pub enum ThreadRealtimeStartTransport {
|
||||
Websocket,
|
||||
Webrtc {
|
||||
/// SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the
|
||||
/// realtime events data channel.
|
||||
sdp: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL - response for starting thread realtime.
|
||||
@@ -3900,6 +3960,15 @@ pub struct ThreadRealtimeOutputAudioDeltaNotification {
|
||||
pub audio: ThreadRealtimeAudioChunk,
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadRealtimeSdpNotification {
|
||||
pub thread_id: String,
|
||||
pub sdp: String,
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL - emitted when thread realtime encounters an error.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -4445,14 +4514,237 @@ impl From<CoreGuardianRiskLevel> for GuardianRiskLevel {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GuardianApprovalReview {
|
||||
pub status: GuardianApprovalReviewStatus,
|
||||
#[serde(alias = "risk_score")]
|
||||
#[ts(type = "number | null")]
|
||||
pub risk_score: Option<u8>,
|
||||
#[serde(alias = "risk_level")]
|
||||
pub risk_level: Option<GuardianRiskLevel>,
|
||||
pub rationale: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum GuardianCommandSource {
|
||||
Shell,
|
||||
UnifiedExec,
|
||||
}
|
||||
|
||||
impl From<CoreGuardianCommandSource> for GuardianCommandSource {
|
||||
fn from(value: CoreGuardianCommandSource) -> Self {
|
||||
match value {
|
||||
CoreGuardianCommandSource::Shell => Self::Shell,
|
||||
CoreGuardianCommandSource::UnifiedExec => Self::UnifiedExec,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GuardianCommandSource> for CoreGuardianCommandSource {
|
||||
fn from(value: GuardianCommandSource) -> Self {
|
||||
match value {
|
||||
GuardianCommandSource::Shell => Self::Shell,
|
||||
GuardianCommandSource::UnifiedExec => Self::UnifiedExec,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GuardianCommandReviewAction {
|
||||
pub source: GuardianCommandSource,
|
||||
pub command: String,
|
||||
pub cwd: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GuardianExecveReviewAction {
|
||||
pub source: GuardianCommandSource,
|
||||
pub program: String,
|
||||
pub argv: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GuardianApplyPatchReviewAction {
|
||||
pub cwd: PathBuf,
|
||||
pub files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GuardianNetworkAccessReviewAction {
|
||||
pub target: String,
|
||||
pub host: String,
|
||||
pub protocol: NetworkApprovalProtocol,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GuardianMcpToolCallReviewAction {
|
||||
pub server: String,
|
||||
pub tool_name: String,
|
||||
pub connector_id: Option<String>,
|
||||
pub connector_name: Option<String>,
|
||||
pub tool_title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum GuardianApprovalReviewAction {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Command {
|
||||
source: GuardianCommandSource,
|
||||
command: String,
|
||||
cwd: PathBuf,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Execve {
|
||||
source: GuardianCommandSource,
|
||||
program: String,
|
||||
argv: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ApplyPatch { cwd: PathBuf, files: Vec<PathBuf> },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
NetworkAccess {
|
||||
target: String,
|
||||
host: String,
|
||||
protocol: NetworkApprovalProtocol,
|
||||
port: u16,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
McpToolCall {
|
||||
server: String,
|
||||
tool_name: String,
|
||||
connector_id: Option<String>,
|
||||
connector_name: Option<String>,
|
||||
tool_title: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<CoreGuardianAssessmentAction> for GuardianApprovalReviewAction {
|
||||
fn from(value: CoreGuardianAssessmentAction) -> Self {
|
||||
match value {
|
||||
CoreGuardianAssessmentAction::Command {
|
||||
source,
|
||||
command,
|
||||
cwd,
|
||||
} => Self::Command {
|
||||
source: source.into(),
|
||||
command,
|
||||
cwd,
|
||||
},
|
||||
CoreGuardianAssessmentAction::Execve {
|
||||
source,
|
||||
program,
|
||||
argv,
|
||||
cwd,
|
||||
} => Self::Execve {
|
||||
source: source.into(),
|
||||
program,
|
||||
argv,
|
||||
cwd,
|
||||
},
|
||||
CoreGuardianAssessmentAction::ApplyPatch { cwd, files } => {
|
||||
Self::ApplyPatch { cwd, files }
|
||||
}
|
||||
CoreGuardianAssessmentAction::NetworkAccess {
|
||||
target,
|
||||
host,
|
||||
protocol,
|
||||
port,
|
||||
} => Self::NetworkAccess {
|
||||
target,
|
||||
host,
|
||||
protocol: protocol.into(),
|
||||
port,
|
||||
},
|
||||
CoreGuardianAssessmentAction::McpToolCall {
|
||||
server,
|
||||
tool_name,
|
||||
connector_id,
|
||||
connector_name,
|
||||
tool_title,
|
||||
} => Self::McpToolCall {
|
||||
server,
|
||||
tool_name,
|
||||
connector_id,
|
||||
connector_name,
|
||||
tool_title,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GuardianApprovalReviewAction> for CoreGuardianAssessmentAction {
|
||||
fn from(value: GuardianApprovalReviewAction) -> Self {
|
||||
match value {
|
||||
GuardianApprovalReviewAction::Command {
|
||||
source,
|
||||
command,
|
||||
cwd,
|
||||
} => Self::Command {
|
||||
source: source.into(),
|
||||
command,
|
||||
cwd,
|
||||
},
|
||||
GuardianApprovalReviewAction::Execve {
|
||||
source,
|
||||
program,
|
||||
argv,
|
||||
cwd,
|
||||
} => Self::Execve {
|
||||
source: source.into(),
|
||||
program,
|
||||
argv,
|
||||
cwd,
|
||||
},
|
||||
GuardianApprovalReviewAction::ApplyPatch { cwd, files } => {
|
||||
Self::ApplyPatch { cwd, files }
|
||||
}
|
||||
GuardianApprovalReviewAction::NetworkAccess {
|
||||
target,
|
||||
host,
|
||||
protocol,
|
||||
port,
|
||||
} => Self::NetworkAccess {
|
||||
target,
|
||||
host,
|
||||
protocol: protocol.to_core(),
|
||||
port,
|
||||
},
|
||||
GuardianApprovalReviewAction::McpToolCall {
|
||||
server,
|
||||
tool_name,
|
||||
connector_id,
|
||||
connector_name,
|
||||
tool_title,
|
||||
} => Self::McpToolCall {
|
||||
server,
|
||||
tool_name,
|
||||
connector_id,
|
||||
connector_name,
|
||||
tool_title,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type", rename_all = "camelCase")]
|
||||
@@ -4744,6 +5036,9 @@ pub struct McpToolCallResult {
|
||||
// representations). Using `JsonValue` keeps the payload wire-shaped and easy to export.
|
||||
pub content: Vec<JsonValue>,
|
||||
pub structured_content: Option<JsonValue>,
|
||||
#[serde(rename = "_meta")]
|
||||
#[ts(rename = "_meta")]
|
||||
pub meta: Option<JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -4933,7 +5228,7 @@ pub struct ItemGuardianApprovalReviewStartedNotification {
|
||||
pub turn_id: String,
|
||||
pub target_item_id: String,
|
||||
pub review: GuardianApprovalReview,
|
||||
pub action: Option<JsonValue>,
|
||||
pub action: GuardianApprovalReviewAction,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -4950,7 +5245,7 @@ pub struct ItemGuardianApprovalReviewCompletedNotification {
|
||||
pub turn_id: String,
|
||||
pub target_item_id: String,
|
||||
pub review: GuardianApprovalReview,
|
||||
pub action: Option<JsonValue>,
|
||||
pub action: GuardianApprovalReviewAction,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -7088,6 +7383,7 @@ mod tests {
|
||||
request_permissions: false,
|
||||
mcp_elicitations: false,
|
||||
}]),
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
feature_requirements: None,
|
||||
@@ -7487,26 +7783,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_approval_review_deserializes_legacy_snake_case_risk_fields() {
|
||||
let review: GuardianApprovalReview = serde_json::from_value(json!({
|
||||
"status": "denied",
|
||||
"risk_score": 91,
|
||||
"risk_level": "high",
|
||||
"rationale": "too risky"
|
||||
}))
|
||||
.expect("legacy snake_case automatic review should deserialize");
|
||||
assert_eq!(
|
||||
review,
|
||||
GuardianApprovalReview {
|
||||
status: GuardianApprovalReviewStatus::Denied,
|
||||
risk_score: Some(91),
|
||||
risk_level: Some(GuardianRiskLevel::High),
|
||||
rationale: Some("too risky".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_approval_review_deserializes_aborted_status() {
|
||||
let review: GuardianApprovalReview = serde_json::from_value(json!({
|
||||
@@ -7527,6 +7803,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_approval_review_action_round_trips_command_shape() {
|
||||
let value = json!({
|
||||
"type": "command",
|
||||
"source": "shell",
|
||||
"command": "rm -rf /tmp/example.sqlite",
|
||||
"cwd": "/tmp",
|
||||
});
|
||||
let action: GuardianApprovalReviewAction =
|
||||
serde_json::from_value(value.clone()).expect("guardian review action");
|
||||
|
||||
assert_eq!(
|
||||
action,
|
||||
GuardianApprovalReviewAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf /tmp/example.sqlite".to_string(),
|
||||
cwd: "/tmp".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&action).expect("serialize guardian review action"),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_requirements_deserializes_legacy_fields() {
|
||||
let requirements: NetworkRequirements = serde_json::from_value(json!({
|
||||
@@ -7547,6 +7848,7 @@ mod tests {
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
domains: None,
|
||||
managed_allowed_domains_only: None,
|
||||
danger_full_access_denylist_only: None,
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
unix_sockets: None,
|
||||
@@ -7573,6 +7875,7 @@ mod tests {
|
||||
),
|
||||
])),
|
||||
managed_allowed_domains_only: Some(true),
|
||||
danger_full_access_denylist_only: Some(true),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
unix_sockets: Some(BTreeMap::from([
|
||||
@@ -7603,6 +7906,7 @@ mod tests {
|
||||
"blocked.example.com": "deny"
|
||||
},
|
||||
"managedAllowedDomainsOnly": true,
|
||||
"dangerFullAccessDenylistOnly": true,
|
||||
"allowedDomains": ["api.openai.com"],
|
||||
"deniedDomains": ["blocked.example.com"],
|
||||
"unixSockets": {
|
||||
|
||||
@@ -3,5 +3,6 @@ load("//:defs.bzl", "codex_rust_crate")
|
||||
codex_rust_crate(
|
||||
name = "app-server",
|
||||
crate_name = "codex_app_server",
|
||||
integration_test_timeout = "long",
|
||||
test_tags = ["no-sandbox"],
|
||||
)
|
||||
|
||||
@@ -29,8 +29,10 @@ axum = { workspace = true, default-features = false, features = [
|
||||
"tokio",
|
||||
"ws",
|
||||
] }
|
||||
codex-analytics = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
@@ -43,19 +45,24 @@ codex-backend-client = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
codex-rmcp-client = { workspace = true }
|
||||
codex-rollout = { workspace = true }
|
||||
codex-sandboxing = { workspace = true }
|
||||
codex-state = { workspace = true }
|
||||
codex-tools = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-json-to-toml = { workspace = true }
|
||||
codex-utils-rustls-provider = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
constant_time_eq = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
gethostname = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
jsonwebtoken = { workspace = true }
|
||||
owo-colors = { workspace = true, features = ["supports-colors"] }
|
||||
@@ -76,6 +83,7 @@ tokio-util = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -25,6 +25,7 @@ Supported transports:
|
||||
|
||||
- stdio (`--listen stdio://`, default): newline-delimited JSON (JSONL)
|
||||
- websocket (`--listen ws://IP:PORT`): one JSON-RPC message per websocket text frame (**experimental / unsupported**)
|
||||
- off (`--listen off`): do not expose a local transport
|
||||
|
||||
When running with `--listen ws://IP:PORT`, the same listener also serves basic HTTP health probes:
|
||||
|
||||
@@ -132,9 +133,9 @@ Example with notification opt-out:
|
||||
|
||||
## API Overview
|
||||
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread.
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`.
|
||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread.
|
||||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
||||
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
@@ -151,7 +152,7 @@ Example with notification opt-out:
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
|
||||
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`.
|
||||
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
|
||||
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); returns `{}` and streams `thread/realtime/*` notifications.
|
||||
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`.
|
||||
- `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`.
|
||||
- `thread/realtime/appendText` — append text input to the active realtime session (experimental); returns `{}`.
|
||||
- `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`.
|
||||
@@ -168,7 +169,7 @@ Example with notification opt-out:
|
||||
- `fs/readDirectory` — list direct child entries for an absolute directory path; each entry contains `fileName`, `isDirectory`, and `isFile`, and `fileName` is just the child name, not a path.
|
||||
- `fs/remove` — remove an absolute file or directory tree; `recursive` and `force` default to `true`.
|
||||
- `fs/copy` — copy between absolute paths; directory copies require `recursive: true`.
|
||||
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path; returns a `watchId` and canonicalized `path`.
|
||||
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`.
|
||||
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
|
||||
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
@@ -186,7 +187,8 @@ Example with notification opt-out:
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
|
||||
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
|
||||
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
|
||||
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination.
|
||||
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools and auth status, plus resources/resource templates for `full` detail; supports cursor+limit pagination. If `detail` is omitted, the server defaults to `full`.
|
||||
- `mcpServer/resource/read` — read a resource from a thread's configured MCP server by `threadId`, `server`, and `uri`, returning text/blob resource `contents`.
|
||||
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
|
||||
- `config/read` — fetch the effective config on disk after resolving config layering.
|
||||
@@ -194,7 +196,7 @@ Example with notification opt-out:
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home).
|
||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
|
||||
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads.
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly`.
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||
|
||||
### Example: Start or resume a thread
|
||||
|
||||
@@ -273,7 +275,7 @@ Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `per
|
||||
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
|
||||
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
|
||||
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
|
||||
- `cwd` — restrict results to threads whose session cwd exactly matches this path.
|
||||
- `cwd` — restrict results to threads whose session cwd exactly matches this path. Relative paths are resolved against the app-server process cwd before matching.
|
||||
- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive).
|
||||
- Responses include `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
|
||||
|
||||
@@ -561,6 +563,51 @@ Invoke a plugin by including a UI mention token such as `@sample` in the text in
|
||||
} } }
|
||||
```
|
||||
|
||||
### Example: Start realtime with WebRTC
|
||||
|
||||
Use `thread/realtime/start` with `transport.type: "webrtc"` when a browser or webview owns the `RTCPeerConnection` and app-server should create the server-side realtime session. The transport `sdp` must be the offer SDP produced by `RTCPeerConnection.createOffer()`, not a hand-written or minimal SDP string.
|
||||
|
||||
The offer should include the media sections the client wants to negotiate. For the standard realtime UI flow, create the audio track/transceiver and the `oai-events` data channel before calling `createOffer()`:
|
||||
|
||||
```javascript
|
||||
const pc = new RTCPeerConnection();
|
||||
|
||||
audioElement.autoplay = true;
|
||||
pc.ontrack = (event) => {
|
||||
audioElement.srcObject = event.streams[0];
|
||||
};
|
||||
|
||||
const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
pc.addTrack(mediaStream.getAudioTracks()[0], mediaStream);
|
||||
pc.createDataChannel("oai-events");
|
||||
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
```
|
||||
|
||||
Then send `offer.sdp` to app-server. Core uses `experimental_realtime_ws_backend_prompt` for the backend instructions and the thread conversation id for the realtime session id. The start response is `{}`; the remote answer SDP arrives later as `thread/realtime/sdp` and should be passed to `setRemoteDescription()`:
|
||||
|
||||
```json
|
||||
{ "method": "thread/realtime/start", "id": 40, "params": {
|
||||
"threadId": "thr_123",
|
||||
"prompt": "You are on a call.",
|
||||
"sessionId": null,
|
||||
"transport": { "type": "webrtc", "sdp": "v=0\r\no=..." }
|
||||
} }
|
||||
{ "id": 40, "result": {} }
|
||||
{ "method": "thread/realtime/sdp", "params": {
|
||||
"threadId": "thr_123",
|
||||
"sdp": "v=0\r\no=..."
|
||||
} }
|
||||
```
|
||||
|
||||
```javascript
|
||||
await pc.setRemoteDescription({
|
||||
type: "answer",
|
||||
sdp: notification.params.sdp,
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Interrupt an active turn
|
||||
|
||||
You can cancel a running Turn with `turn/interrupt`.
|
||||
@@ -814,10 +861,10 @@ All filesystem paths in this section must be absolute.
|
||||
|
||||
```json
|
||||
{ "method": "fs/watch", "id": 44, "params": {
|
||||
"watchId": "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1",
|
||||
"path": "/Users/me/project/.git/HEAD"
|
||||
} }
|
||||
{ "id": 44, "result": {
|
||||
"watchId": "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1",
|
||||
"path": "/Users/me/project/.git/HEAD"
|
||||
} }
|
||||
{ "method": "fs/changed", "params": {
|
||||
@@ -913,10 +960,10 @@ All items emit shared lifecycle events:
|
||||
|
||||
- `item/started` — emits the full `item` when a new unit of work begins so the UI can render it immediately; the `item.id` in this payload matches the `itemId` used by deltas.
|
||||
- `item/completed` — sends the final `item` once that work itself finishes (for example, after a tool call or message completes); treat this as the authoritative execution/result state.
|
||||
- `item/autoApprovalReview/started` — [UNSTABLE] temporary guardian notification carrying `{threadId, turnId, targetItemId, review, action?}` when guardian approval review begins. This shape is expected to change soon.
|
||||
- `item/autoApprovalReview/completed` — [UNSTABLE] temporary guardian notification carrying `{threadId, turnId, targetItemId, review, action?}` when guardian approval review resolves. This shape is expected to change soon.
|
||||
- `item/autoApprovalReview/started` — [UNSTABLE] temporary guardian notification carrying `{threadId, turnId, targetItemId, review, action}` when guardian approval review begins. This shape is expected to change soon.
|
||||
- `item/autoApprovalReview/completed` — [UNSTABLE] temporary guardian notification carrying `{threadId, turnId, targetItemId, review, action}` when guardian approval review resolves. This shape is expected to change soon.
|
||||
|
||||
`review` is [UNSTABLE] and currently has `{status, riskScore?, riskLevel?, rationale?}`, where `status` is one of `inProgress`, `approved`, `denied`, or `aborted`. `action` is the guardian action summary payload from core when available and is intended to support temporary standalone pending-review UI. These notifications are separate from the target item's own `item/completed` lifecycle and are intentionally temporary while the guardian app protocol is still being designed.
|
||||
`review` is [UNSTABLE] and currently has `{status, riskScore?, riskLevel?, rationale?}`, where `status` is one of `inProgress`, `approved`, `denied`, or `aborted`. `action` is a tagged union with `type: "command" | "execve" | "applyPatch" | "networkAccess" | "mcpToolCall"`. Command-like actions include a `source` discriminator (`"shell"` or `"unifiedExec"`). These notifications are separate from the target item's own `item/completed` lifecycle and are intentionally temporary while the guardian app protocol is still being designed.
|
||||
|
||||
There are additional item-specific events:
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ fn transport_name(transport: AppServerTransport) -> &'static str {
|
||||
match transport {
|
||||
AppServerTransport::Stdio => "stdio",
|
||||
AppServerTransport::WebSocket { .. } => "websocket",
|
||||
AppServerTransport::Off => "off",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user