mirror of
https://github.com/openai/codex.git
synced 2026-05-12 23:32:44 +00:00
Compare commits
97 Commits
rust-v0.12
...
codex/pret
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
859aef2016 | ||
|
|
dbff525377 | ||
|
|
c30a4d2756 | ||
|
|
11655ee6b3 | ||
|
|
27c2bdc3e7 | ||
|
|
7113173442 | ||
|
|
49d55626f6 | ||
|
|
280698fcf6 | ||
|
|
a1c0d3e5b3 | ||
|
|
f06870f376 | ||
|
|
ad605dc791 | ||
|
|
fd6644fd82 | ||
|
|
35aaa5d9fc | ||
|
|
ccc920a062 | ||
|
|
f88701f5c8 | ||
|
|
15bb7f5530 | ||
|
|
127434cd8b | ||
|
|
9e905528bb | ||
|
|
cd2760fc08 | ||
|
|
466798aa83 | ||
|
|
a5fbcf1ab4 | ||
|
|
2952beb009 | ||
|
|
d55479488e | ||
|
|
443f6b831e | ||
|
|
aed74e5ee4 | ||
|
|
610eefb86b | ||
|
|
2817866a32 | ||
|
|
ff66b3c7eb | ||
|
|
be71b6fcd1 | ||
|
|
e4d6675632 | ||
|
|
78baa20780 | ||
|
|
9b8d585075 | ||
|
|
6784db51c0 | ||
|
|
41e171fcf2 | ||
|
|
5744b85b9a | ||
|
|
3d1d164aee | ||
|
|
227bee0445 | ||
|
|
f476338f93 | ||
|
|
0b04d1b3cc | ||
|
|
ff27d01676 | ||
|
|
70fc55b8f3 | ||
|
|
97aae46800 | ||
|
|
ad404c8400 | ||
|
|
48791920a8 | ||
|
|
96d2ea9058 | ||
|
|
a62b52f826 | ||
|
|
a93c89f497 | ||
|
|
d898cc8f3f | ||
|
|
fe05acad23 | ||
|
|
f50c02d7bc | ||
|
|
bb60b78c46 | ||
|
|
c39824c2fd | ||
|
|
6b1b227804 | ||
|
|
972b819213 | ||
|
|
af089fb21d | ||
|
|
4f96001fa7 | ||
|
|
0d9a5d20ec | ||
|
|
5affb7f9d5 | ||
|
|
acdf908268 | ||
|
|
b6f81257f8 | ||
|
|
a5ebedef67 | ||
|
|
5de7992ee5 | ||
|
|
2686873e77 | ||
|
|
9ddb267e9c | ||
|
|
6014b6679f | ||
|
|
8426edf71e | ||
|
|
7b3de63041 | ||
|
|
127be0612c | ||
|
|
9121132c8f | ||
|
|
70090c9ff7 | ||
|
|
8121710ffe | ||
|
|
7dd08e304c | ||
|
|
06f3b4836a | ||
|
|
31f8813e3e | ||
|
|
93d53f655b | ||
|
|
719431da6e | ||
|
|
b52083146c | ||
|
|
f2bc2f26a9 | ||
|
|
5cc5f12efc | ||
|
|
c70cdc108f | ||
|
|
487716ae74 | ||
|
|
a85d265097 | ||
|
|
c02814c106 | ||
|
|
3516cb9751 | ||
|
|
8a97f3cf03 | ||
|
|
c37f7434ba | ||
|
|
a73403a890 | ||
|
|
87d0cf1a62 | ||
|
|
ae863e72a2 | ||
|
|
8f3c06cc97 | ||
|
|
ac4332c05b | ||
|
|
ebe602d005 | ||
|
|
4e677d62da | ||
|
|
bb536d65bd | ||
|
|
8b07132e09 | ||
|
|
515aa9a4fb | ||
|
|
fedcefe9da |
19
.bazelrc
19
.bazelrc
@@ -153,6 +153,25 @@ common:ci-macos --config=remote
|
||||
common:ci-macos --strategy=remote
|
||||
common:ci-macos --strategy=TestRunner=darwin-sandbox,local
|
||||
|
||||
# On Windows, use Linux remote execution for build actions but keep test actions
|
||||
# on the Windows runner so Bazel's normal test sharding and flaky-test retries
|
||||
# still run against Windows binaries.
|
||||
common:ci-windows-cross --config=ci-windows
|
||||
common:ci-windows-cross --build_metadata=TAG_windows_cross_compile=true
|
||||
common:ci-windows-cross --config=remote
|
||||
common:ci-windows-cross --host_platform=//:rbe
|
||||
common:ci-windows-cross --strategy=remote
|
||||
common:ci-windows-cross --strategy=TestRunner=local
|
||||
common:ci-windows-cross --local_test_jobs=4
|
||||
common:ci-windows-cross --test_env=RUST_TEST_THREADS=1
|
||||
# Native Windows CI still covers these tests. The cross-built gnullvm binaries
|
||||
# currently crash in V8-backed code-mode tests and hang in PowerShell AST parser
|
||||
# tests when those binaries are run on the Windows runner.
|
||||
common:ci-windows-cross --test_env=CODEX_BAZEL_TEST_SKIP_FILTERS=suite::code_mode::,powershell
|
||||
common:ci-windows-cross --platforms=//:windows_x86_64_gnullvm
|
||||
common:ci-windows-cross --extra_execution_platforms=//:rbe,//:windows_x86_64_msvc
|
||||
common:ci-windows-cross --extra_toolchains=//:windows_gnullvm_tests_on_msvc_host_toolchain
|
||||
|
||||
# Linux-only V8 CI config.
|
||||
common:ci-v8 --config=ci
|
||||
common:ci-v8 --build_metadata=TAG_workflow=v8
|
||||
|
||||
11
.codex/environments/environment.toml
Normal file
11
.codex/environments/environment.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "codex"
|
||||
|
||||
[setup]
|
||||
script = ""
|
||||
|
||||
[[actions]]
|
||||
name = "Run"
|
||||
icon = "run"
|
||||
command = "cargo +1.93.0 run --manifest-path=codex-rs/Cargo.toml --bin codex -- -c mcp_oauth_credentials_store=file"
|
||||
@@ -27,10 +27,10 @@ Accept any of the following:
|
||||
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.
|
||||
5. If the failure is likely caused by the current branch, patch code locally, commit, and push. Do not patch random flaky tests, CI infrastructure, dependency outages, runner issues, or other failures that are unrelated to the branch.
|
||||
6. If `process_review_comment` is present, inspect surfaced review items and decide whether to address them.
|
||||
7. If a review item is actionable and correct, patch code locally, commit, push, and then mark the associated review thread/comment as resolved once the fix is on GitHub.
|
||||
8. If a review item from another author is non-actionable, already addressed, or not valid, post one reply on the comment/thread explaining that decision (for example answering the question or explaining why no change is needed). Prefix the GitHub reply body with `[codex]` so it is clear the response is automated. If the watcher later surfaces your own reply, treat that self-authored item as already handled and do not reply again.
|
||||
8. Do not post replies to human-authored review comments/threads unless the user explicitly confirms the exact response. If a human review item is non-actionable, already addressed, or not valid, surface the item and recommended response to the user instead of replying on GitHub.
|
||||
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.
|
||||
@@ -69,12 +69,18 @@ python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr <number-or-url> --o
|
||||
Use `gh` commands to inspect failed runs before deciding to rerun.
|
||||
|
||||
- `gh run view <run-id> --json jobs,name,workflowName,conclusion,status,url,headSha`
|
||||
- `gh run view <run-id> --log-failed`
|
||||
- `gh api repos/<owner>/<repo>/actions/runs/<run-id>/jobs -X GET -f per_page=100`
|
||||
- `gh api repos/<owner>/<repo>/actions/jobs/<job-id>/logs > /tmp/codex-gh-job-<job-id>-logs.zip`
|
||||
- `gh run view <run-id> --log-failed` as a fallback after the overall workflow run is complete
|
||||
|
||||
Prefer treating failures as branch-related when logs point to changed code (compile/test/lint/typecheck/snapshots/static analysis in touched areas).
|
||||
`gh run view --log-failed` is workflow-run scoped and may not expose failed-job logs until the overall run finishes. For faster diagnosis, poll the run's jobs first and, as soon as a specific job has failed, fetch that job's logs directly from the Actions job logs endpoint. The watcher includes a `failed_jobs` list with each failed job's `job_id` and `logs_endpoint` when GitHub exposes one.
|
||||
|
||||
Prefer treating failures as branch-related when failed-job logs point to changed code (compile/test/lint/typecheck/snapshots/static analysis in touched areas).
|
||||
|
||||
Prefer treating failures as flaky/unrelated when logs show transient infra/external issues (timeouts, runner provisioning failures, registry/network outages, GitHub Actions infra errors).
|
||||
|
||||
Do not attempt to fix flaky/unrelated failures by changing tests, build scripts, CI configuration, dependency pins, or infrastructure-adjacent code unless the logs clearly connect the failure to the PR branch. For flaky/unrelated failures, rerun only when the watcher recommends `retry_failed_checks`; otherwise wait or stop for user help.
|
||||
|
||||
If classification is ambiguous, perform one manual diagnosis attempt before choosing rerun.
|
||||
|
||||
Read `.codex/skills/babysit-pr/references/heuristics.md` for a concise checklist.
|
||||
@@ -99,7 +105,8 @@ When you agree with a comment and it is actionable:
|
||||
5. Resume watching on the new SHA immediately (do not stop after reporting the push).
|
||||
6. If monitoring was running in `--watch` mode, restart `--watch` immediately after the push in the same turn; do not wait for the user to ask again.
|
||||
|
||||
If you disagree or the comment is non-actionable/already addressed, reply once directly on the GitHub comment/thread so the reviewer gets an explicit answer, then continue the watcher loop. Prefix any GitHub reply to a code review comment/thread with `[codex]` so it is clear the response is automated and not from the human user. 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.
|
||||
Do not post replies to human-authored GitHub review comments/threads automatically. If you disagree with a human comment, believe it is non-actionable/already addressed, or need to answer a question, report the item to the user with a suggested response and wait for explicit confirmation before posting anything on GitHub. If the user approves a response, prefix it with `[codex]` so it is clear the response is automated and not from the human user.
|
||||
If the watcher later surfaces your own approved 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
|
||||
@@ -125,11 +132,11 @@ Use this loop in a live Codex session:
|
||||
2. Read `actions`.
|
||||
3. First check whether the PR is now merged or otherwise closed; if so, report that terminal state and stop polling immediately.
|
||||
4. Check CI summary, new review items, and mergeability/conflict status.
|
||||
5. Diagnose CI failures and classify branch-related vs flaky/unrelated.
|
||||
6. For each surfaced review item from another author, either reply once with an explanation if it is non-actionable or patch/commit/push and then resolve it if it is actionable. If a later snapshot surfaces your own reply, treat it as informational and continue without responding again.
|
||||
5. Diagnose CI failures and classify branch-related vs flaky/unrelated. If the overall run is still pending but `failed_jobs` already includes a failed job, fetch that job's logs and diagnose immediately instead of waiting for the whole workflow run to finish. Patch only when the failure is branch-related.
|
||||
6. For each surfaced review item from another author, patch/commit/push and then resolve it if it is actionable. If it is non-actionable, already addressed, or requires a written answer, surface it to the user with a suggested response instead of posting automatically. If a later snapshot surfaces your own approved 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).
|
||||
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. Do not make code changes for unrelated flakes or infrastructure failures just to get CI green.
|
||||
9. If you pushed a commit, resolved a review thread, or triggered a rerun, report the action briefly and continue polling (do not stop). If a human review comment needs a written GitHub response, stop and ask for confirmation before posting.
|
||||
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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface:
|
||||
display_name: "PR Babysitter"
|
||||
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."
|
||||
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. Do not post replies to human-authored review comments unless the user explicitly confirms the exact response. Do not patch unrelated flaky tests, CI infrastructure, dependency outages, runner issues, or other failures that are not caused by the branch. 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."
|
||||
|
||||
@@ -23,9 +23,11 @@ Used to discover failed workflow runs and rerunnable run IDs.
|
||||
### Failed log inspection
|
||||
|
||||
- `gh run view <run-id> --json jobs,name,workflowName,conclusion,status,url,headSha`
|
||||
- `gh api repos/{owner}/{repo}/actions/runs/{run_id}/jobs -X GET -f per_page=100`
|
||||
- `gh api repos/{owner}/{repo}/actions/jobs/{job_id}/logs > /tmp/codex-gh-job-{job_id}-logs.zip`
|
||||
- `gh run view <run-id> --log-failed`
|
||||
|
||||
Used by Codex to classify branch-related vs flaky/unrelated failures.
|
||||
Used by Codex to classify branch-related vs flaky/unrelated failures. Prefer the direct job log endpoint as soon as a job has failed because `gh run view --log-failed` may not produce failed-job logs until the overall workflow run completes.
|
||||
|
||||
### Retry failed jobs only
|
||||
|
||||
@@ -70,3 +72,11 @@ Reruns only failed jobs (and dependencies) for a workflow run.
|
||||
- `conclusion`
|
||||
- `html_url`
|
||||
- `head_sha`
|
||||
|
||||
### Actions run jobs API (`jobs[]`)
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `status`
|
||||
- `conclusion`
|
||||
- `html_url`
|
||||
|
||||
@@ -18,6 +18,8 @@ Treat as **likely flaky or unrelated** when evidence points to transient or exte
|
||||
- Cloud/service rate limits or transient API outages
|
||||
- Non-deterministic failures in unrelated integration tests with known flake patterns
|
||||
|
||||
Do not patch likely flaky/unrelated failures. Use the retry budget for rerunnable failures, wait for pending jobs, or stop and report the blocker when the failure is persistent or infrastructure-owned.
|
||||
|
||||
If uncertain, inspect failed logs once before choosing rerun.
|
||||
|
||||
## Decision tree (fix vs rerun vs stop)
|
||||
@@ -25,9 +27,11 @@ If uncertain, inspect failed logs once before choosing rerun.
|
||||
1. If PR is merged/closed: stop.
|
||||
2. If there are failed checks:
|
||||
- Diagnose first.
|
||||
- If checks are still pending but an individual job has already failed: fetch that job's logs and diagnose now.
|
||||
- If branch-related: fix locally, commit, push.
|
||||
- If likely flaky/unrelated and all checks for the current SHA are terminal: rerun failed jobs.
|
||||
- If checks are still pending: wait.
|
||||
- If likely flaky/unrelated and not safely rerunnable: stop and report the blocker; do not edit unrelated tests, build scripts, CI configuration, dependency pins, or infrastructure code.
|
||||
- If checks are still pending and no failed job is available yet: wait.
|
||||
3. If flaky reruns for the same SHA reach the configured limit (default 3): stop and report persistent failure.
|
||||
4. Independently, process any new human review comments.
|
||||
|
||||
@@ -40,12 +44,15 @@ Address the comment when:
|
||||
- The requested change does not conflict with the user’s intent or recent guidance.
|
||||
- The change can be made safely without unrelated refactors.
|
||||
|
||||
Fix valid human review feedback in code when possible, but do not post a GitHub reply to a human-authored comment/thread unless the user explicitly confirms the exact response.
|
||||
|
||||
Do not auto-fix when:
|
||||
|
||||
- The comment is ambiguous and needs clarification.
|
||||
- The request conflicts with explicit user instructions.
|
||||
- The proposed change requires product/design decisions the user has not made.
|
||||
- The codebase is in a dirty/unrelated state that makes safe editing uncertain.
|
||||
- The comment only needs a written answer or disagreement response; propose the reply to the user instead of posting it automatically.
|
||||
|
||||
## Stop-and-ask conditions
|
||||
|
||||
@@ -56,3 +63,4 @@ Stop and ask the user instead of continuing automatically when:
|
||||
- The PR branch cannot be pushed.
|
||||
- CI failures persist after the flaky retry budget.
|
||||
- Reviewer feedback requires a product decision or cross-team coordination.
|
||||
- A human review comment requires a written GitHub reply instead of a code change.
|
||||
|
||||
@@ -338,6 +338,66 @@ def failed_runs_from_workflow_runs(runs, head_sha):
|
||||
return failed_runs
|
||||
|
||||
|
||||
def get_jobs_for_run(repo, run_id):
|
||||
endpoint = f"repos/{repo}/actions/runs/{run_id}/jobs"
|
||||
data = gh_json(["api", endpoint, "-X", "GET", "-f", "per_page=100"], repo=repo)
|
||||
if not isinstance(data, dict):
|
||||
raise GhCommandError("Unexpected payload from actions run jobs API")
|
||||
jobs = data.get("jobs") or []
|
||||
if not isinstance(jobs, list):
|
||||
raise GhCommandError("Expected `jobs` to be a list")
|
||||
return jobs
|
||||
|
||||
|
||||
def failed_jobs_from_workflow_runs(repo, runs, head_sha):
|
||||
failed_jobs = []
|
||||
for run in runs:
|
||||
if not isinstance(run, dict):
|
||||
continue
|
||||
if str(run.get("head_sha") or "") != head_sha:
|
||||
continue
|
||||
run_id = run.get("id")
|
||||
if run_id in (None, ""):
|
||||
continue
|
||||
run_status = str(run.get("status") or "")
|
||||
run_conclusion = str(run.get("conclusion") or "")
|
||||
if run_status.lower() == "completed" and run_conclusion not in FAILED_RUN_CONCLUSIONS:
|
||||
continue
|
||||
jobs = get_jobs_for_run(repo, run_id)
|
||||
for job in jobs:
|
||||
if not isinstance(job, dict):
|
||||
continue
|
||||
conclusion = str(job.get("conclusion") or "")
|
||||
if conclusion not in FAILED_RUN_CONCLUSIONS:
|
||||
continue
|
||||
job_id = job.get("id")
|
||||
logs_endpoint = None
|
||||
if job_id not in (None, ""):
|
||||
logs_endpoint = f"repos/{repo}/actions/jobs/{job_id}/logs"
|
||||
failed_jobs.append(
|
||||
{
|
||||
"run_id": run_id,
|
||||
"workflow_name": run.get("name") or run.get("display_title") or "",
|
||||
"run_status": run_status,
|
||||
"run_conclusion": run_conclusion,
|
||||
"job_id": job_id,
|
||||
"job_name": str(job.get("name") or ""),
|
||||
"status": str(job.get("status") or ""),
|
||||
"conclusion": conclusion,
|
||||
"html_url": str(job.get("html_url") or ""),
|
||||
"logs_endpoint": logs_endpoint,
|
||||
}
|
||||
)
|
||||
failed_jobs.sort(
|
||||
key=lambda item: (
|
||||
str(item.get("workflow_name") or ""),
|
||||
str(item.get("job_name") or ""),
|
||||
str(item.get("job_id") or ""),
|
||||
)
|
||||
)
|
||||
return failed_jobs
|
||||
|
||||
|
||||
def get_authenticated_login():
|
||||
data = gh_json(["api", "user"])
|
||||
if not isinstance(data, dict) or not data.get("login"):
|
||||
@@ -568,7 +628,7 @@ def is_pr_ready_to_merge(pr, checks_summary, new_review_items):
|
||||
return True
|
||||
|
||||
|
||||
def recommend_actions(pr, checks_summary, failed_runs, new_review_items, retries_used, max_retries):
|
||||
def recommend_actions(pr, checks_summary, failed_runs, failed_jobs, new_review_items, retries_used, max_retries):
|
||||
actions = []
|
||||
if pr["closed"] or pr["merged"]:
|
||||
if new_review_items:
|
||||
@@ -583,7 +643,7 @@ def recommend_actions(pr, checks_summary, failed_runs, new_review_items, retries
|
||||
if new_review_items:
|
||||
actions.append("process_review_comment")
|
||||
|
||||
has_failed_pr_checks = checks_summary["failed_count"] > 0
|
||||
has_failed_pr_checks = checks_summary["failed_count"] > 0 or bool(failed_jobs)
|
||||
if has_failed_pr_checks:
|
||||
if checks_summary["all_terminal"] and retries_used >= max_retries:
|
||||
actions.append("stop_exhausted_retries")
|
||||
@@ -621,12 +681,14 @@ def collect_snapshot(args):
|
||||
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"])
|
||||
failed_jobs = failed_jobs_from_workflow_runs(pr["repo"], workflow_runs, pr["head_sha"])
|
||||
|
||||
retries_used = current_retry_count(state, pr["head_sha"])
|
||||
actions = recommend_actions(
|
||||
pr,
|
||||
checks_summary,
|
||||
failed_runs,
|
||||
failed_jobs,
|
||||
new_review_items,
|
||||
retries_used,
|
||||
args.max_flaky_retries,
|
||||
@@ -641,6 +703,7 @@ def collect_snapshot(args):
|
||||
"pr": pr,
|
||||
"checks": checks_summary,
|
||||
"failed_runs": failed_runs,
|
||||
"failed_jobs": failed_jobs,
|
||||
"new_review_items": new_review_items,
|
||||
"actions": actions,
|
||||
"retry_state": {
|
||||
|
||||
@@ -75,6 +75,11 @@ def test_collect_snapshot_fetches_review_items_before_ci(monkeypatch, tmp_path):
|
||||
"failed_runs_from_workflow_runs",
|
||||
lambda *args, **kwargs: call_order.append("failed_runs") or [],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"failed_jobs_from_workflow_runs",
|
||||
lambda *args, **kwargs: call_order.append("failed_jobs") or [],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"recommend_actions",
|
||||
@@ -100,6 +105,7 @@ def test_recommend_actions_prioritizes_review_comments():
|
||||
sample_pr(),
|
||||
sample_checks(failed_count=1),
|
||||
[{"run_id": 99}],
|
||||
[],
|
||||
[{"kind": "review_comment", "id": "1"}],
|
||||
0,
|
||||
3,
|
||||
@@ -119,6 +125,7 @@ def test_run_watch_keeps_polling_open_ready_to_merge_pr(monkeypatch):
|
||||
"pr": sample_pr(),
|
||||
"checks": sample_checks(),
|
||||
"failed_runs": [],
|
||||
"failed_jobs": [],
|
||||
"new_review_items": [],
|
||||
"actions": ["ready_to_merge"],
|
||||
"retry_state": {
|
||||
@@ -153,3 +160,58 @@ def test_run_watch_keeps_polling_open_ready_to_merge_pr(monkeypatch):
|
||||
|
||||
assert sleeps == [30, 30]
|
||||
assert [event for event, _ in events] == ["snapshot", "snapshot"]
|
||||
|
||||
|
||||
def test_failed_jobs_include_direct_logs_endpoint(monkeypatch):
|
||||
jobs_by_run = {
|
||||
99: [
|
||||
{
|
||||
"id": 555,
|
||||
"name": "unit tests",
|
||||
"status": "completed",
|
||||
"conclusion": "failure",
|
||||
"html_url": "https://github.com/openai/codex/actions/runs/99/job/555",
|
||||
},
|
||||
{
|
||||
"id": 556,
|
||||
"name": "lint",
|
||||
"status": "completed",
|
||||
"conclusion": "success",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
gh_pr_watch,
|
||||
"get_jobs_for_run",
|
||||
lambda repo, run_id: jobs_by_run[run_id],
|
||||
)
|
||||
|
||||
failed_jobs = gh_pr_watch.failed_jobs_from_workflow_runs(
|
||||
"openai/codex",
|
||||
[
|
||||
{
|
||||
"id": 99,
|
||||
"name": "CI",
|
||||
"status": "in_progress",
|
||||
"conclusion": "",
|
||||
"head_sha": "abc123",
|
||||
}
|
||||
],
|
||||
"abc123",
|
||||
)
|
||||
|
||||
assert failed_jobs == [
|
||||
{
|
||||
"run_id": 99,
|
||||
"workflow_name": "CI",
|
||||
"run_status": "in_progress",
|
||||
"run_conclusion": "",
|
||||
"job_id": 555,
|
||||
"job_name": "unit tests",
|
||||
"status": "completed",
|
||||
"conclusion": "failure",
|
||||
"html_url": "https://github.com/openai/codex/actions/runs/99/job/555",
|
||||
"logs_endpoint": "repos/openai/codex/actions/jobs/555/logs",
|
||||
}
|
||||
]
|
||||
|
||||
14
.github/scripts/compute-bazel-windows-path.ps1
vendored
14
.github/scripts/compute-bazel-windows-path.ps1
vendored
@@ -5,9 +5,9 @@ tool entries, such as Maven, that can change independently of this repo and
|
||||
cause avoidable cache misses.
|
||||
|
||||
This script derives a smaller, cache-stable PATH that keeps the Windows
|
||||
toolchain entries Bazel-backed CI tasks need: MSVC and Windows SDK paths, Git,
|
||||
PowerShell, Node, Python, DotSlash, and the standard Windows system
|
||||
directories.
|
||||
toolchain entries Bazel-backed CI tasks need: MSVC and Windows SDK paths,
|
||||
MinGW runtime DLL paths for gnullvm-built tests, Git, PowerShell, Node, Python,
|
||||
DotSlash, and the standard Windows system directories.
|
||||
`setup-bazel-ci` runs this after exporting the MSVC environment, and the script
|
||||
publishes the result via `GITHUB_ENV` as `CODEX_BAZEL_WINDOWS_PATH` so later
|
||||
steps can pass that explicit PATH to Bazel.
|
||||
@@ -49,6 +49,8 @@ foreach ($pathEntry in ($env:PATH -split ';')) {
|
||||
$pathEntry -like '*Microsoft Visual Studio*' -or
|
||||
$pathEntry -like '*Windows Kits*' -or
|
||||
$pathEntry -like '*Microsoft SDKs*' -or
|
||||
$pathEntry -eq 'C:\mingw64\bin' -or
|
||||
$pathEntry -like 'C:\msys64\*\bin' -or
|
||||
$pathEntry -like 'C:\Program Files\Git\*' -or
|
||||
$pathEntry -like 'C:\Program Files\PowerShell\*' -or
|
||||
$pathEntry -like 'C:\hostedtoolcache\windows\node\*' -or
|
||||
@@ -85,6 +87,12 @@ if ($pwshCommand) {
|
||||
Add-StablePathEntry (Split-Path $pwshCommand.Source -Parent)
|
||||
}
|
||||
|
||||
foreach ($mingwPath in @('C:\mingw64\bin', 'C:\msys64\mingw64\bin', 'C:\msys64\ucrt64\bin')) {
|
||||
if (Test-Path $mingwPath) {
|
||||
Add-StablePathEntry $mingwPath
|
||||
}
|
||||
}
|
||||
|
||||
if ($windowsAppsPath) {
|
||||
Add-StablePathEntry $windowsAppsPath
|
||||
}
|
||||
|
||||
110
.github/scripts/run-bazel-ci.sh
vendored
110
.github/scripts/run-bazel-ci.sh
vendored
@@ -6,6 +6,7 @@ print_failed_bazel_test_logs=0
|
||||
print_failed_bazel_action_summary=0
|
||||
remote_download_toplevel=0
|
||||
windows_msvc_host_platform=0
|
||||
windows_cross_compile=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -25,6 +26,10 @@ while [[ $# -gt 0 ]]; do
|
||||
windows_msvc_host_platform=1
|
||||
shift
|
||||
;;
|
||||
--windows-cross-compile)
|
||||
windows_cross_compile=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
@@ -37,7 +42,7 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: $0 [--print-failed-test-logs] [--print-failed-action-summary] [--remote-download-toplevel] [--windows-msvc-host-platform] -- <bazel args> -- <targets>" >&2
|
||||
echo "Usage: $0 [--print-failed-test-logs] [--print-failed-action-summary] [--remote-download-toplevel] [--windows-msvc-host-platform] [--windows-cross-compile] -- <bazel args> -- <targets>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -61,7 +66,11 @@ case "${RUNNER_OS:-}" in
|
||||
ci_config=ci-macos
|
||||
;;
|
||||
Windows)
|
||||
ci_config=ci-windows
|
||||
if [[ $windows_cross_compile -eq 1 ]]; then
|
||||
ci_config=ci-windows-cross
|
||||
else
|
||||
ci_config=ci-windows
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -105,8 +114,8 @@ print_bazel_test_log_tails() {
|
||||
while IFS= read -r target; do
|
||||
failed_targets+=("$target")
|
||||
done < <(
|
||||
grep -E '^FAIL: //' "$console_log" \
|
||||
| sed -E 's#^FAIL: (//[^ ]+).*#\1#' \
|
||||
grep -E '^(FAIL: //|ERROR: .* Testing //)' "$console_log" \
|
||||
| sed -E 's#^FAIL: (//[^ ]+).*#\1#; s#^ERROR: .* Testing (//[^ ]+) failed:.*#\1#' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
@@ -244,6 +253,12 @@ if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -z "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
# Fork PRs do not receive the BuildBuddy secret needed for the remote
|
||||
# cross-compile config. Preserve the previous local Windows build shape.
|
||||
windows_msvc_host_platform=1
|
||||
fi
|
||||
|
||||
post_config_bazel_args=()
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_msvc_host_platform -eq 1 ]]; then
|
||||
has_host_platform_override=0
|
||||
@@ -269,6 +284,25 @@ if [[ $remote_download_toplevel -eq 1 ]]; then
|
||||
post_config_bazel_args+=(--remote_download_toplevel)
|
||||
fi
|
||||
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -n "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
# `--enable_platform_specific_config` expands `common:windows` on Windows
|
||||
# hosts after ordinary rc configs, which can override `ci-windows-cross`'s
|
||||
# RBE host platform. Repeat the host platform on the command line so V8 and
|
||||
# other genrules execute on Linux RBE workers instead of Git Bash locally.
|
||||
#
|
||||
# Bazel also derives the default genrule shell from the client host. Without
|
||||
# an explicit shell executable, remote Linux actions can be asked to run
|
||||
# `C:\Program Files\Git\usr\bin\bash.exe`.
|
||||
post_config_bazel_args+=(--host_platform=//:rbe --shell_executable=/bin/bash)
|
||||
fi
|
||||
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -z "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
# The Windows cross-compile config depends on remote execution. Fork PRs do
|
||||
# not receive the BuildBuddy secret, so fall back to the existing local build
|
||||
# shape and keep its lower concurrency cap.
|
||||
post_config_bazel_args+=(--jobs=8)
|
||||
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
|
||||
@@ -287,37 +321,57 @@ if [[ -n "${CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR:-}" ]]; then
|
||||
fi
|
||||
|
||||
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
|
||||
windows_action_env_vars=(
|
||||
INCLUDE
|
||||
LIB
|
||||
LIBPATH
|
||||
UCRTVersion
|
||||
UniversalCRTSdkDir
|
||||
VCINSTALLDIR
|
||||
VCToolsInstallDir
|
||||
WindowsLibPath
|
||||
WindowsSdkBinPath
|
||||
WindowsSdkDir
|
||||
WindowsSDKLibVersion
|
||||
WindowsSDKVersion
|
||||
)
|
||||
pass_windows_build_env=1
|
||||
if [[ $windows_cross_compile -eq 1 && -n "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
# Remote build actions execute on Linux RBE workers. Passing the Windows
|
||||
# runner's build environment there makes Bazel genrules try to execute
|
||||
# C:\Program Files\Git\usr\bin\bash.exe on Linux.
|
||||
pass_windows_build_env=0
|
||||
fi
|
||||
|
||||
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
|
||||
if [[ $pass_windows_build_env -eq 1 ]]; then
|
||||
windows_action_env_vars=(
|
||||
INCLUDE
|
||||
LIB
|
||||
LIBPATH
|
||||
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
|
||||
|
||||
if [[ -z "${CODEX_BAZEL_WINDOWS_PATH:-}" ]]; then
|
||||
echo "CODEX_BAZEL_WINDOWS_PATH must be set for Windows Bazel CI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
post_config_bazel_args+=(
|
||||
"--action_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
|
||||
"--host_action_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
|
||||
"--test_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
|
||||
)
|
||||
if [[ $pass_windows_build_env -eq 1 ]]; then
|
||||
post_config_bazel_args+=(
|
||||
"--action_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
|
||||
"--host_action_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
|
||||
)
|
||||
elif [[ $windows_cross_compile -eq 1 ]]; then
|
||||
# Remote build actions run on Linux RBE workers. Give their shell snippets
|
||||
# a Linux PATH while preserving CODEX_BAZEL_WINDOWS_PATH below for local
|
||||
# Windows test execution.
|
||||
post_config_bazel_args+=(
|
||||
"--action_env=PATH=/usr/bin:/bin"
|
||||
"--host_action_env=PATH=/usr/bin:/bin"
|
||||
)
|
||||
fi
|
||||
post_config_bazel_args+=("--test_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}")
|
||||
fi
|
||||
|
||||
bazel_console_log="$(mktemp)"
|
||||
|
||||
13
.github/scripts/run-bazel-query-ci.sh
vendored
13
.github/scripts/run-bazel-query-ci.sh
vendored
@@ -6,8 +6,13 @@ set -euo pipefail
|
||||
# invocation so target-discovery queries can reuse the same Bazel server.
|
||||
|
||||
query_args=()
|
||||
windows_cross_compile=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--windows-cross-compile)
|
||||
windows_cross_compile=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
@@ -20,7 +25,7 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 [<bazel query args>...] -- <query expression>" >&2
|
||||
echo "Usage: $0 [--windows-cross-compile] [<bazel query args>...] -- <query expression>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -32,7 +37,11 @@ case "${RUNNER_OS:-}" in
|
||||
ci_config=ci-macos
|
||||
;;
|
||||
Windows)
|
||||
ci_config=ci-windows
|
||||
if [[ $windows_cross_compile -eq 1 ]]; then
|
||||
ci_config=ci-windows-cross
|
||||
else
|
||||
ci_config=ci-windows
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
133
.github/workflows/bazel.yml
vendored
133
.github/workflows/bazel.yml
vendored
@@ -17,13 +17,10 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.ref_name != 'main' }}
|
||||
jobs:
|
||||
test:
|
||||
# Even though a no-cache-hit Windows build seems to exceed the 30-minute
|
||||
# limit on occasion, the more common reason for exceeding the limit is a
|
||||
# true test failure in a rust_test() marked "flaky" that gets run 3x.
|
||||
# In that case, extra time generally does not give us more signal.
|
||||
#
|
||||
# Ultimately we need true distributed builds (e.g.,
|
||||
# https://www.buildbuddy.io/docs/rbe-setup/) to speed things up.
|
||||
# PRs use a fast Windows cross-compiled test leg for pre-merge signal.
|
||||
# Post-merge pushes to main also run the native Windows test job below,
|
||||
# which keeps V8/code-mode coverage without putting PR latency back on the
|
||||
# critical path.
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -47,13 +44,16 @@ jobs:
|
||||
# - os: ubuntu-24.04-arm
|
||||
# target: aarch64-unknown-linux-gnu
|
||||
|
||||
# Windows
|
||||
# Windows fast path: build the windows-gnullvm binaries with Linux
|
||||
# RBE, then run the resulting Windows tests on the Windows runner.
|
||||
# The main-only native Windows job below preserves full V8/code-mode
|
||||
# coverage post-merge.
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-gnullvm
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
# Configure a human readable name for each job
|
||||
name: Local Bazel build on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
name: Bazel test on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
@@ -91,6 +91,7 @@ jobs:
|
||||
)
|
||||
|
||||
bazel_wrapper_args=(
|
||||
--print-failed-action-summary
|
||||
--print-failed-test-logs
|
||||
)
|
||||
bazel_test_args=(
|
||||
@@ -100,8 +101,19 @@ jobs:
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA}
|
||||
)
|
||||
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
bazel_wrapper_args+=(--windows-msvc-host-platform)
|
||||
bazel_test_args+=(--jobs=8)
|
||||
bazel_wrapper_args+=(
|
||||
--windows-cross-compile
|
||||
--remote-download-toplevel
|
||||
)
|
||||
# Tradeoff: the Linux-RBE-built windows-gnullvm V8 archive
|
||||
# currently crashes during direct V8/code-mode smoke tests on the
|
||||
# Windows runner. Keep the broader fast Windows suite in PR CI and
|
||||
# rely on the main-only native Windows job below for full
|
||||
# V8/code-mode signal while we investigate the cross-built archive.
|
||||
bazel_targets+=(
|
||||
-//codex-rs/code-mode:code-mode-unit-tests
|
||||
-//codex-rs/v8-poc:v8-poc-unit-tests
|
||||
)
|
||||
fi
|
||||
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
@@ -130,6 +142,75 @@ jobs:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
|
||||
test-windows-native-main:
|
||||
# Native Windows Bazel tests are slower and frequently approach the
|
||||
# 30-minute PR budget, but they provide the full V8/code-mode signal that
|
||||
# the fast cross-compiled PR leg intentionally trades away. Run this only
|
||||
# for post-merge commits to main and give it a larger timeout.
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
timeout-minutes: 40
|
||||
runs-on: windows-latest
|
||||
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main)
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Prepare Bazel CI
|
||||
id: prepare_bazel
|
||||
uses: ./.github/actions/prepare-bazel-ci
|
||||
with:
|
||||
target: x86_64-pc-windows-gnullvm
|
||||
cache-scope: bazel-${{ github.job }}
|
||||
install-test-prereqs: "true"
|
||||
|
||||
- name: bazel test //...
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
bazel_targets=(
|
||||
//...
|
||||
# Keep standalone V8 library targets out of the ordinary Bazel CI
|
||||
# path. V8 consumers under `//codex-rs/...` still participate
|
||||
# transitively through `//...`.
|
||||
-//third_party/v8:all
|
||||
)
|
||||
|
||||
bazel_test_args=(
|
||||
test
|
||||
--test_tag_filters=-argument-comment-lint
|
||||
--test_verbose_timeout_warnings
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA}
|
||||
--build_metadata=TAG_windows_native_main=true
|
||||
)
|
||||
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
--print-failed-action-summary \
|
||||
--print-failed-test-logs \
|
||||
-- \
|
||||
"${bazel_test_args[@]}" \
|
||||
-- \
|
||||
"${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-windows-native-x86_64-pc-windows-gnullvm
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Save the job-scoped Bazel repository cache after cache misses. Keep the
|
||||
# upload non-fatal so cache service issues never fail the job itself.
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
|
||||
clippy:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
@@ -170,17 +251,24 @@ jobs:
|
||||
--build_metadata=TAG_job=clippy
|
||||
)
|
||||
bazel_wrapper_args=()
|
||||
bazel_target_list_args=()
|
||||
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
# Keep this aligned with the Windows Bazel test job. With the
|
||||
# default `//:local_windows` host platform, Windows `rust_test`
|
||||
# targets such as `//codex-rs/core:core-all-test` can be skipped
|
||||
# by `--skip_incompatible_explicit_targets`, which hides clippy
|
||||
# diagnostics from integration-test modules.
|
||||
bazel_wrapper_args+=(--windows-msvc-host-platform)
|
||||
bazel_clippy_args+=(--skip_incompatible_explicit_targets)
|
||||
# Keep this aligned with the fast Windows Bazel test job: use
|
||||
# Linux RBE for clippy build actions while targeting Windows
|
||||
# gnullvm. Fork/community PRs without the BuildBuddy secret fall
|
||||
# back inside `run-bazel-ci.sh` to the previous local Windows MSVC
|
||||
# host-platform shape.
|
||||
bazel_wrapper_args+=(--windows-cross-compile)
|
||||
bazel_target_list_args+=(--windows-cross-compile)
|
||||
if [[ -z "${BUILDBUDDY_API_KEY:-}" ]]; then
|
||||
# The fork fallback can see incompatible explicit Windows-cross
|
||||
# internal test binaries in the generated target list. Preserve
|
||||
# the old local-fallback behavior there.
|
||||
bazel_clippy_args+=(--skip_incompatible_explicit_targets)
|
||||
fi
|
||||
fi
|
||||
|
||||
bazel_target_lines="$(./scripts/list-bazel-clippy-targets.sh)"
|
||||
bazel_target_lines="$(./scripts/list-bazel-clippy-targets.sh "${bazel_target_list_args[@]}")"
|
||||
bazel_targets=()
|
||||
while IFS= read -r target; do
|
||||
bazel_targets+=("${target}")
|
||||
@@ -252,7 +340,12 @@ jobs:
|
||||
# Rust debug assertions explicitly.
|
||||
bazel_wrapper_args=()
|
||||
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
bazel_wrapper_args+=(--windows-msvc-host-platform)
|
||||
# This is build-only signal, so use the same Linux-RBE
|
||||
# cross-compile path as the fast Windows test and clippy jobs.
|
||||
# Fork/community PRs without the BuildBuddy secret fall back
|
||||
# inside `run-bazel-ci.sh` to the previous local Windows MSVC
|
||||
# host-platform shape.
|
||||
bazel_wrapper_args+=(--windows-cross-compile)
|
||||
fi
|
||||
|
||||
bazel_build_args=(
|
||||
|
||||
4
.github/workflows/cargo-deny.yml
vendored
4
.github/workflows/cargo-deny.yml
vendored
@@ -17,10 +17,10 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2
|
||||
with:
|
||||
rust-version: stable
|
||||
rust-version: 1.93.0
|
||||
manifest-path: ./codex-rs/Cargo.toml
|
||||
|
||||
6
.github/workflows/rust-release-windows.yml
vendored
6
.github/workflows/rust-release-windows.yml
vendored
@@ -24,7 +24,9 @@ jobs:
|
||||
build-windows-binaries:
|
||||
name: Build Windows binaries - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }}
|
||||
runs-on: ${{ matrix.runs_on }}
|
||||
timeout-minutes: 60
|
||||
# Windows release builds can exceed an hour on fat-LTO mainline releases,
|
||||
# so keep the timeout aligned with the top-level release build headroom.
|
||||
timeout-minutes: 90
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
@@ -137,7 +139,7 @@ jobs:
|
||||
- build-windows-binaries
|
||||
name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.runs_on }}
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 90
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
7
.github/workflows/rust-release.yml
vendored
7
.github/workflows/rust-release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dtolnay/rust-toolchain@c2b55edffaf41a251c410bb32bed22afefa800f1 # 1.92
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- name: Validate tag matches Cargo.toml version
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -49,9 +49,8 @@ jobs:
|
||||
needs: tag-check
|
||||
name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }}
|
||||
runs-on: ${{ matrix.runs_on || matrix.runner }}
|
||||
# Codex binaries can take a long time to build, particularly on Windows.
|
||||
# Ideally, this would be 60 minutes, but let's add some headroom because
|
||||
# having to restart a release build due to a timeout is a major pain.
|
||||
# Release builds can take a long time, so leave some headroom to avoid
|
||||
# having to restart the full workflow due to a timeout.
|
||||
timeout-minutes: 90
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
34
BUILD.bazel
34
BUILD.bazel
@@ -30,6 +30,40 @@ platform(
|
||||
parents = ["@platforms//host"],
|
||||
)
|
||||
|
||||
platform(
|
||||
name = "windows_x86_64_gnullvm",
|
||||
constraint_values = [
|
||||
"@platforms//cpu:x86_64",
|
||||
"@platforms//os:windows",
|
||||
"@rules_rs//rs/experimental/platforms/constraints:windows_gnullvm",
|
||||
],
|
||||
)
|
||||
|
||||
platform(
|
||||
name = "windows_x86_64_msvc",
|
||||
constraint_values = [
|
||||
"@platforms//cpu:x86_64",
|
||||
"@platforms//os:windows",
|
||||
"@rules_rs//rs/experimental/platforms/constraints:windows_msvc",
|
||||
],
|
||||
)
|
||||
|
||||
toolchain(
|
||||
name = "windows_gnullvm_tests_on_msvc_host_toolchain",
|
||||
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_gnullvm",
|
||||
],
|
||||
toolchain = "@bazel_tools//tools/test:empty_toolchain",
|
||||
toolchain_type = "@bazel_tools//tools/test:default_test_toolchain_type",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "rbe",
|
||||
actual = "@rbe_platform",
|
||||
|
||||
@@ -6,4 +6,6 @@ ignore = [
|
||||
"RUSTSEC-2024-0436", # paste 1.0.15 via starlark/ratatui; upstream crate is unmaintained
|
||||
"RUSTSEC-2024-0320", # yaml-rust via syntect; remove when syntect drops or updates it
|
||||
"RUSTSEC-2025-0141", # bincode via syntect; remove when syntect drops or updates it
|
||||
"RUSTSEC-2026-0118", # hickory-proto via rama-dns/rama-tcp; remove when rama updates to hickory 0.26.1 or hickory-net
|
||||
"RUSTSEC-2026-0119", # hickory-proto via rama-dns/rama-tcp; remove when rama updates to hickory 0.26.1 or hickory-net
|
||||
]
|
||||
|
||||
2
codex-rs/.github/workflows/cargo-audit.yml
vendored
2
codex-rs/.github/workflows/cargo-audit.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- name: Install cargo-audit
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
|
||||
58
codex-rs/Cargo.lock
generated
58
codex-rs/Cargo.lock
generated
@@ -1857,8 +1857,8 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-analytics",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-app-server-transport",
|
||||
"codex-arg0",
|
||||
"codex-backend-client",
|
||||
"codex-chatgpt",
|
||||
@@ -1882,6 +1882,7 @@ dependencies = [
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
"codex-rmcp-client",
|
||||
"codex-rollout",
|
||||
@@ -1890,23 +1891,17 @@ dependencies = [
|
||||
"codex-state",
|
||||
"codex-thread-store",
|
||||
"codex-tools",
|
||||
"codex-uds",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
"codex-utils-json-to-toml",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-rustls-provider",
|
||||
"constant_time_eq 0.3.1",
|
||||
"core_test_support",
|
||||
"flate2",
|
||||
"futures",
|
||||
"gethostname",
|
||||
"hmac",
|
||||
"jsonwebtoken",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"owo-colors",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"rmcp",
|
||||
@@ -2004,6 +1999,45 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-app-server-transport"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-state",
|
||||
"codex-uds",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-rustls-provider",
|
||||
"constant_time_eq 0.3.1",
|
||||
"futures",
|
||||
"gethostname",
|
||||
"hmac",
|
||||
"jsonwebtoken",
|
||||
"owo-colors",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-apply-patch"
|
||||
version = "0.0.0"
|
||||
@@ -2100,9 +2134,11 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-connectors",
|
||||
"codex-core",
|
||||
"codex-core-plugins",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-plugin",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
"pretty_assertions",
|
||||
@@ -2184,6 +2220,7 @@ dependencies = [
|
||||
"opentelemetry_sdk",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.3",
|
||||
"rcgen",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
@@ -2485,6 +2522,7 @@ name = "codex-core-api"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-analytics",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
@@ -2503,6 +2541,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"codex-analytics",
|
||||
"codex-app-server-protocol",
|
||||
"codex-config",
|
||||
"codex-core-skills",
|
||||
@@ -2977,9 +3016,7 @@ dependencies = [
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-models-manager",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -3517,6 +3554,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-core-api",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3591,6 +3629,7 @@ dependencies = [
|
||||
"codex-install-context",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
@@ -3904,6 +3943,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-pty",
|
||||
|
||||
@@ -8,6 +8,7 @@ members = [
|
||||
"ansi-escape",
|
||||
"async-utils",
|
||||
"app-server",
|
||||
"app-server-transport",
|
||||
"app-server-client",
|
||||
"app-server-protocol",
|
||||
"app-server-test-client",
|
||||
@@ -109,7 +110,7 @@ members = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.126.0-alpha.17"
|
||||
version = "0.0.0"
|
||||
# Track the edition for all workspace crates in one place. Individual
|
||||
# crates can still override this value, but keeping it here means new
|
||||
# crates created with `cargo new -w ...` automatically inherit the 2024
|
||||
@@ -127,6 +128,7 @@ codex-ansi-escape = { path = "ansi-escape" }
|
||||
codex-api = { path = "codex-api" }
|
||||
codex-aws-auth = { path = "aws-auth" }
|
||||
codex-app-server = { path = "app-server" }
|
||||
codex-app-server-transport = { path = "app-server-transport" }
|
||||
codex-app-server-client = { path = "app-server-client" }
|
||||
codex-app-server-protocol = { path = "app-server-protocol" }
|
||||
codex-app-server-test-client = { path = "app-server-test-client" }
|
||||
@@ -320,6 +322,10 @@ quick-xml = "0.38.4"
|
||||
rand = "0.9"
|
||||
ratatui = "0.29.0"
|
||||
ratatui-macros = "0.6.0"
|
||||
rcgen = { version = "0.14.7", default-features = false, features = [
|
||||
"aws_lc_rs",
|
||||
"pem",
|
||||
] }
|
||||
regex = "1.12.3"
|
||||
regex-lite = "0.1.8"
|
||||
reqwest = { version = "0.12", features = ["cookies"] }
|
||||
|
||||
@@ -46,7 +46,7 @@ Use `codex mcp` to add/list/get/remove MCP server launchers defined in `config.t
|
||||
|
||||
### Notifications
|
||||
|
||||
You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](../docs/config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS. When Codex detects that it is running under WSL 2 inside Windows Terminal (`WT_SESSION` is set), the TUI automatically falls back to native Windows toast notifications so approval prompts and completed turns surface even though Windows Terminal does not implement OSC 9.
|
||||
The legacy `notify` setting is deprecated and will be removed in a future release. Existing configurations still work, but new automation should use lifecycle hooks instead. The [notify documentation](../docs/config.md#notify) explains the remaining compatibility behavior. When Codex detects that it is running under WSL 2 inside Windows Terminal (`WT_SESSION` is set), the TUI automatically falls back to native Windows toast notifications so approval prompts and completed turns surface even though Windows Terminal does not implement OSC 9.
|
||||
|
||||
### `codex exec` to run Codex programmatically/non-interactively
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::NonSteerableTurnKind;
|
||||
use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
@@ -158,15 +157,12 @@ fn sample_thread_start_response(
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
permission_profile: Some(sample_permission_profile()),
|
||||
permission_profile: None,
|
||||
active_permission_profile: None,
|
||||
reasoning_effort: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_permission_profile() -> AppServerPermissionProfile {
|
||||
CorePermissionProfile::Disabled.into()
|
||||
}
|
||||
|
||||
fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata {
|
||||
CodexAppServerClientMetadata {
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
@@ -215,7 +211,8 @@ fn sample_thread_resume_response_with_source(
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
permission_profile: Some(sample_permission_profile()),
|
||||
permission_profile: None,
|
||||
active_permission_profile: None,
|
||||
reasoning_effort: None,
|
||||
})
|
||||
}
|
||||
@@ -306,10 +303,10 @@ fn sample_turn_completed_notification(
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact {
|
||||
fn sample_turn_resolved_config(thread_id: &str, turn_id: &str) -> TurnResolvedConfigFact {
|
||||
TurnResolvedConfigFact {
|
||||
turn_id: turn_id.to_string(),
|
||||
thread_id: "thread-2".to_string(),
|
||||
thread_id: thread_id.to_string(),
|
||||
num_input_images: 1,
|
||||
submission_type: None,
|
||||
ephemeral: false,
|
||||
@@ -422,6 +419,38 @@ async fn ingest_rejected_turn_steer(
|
||||
/*include_started*/ false, /*include_token_usage*/ false,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Initialize {
|
||||
connection_id: 8,
|
||||
params: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-web".to_string(),
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: None,
|
||||
},
|
||||
product_client_id: "codex-web".to_string(),
|
||||
runtime: sample_runtime_metadata(),
|
||||
rpc_transport: AppServerRpcTransport::Stdio,
|
||||
},
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientResponse {
|
||||
connection_id: 8,
|
||||
request_id: RequestId::Integer(6),
|
||||
response: Box::new(sample_thread_resume_response(
|
||||
"thread-2", /*ephemeral*/ false, "gpt-5",
|
||||
)),
|
||||
},
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
out.clear();
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientRequest {
|
||||
@@ -522,7 +551,7 @@ async fn ingest_turn_prerequisites(
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new(
|
||||
sample_turn_resolved_config("turn-2"),
|
||||
sample_turn_resolved_config("thread-2", "turn-2"),
|
||||
))),
|
||||
out,
|
||||
)
|
||||
@@ -1439,6 +1468,110 @@ async fn subagent_thread_started_publishes_without_initialize() {
|
||||
assert_eq!(payload[0]["event_params"]["subagent_source"], "review");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
let parent_thread_id =
|
||||
codex_protocol::ThreadId::from_string("44444444-4444-4444-4444-444444444444")
|
||||
.expect("valid parent thread id");
|
||||
let parent_thread_id_string = parent_thread_id.to_string();
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Initialize {
|
||||
connection_id: 7,
|
||||
params: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "parent-client".to_string(),
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: None,
|
||||
},
|
||||
product_client_id: "parent-client".to_string(),
|
||||
runtime: sample_runtime_metadata(),
|
||||
rpc_transport: AppServerRpcTransport::Stdio,
|
||||
},
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(1),
|
||||
response: Box::new(sample_thread_start_response(
|
||||
&parent_thread_id_string,
|
||||
/*ephemeral*/ false,
|
||||
"gpt-5",
|
||||
)),
|
||||
},
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted(
|
||||
SubAgentThreadStartedInput {
|
||||
thread_id: "thread-review".to_string(),
|
||||
parent_thread_id: None,
|
||||
product_client_id: "parent-client".to_string(),
|
||||
client_name: "parent-client".to_string(),
|
||||
client_version: "1.0.0".to_string(),
|
||||
model: "gpt-5".to_string(),
|
||||
ephemeral: false,
|
||||
subagent_source: SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
},
|
||||
created_at: 130,
|
||||
},
|
||||
)),
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
|
||||
events.clear();
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::Compaction(Box::new(
|
||||
CodexCompactionEvent {
|
||||
thread_id: "thread-review".to_string(),
|
||||
turn_id: "turn-compact".to_string(),
|
||||
trigger: CompactionTrigger::Manual,
|
||||
reason: CompactionReason::UserRequested,
|
||||
implementation: CompactionImplementation::Responses,
|
||||
phase: CompactionPhase::StandaloneTurn,
|
||||
strategy: CompactionStrategy::Memento,
|
||||
status: CompactionStatus::Completed,
|
||||
error: None,
|
||||
active_context_tokens_before: 131_000,
|
||||
active_context_tokens_after: 64_000,
|
||||
started_at: 100,
|
||||
completed_at: 101,
|
||||
duration_ms: Some(1200),
|
||||
},
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
|
||||
let payload = serde_json::to_value(&events).expect("serialize events");
|
||||
assert_eq!(
|
||||
payload[0]["event_params"]["app_server_client"]["product_client_id"],
|
||||
"parent-client"
|
||||
);
|
||||
assert_eq!(
|
||||
payload[0]["event_params"]["parent_thread_id"],
|
||||
"44444444-4444-4444-4444-444444444444"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_used_event_serializes_expected_shape() {
|
||||
let tracking = TrackEventsContext {
|
||||
@@ -1499,6 +1632,25 @@ fn plugin_management_event_serializes_expected_shape() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_management_event_can_use_remote_plugin_id_override() {
|
||||
let mut plugin = sample_plugin_metadata();
|
||||
plugin.remote_plugin_id = Some("plugins~Plugin_remote".to_string());
|
||||
let event = TrackEventRequest::PluginInstalled(CodexPluginEventRequest {
|
||||
event_type: "codex_plugin_installed",
|
||||
event_params: codex_plugin_metadata(plugin),
|
||||
});
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize plugin installed event");
|
||||
|
||||
assert_eq!(
|
||||
payload["event_params"]["plugin_id"],
|
||||
"plugins~Plugin_remote"
|
||||
);
|
||||
assert_eq!(payload["event_params"]["plugin_name"], "sample");
|
||||
assert_eq!(payload["event_params"]["marketplace_name"], "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hook_run_event_serializes_expected_shape() {
|
||||
let tracking = TrackEventsContext {
|
||||
@@ -1562,6 +1714,15 @@ fn hook_run_metadata_maps_sources_and_statuses() {
|
||||
},
|
||||
))
|
||||
.expect("serialize project hook");
|
||||
let cloud_requirements = serde_json::to_value(codex_hook_run_metadata(
|
||||
&tracking,
|
||||
HookRunFact {
|
||||
event_name: HookEventName::Stop,
|
||||
hook_source: HookSource::CloudRequirements,
|
||||
status: HookRunStatus::Blocked,
|
||||
},
|
||||
))
|
||||
.expect("serialize cloud requirements hook");
|
||||
let unknown = serde_json::to_value(codex_hook_run_metadata(
|
||||
&tracking,
|
||||
HookRunFact {
|
||||
@@ -1576,6 +1737,8 @@ fn hook_run_metadata_maps_sources_and_statuses() {
|
||||
assert_eq!(system["status"], "completed");
|
||||
assert_eq!(project["hook_source"], "project");
|
||||
assert_eq!(project["status"], "blocked");
|
||||
assert_eq!(cloud_requirements["hook_source"], "cloud_requirements");
|
||||
assert_eq!(cloud_requirements["status"], "blocked");
|
||||
assert_eq!(unknown["hook_source"], "unknown");
|
||||
assert_eq!(unknown["status"], "failed");
|
||||
}
|
||||
@@ -2122,7 +2285,7 @@ async fn turn_start_error_response_discards_pending_start_request() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new(
|
||||
sample_turn_resolved_config("turn-2"),
|
||||
sample_turn_resolved_config("thread-2", "turn-2"),
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
@@ -2474,6 +2637,7 @@ async fn turn_completed_without_started_notification_emits_null_started_at() {
|
||||
fn sample_plugin_metadata() -> PluginTelemetryMetadata {
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("sample@test").expect("valid plugin id"),
|
||||
remote_plugin_id: None,
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "sample@test".to_string(),
|
||||
display_name: "sample".to_string(),
|
||||
|
||||
@@ -111,6 +111,7 @@ fn sample_thread_start_response() -> ClientResponsePayload {
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
permission_profile: Some(sample_permission_profile()),
|
||||
active_permission_profile: None,
|
||||
reasoning_effort: None,
|
||||
})
|
||||
}
|
||||
@@ -127,6 +128,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload {
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
permission_profile: Some(sample_permission_profile()),
|
||||
active_permission_profile: None,
|
||||
reasoning_effort: None,
|
||||
})
|
||||
}
|
||||
@@ -143,6 +145,7 @@ fn sample_thread_fork_response() -> ClientResponsePayload {
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
permission_profile: Some(sample_permission_profile()),
|
||||
active_permission_profile: None,
|
||||
reasoning_effort: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -587,11 +587,16 @@ pub(crate) fn codex_app_metadata(
|
||||
}
|
||||
|
||||
pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPluginMetadata {
|
||||
let capability_summary = plugin.capability_summary;
|
||||
let PluginTelemetryMetadata {
|
||||
plugin_id,
|
||||
remote_plugin_id,
|
||||
capability_summary,
|
||||
} = plugin;
|
||||
let event_plugin_id = remote_plugin_id.unwrap_or_else(|| plugin_id.as_key());
|
||||
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),
|
||||
plugin_id: Some(event_plugin_id),
|
||||
plugin_name: Some(plugin_id.plugin_name),
|
||||
marketplace_name: Some(plugin_id.marketplace_name),
|
||||
has_skills: capability_summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.has_skills),
|
||||
@@ -685,6 +690,7 @@ fn analytics_hook_source(source: HookSource) -> &'static str {
|
||||
HookSource::Mdm => "mdm",
|
||||
HookSource::SessionFlags => "session_flags",
|
||||
HookSource::Plugin => "plugin",
|
||||
HookSource::CloudRequirements => "cloud_requirements",
|
||||
HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
|
||||
HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
|
||||
HookSource::Unknown => "unknown",
|
||||
|
||||
@@ -74,8 +74,7 @@ pub(crate) struct AnalyticsReducer {
|
||||
requests: HashMap<(u64, RequestId), RequestState>,
|
||||
turns: HashMap<String, TurnState>,
|
||||
connections: HashMap<u64, ConnectionState>,
|
||||
thread_connections: HashMap<String, u64>,
|
||||
thread_metadata: HashMap<String, ThreadMetadataState>,
|
||||
threads: HashMap<String, ThreadAnalyticsState>,
|
||||
}
|
||||
|
||||
struct ConnectionState {
|
||||
@@ -83,6 +82,69 @@ struct ConnectionState {
|
||||
runtime: CodexRuntimeMetadata,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ThreadAnalyticsState {
|
||||
connection_id: Option<u64>,
|
||||
metadata: Option<ThreadMetadataState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct AnalyticsDropSite<'a> {
|
||||
event_name: &'static str,
|
||||
thread_id: &'a str,
|
||||
turn_id: Option<&'a str>,
|
||||
review_id: Option<&'a str>,
|
||||
item_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> AnalyticsDropSite<'a> {
|
||||
fn guardian(input: &'a GuardianReviewEventParams) -> Self {
|
||||
Self {
|
||||
event_name: "guardian",
|
||||
thread_id: &input.thread_id,
|
||||
turn_id: Some(&input.turn_id),
|
||||
review_id: Some(&input.review_id),
|
||||
item_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn compaction(input: &'a CodexCompactionEvent) -> Self {
|
||||
Self {
|
||||
event_name: "compaction",
|
||||
thread_id: &input.thread_id,
|
||||
turn_id: Some(&input.turn_id),
|
||||
review_id: None,
|
||||
item_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn turn_steer(thread_id: &'a str) -> Self {
|
||||
Self {
|
||||
event_name: "turn steer",
|
||||
thread_id,
|
||||
turn_id: None,
|
||||
review_id: None,
|
||||
item_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn turn(thread_id: &'a str, turn_id: &'a str) -> Self {
|
||||
Self {
|
||||
event_name: "turn",
|
||||
thread_id,
|
||||
turn_id: Some(turn_id),
|
||||
review_id: None,
|
||||
item_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MissingAnalyticsContext {
|
||||
ThreadConnection,
|
||||
Connection { connection_id: u64 },
|
||||
ThreadMetadata,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ThreadMetadataState {
|
||||
thread_source: Option<&'static str>,
|
||||
@@ -274,6 +336,26 @@ impl AnalyticsReducer {
|
||||
input: SubAgentThreadStartedInput,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let parent_thread_id = input
|
||||
.parent_thread_id
|
||||
.clone()
|
||||
.or_else(|| subagent_parent_thread_id(&input.subagent_source));
|
||||
let parent_connection_id = parent_thread_id
|
||||
.as_ref()
|
||||
.and_then(|parent_thread_id| self.threads.get(parent_thread_id))
|
||||
.and_then(|thread| thread.connection_id);
|
||||
let thread_state = self.threads.entry(input.thread_id.clone()).or_default();
|
||||
thread_state
|
||||
.metadata
|
||||
.get_or_insert_with(|| ThreadMetadataState {
|
||||
thread_source: Some("subagent"),
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
subagent_source: Some(subagent_source_name(&input.subagent_source)),
|
||||
parent_thread_id,
|
||||
});
|
||||
if thread_state.connection_id.is_none() {
|
||||
thread_state.connection_id = parent_connection_id;
|
||||
}
|
||||
out.push(TrackEventRequest::ThreadInitialized(
|
||||
subagent_thread_started_event_request(input),
|
||||
));
|
||||
@@ -284,23 +366,9 @@ impl AnalyticsReducer {
|
||||
input: GuardianReviewEventParams,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let Some(connection_id) = self.thread_connections.get(&input.thread_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %input.thread_id,
|
||||
turn_id = %input.turn_id,
|
||||
review_id = %input.review_id,
|
||||
"dropping guardian analytics event: missing thread connection metadata"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(connection_state) = self.connections.get(connection_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %input.thread_id,
|
||||
turn_id = %input.turn_id,
|
||||
review_id = %input.review_id,
|
||||
connection_id,
|
||||
"dropping guardian analytics event: missing connection metadata"
|
||||
);
|
||||
let Some(connection_state) =
|
||||
self.thread_connection_or_warn(AnalyticsDropSite::guardian(&input))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
out.push(TrackEventRequest::GuardianReview(Box::new(
|
||||
@@ -686,10 +754,13 @@ impl AnalyticsReducer {
|
||||
};
|
||||
let thread_metadata =
|
||||
ThreadMetadataState::from_thread_metadata(&thread_source, initialization_mode);
|
||||
self.thread_connections
|
||||
.insert(thread_id.clone(), connection_id);
|
||||
self.thread_metadata
|
||||
.insert(thread_id.clone(), thread_metadata.clone());
|
||||
self.threads.insert(
|
||||
thread_id.clone(),
|
||||
ThreadAnalyticsState {
|
||||
connection_id: Some(connection_id),
|
||||
metadata: Some(thread_metadata.clone()),
|
||||
},
|
||||
);
|
||||
out.push(TrackEventRequest::ThreadInitialized(
|
||||
ThreadInitializedEvent {
|
||||
event_type: "codex_thread_initialized",
|
||||
@@ -710,29 +781,9 @@ impl AnalyticsReducer {
|
||||
}
|
||||
|
||||
fn ingest_compaction(&mut self, input: CodexCompactionEvent, out: &mut Vec<TrackEventRequest>) {
|
||||
let Some(connection_id) = self.thread_connections.get(&input.thread_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %input.thread_id,
|
||||
turn_id = %input.turn_id,
|
||||
"dropping compaction analytics event: missing thread connection metadata"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(connection_state) = self.connections.get(connection_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %input.thread_id,
|
||||
turn_id = %input.turn_id,
|
||||
connection_id,
|
||||
"dropping compaction analytics event: missing connection metadata"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(thread_metadata) = self.thread_metadata.get(&input.thread_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %input.thread_id,
|
||||
turn_id = %input.turn_id,
|
||||
"dropping compaction analytics event: missing thread lifecycle metadata"
|
||||
);
|
||||
let Some((connection_state, thread_metadata)) =
|
||||
self.thread_context_or_warn(AnalyticsDropSite::compaction(&input))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
out.push(TrackEventRequest::Compaction(Box::new(
|
||||
@@ -787,11 +838,13 @@ impl AnalyticsReducer {
|
||||
let Some(connection_state) = self.connections.get(&connection_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(thread_metadata) = self.thread_metadata.get(&pending_request.thread_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %pending_request.thread_id,
|
||||
"dropping turn steer analytics event: missing thread lifecycle metadata"
|
||||
);
|
||||
let drop_site = AnalyticsDropSite::turn_steer(&pending_request.thread_id);
|
||||
let Some(thread_metadata) = self
|
||||
.threads
|
||||
.get(drop_site.thread_id)
|
||||
.and_then(|thread| thread.metadata.as_ref())
|
||||
else {
|
||||
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
||||
return;
|
||||
};
|
||||
out.push(TrackEventRequest::TurnSteer(CodexTurnSteerEventRequest {
|
||||
@@ -824,42 +877,34 @@ impl AnalyticsReducer {
|
||||
{
|
||||
return;
|
||||
}
|
||||
let connection_metadata = turn_state
|
||||
.connection_id
|
||||
.and_then(|connection_id| self.connections.get(&connection_id))
|
||||
.map(|connection_state| {
|
||||
(
|
||||
connection_state.app_server_client.clone(),
|
||||
connection_state.runtime.clone(),
|
||||
)
|
||||
});
|
||||
let Some((app_server_client, runtime)) = connection_metadata else {
|
||||
if let Some(connection_id) = turn_state.connection_id {
|
||||
tracing::warn!(
|
||||
turn_id,
|
||||
connection_id,
|
||||
"dropping turn analytics event: missing connection metadata"
|
||||
);
|
||||
}
|
||||
return;
|
||||
};
|
||||
let Some(thread_id) = turn_state.thread_id.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(thread_metadata) = self.thread_metadata.get(thread_id) else {
|
||||
tracing::warn!(
|
||||
thread_id,
|
||||
turn_id,
|
||||
"dropping turn analytics event: missing thread lifecycle metadata"
|
||||
let Some(connection_id) = turn_state.connection_id else {
|
||||
return;
|
||||
};
|
||||
let Some(connection_state) = self.connections.get(&connection_id) else {
|
||||
warn_missing_analytics_context(
|
||||
&AnalyticsDropSite::turn(thread_id, turn_id),
|
||||
MissingAnalyticsContext::Connection { connection_id },
|
||||
);
|
||||
return;
|
||||
};
|
||||
let drop_site = AnalyticsDropSite::turn(thread_id, turn_id);
|
||||
let Some(thread_metadata) = self
|
||||
.threads
|
||||
.get(drop_site.thread_id)
|
||||
.and_then(|thread| thread.metadata.as_ref())
|
||||
else {
|
||||
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
||||
return;
|
||||
};
|
||||
out.push(TrackEventRequest::TurnEvent(Box::new(
|
||||
CodexTurnEventRequest {
|
||||
event_type: "codex_turn_event",
|
||||
event_params: codex_turn_event_params(
|
||||
app_server_client,
|
||||
runtime,
|
||||
connection_state.app_server_client.clone(),
|
||||
connection_state.runtime.clone(),
|
||||
turn_id.to_string(),
|
||||
turn_state,
|
||||
thread_metadata,
|
||||
@@ -868,6 +913,67 @@ impl AnalyticsReducer {
|
||||
)));
|
||||
self.turns.remove(turn_id);
|
||||
}
|
||||
|
||||
fn thread_connection_or_warn(
|
||||
&self,
|
||||
drop_site: AnalyticsDropSite<'_>,
|
||||
) -> Option<&ConnectionState> {
|
||||
let Some(thread_state) = self.threads.get(drop_site.thread_id) else {
|
||||
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadConnection);
|
||||
return None;
|
||||
};
|
||||
let Some(connection_id) = thread_state.connection_id else {
|
||||
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadConnection);
|
||||
return None;
|
||||
};
|
||||
let Some(connection_state) = self.connections.get(&connection_id) else {
|
||||
warn_missing_analytics_context(
|
||||
&drop_site,
|
||||
MissingAnalyticsContext::Connection { connection_id },
|
||||
);
|
||||
return None;
|
||||
};
|
||||
Some(connection_state)
|
||||
}
|
||||
|
||||
fn thread_context_or_warn(
|
||||
&self,
|
||||
drop_site: AnalyticsDropSite<'_>,
|
||||
) -> Option<(&ConnectionState, &ThreadMetadataState)> {
|
||||
let connection_state = self.thread_connection_or_warn(drop_site)?;
|
||||
let Some(thread_metadata) = self
|
||||
.threads
|
||||
.get(drop_site.thread_id)
|
||||
.and_then(|thread| thread.metadata.as_ref())
|
||||
else {
|
||||
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
||||
return None;
|
||||
};
|
||||
Some((connection_state, thread_metadata))
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_missing_analytics_context(
|
||||
drop_site: &AnalyticsDropSite<'_>,
|
||||
missing: MissingAnalyticsContext,
|
||||
) {
|
||||
let (missing_context, connection_id) = match missing {
|
||||
MissingAnalyticsContext::ThreadConnection => ("thread_connection", None),
|
||||
MissingAnalyticsContext::Connection { connection_id } => {
|
||||
("connection", Some(connection_id))
|
||||
}
|
||||
MissingAnalyticsContext::ThreadMetadata => ("thread_metadata", None),
|
||||
};
|
||||
tracing::warn!(
|
||||
thread_id = %drop_site.thread_id,
|
||||
turn_id = ?drop_site.turn_id,
|
||||
review_id = ?drop_site.review_id,
|
||||
item_id = ?drop_site.item_id,
|
||||
missing_context,
|
||||
connection_id,
|
||||
"dropping {} analytics event: missing analytics context",
|
||||
drop_site.event_name
|
||||
);
|
||||
}
|
||||
|
||||
fn codex_turn_event_params(
|
||||
|
||||
@@ -99,10 +99,6 @@ pub mod legacy_core {
|
||||
pub use codex_core::personality_migration::*;
|
||||
}
|
||||
|
||||
pub mod plugins {
|
||||
pub use codex_core::plugins::PluginsManager;
|
||||
}
|
||||
|
||||
pub mod review_format {
|
||||
pub use codex_core::review_format::*;
|
||||
}
|
||||
@@ -304,7 +300,15 @@ impl fmt::Display for TypedRequestError {
|
||||
write!(f, "{method} transport error: {source}")
|
||||
}
|
||||
Self::Server { method, source } => {
|
||||
write!(f, "{method} failed: {}", source.message)
|
||||
write!(
|
||||
f,
|
||||
"{method} failed: {} (code {})",
|
||||
source.message, source.code
|
||||
)?;
|
||||
if let Some(data) = source.data.as_ref() {
|
||||
write!(f, ", data: {data}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Self::Deserialize { method, source } => {
|
||||
write!(f, "{method} response decode error: {source}")
|
||||
@@ -1919,11 +1923,15 @@ mod tests {
|
||||
method: "thread/read".to_string(),
|
||||
source: JSONRPCErrorError {
|
||||
code: -32603,
|
||||
data: None,
|
||||
data: Some(serde_json::json!({"detail": "config lock mismatch"})),
|
||||
message: "internal".to_string(),
|
||||
},
|
||||
};
|
||||
assert_eq!(std::error::Error::source(&server).is_some(), false);
|
||||
assert_eq!(
|
||||
server.to_string(),
|
||||
"thread/read failed: internal (code -32603), data: {\"detail\":\"config lock mismatch\"}"
|
||||
);
|
||||
|
||||
let deserialize = TypedRequestError::Deserialize {
|
||||
method: "thread/start".to_string(),
|
||||
|
||||
@@ -2039,6 +2039,31 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileModificationParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -2050,6 +2075,40 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileSelectionParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionProfileModificationParams"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"profile"
|
||||
],
|
||||
"title": "ProfilePermissionProfileSelectionParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "ProfilePermissionProfileSelectionParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
@@ -2127,6 +2186,56 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDeleteParams": {
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareListParams": {
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareSaveParams": {
|
||||
"properties": {
|
||||
"pluginPath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pluginPath"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSkillReadParams": {
|
||||
"properties": {
|
||||
"remoteMarketplaceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"skillName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remoteMarketplaceName",
|
||||
"remotePluginId",
|
||||
"skillName"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginUninstallParams": {
|
||||
"properties": {
|
||||
"pluginId": {
|
||||
@@ -3363,10 +3472,6 @@
|
||||
"ephemeral": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"excludeTurns": {
|
||||
"description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"description": "Configuration overrides for the forked thread, if any.",
|
||||
"type": [
|
||||
@@ -3768,10 +3873,6 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"excludeTurns": {
|
||||
"description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"description": "Configuration overrides for the resumed thread, if any.",
|
||||
"type": [
|
||||
@@ -4031,44 +4132,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadTurnsListParams": {
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"description": "Opaque cursor to pass to the next call to continue after the last turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional turn page size.",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sortDirection": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SortDirection"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional turn pagination direction; defaults to descending."
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadUnarchiveParams": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
@@ -4799,30 +4862,6 @@
|
||||
"title": "Thread/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/turns/list"
|
||||
],
|
||||
"title": "Thread/turns/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadTurnsListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/turns/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Append raw Responses API items to the thread history without starting a user turn.",
|
||||
"properties": {
|
||||
@@ -5016,6 +5055,102 @@
|
||||
"title": "Plugin/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/skill/read"
|
||||
],
|
||||
"title": "Plugin/skill/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginSkillReadParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/skill/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/save"
|
||||
],
|
||||
"title": "Plugin/share/saveRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginShareSaveParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/saveRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/list"
|
||||
],
|
||||
"title": "Plugin/share/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginShareListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/delete"
|
||||
],
|
||||
"title": "Plugin/share/deleteRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginShareDeleteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/deleteRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -1032,6 +1032,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"FileChangeOutputDeltaNotification": {
|
||||
"description": "Deprecated legacy notification for `apply_patch` textual output.\n\nThe server no longer emits this notification.",
|
||||
"properties": {
|
||||
"delta": {
|
||||
"type": "string"
|
||||
@@ -1901,6 +1902,7 @@
|
||||
"mdm",
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
@@ -3929,7 +3931,7 @@
|
||||
"ThreadRealtimeStartedNotification": {
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"realtimeSessionId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -5190,6 +5192,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Deprecated legacy apply_patch output stream notification.",
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
|
||||
@@ -569,30 +569,6 @@
|
||||
"title": "Thread/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/turns/list"
|
||||
],
|
||||
"title": "Thread/turns/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadTurnsListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/turns/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Append raw Responses API items to the thread history without starting a user turn.",
|
||||
"properties": {
|
||||
@@ -786,6 +762,102 @@
|
||||
"title": "Plugin/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/skill/read"
|
||||
],
|
||||
"title": "Plugin/skill/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/PluginSkillReadParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/skill/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/save"
|
||||
],
|
||||
"title": "Plugin/share/saveRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/PluginShareSaveParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/saveRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/list"
|
||||
],
|
||||
"title": "Plugin/share/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/PluginShareListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/delete"
|
||||
],
|
||||
"title": "Plugin/share/deleteRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/PluginShareDeleteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/deleteRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -4217,6 +4289,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Deprecated legacy apply_patch output stream notification.",
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -5413,6 +5486,59 @@
|
||||
"title": "AccountUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfile": {
|
||||
"properties": {
|
||||
"extends": {
|
||||
"default": null,
|
||||
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AddCreditsNudgeCreditType": {
|
||||
"enum": [
|
||||
"credits",
|
||||
@@ -8475,6 +8601,7 @@
|
||||
},
|
||||
"FileChangeOutputDeltaNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Deprecated legacy notification for `apply_patch` textual output.\n\nThe server no longer emits this notification.",
|
||||
"properties": {
|
||||
"delta": {
|
||||
"type": "string"
|
||||
@@ -9645,12 +9772,21 @@
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"eventName": {
|
||||
"$ref": "#/definitions/v2/HookEventName"
|
||||
},
|
||||
"handlerType": {
|
||||
"$ref": "#/definitions/v2/HookHandlerType"
|
||||
},
|
||||
"isManaged": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"matcher": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -9683,8 +9819,11 @@
|
||||
},
|
||||
"required": [
|
||||
"displayOrder",
|
||||
"enabled",
|
||||
"eventName",
|
||||
"handlerType",
|
||||
"isManaged",
|
||||
"key",
|
||||
"source",
|
||||
"sourcePath",
|
||||
"timeoutSec"
|
||||
@@ -9847,6 +9986,7 @@
|
||||
"mdm",
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
@@ -11688,6 +11828,31 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileModificationParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -11699,6 +11864,40 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileSelectionParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PermissionProfileModificationParams"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"profile"
|
||||
],
|
||||
"title": "ProfilePermissionProfileSelectionParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "ProfilePermissionProfileSelectionParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
@@ -11757,6 +11956,23 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginDetail": {
|
||||
"properties": {
|
||||
"apps": {
|
||||
@@ -12124,6 +12340,140 @@
|
||||
"title": "PluginReadResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDeleteParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId"
|
||||
],
|
||||
"title": "PluginShareDeleteParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDeleteResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PluginShareDeleteResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareListItem": {
|
||||
"properties": {
|
||||
"localPluginPath": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugin": {
|
||||
"$ref": "#/definitions/v2/PluginSummary"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugin",
|
||||
"shareUrl"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PluginShareListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginShareListItem"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PluginShareListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareSaveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"pluginPath": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pluginPath"
|
||||
],
|
||||
"title": "PluginShareSaveParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareSaveResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId",
|
||||
"shareUrl"
|
||||
],
|
||||
"title": "PluginShareSaveResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSkillReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remoteMarketplaceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"skillName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remoteMarketplaceName",
|
||||
"remotePluginId",
|
||||
"skillName"
|
||||
],
|
||||
"title": "PluginSkillReadParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSkillReadResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"contents": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginSkillReadResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -12208,6 +12558,15 @@
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/v2/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -14779,10 +15138,6 @@
|
||||
"ephemeral": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"excludeTurns": {
|
||||
"description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"description": "Configuration overrides for the forked thread, if any.",
|
||||
"type": [
|
||||
@@ -14880,7 +15235,7 @@
|
||||
"$ref": "#/definitions/v2/SandboxPolicy"
|
||||
}
|
||||
],
|
||||
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
@@ -16164,7 +16519,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"realtimeSessionId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -16280,10 +16635,6 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"excludeTurns": {
|
||||
"description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"description": "Configuration overrides for the resumed thread, if any.",
|
||||
"type": [
|
||||
@@ -16391,7 +16742,7 @@
|
||||
"$ref": "#/definitions/v2/SandboxPolicy"
|
||||
}
|
||||
],
|
||||
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
@@ -16695,7 +17046,7 @@
|
||||
"$ref": "#/definitions/v2/SandboxPolicy"
|
||||
}
|
||||
],
|
||||
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
@@ -16878,76 +17229,6 @@
|
||||
"title": "ThreadTokenUsageUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadTurnsListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"description": "Opaque cursor to pass to the next call to continue after the last turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional turn page size.",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sortDirection": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/SortDirection"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional turn pagination direction; defaults to descending."
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadTurnsListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadTurnsListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"backwardsCursor": {
|
||||
"description": "Opaque cursor to pass as `cursor` when reversing `sortDirection`. This is only populated when the page contains at least one turn. Use it with the opposite `sortDirection` to include the anchor turn again and catch updates to that turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/Turn"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"nextCursor": {
|
||||
"description": "Opaque cursor to pass to the next call to continue after the last turn. if None, there are no more turns to return.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "ThreadTurnsListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadUnarchiveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
@@ -130,6 +130,59 @@
|
||||
"title": "AccountUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfile": {
|
||||
"properties": {
|
||||
"extends": {
|
||||
"default": null,
|
||||
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AddCreditsNudgeCreditType": {
|
||||
"enum": [
|
||||
"credits",
|
||||
@@ -1275,30 +1328,6 @@
|
||||
"title": "Thread/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/turns/list"
|
||||
],
|
||||
"title": "Thread/turns/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadTurnsListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/turns/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Append raw Responses API items to the thread history without starting a user turn.",
|
||||
"properties": {
|
||||
@@ -1492,6 +1521,102 @@
|
||||
"title": "Plugin/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/skill/read"
|
||||
],
|
||||
"title": "Plugin/skill/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginSkillReadParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/skill/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/save"
|
||||
],
|
||||
"title": "Plugin/share/saveRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginShareSaveParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/saveRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/list"
|
||||
],
|
||||
"title": "Plugin/share/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginShareListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/delete"
|
||||
],
|
||||
"title": "Plugin/share/deleteRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginShareDeleteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/deleteRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -4974,6 +5099,7 @@
|
||||
},
|
||||
"FileChangeOutputDeltaNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Deprecated legacy notification for `apply_patch` textual output.\n\nThe server no longer emits this notification.",
|
||||
"properties": {
|
||||
"delta": {
|
||||
"type": "string"
|
||||
@@ -6255,12 +6381,21 @@
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"eventName": {
|
||||
"$ref": "#/definitions/HookEventName"
|
||||
},
|
||||
"handlerType": {
|
||||
"$ref": "#/definitions/HookHandlerType"
|
||||
},
|
||||
"isManaged": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"matcher": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -6293,8 +6428,11 @@
|
||||
},
|
||||
"required": [
|
||||
"displayOrder",
|
||||
"enabled",
|
||||
"eventName",
|
||||
"handlerType",
|
||||
"isManaged",
|
||||
"key",
|
||||
"source",
|
||||
"sourcePath",
|
||||
"timeoutSec"
|
||||
@@ -6457,6 +6595,7 @@
|
||||
"mdm",
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
@@ -8342,6 +8481,31 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileModificationParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -8353,6 +8517,40 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionProfileSelectionParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionProfileModificationParams"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"profile"
|
||||
],
|
||||
"title": "ProfilePermissionProfileSelectionParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "ProfilePermissionProfileSelectionParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
@@ -8411,6 +8609,23 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginDetail": {
|
||||
"properties": {
|
||||
"apps": {
|
||||
@@ -8778,6 +8993,140 @@
|
||||
"title": "PluginReadResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDeleteParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId"
|
||||
],
|
||||
"title": "PluginShareDeleteParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDeleteResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PluginShareDeleteResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareListItem": {
|
||||
"properties": {
|
||||
"localPluginPath": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugin": {
|
||||
"$ref": "#/definitions/PluginSummary"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugin",
|
||||
"shareUrl"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PluginShareListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginShareListItem"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PluginShareListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareSaveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"pluginPath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pluginPath"
|
||||
],
|
||||
"title": "PluginShareSaveParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareSaveResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId",
|
||||
"shareUrl"
|
||||
],
|
||||
"title": "PluginShareSaveResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSkillReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remoteMarketplaceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"skillName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remoteMarketplaceName",
|
||||
"remotePluginId",
|
||||
"skillName"
|
||||
],
|
||||
"title": "PluginSkillReadParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSkillReadResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"contents": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginSkillReadResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -8862,6 +9211,15 @@
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -11035,6 +11393,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Deprecated legacy apply_patch output stream notification.",
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -12665,10 +13024,6 @@
|
||||
"ephemeral": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"excludeTurns": {
|
||||
"description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"description": "Configuration overrides for the forked thread, if any.",
|
||||
"type": [
|
||||
@@ -12766,7 +13121,7 @@
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
}
|
||||
],
|
||||
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
@@ -14050,7 +14405,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"realtimeSessionId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -14166,10 +14521,6 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"excludeTurns": {
|
||||
"description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"description": "Configuration overrides for the resumed thread, if any.",
|
||||
"type": [
|
||||
@@ -14277,7 +14628,7 @@
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
}
|
||||
],
|
||||
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
@@ -14581,7 +14932,7 @@
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
}
|
||||
],
|
||||
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
@@ -14764,76 +15115,6 @@
|
||||
"title": "ThreadTokenUsageUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadTurnsListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"description": "Opaque cursor to pass to the next call to continue after the last turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional turn page size.",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sortDirection": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SortDirection"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional turn pagination direction; defaults to descending."
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadTurnsListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadTurnsListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"backwardsCursor": {
|
||||
"description": "Opaque cursor to pass as `cursor` when reversing `sortDirection`. This is only populated when the page contains at least one turn. Use it with the opposite `sortDirection` to include the anchor turn again and catch updates to that turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/Turn"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"nextCursor": {
|
||||
"description": "Opaque cursor to pass to the next call to continue after the last turn. if None, there are no more turns to return.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "ThreadTurnsListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadUnarchiveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Deprecated legacy notification for `apply_patch` textual output.\n\nThe server no longer emits this notification.",
|
||||
"properties": {
|
||||
"delta": {
|
||||
"type": "string"
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"mdm",
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"mdm",
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
|
||||
@@ -51,12 +51,21 @@
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"eventName": {
|
||||
"$ref": "#/definitions/HookEventName"
|
||||
},
|
||||
"handlerType": {
|
||||
"$ref": "#/definitions/HookHandlerType"
|
||||
},
|
||||
"isManaged": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"matcher": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -89,8 +98,11 @@
|
||||
},
|
||||
"required": [
|
||||
"displayOrder",
|
||||
"enabled",
|
||||
"eventName",
|
||||
"handlerType",
|
||||
"isManaged",
|
||||
"key",
|
||||
"source",
|
||||
"sourcePath",
|
||||
"timeoutSec"
|
||||
@@ -105,6 +117,7 @@
|
||||
"mdm",
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"cloudRequirements",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
|
||||
@@ -38,6 +38,23 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginInstallPolicy": {
|
||||
"enum": [
|
||||
"NOT_AVAILABLE",
|
||||
@@ -299,6 +316,15 @@
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -44,6 +44,23 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginDetail": {
|
||||
"properties": {
|
||||
"apps": {
|
||||
@@ -318,6 +335,15 @@
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
13
codex-rs/app-server-protocol/schema/json/v2/PluginShareDeleteParams.json
generated
Normal file
13
codex-rs/app-server-protocol/schema/json/v2/PluginShareDeleteParams.json
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId"
|
||||
],
|
||||
"title": "PluginShareDeleteParams",
|
||||
"type": "object"
|
||||
}
|
||||
5
codex-rs/app-server-protocol/schema/json/v2/PluginShareDeleteResponse.json
generated
Normal file
5
codex-rs/app-server-protocol/schema/json/v2/PluginShareDeleteResponse.json
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PluginShareDeleteResponse",
|
||||
"type": "object"
|
||||
}
|
||||
5
codex-rs/app-server-protocol/schema/json/v2/PluginShareListParams.json
generated
Normal file
5
codex-rs/app-server-protocol/schema/json/v2/PluginShareListParams.json
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PluginShareListParams",
|
||||
"type": "object"
|
||||
}
|
||||
342
codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json
generated
Normal file
342
codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json
generated
Normal file
@@ -0,0 +1,342 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAuthPolicy": {
|
||||
"enum": [
|
||||
"ON_INSTALL",
|
||||
"ON_USE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginAvailability": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"DISABLED_BY_ADMIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.",
|
||||
"enum": [
|
||||
"AVAILABLE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginInstallPolicy": {
|
||||
"enum": [
|
||||
"NOT_AVAILABLE",
|
||||
"AVAILABLE",
|
||||
"INSTALLED_BY_DEFAULT"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginInterface": {
|
||||
"properties": {
|
||||
"brandColor": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"category": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"composerIcon": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local composer icon path, resolved from the installed plugin package."
|
||||
},
|
||||
"composerIconUrl": {
|
||||
"description": "Remote composer icon URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"defaultPrompt": {
|
||||
"description": "Starter prompts for the plugin. Capped at 3 entries with a maximum of 128 characters per entry.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"developerName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"logo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoUrl": {
|
||||
"description": "Remote logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"privacyPolicyUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"screenshotUrls": {
|
||||
"description": "Remote screenshot URLs from the plugin catalog.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"screenshots": {
|
||||
"description": "Local screenshot paths, resolved from the installed plugin package.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"shortDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"termsOfServiceUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"websiteUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"capabilities",
|
||||
"screenshotUrls",
|
||||
"screenshots"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareListItem": {
|
||||
"properties": {
|
||||
"localPluginPath": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugin": {
|
||||
"$ref": "#/definitions/PluginSummary"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugin",
|
||||
"shareUrl"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"local"
|
||||
],
|
||||
"title": "LocalPluginSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "LocalPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"refName": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sha": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"git"
|
||||
],
|
||||
"title": "GitPluginSourceType",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"title": "GitPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote"
|
||||
],
|
||||
"title": "RemotePluginSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "RemotePluginSource",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginSummary": {
|
||||
"properties": {
|
||||
"authPolicy": {
|
||||
"$ref": "#/definitions/PluginAuthPolicy"
|
||||
},
|
||||
"availability": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginAvailability"
|
||||
}
|
||||
],
|
||||
"default": "AVAILABLE",
|
||||
"description": "Availability state for installing and using the plugin."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"installPolicy": {
|
||||
"$ref": "#/definitions/PluginInstallPolicy"
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"interface": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginInterface"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"authPolicy",
|
||||
"enabled",
|
||||
"id",
|
||||
"installPolicy",
|
||||
"installed",
|
||||
"name",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginShareListItem"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PluginShareListResponse",
|
||||
"type": "object"
|
||||
}
|
||||
25
codex-rs/app-server-protocol/schema/json/v2/PluginShareSaveParams.json
generated
Normal file
25
codex-rs/app-server-protocol/schema/json/v2/PluginShareSaveParams.json
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"pluginPath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pluginPath"
|
||||
],
|
||||
"title": "PluginShareSaveParams",
|
||||
"type": "object"
|
||||
}
|
||||
17
codex-rs/app-server-protocol/schema/json/v2/PluginShareSaveResponse.json
generated
Normal file
17
codex-rs/app-server-protocol/schema/json/v2/PluginShareSaveResponse.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId",
|
||||
"shareUrl"
|
||||
],
|
||||
"title": "PluginShareSaveResponse",
|
||||
"type": "object"
|
||||
}
|
||||
21
codex-rs/app-server-protocol/schema/json/v2/PluginSkillReadParams.json
generated
Normal file
21
codex-rs/app-server-protocol/schema/json/v2/PluginSkillReadParams.json
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remoteMarketplaceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"skillName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remoteMarketplaceName",
|
||||
"remotePluginId",
|
||||
"skillName"
|
||||
],
|
||||
"title": "PluginSkillReadParams",
|
||||
"type": "object"
|
||||
}
|
||||
13
codex-rs/app-server-protocol/schema/json/v2/PluginSkillReadResponse.json
generated
Normal file
13
codex-rs/app-server-protocol/schema/json/v2/PluginSkillReadResponse.json
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"contents": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginSkillReadResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -64,26 +64,19 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FileSystemPath": {
|
||||
"PermissionProfileModificationParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"path"
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "PathFileSystemPathType",
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -91,304 +84,45 @@
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "PathFileSystemPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"glob_pattern"
|
||||
],
|
||||
"title": "GlobPatternFileSystemPathType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern",
|
||||
"type"
|
||||
],
|
||||
"title": "GlobPatternFileSystemPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"special"
|
||||
],
|
||||
"title": "SpecialFileSystemPathType",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/definitions/FileSystemSpecialPath"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"value"
|
||||
],
|
||||
"title": "SpecialFileSystemPath",
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FileSystemSandboxEntry": {
|
||||
"properties": {
|
||||
"access": {
|
||||
"$ref": "#/definitions/FileSystemAccessMode"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/FileSystemPath"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"access",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemSpecialPath": {
|
||||
"PermissionProfileSelectionParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"root"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "RootFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"minimal"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "MinimalFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"project_roots"
|
||||
],
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "KindFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"tmpdir"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "TmpdirFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"slash_tmp"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "SlashTmpFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"entries": {
|
||||
"modifications": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
"$ref": "#/definitions/PermissionProfileModificationParams"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
"profile"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"title": "ProfilePermissionProfileSelectionParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"title": "ProfilePermissionProfileSelectionParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SandboxMode": {
|
||||
"enum": [
|
||||
"read-only",
|
||||
@@ -456,10 +190,6 @@
|
||||
"ephemeral": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"excludeTurns": {
|
||||
"description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"description": "Configuration overrides for the forked thread, if any.",
|
||||
"type": [
|
||||
|
||||
@@ -5,6 +5,59 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"ActivePermissionProfile": {
|
||||
"properties": {
|
||||
"extends": {
|
||||
"default": null,
|
||||
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2501,7 +2554,7 @@
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
}
|
||||
],
|
||||
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"realtimeSessionId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
|
||||
@@ -138,202 +138,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FileSystemPath": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"path"
|
||||
],
|
||||
"title": "PathFileSystemPathType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "PathFileSystemPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"glob_pattern"
|
||||
],
|
||||
"title": "GlobPatternFileSystemPathType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern",
|
||||
"type"
|
||||
],
|
||||
"title": "GlobPatternFileSystemPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"special"
|
||||
],
|
||||
"title": "SpecialFileSystemPathType",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/definitions/FileSystemSpecialPath"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"value"
|
||||
],
|
||||
"title": "SpecialFileSystemPath",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FileSystemSandboxEntry": {
|
||||
"properties": {
|
||||
"access": {
|
||||
"$ref": "#/definitions/FileSystemAccessMode"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/FileSystemPath"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"access",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemSpecialPath": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"root"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "RootFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"minimal"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "MinimalFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"project_roots"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "KindFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"tmpdir"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "TmpdirFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"slash_tmp"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "SlashTmpFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FunctionCallOutputBody": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -494,135 +298,65 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"PermissionProfileModificationParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"PermissionProfileSelectionParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"modifications": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionProfileModificationParams"
|
||||
},
|
||||
"type": [
|
||||
"integer",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
"profile"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"title": "ProfilePermissionProfileSelectionParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"title": "ProfilePermissionProfileSelectionParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
@@ -1311,10 +1045,6 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"excludeTurns": {
|
||||
"description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"description": "Configuration overrides for the resumed thread, if any.",
|
||||
"type": [
|
||||
|
||||
@@ -5,6 +5,59 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"ActivePermissionProfile": {
|
||||
"properties": {
|
||||
"extends": {
|
||||
"default": null,
|
||||
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2501,7 +2554,7 @@
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
}
|
||||
],
|
||||
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -90,26 +90,19 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FileSystemPath": {
|
||||
"PermissionProfileModificationParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"path"
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "PathFileSystemPathType",
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -117,304 +110,45 @@
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "PathFileSystemPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"glob_pattern"
|
||||
],
|
||||
"title": "GlobPatternFileSystemPathType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern",
|
||||
"type"
|
||||
],
|
||||
"title": "GlobPatternFileSystemPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"special"
|
||||
],
|
||||
"title": "SpecialFileSystemPathType",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/definitions/FileSystemSpecialPath"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"value"
|
||||
],
|
||||
"title": "SpecialFileSystemPath",
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FileSystemSandboxEntry": {
|
||||
"properties": {
|
||||
"access": {
|
||||
"$ref": "#/definitions/FileSystemAccessMode"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/FileSystemPath"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"access",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemSpecialPath": {
|
||||
"PermissionProfileSelectionParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"root"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "RootFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"minimal"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "MinimalFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"project_roots"
|
||||
],
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "KindFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"tmpdir"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "TmpdirFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"slash_tmp"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "SlashTmpFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"entries": {
|
||||
"modifications": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
"$ref": "#/definitions/PermissionProfileModificationParams"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
"profile"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"title": "ProfilePermissionProfileSelectionParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"title": "ProfilePermissionProfileSelectionParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
|
||||
@@ -5,6 +5,59 @@
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"ActivePermissionProfile": {
|
||||
"properties": {
|
||||
"extends": {
|
||||
"default": null,
|
||||
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2501,7 +2554,7 @@
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
}
|
||||
],
|
||||
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"SortDirection": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"description": "Opaque cursor to pass to the next call to continue after the last turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"description": "Optional turn page size.",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sortDirection": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SortDirection"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional turn pagination direction; defaults to descending."
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadTurnsListParams",
|
||||
"type": "object"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -99,202 +99,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FileSystemPath": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"path"
|
||||
],
|
||||
"title": "PathFileSystemPathType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "PathFileSystemPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"glob_pattern"
|
||||
],
|
||||
"title": "GlobPatternFileSystemPathType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern",
|
||||
"type"
|
||||
],
|
||||
"title": "GlobPatternFileSystemPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"special"
|
||||
],
|
||||
"title": "SpecialFileSystemPathType",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/definitions/FileSystemSpecialPath"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"value"
|
||||
],
|
||||
"title": "SpecialFileSystemPath",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FileSystemSandboxEntry": {
|
||||
"properties": {
|
||||
"access": {
|
||||
"$ref": "#/definitions/FileSystemAccessMode"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/FileSystemPath"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"access",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemSpecialPath": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"root"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "RootFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"minimal"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "MinimalFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"project_roots"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "KindFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"tmpdir"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "TmpdirFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"slash_tmp"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"title": "SlashTmpFileSystemSpecialPath",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
@@ -310,135 +114,65 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionProfile": {
|
||||
"PermissionProfileModificationParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Codex owns sandbox construction for this profile.",
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
|
||||
},
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"managed"
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "ManagedPermissionProfileType",
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fileSystem",
|
||||
"network",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "ManagedPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Do not apply an outer sandbox.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"disabled"
|
||||
],
|
||||
"title": "DisabledPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DisabledPermissionProfile",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Filesystem isolation is enforced by an external caller.",
|
||||
"properties": {
|
||||
"network": {
|
||||
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"external"
|
||||
],
|
||||
"title": "ExternalPermissionProfileType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"network",
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalPermissionProfile",
|
||||
"title": "AdditionalWritableRootPermissionProfileModificationParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileFileSystemPermissions": {
|
||||
"PermissionProfileSelectionParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": "array"
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 1.0,
|
||||
"modifications": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionProfileModificationParams"
|
||||
},
|
||||
"type": [
|
||||
"integer",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"restricted"
|
||||
"profile"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"title": "ProfilePermissionProfileSelectionParamsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entries",
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"title": "RestrictedPermissionProfileFileSystemPermissions",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"unrestricted"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
|
||||
"title": "ProfilePermissionProfileSelectionParams",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionProfileNetworkPermissions": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
|
||||
File diff suppressed because one or more lines are too long
21
codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfile.ts
generated
Normal file
21
codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfile.ts
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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 { ActivePermissionProfileModification } from "./ActivePermissionProfileModification";
|
||||
|
||||
export type ActivePermissionProfile = {
|
||||
/**
|
||||
* Identifier from `default_permissions` or the implicit built-in default,
|
||||
* such as `:workspace` or a user-defined `[permissions.<id>]` profile.
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* Parent profile identifier once permissions profiles support
|
||||
* inheritance. This is currently always `null`.
|
||||
*/
|
||||
extends: string | null,
|
||||
/**
|
||||
* Bounded user-requested modifications applied on top of the named
|
||||
* profile, if any.
|
||||
*/
|
||||
modifications: Array<ActivePermissionProfileModification>, };
|
||||
6
codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfileModification.ts
generated
Normal file
6
codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfileModification.ts
generated
Normal file
@@ -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 { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type ActivePermissionProfileModification = { "type": "additionalWritableRoot", path: AbsolutePathBuf, };
|
||||
@@ -2,4 +2,9 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Deprecated legacy notification for `apply_patch` textual output.
|
||||
*
|
||||
* The server no longer emits this notification.
|
||||
*/
|
||||
export type FileChangeOutputDeltaNotification = { threadId: string, turnId: string, itemId: string, delta: string, };
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { HookEventName } from "./HookEventName";
|
||||
import type { HookHandlerType } from "./HookHandlerType";
|
||||
import type { HookSource } from "./HookSource";
|
||||
|
||||
export type HookMetadata = { eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null, sourcePath: AbsolutePathBuf, source: HookSource, pluginId: string | null, displayOrder: bigint, };
|
||||
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null, sourcePath: AbsolutePathBuf, source: HookSource, pluginId: string | null, displayOrder: bigint, enabled: boolean, isManaged: boolean, };
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown";
|
||||
export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "cloudRequirements" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown";
|
||||
|
||||
6
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileModificationParams.ts
generated
Normal file
6
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileModificationParams.ts
generated
Normal file
@@ -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 { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type PermissionProfileModificationParams = { "type": "additionalWritableRoot", path: AbsolutePathBuf, };
|
||||
6
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSelectionParams.ts
generated
Normal file
6
codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSelectionParams.ts
generated
Normal file
@@ -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 { PermissionProfileModificationParams } from "./PermissionProfileModificationParams";
|
||||
|
||||
export type PermissionProfileSelectionParams = { "type": "profile", id: string, modifications?: Array<PermissionProfileModificationParams> | null, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginAvailability.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginAvailability.ts
generated
Normal file
@@ -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 PluginAvailability = "AVAILABLE" | "DISABLED_BY_ADMIN";
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareDeleteParams.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareDeleteParams.ts
generated
Normal file
@@ -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 PluginShareDeleteParams = { remotePluginId: string, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareDeleteResponse.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareDeleteResponse.ts
generated
Normal file
@@ -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 PluginShareDeleteResponse = Record<string, never>;
|
||||
7
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareListItem.ts
generated
Normal file
7
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareListItem.ts
generated
Normal file
@@ -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 { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { PluginSummary } from "./PluginSummary";
|
||||
|
||||
export type PluginShareListItem = { plugin: PluginSummary, shareUrl: string, localPluginPath: AbsolutePathBuf | null, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareListParams.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareListParams.ts
generated
Normal file
@@ -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 PluginShareListParams = Record<string, never>;
|
||||
6
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareListResponse.ts
generated
Normal file
6
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareListResponse.ts
generated
Normal file
@@ -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 { PluginShareListItem } from "./PluginShareListItem";
|
||||
|
||||
export type PluginShareListResponse = { data: Array<PluginShareListItem>, };
|
||||
6
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareSaveParams.ts
generated
Normal file
6
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareSaveParams.ts
generated
Normal file
@@ -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 { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type PluginShareSaveParams = { pluginPath: AbsolutePathBuf, remotePluginId?: string | null, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareSaveResponse.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginShareSaveResponse.ts
generated
Normal file
@@ -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 PluginShareSaveResponse = { remotePluginId: string, shareUrl: string, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginSkillReadParams.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginSkillReadParams.ts
generated
Normal file
@@ -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 PluginSkillReadParams = { remoteMarketplaceName: string, remotePluginId: string, skillName: string, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginSkillReadResponse.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/PluginSkillReadResponse.ts
generated
Normal file
@@ -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 PluginSkillReadResponse = { contents: string | null, };
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginAuthPolicy } from "./PluginAuthPolicy";
|
||||
import type { PluginAvailability } from "./PluginAvailability";
|
||||
import type { PluginInstallPolicy } from "./PluginInstallPolicy";
|
||||
import type { PluginInterface } from "./PluginInterface";
|
||||
import type { PluginSource } from "./PluginSource";
|
||||
|
||||
export type PluginSummary = { id: string, name: string, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, authPolicy: PluginAuthPolicy, interface: PluginInterface | null, };
|
||||
export type PluginSummary = { id: string, name: string, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, authPolicy: PluginAuthPolicy,
|
||||
/**
|
||||
* Availability state for installing and using the plugin.
|
||||
*/
|
||||
availability: PluginAvailability, interface: PluginInterface | null, };
|
||||
|
||||
@@ -23,9 +23,4 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /**
|
||||
* When true, return only thread metadata and live fork state without
|
||||
* populating `thread.turns`. This is useful when the client plans to call
|
||||
* `thread/turns/list` immediately after forking.
|
||||
*/
|
||||
excludeTurns?: boolean};
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean};
|
||||
|
||||
@@ -16,8 +16,8 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
* Reviewer currently used for approval requests on this thread.
|
||||
*/
|
||||
approvalsReviewer: ApprovalsReviewer, /**
|
||||
* Legacy sandbox policy retained for compatibility. New clients should use
|
||||
* `permissionProfile` when present as the canonical active permissions
|
||||
* view.
|
||||
* Legacy sandbox policy retained for compatibility. Experimental clients
|
||||
* should prefer `permissionProfile` when they need exact runtime
|
||||
* permissions.
|
||||
*/
|
||||
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { RealtimeConversationVersion } from "../RealtimeConversationVersion
|
||||
/**
|
||||
* EXPERIMENTAL - emitted when thread realtime startup is accepted.
|
||||
*/
|
||||
export type ThreadRealtimeStartedNotification = { threadId: string, sessionId: string | null, version: RealtimeConversationVersion, };
|
||||
export type ThreadRealtimeStartedNotification = { threadId: string, realtimeSessionId: string | null, version: RealtimeConversationVersion, };
|
||||
|
||||
@@ -26,9 +26,4 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /**
|
||||
* When true, return only thread metadata and live-resume state without
|
||||
* populating `thread.turns`. This is useful when the client plans to call
|
||||
* `thread/turns/list` immediately after resuming.
|
||||
*/
|
||||
excludeTurns?: boolean};
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null};
|
||||
|
||||
@@ -16,8 +16,8 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
* Reviewer currently used for approval requests on this thread.
|
||||
*/
|
||||
approvalsReviewer: ApprovalsReviewer, /**
|
||||
* Legacy sandbox policy retained for compatibility. New clients should use
|
||||
* `permissionProfile` when present as the canonical active permissions
|
||||
* view.
|
||||
* Legacy sandbox policy retained for compatibility. Experimental clients
|
||||
* should prefer `permissionProfile` when they need exact runtime
|
||||
* permissions.
|
||||
*/
|
||||
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
|
||||
|
||||
@@ -16,8 +16,8 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
* Reviewer currently used for approval requests on this thread.
|
||||
*/
|
||||
approvalsReviewer: ApprovalsReviewer, /**
|
||||
* Legacy sandbox policy retained for compatibility. New clients should use
|
||||
* `permissionProfile` when present as the canonical active permissions
|
||||
* view.
|
||||
* Legacy sandbox policy retained for compatibility. Experimental clients
|
||||
* should prefer `permissionProfile` when they need exact runtime
|
||||
* permissions.
|
||||
*/
|
||||
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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 { SortDirection } from "./SortDirection";
|
||||
|
||||
export type ThreadTurnsListParams = { threadId: string,
|
||||
/**
|
||||
* Opaque cursor to pass to the next call to continue after the last turn.
|
||||
*/
|
||||
cursor?: string | null,
|
||||
/**
|
||||
* Optional turn page size.
|
||||
*/
|
||||
limit?: number | null,
|
||||
/**
|
||||
* Optional turn pagination direction; defaults to descending.
|
||||
*/
|
||||
sortDirection?: SortDirection | null, };
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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 { Turn } from "./Turn";
|
||||
|
||||
export type ThreadTurnsListResponse = { data: Array<Turn>,
|
||||
/**
|
||||
* Opaque cursor to pass to the next call to continue after the last turn.
|
||||
* if None, there are no more turns to return.
|
||||
*/
|
||||
nextCursor: string | null,
|
||||
/**
|
||||
* Opaque cursor to pass as `cursor` when reversing `sortDirection`.
|
||||
* This is only populated when the page contains at least one turn.
|
||||
* Use it with the opposite `sortDirection` to include the anchor turn again
|
||||
* and catch updates to that turn.
|
||||
*/
|
||||
backwardsCursor: string | null, };
|
||||
@@ -4,6 +4,8 @@ export type { Account } from "./Account";
|
||||
export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification";
|
||||
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
|
||||
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { ActivePermissionProfile } from "./ActivePermissionProfile";
|
||||
export type { ActivePermissionProfileModification } from "./ActivePermissionProfileModification";
|
||||
export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType";
|
||||
export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus";
|
||||
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
|
||||
@@ -261,11 +263,14 @@ export type { PatchChangeKind } from "./PatchChangeKind";
|
||||
export type { PermissionGrantScope } from "./PermissionGrantScope";
|
||||
export type { PermissionProfile } from "./PermissionProfile";
|
||||
export type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions";
|
||||
export type { PermissionProfileModificationParams } from "./PermissionProfileModificationParams";
|
||||
export type { PermissionProfileNetworkPermissions } from "./PermissionProfileNetworkPermissions";
|
||||
export type { PermissionProfileSelectionParams } from "./PermissionProfileSelectionParams";
|
||||
export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams";
|
||||
export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse";
|
||||
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
|
||||
export type { PluginAuthPolicy } from "./PluginAuthPolicy";
|
||||
export type { PluginAvailability } from "./PluginAvailability";
|
||||
export type { PluginDetail } from "./PluginDetail";
|
||||
export type { PluginInstallParams } from "./PluginInstallParams";
|
||||
export type { PluginInstallPolicy } from "./PluginInstallPolicy";
|
||||
@@ -276,6 +281,15 @@ export type { PluginListResponse } from "./PluginListResponse";
|
||||
export type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry";
|
||||
export type { PluginReadParams } from "./PluginReadParams";
|
||||
export type { PluginReadResponse } from "./PluginReadResponse";
|
||||
export type { PluginShareDeleteParams } from "./PluginShareDeleteParams";
|
||||
export type { PluginShareDeleteResponse } from "./PluginShareDeleteResponse";
|
||||
export type { PluginShareListItem } from "./PluginShareListItem";
|
||||
export type { PluginShareListParams } from "./PluginShareListParams";
|
||||
export type { PluginShareListResponse } from "./PluginShareListResponse";
|
||||
export type { PluginShareSaveParams } from "./PluginShareSaveParams";
|
||||
export type { PluginShareSaveResponse } from "./PluginShareSaveResponse";
|
||||
export type { PluginSkillReadParams } from "./PluginSkillReadParams";
|
||||
export type { PluginSkillReadResponse } from "./PluginSkillReadResponse";
|
||||
export type { PluginSource } from "./PluginSource";
|
||||
export type { PluginSummary } from "./PluginSummary";
|
||||
export type { PluginUninstallParams } from "./PluginUninstallParams";
|
||||
@@ -385,8 +399,6 @@ export type { ThreadStatus } from "./ThreadStatus";
|
||||
export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification";
|
||||
export type { ThreadTokenUsage } from "./ThreadTokenUsage";
|
||||
export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification";
|
||||
export type { ThreadTurnsListParams } from "./ThreadTurnsListParams";
|
||||
export type { ThreadTurnsListResponse } from "./ThreadTurnsListResponse";
|
||||
export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams";
|
||||
export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse";
|
||||
export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification";
|
||||
|
||||
@@ -14,6 +14,7 @@ pub use export::generate_ts_with_options;
|
||||
pub use export::generate_types;
|
||||
pub use jsonrpc_lite::*;
|
||||
pub use protocol::common::*;
|
||||
pub use protocol::event_mapping::*;
|
||||
pub use protocol::item_builders::*;
|
||||
pub use protocol::thread_history::*;
|
||||
pub use protocol::v1::ApplyPatchApprovalParams;
|
||||
|
||||
@@ -564,6 +564,7 @@ client_request_definitions! {
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadReadResponse,
|
||||
},
|
||||
#[experimental("thread/turns/list")]
|
||||
ThreadTurnsList => "thread/turns/list" {
|
||||
params: v2::ThreadTurnsListParams,
|
||||
// Explicitly concurrent: this primarily reads append-only rollout storage.
|
||||
@@ -611,6 +612,26 @@ client_request_definitions! {
|
||||
serialization: global("config"),
|
||||
response: v2::PluginReadResponse,
|
||||
},
|
||||
PluginSkillRead => "plugin/skill/read" {
|
||||
params: v2::PluginSkillReadParams,
|
||||
serialization: global("config"),
|
||||
response: v2::PluginSkillReadResponse,
|
||||
},
|
||||
PluginShareSave => "plugin/share/save" {
|
||||
params: v2::PluginShareSaveParams,
|
||||
serialization: global("config"),
|
||||
response: v2::PluginShareSaveResponse,
|
||||
},
|
||||
PluginShareList => "plugin/share/list" {
|
||||
params: v2::PluginShareListParams,
|
||||
serialization: global("config"),
|
||||
response: v2::PluginShareListResponse,
|
||||
},
|
||||
PluginShareDelete => "plugin/share/delete" {
|
||||
params: v2::PluginShareDeleteParams,
|
||||
serialization: global("config"),
|
||||
response: v2::PluginShareDeleteResponse,
|
||||
},
|
||||
AppsList => "app/list" {
|
||||
params: v2::AppsListParams,
|
||||
serialization: None,
|
||||
@@ -1382,6 +1403,7 @@ server_notification_definitions! {
|
||||
CommandExecOutputDelta => "command/exec/outputDelta" (v2::CommandExecOutputDeltaNotification),
|
||||
CommandExecutionOutputDelta => "item/commandExecution/outputDelta" (v2::CommandExecutionOutputDeltaNotification),
|
||||
TerminalInteraction => "item/commandExecution/terminalInteraction" (v2::TerminalInteractionNotification),
|
||||
/// Deprecated legacy apply_patch output stream notification.
|
||||
FileChangeOutputDelta => "item/fileChange/outputDelta" (v2::FileChangeOutputDeltaNotification),
|
||||
FileChangePatchUpdated => "item/fileChange/patchUpdated" (v2::FileChangePatchUpdatedNotification),
|
||||
ServerRequestResolved => "serverRequest/resolved" (v2::ServerRequestResolvedNotification),
|
||||
@@ -2131,12 +2153,8 @@ mod tests {
|
||||
approval_policy: v2::AskForApproval::OnFailure,
|
||||
approvals_reviewer: v2::ApprovalsReviewer::User,
|
||||
sandbox: v2::SandboxPolicy::DangerFullAccess,
|
||||
permission_profile: Some(
|
||||
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
permission_profile: None,
|
||||
active_permission_profile: None,
|
||||
reasoning_effort: None,
|
||||
},
|
||||
};
|
||||
@@ -2179,9 +2197,8 @@ mod tests {
|
||||
"sandbox": {
|
||||
"type": "dangerFullAccess"
|
||||
},
|
||||
"permissionProfile": {
|
||||
"type": "disabled"
|
||||
},
|
||||
"permissionProfile": null,
|
||||
"activePermissionProfile": null,
|
||||
"reasoningEffort": null
|
||||
}
|
||||
}),
|
||||
@@ -2548,7 +2565,7 @@ mod tests {
|
||||
thread_id: "thr_123".to_string(),
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
prompt: Some(Some("You are on a call".to_string())),
|
||||
session_id: Some("sess_456".to_string()),
|
||||
realtime_session_id: Some("sess_456".to_string()),
|
||||
transport: None,
|
||||
voice: Some(RealtimeVoice::Marin),
|
||||
},
|
||||
@@ -2561,7 +2578,7 @@ mod tests {
|
||||
"threadId": "thr_123",
|
||||
"outputModality": "audio",
|
||||
"prompt": "You are on a call",
|
||||
"sessionId": "sess_456",
|
||||
"realtimeSessionId": "sess_456",
|
||||
"transport": null,
|
||||
"voice": "marin"
|
||||
}
|
||||
@@ -2579,7 +2596,7 @@ mod tests {
|
||||
thread_id: "thr_123".to_string(),
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
prompt: None,
|
||||
session_id: None,
|
||||
realtime_session_id: None,
|
||||
transport: None,
|
||||
voice: None,
|
||||
},
|
||||
@@ -2591,7 +2608,7 @@ mod tests {
|
||||
"params": {
|
||||
"threadId": "thr_123",
|
||||
"outputModality": "audio",
|
||||
"sessionId": null,
|
||||
"realtimeSessionId": null,
|
||||
"transport": null,
|
||||
"voice": null
|
||||
}
|
||||
@@ -2605,7 +2622,7 @@ mod tests {
|
||||
thread_id: "thr_123".to_string(),
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
prompt: Some(None),
|
||||
session_id: None,
|
||||
realtime_session_id: None,
|
||||
transport: None,
|
||||
voice: None,
|
||||
},
|
||||
@@ -2618,7 +2635,7 @@ mod tests {
|
||||
"threadId": "thr_123",
|
||||
"outputModality": "audio",
|
||||
"prompt": null,
|
||||
"sessionId": null,
|
||||
"realtimeSessionId": null,
|
||||
"transport": null,
|
||||
"voice": null
|
||||
}
|
||||
@@ -2632,7 +2649,7 @@ mod tests {
|
||||
"params": {
|
||||
"threadId": "thr_123",
|
||||
"outputModality": "audio",
|
||||
"sessionId": null,
|
||||
"realtimeSessionId": null,
|
||||
"transport": null,
|
||||
"voice": null
|
||||
}
|
||||
@@ -2649,7 +2666,7 @@ mod tests {
|
||||
"threadId": "thr_123",
|
||||
"outputModality": "audio",
|
||||
"prompt": null,
|
||||
"sessionId": null,
|
||||
"realtimeSessionId": null,
|
||||
"transport": null,
|
||||
"voice": null
|
||||
}
|
||||
@@ -2761,7 +2778,7 @@ mod tests {
|
||||
thread_id: "thr_123".to_string(),
|
||||
output_modality: RealtimeOutputModality::Audio,
|
||||
prompt: Some(Some("You are on a call".to_string())),
|
||||
session_id: None,
|
||||
realtime_session_id: None,
|
||||
transport: None,
|
||||
voice: None,
|
||||
},
|
||||
@@ -2844,7 +2861,7 @@ mod tests {
|
||||
let notification =
|
||||
ServerNotification::ThreadRealtimeStarted(v2::ThreadRealtimeStartedNotification {
|
||||
thread_id: "thr_123".to_string(),
|
||||
session_id: Some("sess_456".to_string()),
|
||||
realtime_session_id: Some("sess_456".to_string()),
|
||||
version: RealtimeConversationVersion::V1,
|
||||
});
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¬ification);
|
||||
|
||||
819
codex-rs/app-server-protocol/src/protocol/event_mapping.rs
Normal file
819
codex-rs/app-server-protocol/src/protocol/event_mapping.rs
Normal file
@@ -0,0 +1,819 @@
|
||||
use crate::protocol::common::ServerNotification;
|
||||
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::convert_patch_changes;
|
||||
use crate::protocol::v2::AgentMessageDeltaNotification;
|
||||
use crate::protocol::v2::CollabAgentState;
|
||||
use crate::protocol::v2::CollabAgentTool;
|
||||
use crate::protocol::v2::CollabAgentToolCallStatus;
|
||||
use crate::protocol::v2::CommandExecutionOutputDeltaNotification;
|
||||
use crate::protocol::v2::DynamicToolCallOutputContentItem;
|
||||
use crate::protocol::v2::DynamicToolCallStatus;
|
||||
use crate::protocol::v2::FileChangePatchUpdatedNotification;
|
||||
use crate::protocol::v2::ItemCompletedNotification;
|
||||
use crate::protocol::v2::ItemStartedNotification;
|
||||
use crate::protocol::v2::McpToolCallError;
|
||||
use crate::protocol::v2::McpToolCallResult;
|
||||
use crate::protocol::v2::McpToolCallStatus;
|
||||
use crate::protocol::v2::PlanDeltaNotification;
|
||||
use crate::protocol::v2::ReasoningSummaryPartAddedNotification;
|
||||
use crate::protocol::v2::ReasoningSummaryTextDeltaNotification;
|
||||
use crate::protocol::v2::ReasoningTextDeltaNotification;
|
||||
use crate::protocol::v2::TerminalInteractionNotification;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Build the v2 app-server notification that directly corresponds to a single core event.
|
||||
///
|
||||
/// This only covers the stateless event-to-notification projections that have a one-to-one
|
||||
/// mapping. Callers remain responsible for any surrounding state checks or side effects before
|
||||
/// invoking this helper.
|
||||
pub fn item_event_to_server_notification(
|
||||
msg: EventMsg,
|
||||
thread_id: &str,
|
||||
turn_id: &str,
|
||||
) -> ServerNotification {
|
||||
let thread_id = thread_id.to_string();
|
||||
let turn_id = turn_id.to_string();
|
||||
match msg {
|
||||
EventMsg::DynamicToolCallResponse(response) => {
|
||||
let status = if response.success {
|
||||
DynamicToolCallStatus::Completed
|
||||
} else {
|
||||
DynamicToolCallStatus::Failed
|
||||
};
|
||||
let duration_ms = i64::try_from(response.duration.as_millis()).ok();
|
||||
let item = ThreadItem::DynamicToolCall {
|
||||
id: response.call_id,
|
||||
namespace: response.namespace,
|
||||
tool: response.tool,
|
||||
arguments: response.arguments,
|
||||
status,
|
||||
content_items: Some(
|
||||
response
|
||||
.content_items
|
||||
.into_iter()
|
||||
.map(|item| match item {
|
||||
CoreDynamicToolCallOutputContentItem::InputText { text } => {
|
||||
DynamicToolCallOutputContentItem::InputText { text }
|
||||
}
|
||||
CoreDynamicToolCallOutputContentItem::InputImage { image_url } => {
|
||||
DynamicToolCallOutputContentItem::InputImage { image_url }
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
success: Some(response.success),
|
||||
duration_ms,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id: response.turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::McpToolCallBegin(begin_event) => {
|
||||
let item = ThreadItem::McpToolCall {
|
||||
id: begin_event.call_id,
|
||||
server: begin_event.invocation.server,
|
||||
tool: begin_event.invocation.tool,
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: begin_event.invocation.arguments.unwrap_or(JsonValue::Null),
|
||||
mcp_app_resource_uri: begin_event.mcp_app_resource_uri,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::McpToolCallEnd(end_event) => {
|
||||
let status = if end_event.is_success() {
|
||||
McpToolCallStatus::Completed
|
||||
} else {
|
||||
McpToolCallStatus::Failed
|
||||
};
|
||||
let duration_ms = i64::try_from(end_event.duration.as_millis()).ok();
|
||||
let (result, error) = match &end_event.result {
|
||||
Ok(value) => (
|
||||
Some(Box::new(McpToolCallResult {
|
||||
content: value.content.clone(),
|
||||
structured_content: value.structured_content.clone(),
|
||||
meta: value.meta.clone(),
|
||||
})),
|
||||
None,
|
||||
),
|
||||
Err(message) => (
|
||||
None,
|
||||
Some(McpToolCallError {
|
||||
message: message.clone(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
let item = ThreadItem::McpToolCall {
|
||||
id: end_event.call_id,
|
||||
server: end_event.invocation.server,
|
||||
tool: end_event.invocation.tool,
|
||||
status,
|
||||
arguments: end_event.invocation.arguments.unwrap_or(JsonValue::Null),
|
||||
mcp_app_resource_uri: end_event.mcp_app_resource_uri,
|
||||
result,
|
||||
error,
|
||||
duration_ms,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabAgentSpawnBegin(begin_event) => {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: begin_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: Vec::new(),
|
||||
prompt: Some(begin_event.prompt),
|
||||
model: Some(begin_event.model),
|
||||
reasoning_effort: Some(begin_event.reasoning_effort),
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabAgentSpawnEnd(end_event) => {
|
||||
let has_receiver = end_event.new_thread_id.is_some();
|
||||
let status = match &end_event.status {
|
||||
codex_protocol::protocol::AgentStatus::Errored(_)
|
||||
| codex_protocol::protocol::AgentStatus::NotFound => {
|
||||
CollabAgentToolCallStatus::Failed
|
||||
}
|
||||
_ if has_receiver => CollabAgentToolCallStatus::Completed,
|
||||
_ => CollabAgentToolCallStatus::Failed,
|
||||
};
|
||||
let (receiver_thread_ids, agents_states) = match end_event.new_thread_id {
|
||||
Some(id) => {
|
||||
let receiver_id = id.to_string();
|
||||
let received_status = CollabAgentState::from(end_event.status.clone());
|
||||
(
|
||||
vec![receiver_id.clone()],
|
||||
[(receiver_id, received_status)].into_iter().collect(),
|
||||
)
|
||||
}
|
||||
None => (Vec::new(), HashMap::new()),
|
||||
};
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status,
|
||||
sender_thread_id: end_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids,
|
||||
prompt: Some(end_event.prompt),
|
||||
model: Some(end_event.model),
|
||||
reasoning_effort: Some(end_event.reasoning_effort),
|
||||
agents_states,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabAgentInteractionBegin(begin_event) => {
|
||||
let receiver_thread_ids = vec![begin_event.receiver_thread_id.to_string()];
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::SendInput,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: begin_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids,
|
||||
prompt: Some(begin_event.prompt),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabAgentInteractionEnd(end_event) => {
|
||||
let status = match &end_event.status {
|
||||
codex_protocol::protocol::AgentStatus::Errored(_)
|
||||
| codex_protocol::protocol::AgentStatus::NotFound => {
|
||||
CollabAgentToolCallStatus::Failed
|
||||
}
|
||||
_ => CollabAgentToolCallStatus::Completed,
|
||||
};
|
||||
let receiver_id = end_event.receiver_thread_id.to_string();
|
||||
let received_status = CollabAgentState::from(end_event.status);
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::SendInput,
|
||||
status,
|
||||
sender_thread_id: end_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id.clone()],
|
||||
prompt: Some(end_event.prompt),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states: [(receiver_id, received_status)].into_iter().collect(),
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabWaitingBegin(begin_event) => {
|
||||
let receiver_thread_ids = begin_event
|
||||
.receiver_thread_ids
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::Wait,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: begin_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids,
|
||||
prompt: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabWaitingEnd(end_event) => {
|
||||
let status = if end_event.statuses.values().any(|status| {
|
||||
matches!(
|
||||
status,
|
||||
codex_protocol::protocol::AgentStatus::Errored(_)
|
||||
| codex_protocol::protocol::AgentStatus::NotFound
|
||||
)
|
||||
}) {
|
||||
CollabAgentToolCallStatus::Failed
|
||||
} else {
|
||||
CollabAgentToolCallStatus::Completed
|
||||
};
|
||||
let receiver_thread_ids = end_event.statuses.keys().map(ToString::to_string).collect();
|
||||
let agents_states = end_event
|
||||
.statuses
|
||||
.iter()
|
||||
.map(|(id, status)| (id.to_string(), CollabAgentState::from(status.clone())))
|
||||
.collect();
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::Wait,
|
||||
status,
|
||||
sender_thread_id: end_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids,
|
||||
prompt: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabCloseBegin(begin_event) => {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::CloseAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: begin_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![begin_event.receiver_thread_id.to_string()],
|
||||
prompt: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabCloseEnd(end_event) => {
|
||||
let status = match &end_event.status {
|
||||
codex_protocol::protocol::AgentStatus::Errored(_)
|
||||
| codex_protocol::protocol::AgentStatus::NotFound => {
|
||||
CollabAgentToolCallStatus::Failed
|
||||
}
|
||||
_ => CollabAgentToolCallStatus::Completed,
|
||||
};
|
||||
let receiver_id = end_event.receiver_thread_id.to_string();
|
||||
let agents_states = [(
|
||||
receiver_id.clone(),
|
||||
CollabAgentState::from(end_event.status),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::CloseAgent,
|
||||
status,
|
||||
sender_thread_id: end_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id],
|
||||
prompt: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabResumeBegin(begin_event) => {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: begin_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![begin_event.receiver_thread_id.to_string()],
|
||||
prompt: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::CollabResumeEnd(end_event) => {
|
||||
let status = match &end_event.status {
|
||||
codex_protocol::protocol::AgentStatus::Errored(_)
|
||||
| codex_protocol::protocol::AgentStatus::NotFound => {
|
||||
CollabAgentToolCallStatus::Failed
|
||||
}
|
||||
_ => CollabAgentToolCallStatus::Completed,
|
||||
};
|
||||
let receiver_id = end_event.receiver_thread_id.to_string();
|
||||
let agents_states = [(
|
||||
receiver_id.clone(),
|
||||
CollabAgentState::from(end_event.status),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status,
|
||||
sender_thread_id: end_event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id],
|
||||
prompt: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
})
|
||||
}
|
||||
EventMsg::AgentMessageContentDelta(event) => {
|
||||
let codex_protocol::protocol::AgentMessageContentDeltaEvent { item_id, delta, .. } =
|
||||
event;
|
||||
ServerNotification::AgentMessageDelta(AgentMessageDeltaNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item_id,
|
||||
delta,
|
||||
})
|
||||
}
|
||||
EventMsg::PlanDelta(event) => ServerNotification::PlanDelta(PlanDeltaNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item_id: event.item_id,
|
||||
delta: event.delta,
|
||||
}),
|
||||
EventMsg::ReasoningContentDelta(event) => {
|
||||
ServerNotification::ReasoningSummaryTextDelta(ReasoningSummaryTextDeltaNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item_id: event.item_id,
|
||||
delta: event.delta,
|
||||
summary_index: event.summary_index,
|
||||
})
|
||||
}
|
||||
EventMsg::ReasoningRawContentDelta(event) => {
|
||||
ServerNotification::ReasoningTextDelta(ReasoningTextDeltaNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item_id: event.item_id,
|
||||
delta: event.delta,
|
||||
content_index: event.content_index,
|
||||
})
|
||||
}
|
||||
EventMsg::AgentReasoningSectionBreak(event) => {
|
||||
ServerNotification::ReasoningSummaryPartAdded(ReasoningSummaryPartAddedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item_id: event.item_id,
|
||||
summary_index: event.summary_index,
|
||||
})
|
||||
}
|
||||
EventMsg::ItemStarted(item_started_event) => {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item: item_started_event.item.into(),
|
||||
})
|
||||
}
|
||||
EventMsg::ItemCompleted(item_completed_event) => {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item: item_completed_event.item.into(),
|
||||
})
|
||||
}
|
||||
EventMsg::PatchApplyUpdated(event) => {
|
||||
ServerNotification::FileChangePatchUpdated(FileChangePatchUpdatedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item_id: event.call_id,
|
||||
changes: convert_patch_changes(&event.changes),
|
||||
})
|
||||
}
|
||||
EventMsg::ExecCommandBegin(exec_command_begin_event) => {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item: build_command_execution_begin_item(&exec_command_begin_event),
|
||||
})
|
||||
}
|
||||
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
|
||||
let item_id = exec_command_output_delta_event.call_id;
|
||||
let delta = String::from_utf8_lossy(&exec_command_output_delta_event.chunk).to_string();
|
||||
ServerNotification::CommandExecutionOutputDelta(
|
||||
CommandExecutionOutputDeltaNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item_id,
|
||||
delta,
|
||||
},
|
||||
)
|
||||
}
|
||||
EventMsg::TerminalInteraction(terminal_event) => {
|
||||
ServerNotification::TerminalInteraction(TerminalInteractionNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item_id: terminal_event.call_id,
|
||||
process_id: terminal_event.process_id,
|
||||
stdin: terminal_event.stdin,
|
||||
})
|
||||
}
|
||||
EventMsg::ExecCommandEnd(exec_command_end_event) => {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item: build_command_execution_end_item(&exec_command_end_event),
|
||||
})
|
||||
}
|
||||
_ => unreachable!("unsupported item event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::protocol::CollabResumeBeginEvent;
|
||||
use codex_protocol::protocol::CollabResumeEndEvent;
|
||||
use codex_protocol::protocol::ExecCommandOutputDeltaEvent;
|
||||
use codex_protocol::protocol::ExecOutputStream;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpToolCallBeginEvent;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::Content;
|
||||
use std::time::Duration;
|
||||
|
||||
fn assert_item_started_server_notification(
|
||||
notification: ServerNotification,
|
||||
expected: ItemStartedNotification,
|
||||
) {
|
||||
match notification {
|
||||
ServerNotification::ItemStarted(payload) => assert_eq!(payload, expected),
|
||||
other => panic!("expected item started notification, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_item_completed_server_notification(
|
||||
notification: ServerNotification,
|
||||
expected: ItemCompletedNotification,
|
||||
) {
|
||||
match notification {
|
||||
ServerNotification::ItemCompleted(payload) => assert_eq!(payload, expected),
|
||||
other => panic!("expected item completed notification, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_command_execution_output_delta_server_notification(
|
||||
notification: ServerNotification,
|
||||
expected: CommandExecutionOutputDeltaNotification,
|
||||
) {
|
||||
match notification {
|
||||
ServerNotification::CommandExecutionOutputDelta(payload) => {
|
||||
assert_eq!(payload, expected)
|
||||
}
|
||||
other => panic!("expected command execution output delta, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collab_resume_begin_maps_to_item_started_resume_agent() {
|
||||
let event = CollabResumeBeginEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
sender_thread_id: ThreadId::new(),
|
||||
receiver_thread_id: ThreadId::new(),
|
||||
receiver_agent_nickname: None,
|
||||
receiver_agent_role: None,
|
||||
};
|
||||
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::CollabResumeBegin(event.clone()),
|
||||
"thread-1",
|
||||
"turn-1",
|
||||
);
|
||||
assert_item_started_server_notification(
|
||||
notification,
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: ThreadItem::CollabAgentToolCall {
|
||||
id: event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
sender_thread_id: event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![event.receiver_thread_id.to_string()],
|
||||
prompt: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collab_resume_end_maps_to_item_completed_resume_agent() {
|
||||
let event = CollabResumeEndEvent {
|
||||
call_id: "call-2".to_string(),
|
||||
sender_thread_id: ThreadId::new(),
|
||||
receiver_thread_id: ThreadId::new(),
|
||||
receiver_agent_nickname: None,
|
||||
receiver_agent_role: None,
|
||||
status: codex_protocol::protocol::AgentStatus::NotFound,
|
||||
};
|
||||
|
||||
let receiver_id = event.receiver_thread_id.to_string();
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::CollabResumeEnd(event.clone()),
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
);
|
||||
assert_item_completed_server_notification(
|
||||
notification,
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
item: ThreadItem::CollabAgentToolCall {
|
||||
id: event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: CollabAgentToolCallStatus::Failed,
|
||||
sender_thread_id: event.sender_thread_id.to_string(),
|
||||
receiver_thread_ids: vec![receiver_id.clone()],
|
||||
prompt: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states: [(
|
||||
receiver_id,
|
||||
CollabAgentState::from(codex_protocol::protocol::AgentStatus::NotFound),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_begin_maps_to_item_started_notification_with_args() {
|
||||
let begin_event = McpToolCallBeginEvent {
|
||||
call_id: "call_123".to_string(),
|
||||
invocation: McpInvocation {
|
||||
server: "codex".to_string(),
|
||||
tool: "list_mcp_resources".to_string(),
|
||||
arguments: Some(serde_json::json!({"server": ""})),
|
||||
},
|
||||
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
|
||||
};
|
||||
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::McpToolCallBegin(begin_event.clone()),
|
||||
"thread-1",
|
||||
"turn_1",
|
||||
);
|
||||
assert_item_started_server_notification(
|
||||
notification,
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn_1".to_string(),
|
||||
item: ThreadItem::McpToolCall {
|
||||
id: begin_event.call_id,
|
||||
server: begin_event.invocation.server,
|
||||
tool: begin_event.invocation.tool,
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: serde_json::json!({"server": ""}),
|
||||
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_begin_maps_to_item_started_notification_without_args() {
|
||||
let begin_event = McpToolCallBeginEvent {
|
||||
call_id: "call_456".to_string(),
|
||||
invocation: McpInvocation {
|
||||
server: "codex".to_string(),
|
||||
tool: "list_mcp_resources".to_string(),
|
||||
arguments: None,
|
||||
},
|
||||
mcp_app_resource_uri: None,
|
||||
};
|
||||
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::McpToolCallBegin(begin_event.clone()),
|
||||
"thread-2",
|
||||
"turn_2",
|
||||
);
|
||||
assert_item_started_server_notification(
|
||||
notification,
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn_2".to_string(),
|
||||
item: ThreadItem::McpToolCall {
|
||||
id: begin_event.call_id,
|
||||
server: begin_event.invocation.server,
|
||||
tool: begin_event.invocation.tool,
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: JsonValue::Null,
|
||||
mcp_app_resource_uri: None,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_end_maps_to_item_completed_notification_on_success() {
|
||||
let content = vec![
|
||||
serde_json::to_value(Content::text("{\"resources\":[]}"))
|
||||
.expect("content should serialize"),
|
||||
];
|
||||
let result = CallToolResult {
|
||||
content: content.clone(),
|
||||
is_error: Some(false),
|
||||
structured_content: None,
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/list-resources.html"
|
||||
})),
|
||||
};
|
||||
|
||||
let end_event = McpToolCallEndEvent {
|
||||
call_id: "call_789".to_string(),
|
||||
invocation: McpInvocation {
|
||||
server: "codex".to_string(),
|
||||
tool: "list_mcp_resources".to_string(),
|
||||
arguments: Some(serde_json::json!({"server": ""})),
|
||||
},
|
||||
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
|
||||
duration: Duration::from_nanos(92708),
|
||||
result: Ok(result),
|
||||
};
|
||||
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::McpToolCallEnd(end_event.clone()),
|
||||
"thread-3",
|
||||
"turn_3",
|
||||
);
|
||||
assert_item_completed_server_notification(
|
||||
notification,
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-3".to_string(),
|
||||
turn_id: "turn_3".to_string(),
|
||||
item: ThreadItem::McpToolCall {
|
||||
id: end_event.call_id,
|
||||
server: end_event.invocation.server,
|
||||
tool: end_event.invocation.tool,
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: serde_json::json!({"server": ""}),
|
||||
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
content,
|
||||
structured_content: None,
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/list-resources.html"
|
||||
})),
|
||||
})),
|
||||
error: None,
|
||||
duration_ms: Some(0),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_end_maps_to_item_completed_notification_on_error() {
|
||||
let end_event = McpToolCallEndEvent {
|
||||
call_id: "call_err".to_string(),
|
||||
invocation: McpInvocation {
|
||||
server: "codex".to_string(),
|
||||
tool: "list_mcp_resources".to_string(),
|
||||
arguments: None,
|
||||
},
|
||||
mcp_app_resource_uri: None,
|
||||
duration: Duration::from_millis(1),
|
||||
result: Err("boom".to_string()),
|
||||
};
|
||||
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::McpToolCallEnd(end_event.clone()),
|
||||
"thread-4",
|
||||
"turn_4",
|
||||
);
|
||||
assert_item_completed_server_notification(
|
||||
notification,
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-4".to_string(),
|
||||
turn_id: "turn_4".to_string(),
|
||||
item: ThreadItem::McpToolCall {
|
||||
id: end_event.call_id,
|
||||
server: end_event.invocation.server,
|
||||
tool: end_event.invocation.tool,
|
||||
status: McpToolCallStatus::Failed,
|
||||
arguments: JsonValue::Null,
|
||||
mcp_app_resource_uri: None,
|
||||
result: None,
|
||||
error: Some(McpToolCallError {
|
||||
message: "boom".to_string(),
|
||||
}),
|
||||
duration_ms: Some(1),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_command_output_delta_maps_to_command_execution_output_delta() {
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::ExecCommandOutputDelta(ExecCommandOutputDeltaEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
stream: ExecOutputStream::Stdout,
|
||||
chunk: b"hello".to_vec(),
|
||||
}),
|
||||
"thread-1",
|
||||
"turn-1",
|
||||
);
|
||||
|
||||
assert_command_execution_output_delta_server_notification(
|
||||
notification,
|
||||
CommandExecutionOutputDeltaNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "call-1".to_string(),
|
||||
delta: "hello".to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
//! Shared builders for synthetic [`ThreadItem`] values emitted by the app-server layer.
|
||||
//! Shared builders for app-server [`ThreadItem`] values derived from compatibility events.
|
||||
//!
|
||||
//! 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.
|
||||
//! Most live tool items now come from first-class core `ItemStarted` / `ItemCompleted` events.
|
||||
//! These builders remain for approval flows, rebuilt legacy history, and other pre-execution
|
||||
//! paths where the underlying tool has not started or 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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Exposes protocol pieces used by `lib.rs` via `pub use protocol::common::*;`.
|
||||
|
||||
pub mod common;
|
||||
pub mod event_mapping;
|
||||
pub mod item_builders;
|
||||
mod mappers;
|
||||
mod serde_helpers;
|
||||
|
||||
@@ -217,7 +217,6 @@ impl ThreadHistoryBuilder {
|
||||
EventMsg::Error(payload) => self.handle_error(payload),
|
||||
EventMsg::TokenCount(_) => {}
|
||||
EventMsg::ThreadRolledBack(payload) => self.handle_thread_rollback(payload),
|
||||
EventMsg::UndoCompleted(_) => {}
|
||||
EventMsg::TurnAborted(payload) => self.handle_turn_aborted(payload),
|
||||
EventMsg::TurnStarted(payload) => self.handle_turn_started(payload),
|
||||
EventMsg::TurnComplete(payload) => self.handle_turn_complete(payload),
|
||||
@@ -357,7 +356,9 @@ impl ThreadHistoryBuilder {
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
| codex_protocol::items::TurnItem::ImageView(_)
|
||||
| codex_protocol::items::TurnItem::ImageGeneration(_)
|
||||
| codex_protocol::items::TurnItem::FileChange(_)
|
||||
| codex_protocol::items::TurnItem::ContextCompaction(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -378,7 +379,9 @@ impl ThreadHistoryBuilder {
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
| codex_protocol::items::TurnItem::ImageView(_)
|
||||
| codex_protocol::items::TurnItem::ImageGeneration(_)
|
||||
| codex_protocol::items::TurnItem::FileChange(_)
|
||||
| codex_protocol::items::TurnItem::ContextCompaction(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::path::PathBuf;
|
||||
|
||||
use crate::RequestId;
|
||||
use crate::protocol::common::AuthMode;
|
||||
use crate::protocol::item_builders::convert_patch_changes;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::account::ProviderAccount;
|
||||
@@ -38,6 +39,8 @@ use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
|
||||
use codex_protocol::mcp::Tool as McpTool;
|
||||
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
|
||||
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
|
||||
use codex_protocol::models::ActivePermissionProfile as CoreActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification as CoreActivePermissionProfileModification;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions;
|
||||
@@ -470,6 +473,7 @@ v2_enum_from_core!(
|
||||
Mdm,
|
||||
SessionFlags,
|
||||
Plugin,
|
||||
CloudRequirements,
|
||||
LegacyManagedConfigFile,
|
||||
LegacyManagedConfigMdm,
|
||||
Unknown,
|
||||
@@ -1703,6 +1707,109 @@ impl From<PermissionProfile> for CorePermissionProfile {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ActivePermissionProfile {
|
||||
/// Identifier from `default_permissions` or the implicit built-in default,
|
||||
/// such as `:workspace` or a user-defined `[permissions.<id>]` profile.
|
||||
pub id: String,
|
||||
/// Parent profile identifier once permissions profiles support
|
||||
/// inheritance. This is currently always `null`.
|
||||
#[serde(default)]
|
||||
pub extends: Option<String>,
|
||||
/// Bounded user-requested modifications applied on top of the named
|
||||
/// profile, if any.
|
||||
#[serde(default)]
|
||||
pub modifications: Vec<ActivePermissionProfileModification>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ActivePermissionProfileModification {
|
||||
/// Additional concrete directory that should be writable.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
AdditionalWritableRoot { path: AbsolutePathBuf },
|
||||
}
|
||||
|
||||
impl From<CoreActivePermissionProfileModification> for ActivePermissionProfileModification {
|
||||
fn from(value: CoreActivePermissionProfileModification) -> Self {
|
||||
match value {
|
||||
CoreActivePermissionProfileModification::AdditionalWritableRoot { path } => {
|
||||
Self::AdditionalWritableRoot { path }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ActivePermissionProfileModification> for CoreActivePermissionProfileModification {
|
||||
fn from(value: ActivePermissionProfileModification) -> Self {
|
||||
match value {
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot { path } => {
|
||||
Self::AdditionalWritableRoot { path }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreActivePermissionProfile> for ActivePermissionProfile {
|
||||
fn from(value: CoreActivePermissionProfile) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
extends: value.extends,
|
||||
modifications: value
|
||||
.modifications
|
||||
.into_iter()
|
||||
.map(ActivePermissionProfileModification::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ActivePermissionProfile> for CoreActivePermissionProfile {
|
||||
fn from(value: ActivePermissionProfile) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
extends: value.extends,
|
||||
modifications: value
|
||||
.modifications
|
||||
.into_iter()
|
||||
.map(CoreActivePermissionProfileModification::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PermissionProfileSelectionParams {
|
||||
/// Select a named built-in or user-defined profile and optionally apply
|
||||
/// bounded modifications that Codex knows how to validate.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Profile {
|
||||
id: String,
|
||||
#[ts(optional = nullable)]
|
||||
modifications: Option<Vec<PermissionProfileModificationParams>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PermissionProfileModificationParams {
|
||||
/// Additional concrete directory that should be writable.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
AdditionalWritableRoot { path: AbsolutePathBuf },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -3462,11 +3569,12 @@ pub struct ThreadStartParams {
|
||||
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
#[ts(optional = nullable)]
|
||||
pub sandbox: Option<SandboxMode>,
|
||||
/// Full permissions override for this thread. Cannot be combined with
|
||||
/// `sandbox`.
|
||||
#[experimental("thread/start.permissionProfile")]
|
||||
/// Named profile selection for this thread. Cannot be combined with
|
||||
/// `sandbox`. Use bounded `modifications` for supported turn/thread
|
||||
/// adjustments instead of replacing the full permissions profile.
|
||||
#[experimental("thread/start.permissions")]
|
||||
#[ts(optional = nullable)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub permissions: Option<PermissionProfileSelectionParams>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, JsonValue>>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -3543,14 +3651,20 @@ pub struct ThreadStartResponse {
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
/// Legacy sandbox policy retained for compatibility. New clients should use
|
||||
/// `permissionProfile` when present as the canonical active permissions
|
||||
/// view.
|
||||
/// Legacy sandbox policy retained for compatibility. Experimental clients
|
||||
/// should prefer `permissionProfile` when they need exact runtime
|
||||
/// permissions.
|
||||
pub sandbox: SandboxPolicy,
|
||||
/// Canonical active permissions view for this thread.
|
||||
/// Full active permissions for this thread. `activePermissionProfile`
|
||||
/// carries display/provenance metadata for this runtime profile.
|
||||
#[experimental("thread/start.permissionProfile")]
|
||||
#[serde(default)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
/// Named or implicit built-in profile that produced the active
|
||||
/// permissions, when known.
|
||||
#[experimental("thread/start.activePermissionProfile")]
|
||||
#[serde(default)]
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
@@ -3608,11 +3722,12 @@ pub struct ThreadResumeParams {
|
||||
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
#[ts(optional = nullable)]
|
||||
pub sandbox: Option<SandboxMode>,
|
||||
/// Full permissions override for the resumed thread. Cannot be combined
|
||||
/// with `sandbox`.
|
||||
#[experimental("thread/resume.permissionProfile")]
|
||||
/// Named profile selection for the resumed thread. Cannot be combined
|
||||
/// with `sandbox`. Use bounded `modifications` for supported thread
|
||||
/// adjustments instead of replacing the full permissions profile.
|
||||
#[experimental("thread/resume.permissions")]
|
||||
#[ts(optional = nullable)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub permissions: Option<PermissionProfileSelectionParams>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -3624,6 +3739,7 @@ pub struct ThreadResumeParams {
|
||||
/// When true, return only thread metadata and live-resume state without
|
||||
/// populating `thread.turns`. This is useful when the client plans to call
|
||||
/// `thread/turns/list` immediately after resuming.
|
||||
#[experimental("thread/resume.excludeTurns")]
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub exclude_turns: bool,
|
||||
/// If true, persist additional rollout EventMsg variants required to
|
||||
@@ -3649,14 +3765,20 @@ pub struct ThreadResumeResponse {
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
/// Legacy sandbox policy retained for compatibility. New clients should use
|
||||
/// `permissionProfile` when present as the canonical active permissions
|
||||
/// view.
|
||||
/// Legacy sandbox policy retained for compatibility. Experimental clients
|
||||
/// should prefer `permissionProfile` when they need exact runtime
|
||||
/// permissions.
|
||||
pub sandbox: SandboxPolicy,
|
||||
/// Canonical active permissions view for this thread.
|
||||
/// Full active permissions for this thread. `activePermissionProfile`
|
||||
/// carries display/provenance metadata for this runtime profile.
|
||||
#[experimental("thread/resume.permissionProfile")]
|
||||
#[serde(default)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
/// Named or implicit built-in profile that produced the active
|
||||
/// permissions, when known.
|
||||
#[experimental("thread/resume.activePermissionProfile")]
|
||||
#[serde(default)]
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
@@ -3705,11 +3827,12 @@ pub struct ThreadForkParams {
|
||||
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
#[ts(optional = nullable)]
|
||||
pub sandbox: Option<SandboxMode>,
|
||||
/// Full permissions override for the forked thread. Cannot be combined
|
||||
/// with `sandbox`.
|
||||
#[experimental("thread/fork.permissionProfile")]
|
||||
/// Named profile selection for the forked thread. Cannot be combined with
|
||||
/// `sandbox`. Use bounded `modifications` for supported thread
|
||||
/// adjustments instead of replacing the full permissions profile.
|
||||
#[experimental("thread/fork.permissions")]
|
||||
#[ts(optional = nullable)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub permissions: Option<PermissionProfileSelectionParams>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -3721,6 +3844,7 @@ pub struct ThreadForkParams {
|
||||
/// When true, return only thread metadata and live fork state without
|
||||
/// populating `thread.turns`. This is useful when the client plans to call
|
||||
/// `thread/turns/list` immediately after forking.
|
||||
#[experimental("thread/fork.excludeTurns")]
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub exclude_turns: bool,
|
||||
/// If true, persist additional rollout EventMsg variants required to
|
||||
@@ -3746,14 +3870,20 @@ pub struct ThreadForkResponse {
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
/// Legacy sandbox policy retained for compatibility. New clients should use
|
||||
/// `permissionProfile` when present as the canonical active permissions
|
||||
/// view.
|
||||
/// Legacy sandbox policy retained for compatibility. Experimental clients
|
||||
/// should prefer `permissionProfile` when they need exact runtime
|
||||
/// permissions.
|
||||
pub sandbox: SandboxPolicy,
|
||||
/// Canonical active permissions view for this thread.
|
||||
/// Full active permissions for this thread. `activePermissionProfile`
|
||||
/// carries display/provenance metadata for this runtime profile.
|
||||
#[experimental("thread/fork.permissionProfile")]
|
||||
#[serde(default)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
/// Named or implicit built-in profile that produced the active
|
||||
/// permissions, when known.
|
||||
#[experimental("thread/fork.activePermissionProfile")]
|
||||
#[serde(default)]
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
@@ -4480,6 +4610,72 @@ pub struct PluginReadResponse {
|
||||
pub plugin: PluginDetail,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginSkillReadParams {
|
||||
pub remote_marketplace_name: String,
|
||||
pub remote_plugin_id: String,
|
||||
pub skill_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginSkillReadResponse {
|
||||
pub contents: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareSaveParams {
|
||||
pub plugin_path: AbsolutePathBuf,
|
||||
#[ts(optional = nullable)]
|
||||
pub remote_plugin_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareSaveResponse {
|
||||
pub remote_plugin_id: String,
|
||||
pub share_url: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareListParams {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareListResponse {
|
||||
pub data: Vec<PluginShareListItem>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareDeleteParams {
|
||||
pub remote_plugin_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareDeleteResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareListItem {
|
||||
pub plugin: PluginSummary,
|
||||
pub share_url: String,
|
||||
pub local_plugin_path: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
@@ -4590,6 +4786,7 @@ pub struct HooksListEntry {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct HookMetadata {
|
||||
pub key: String,
|
||||
pub event_name: HookEventName,
|
||||
pub handler_type: HookHandlerType,
|
||||
pub matcher: Option<String>,
|
||||
@@ -4600,6 +4797,8 @@ pub struct HookMetadata {
|
||||
pub source: HookSource,
|
||||
pub plugin_id: Option<String>,
|
||||
pub display_order: i64,
|
||||
pub enabled: bool,
|
||||
pub is_managed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -4654,6 +4853,21 @@ pub enum PluginAuthPolicy {
|
||||
OnUse,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema, TS)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginAvailability {
|
||||
/// Plugin-service currently sends `"ENABLED"` for available remote plugins.
|
||||
/// Codex app-server exposes `"AVAILABLE"` in its API; the alias keeps
|
||||
/// decoding compatible with that upstream response.
|
||||
#[serde(rename = "AVAILABLE", alias = "ENABLED")]
|
||||
#[ts(rename = "AVAILABLE")]
|
||||
#[default]
|
||||
Available,
|
||||
#[serde(rename = "DISABLED_BY_ADMIN")]
|
||||
#[ts(rename = "DISABLED_BY_ADMIN")]
|
||||
DisabledByAdmin,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4665,6 +4879,9 @@ pub struct PluginSummary {
|
||||
pub enabled: bool,
|
||||
pub install_policy: PluginInstallPolicy,
|
||||
pub auth_policy: PluginAuthPolicy,
|
||||
/// Availability state for installing and using the plugin.
|
||||
#[serde(default)]
|
||||
pub availability: PluginAvailability,
|
||||
pub interface: Option<PluginInterface>,
|
||||
}
|
||||
|
||||
@@ -5122,7 +5339,7 @@ pub struct ThreadRealtimeStartParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub prompt: Option<Option<String>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub session_id: Option<String>,
|
||||
pub realtime_session_id: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub transport: Option<ThreadRealtimeStartTransport>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -5212,7 +5429,7 @@ pub struct ThreadRealtimeListVoicesResponse {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadRealtimeStartedNotification {
|
||||
pub thread_id: String,
|
||||
pub session_id: Option<String>,
|
||||
pub realtime_session_id: Option<String>,
|
||||
pub version: RealtimeConversationVersion,
|
||||
}
|
||||
|
||||
@@ -5338,11 +5555,13 @@ pub struct TurnStartParams {
|
||||
/// Override the sandbox policy for this turn and subsequent turns.
|
||||
#[ts(optional = nullable)]
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
/// Override the full permissions profile for this turn and subsequent
|
||||
/// turns. Cannot be combined with `sandboxPolicy`.
|
||||
#[experimental("turn/start.permissionProfile")]
|
||||
/// Select a named permissions profile for this turn and subsequent turns.
|
||||
/// Cannot be combined with `sandboxPolicy`. Use bounded `modifications`
|
||||
/// for supported turn adjustments instead of replacing the full
|
||||
/// permissions profile.
|
||||
#[experimental("turn/start.permissions")]
|
||||
#[ts(optional = nullable)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub permissions: Option<PermissionProfileSelectionParams>,
|
||||
/// Override the model for this turn and subsequent turns.
|
||||
#[ts(optional = nullable)]
|
||||
pub model: Option<String>,
|
||||
@@ -6244,6 +6463,10 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
query: search.query,
|
||||
action: Some(WebSearchAction::from(search.action)),
|
||||
},
|
||||
CoreTurnItem::ImageView(image) => ThreadItem::ImageView {
|
||||
id: image.id,
|
||||
path: image.path,
|
||||
},
|
||||
CoreTurnItem::ImageGeneration(image) => ThreadItem::ImageGeneration {
|
||||
id: image.id,
|
||||
status: image.status,
|
||||
@@ -6251,6 +6474,15 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
result: image.result,
|
||||
saved_path: image.saved_path,
|
||||
},
|
||||
CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange {
|
||||
id: file_change.id,
|
||||
changes: convert_patch_changes(&file_change.changes),
|
||||
status: file_change
|
||||
.status
|
||||
.as_ref()
|
||||
.map(PatchApplyStatus::from)
|
||||
.unwrap_or(PatchApplyStatus::InProgress),
|
||||
},
|
||||
CoreTurnItem::ContextCompaction(compaction) => {
|
||||
ThreadItem::ContextCompaction { id: compaction.id }
|
||||
}
|
||||
@@ -6819,6 +7051,9 @@ pub struct CommandExecOutputDeltaNotification {
|
||||
pub cap_reached: bool,
|
||||
}
|
||||
|
||||
/// Deprecated legacy notification for `apply_patch` textual output.
|
||||
///
|
||||
/// The server no longer emits this notification.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -7857,6 +8092,8 @@ mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::AgentMessageItem;
|
||||
use codex_protocol::items::FileChangeItem;
|
||||
use codex_protocol::items::ImageViewItem;
|
||||
use codex_protocol::items::ReasoningItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
@@ -10137,6 +10374,48 @@ mod tests {
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
let image_view_item = TurnItem::ImageView(ImageViewItem {
|
||||
id: "view-image-1".to_string(),
|
||||
path: test_path_buf("/tmp/view-image.png").abs(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(image_view_item),
|
||||
ThreadItem::ImageView {
|
||||
id: "view-image-1".to_string(),
|
||||
path: test_path_buf("/tmp/view-image.png").abs(),
|
||||
}
|
||||
);
|
||||
|
||||
let file_change_item = TurnItem::FileChange(FileChangeItem {
|
||||
id: "patch-1".to_string(),
|
||||
changes: [(
|
||||
PathBuf::from("README.md"),
|
||||
codex_protocol::protocol::FileChange::Add {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
status: Some(codex_protocol::protocol::PatchApplyStatus::Completed),
|
||||
auto_approved: None,
|
||||
stdout: Some("Done!".to_string()),
|
||||
stderr: Some(String::new()),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(file_change_item),
|
||||
ThreadItem::FileChange {
|
||||
id: "patch-1".to_string(),
|
||||
changes: vec![FileUpdateChange {
|
||||
path: "README.md".to_string(),
|
||||
kind: PatchChangeKind::Add,
|
||||
diff: "hello\n".to_string(),
|
||||
}],
|
||||
status: PatchApplyStatus::Completed,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -10471,6 +10750,156 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_skill_read_params_serialization_uses_remote_plugin_id() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginSkillReadParams {
|
||||
remote_marketplace_name: "chatgpt-global".to_string(),
|
||||
remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
skill_name: "plan-work".to_string(),
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"remoteMarketplaceName": "chatgpt-global",
|
||||
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"skillName": "plan-work",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
let plugin_path = if cfg!(windows) {
|
||||
r"C:\plugins\gmail"
|
||||
} else {
|
||||
"/plugins/gmail"
|
||||
};
|
||||
let plugin_path = AbsolutePathBuf::try_from(PathBuf::from(plugin_path)).unwrap();
|
||||
let plugin_path_json = plugin_path.as_path().display().to_string();
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginShareSaveParams {
|
||||
plugin_path: plugin_path.clone(),
|
||||
remote_plugin_id: None,
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"pluginPath": plugin_path_json,
|
||||
"remotePluginId": null,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginShareSaveParams {
|
||||
plugin_path,
|
||||
remote_plugin_id: Some(
|
||||
"plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
),
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"pluginPath": plugin_path_json,
|
||||
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginShareSaveResponse {
|
||||
remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
share_url: String::new(),
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"shareUrl": "",
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_value::<PluginShareListParams>(json!({})).unwrap(),
|
||||
PluginShareListParams {},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginShareDeleteParams {
|
||||
remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_share_list_response_serializes_share_items() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginShareListResponse {
|
||||
data: vec![PluginShareListItem {
|
||||
plugin: PluginSummary {
|
||||
id: "plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
name: "gmail".to_string(),
|
||||
source: PluginSource::Remote,
|
||||
installed: false,
|
||||
enabled: false,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnUse,
|
||||
availability: PluginAvailability::Available,
|
||||
interface: None,
|
||||
},
|
||||
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
|
||||
local_plugin_path: None,
|
||||
}],
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"data": [{
|
||||
"plugin": {
|
||||
"id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"name": "gmail",
|
||||
"source": { "type": "remote" },
|
||||
"installed": false,
|
||||
"enabled": false,
|
||||
"installPolicy": "AVAILABLE",
|
||||
"authPolicy": "ON_USE",
|
||||
"availability": "AVAILABLE",
|
||||
"interface": null,
|
||||
},
|
||||
"shareUrl": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"localPluginPath": null,
|
||||
}],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_summary_defaults_missing_availability_to_available() {
|
||||
let summary: PluginSummary = serde_json::from_value(json!({
|
||||
"id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"name": "gmail",
|
||||
"source": { "type": "remote" },
|
||||
"installed": false,
|
||||
"enabled": false,
|
||||
"installPolicy": "AVAILABLE",
|
||||
"authPolicy": "ON_USE",
|
||||
"interface": null,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(summary.availability, PluginAvailability::Available);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_availability_deserializes_enabled_alias() {
|
||||
let availability: PluginAvailability = serde_json::from_value(json!("ENABLED")).unwrap();
|
||||
|
||||
assert_eq!(availability, PluginAvailability::Available);
|
||||
assert_eq!(
|
||||
serde_json::to_value(availability).unwrap(),
|
||||
json!("AVAILABLE")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_uninstall_params_serialization_omits_force_remote_sync() {
|
||||
assert_eq!(
|
||||
@@ -10790,6 +11219,9 @@ mod tests {
|
||||
assert_eq!(start.permission_profile, None);
|
||||
assert_eq!(resume.permission_profile, None);
|
||||
assert_eq!(fork.permission_profile, None);
|
||||
assert_eq!(start.active_permission_profile, None);
|
||||
assert_eq!(resume.active_permission_profile, None);
|
||||
assert_eq!(fork.active_permission_profile, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -10817,7 +11249,7 @@ mod tests {
|
||||
approval_policy: None,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
model: None,
|
||||
service_tier: None,
|
||||
effort: None,
|
||||
|
||||
6
codex-rs/app-server-transport/BUILD.bazel
Normal file
6
codex-rs/app-server-transport/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "app-server-transport",
|
||||
crate_name = "codex_app_server_transport",
|
||||
)
|
||||
58
codex-rs/app-server-transport/Cargo.toml
Normal file
58
codex-rs/app-server-transport/Cargo.toml
Normal file
@@ -0,0 +1,58 @@
|
||||
[package]
|
||||
name = "codex-app-server-transport"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_app_server_transport"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true, default-features = false, features = [
|
||||
"http1",
|
||||
"json",
|
||||
"tokio",
|
||||
"ws",
|
||||
] }
|
||||
base64 = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-api = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-state = { workspace = true }
|
||||
codex-uds = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-rustls-provider = { workspace = true }
|
||||
constant_time_eq = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
gethostname = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
jsonwebtoken = { workspace = true }
|
||||
owo-colors = { workspace = true, features = ["supports-colors"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
chrono = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
20
codex-rs/app-server-transport/src/lib.rs
Normal file
20
codex-rs/app-server-transport/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
mod outgoing_message;
|
||||
mod transport;
|
||||
|
||||
pub use outgoing_message::ConnectionId;
|
||||
pub use outgoing_message::OutgoingError;
|
||||
pub use outgoing_message::OutgoingMessage;
|
||||
pub use outgoing_message::OutgoingResponse;
|
||||
pub use outgoing_message::QueuedOutgoingMessage;
|
||||
pub use transport::AppServerTransport;
|
||||
pub use transport::AppServerTransportParseError;
|
||||
pub use transport::CHANNEL_CAPACITY;
|
||||
pub use transport::ConnectionOrigin;
|
||||
pub use transport::RemoteControlHandle;
|
||||
pub use transport::TransportEvent;
|
||||
pub use transport::app_server_control_socket_path;
|
||||
pub use transport::auth;
|
||||
pub use transport::start_control_socket_acceptor;
|
||||
pub use transport::start_remote_control;
|
||||
pub use transport::start_stdio_connection;
|
||||
pub use transport::start_websocket_acceptor;
|
||||
58
codex-rs/app-server-transport/src/outgoing_message.rs
Normal file
58
codex-rs/app-server-transport/src/outgoing_message.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use std::fmt;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::Result;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
/// Stable identifier for a transport connection.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct ConnectionId(pub u64);
|
||||
|
||||
impl fmt::Display for ConnectionId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outgoing message from the server to the client.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OutgoingMessage {
|
||||
Request(ServerRequest),
|
||||
/// AppServerNotification is specific to the case where this is run as an
|
||||
/// "app server" as opposed to an MCP server.
|
||||
AppServerNotification(ServerNotification),
|
||||
Response(OutgoingResponse),
|
||||
Error(OutgoingError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub struct OutgoingResponse {
|
||||
pub id: RequestId,
|
||||
pub result: Result,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub struct OutgoingError {
|
||||
pub error: JSONRPCErrorError,
|
||||
pub id: RequestId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QueuedOutgoingMessage {
|
||||
pub message: OutgoingMessage,
|
||||
pub write_complete_tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl QueuedOutgoingMessage {
|
||||
pub fn new(message: OutgoingMessage) -> Self {
|
||||
Self {
|
||||
message,
|
||||
write_complete_tx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ pub enum AppServerWebsocketCapabilityTokenSource {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct WebsocketAuthPolicy {
|
||||
pub struct WebsocketAuthPolicy {
|
||||
pub(crate) mode: Option<WebsocketAuthMode>,
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ impl AppServerWebsocketAuthArgs {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn policy_from_settings(
|
||||
pub fn policy_from_settings(
|
||||
settings: &AppServerWebsocketAuthSettings,
|
||||
) -> io::Result<WebsocketAuthPolicy> {
|
||||
let mode = match settings.config.as_ref() {
|
||||
478
codex-rs/app-server-transport/src/transport/mod.rs
Normal file
478
codex-rs/app-server-transport/src/transport/mod.rs
Normal file
@@ -0,0 +1,478 @@
|
||||
pub mod auth;
|
||||
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::OutgoingError;
|
||||
use crate::outgoing_message::OutgoingMessage;
|
||||
use crate::outgoing_message::QueuedOutgoingMessage;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::error;
|
||||
use tracing::warn;
|
||||
|
||||
/// Size of the bounded channels used to communicate between tasks. The value
|
||||
/// is a balance between throughput and memory usage - 128 messages should be
|
||||
/// plenty for an interactive CLI.
|
||||
pub const CHANNEL_CAPACITY: usize = 128;
|
||||
|
||||
mod remote_control;
|
||||
mod stdio;
|
||||
mod unix_socket;
|
||||
#[cfg(test)]
|
||||
mod unix_socket_tests;
|
||||
mod websocket;
|
||||
|
||||
pub use remote_control::RemoteControlHandle;
|
||||
pub use remote_control::start_remote_control;
|
||||
pub use stdio::start_stdio_connection;
|
||||
pub use unix_socket::start_control_socket_acceptor;
|
||||
pub use websocket::start_websocket_acceptor;
|
||||
|
||||
const OVERLOADED_ERROR_CODE: i64 = -32001;
|
||||
|
||||
const APP_SERVER_CONTROL_SOCKET_DIR_NAME: &str = "app-server-control";
|
||||
const APP_SERVER_CONTROL_SOCKET_FILE_NAME: &str = "app-server-control.sock";
|
||||
|
||||
pub fn app_server_control_socket_path(codex_home: &Path) -> std::io::Result<AbsolutePathBuf> {
|
||||
AbsolutePathBuf::from_absolute_path(
|
||||
codex_home
|
||||
.join(APP_SERVER_CONTROL_SOCKET_DIR_NAME)
|
||||
.join(APP_SERVER_CONTROL_SOCKET_FILE_NAME),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum AppServerTransport {
|
||||
Stdio,
|
||||
UnixSocket { socket_path: AbsolutePathBuf },
|
||||
WebSocket { bind_address: SocketAddr },
|
||||
Off,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum AppServerTransportParseError {
|
||||
UnsupportedListenUrl(String),
|
||||
InvalidUnixSocketPath { listen_url: String, message: String },
|
||||
InvalidWebSocketListenUrl(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AppServerTransportParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AppServerTransportParseError::UnsupportedListenUrl(listen_url) => write!(
|
||||
f,
|
||||
"unsupported --listen URL `{listen_url}`; expected `stdio://`, `unix://`, `unix://PATH`, `ws://IP:PORT`, or `off`"
|
||||
),
|
||||
AppServerTransportParseError::InvalidUnixSocketPath {
|
||||
listen_url,
|
||||
message,
|
||||
} => write!(
|
||||
f,
|
||||
"invalid unix socket --listen URL `{listen_url}`; failed to resolve socket path: {message}"
|
||||
),
|
||||
AppServerTransportParseError::InvalidWebSocketListenUrl(listen_url) => write!(
|
||||
f,
|
||||
"invalid websocket --listen URL `{listen_url}`; expected `ws://IP:PORT`"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AppServerTransportParseError {}
|
||||
|
||||
impl AppServerTransport {
|
||||
pub const DEFAULT_LISTEN_URL: &'static str = "stdio://";
|
||||
|
||||
pub fn from_listen_url(listen_url: &str) -> Result<Self, AppServerTransportParseError> {
|
||||
if listen_url == Self::DEFAULT_LISTEN_URL {
|
||||
return Ok(Self::Stdio);
|
||||
}
|
||||
|
||||
if let Some(raw_socket_path) = listen_url.strip_prefix("unix://") {
|
||||
let socket_path = if raw_socket_path.is_empty() {
|
||||
let codex_home = find_codex_home().map_err(|err| {
|
||||
AppServerTransportParseError::InvalidUnixSocketPath {
|
||||
listen_url: listen_url.to_string(),
|
||||
message: format!("failed to resolve CODEX_HOME: {err}"),
|
||||
}
|
||||
})?;
|
||||
app_server_control_socket_path(&codex_home).map_err(|err| {
|
||||
AppServerTransportParseError::InvalidUnixSocketPath {
|
||||
listen_url: listen_url.to_string(),
|
||||
message: err.to_string(),
|
||||
}
|
||||
})?
|
||||
} else {
|
||||
AbsolutePathBuf::relative_to_current_dir(raw_socket_path).map_err(|err| {
|
||||
AppServerTransportParseError::InvalidUnixSocketPath {
|
||||
listen_url: listen_url.to_string(),
|
||||
message: err.to_string(),
|
||||
}
|
||||
})?
|
||||
};
|
||||
return Ok(Self::UnixSocket { socket_path });
|
||||
}
|
||||
|
||||
if listen_url == "off" {
|
||||
return Ok(Self::Off);
|
||||
}
|
||||
|
||||
if let Some(socket_addr) = listen_url.strip_prefix("ws://") {
|
||||
let bind_address = socket_addr.parse::<SocketAddr>().map_err(|_| {
|
||||
AppServerTransportParseError::InvalidWebSocketListenUrl(listen_url.to_string())
|
||||
})?;
|
||||
return Ok(Self::WebSocket { bind_address });
|
||||
}
|
||||
|
||||
Err(AppServerTransportParseError::UnsupportedListenUrl(
|
||||
listen_url.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AppServerTransport {
|
||||
type Err = AppServerTransportParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::from_listen_url(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TransportEvent {
|
||||
ConnectionOpened {
|
||||
connection_id: ConnectionId,
|
||||
origin: ConnectionOrigin,
|
||||
writer: mpsc::Sender<QueuedOutgoingMessage>,
|
||||
disconnect_sender: Option<CancellationToken>,
|
||||
},
|
||||
ConnectionClosed {
|
||||
connection_id: ConnectionId,
|
||||
},
|
||||
IncomingMessage {
|
||||
connection_id: ConnectionId,
|
||||
message: JSONRPCMessage,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionOrigin {
|
||||
Stdio,
|
||||
InProcess,
|
||||
WebSocket,
|
||||
RemoteControl,
|
||||
}
|
||||
|
||||
impl ConnectionOrigin {
|
||||
pub fn allows_device_key_requests(self) -> bool {
|
||||
// Device-key endpoints are only for local connections that own the app-server instance.
|
||||
// Do not include remote transports such as SSH or remote-control websocket connections.
|
||||
matches!(self, Self::Stdio | Self::InProcess)
|
||||
}
|
||||
}
|
||||
|
||||
static CONNECTION_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn next_connection_id() -> ConnectionId {
|
||||
ConnectionId(CONNECTION_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
|
||||
async fn forward_incoming_message(
|
||||
transport_event_tx: &mpsc::Sender<TransportEvent>,
|
||||
writer: &mpsc::Sender<QueuedOutgoingMessage>,
|
||||
connection_id: ConnectionId,
|
||||
payload: &str,
|
||||
) -> bool {
|
||||
match serde_json::from_str::<JSONRPCMessage>(payload) {
|
||||
Ok(message) => {
|
||||
enqueue_incoming_message(transport_event_tx, writer, connection_id, message).await
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to deserialize JSONRPCMessage: {err}");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn enqueue_incoming_message(
|
||||
transport_event_tx: &mpsc::Sender<TransportEvent>,
|
||||
writer: &mpsc::Sender<QueuedOutgoingMessage>,
|
||||
connection_id: ConnectionId,
|
||||
message: JSONRPCMessage,
|
||||
) -> bool {
|
||||
let event = TransportEvent::IncomingMessage {
|
||||
connection_id,
|
||||
message,
|
||||
};
|
||||
match transport_event_tx.try_send(event) {
|
||||
Ok(()) => true,
|
||||
Err(mpsc::error::TrySendError::Closed(_)) => false,
|
||||
Err(mpsc::error::TrySendError::Full(TransportEvent::IncomingMessage {
|
||||
connection_id,
|
||||
message: JSONRPCMessage::Request(request),
|
||||
})) => {
|
||||
let overload_error = OutgoingMessage::Error(OutgoingError {
|
||||
id: request.id,
|
||||
error: JSONRPCErrorError {
|
||||
code: OVERLOADED_ERROR_CODE,
|
||||
message: "Server overloaded; retry later.".to_string(),
|
||||
data: None,
|
||||
},
|
||||
});
|
||||
match writer.try_send(QueuedOutgoingMessage::new(overload_error)) {
|
||||
Ok(()) => true,
|
||||
Err(mpsc::error::TrySendError::Closed(_)) => false,
|
||||
Err(mpsc::error::TrySendError::Full(_overload_error)) => {
|
||||
warn!(
|
||||
"dropping overload response for connection {:?}: outbound queue is full",
|
||||
connection_id
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(mpsc::error::TrySendError::Full(event)) => transport_event_tx.send(event).await.is_ok(),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_outgoing_message(outgoing_message: OutgoingMessage) -> Option<String> {
|
||||
let value = match serde_json::to_value(outgoing_message) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
error!("Failed to convert OutgoingMessage to JSON value: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match serde_json::to_string(&value) {
|
||||
Ok(json) => Some(json),
|
||||
Err(err) => {
|
||||
error!("Failed to serialize JSONRPCMessage: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[test]
|
||||
fn listen_off_parses_as_off_transport() {
|
||||
assert_eq!(
|
||||
AppServerTransport::from_listen_url("off"),
|
||||
Ok(AppServerTransport::Off)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enqueue_incoming_request_returns_overload_error_when_queue_is_full() {
|
||||
let connection_id = ConnectionId(42);
|
||||
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(1);
|
||||
let (writer_tx, mut writer_rx) = mpsc::channel(1);
|
||||
|
||||
let first_message = JSONRPCMessage::Notification(JSONRPCNotification {
|
||||
method: "initialized".to_string(),
|
||||
params: None,
|
||||
});
|
||||
transport_event_tx
|
||||
.send(TransportEvent::IncomingMessage {
|
||||
connection_id,
|
||||
message: first_message.clone(),
|
||||
})
|
||||
.await
|
||||
.expect("queue should accept first message");
|
||||
|
||||
let request = JSONRPCMessage::Request(JSONRPCRequest {
|
||||
id: RequestId::Integer(7),
|
||||
method: "config/read".to_string(),
|
||||
params: Some(json!({ "includeLayers": false })),
|
||||
trace: None,
|
||||
});
|
||||
assert!(
|
||||
enqueue_incoming_message(&transport_event_tx, &writer_tx, connection_id, request).await
|
||||
);
|
||||
|
||||
let queued_event = transport_event_rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("first event should stay queued");
|
||||
match queued_event {
|
||||
TransportEvent::IncomingMessage {
|
||||
connection_id: queued_connection_id,
|
||||
message,
|
||||
} => {
|
||||
assert_eq!(queued_connection_id, connection_id);
|
||||
assert_eq!(message, first_message);
|
||||
}
|
||||
_ => panic!("expected queued incoming message"),
|
||||
}
|
||||
|
||||
let overload = writer_rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("request should receive overload error");
|
||||
let overload_json =
|
||||
serde_json::to_value(overload.message).expect("serialize overload error");
|
||||
assert_eq!(
|
||||
overload_json,
|
||||
json!({
|
||||
"id": 7,
|
||||
"error": {
|
||||
"code": OVERLOADED_ERROR_CODE,
|
||||
"message": "Server overloaded; retry later."
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enqueue_incoming_response_waits_instead_of_dropping_when_queue_is_full() {
|
||||
let connection_id = ConnectionId(42);
|
||||
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(1);
|
||||
let (writer_tx, _writer_rx) = mpsc::channel(1);
|
||||
|
||||
let first_message = JSONRPCMessage::Notification(JSONRPCNotification {
|
||||
method: "initialized".to_string(),
|
||||
params: None,
|
||||
});
|
||||
transport_event_tx
|
||||
.send(TransportEvent::IncomingMessage {
|
||||
connection_id,
|
||||
message: first_message.clone(),
|
||||
})
|
||||
.await
|
||||
.expect("queue should accept first message");
|
||||
|
||||
let response = JSONRPCMessage::Response(JSONRPCResponse {
|
||||
id: RequestId::Integer(7),
|
||||
result: json!({"ok": true}),
|
||||
});
|
||||
let transport_event_tx_for_enqueue = transport_event_tx.clone();
|
||||
let writer_tx_for_enqueue = writer_tx.clone();
|
||||
let enqueue_handle = tokio::spawn(async move {
|
||||
enqueue_incoming_message(
|
||||
&transport_event_tx_for_enqueue,
|
||||
&writer_tx_for_enqueue,
|
||||
connection_id,
|
||||
response,
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
let queued_event = transport_event_rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("first event should be dequeued");
|
||||
match queued_event {
|
||||
TransportEvent::IncomingMessage {
|
||||
connection_id: queued_connection_id,
|
||||
message,
|
||||
} => {
|
||||
assert_eq!(queued_connection_id, connection_id);
|
||||
assert_eq!(message, first_message);
|
||||
}
|
||||
_ => panic!("expected queued incoming message"),
|
||||
}
|
||||
|
||||
let enqueue_result = enqueue_handle.await.expect("enqueue task should not panic");
|
||||
assert!(enqueue_result);
|
||||
|
||||
let forwarded_event = transport_event_rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("response should be forwarded instead of dropped");
|
||||
match forwarded_event {
|
||||
TransportEvent::IncomingMessage {
|
||||
connection_id: queued_connection_id,
|
||||
message: JSONRPCMessage::Response(JSONRPCResponse { id, result }),
|
||||
} => {
|
||||
assert_eq!(queued_connection_id, connection_id);
|
||||
assert_eq!(id, RequestId::Integer(7));
|
||||
assert_eq!(result, json!({"ok": true}));
|
||||
}
|
||||
_ => panic!("expected forwarded response message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enqueue_incoming_request_does_not_block_when_writer_queue_is_full() {
|
||||
let connection_id = ConnectionId(42);
|
||||
let (transport_event_tx, _transport_event_rx) = mpsc::channel(1);
|
||||
let (writer_tx, mut writer_rx) = mpsc::channel(1);
|
||||
|
||||
transport_event_tx
|
||||
.send(TransportEvent::IncomingMessage {
|
||||
connection_id,
|
||||
message: JSONRPCMessage::Notification(JSONRPCNotification {
|
||||
method: "initialized".to_string(),
|
||||
params: None,
|
||||
}),
|
||||
})
|
||||
.await
|
||||
.expect("transport queue should accept first message");
|
||||
|
||||
writer_tx
|
||||
.send(QueuedOutgoingMessage::new(
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::ConfigWarning(
|
||||
ConfigWarningNotification {
|
||||
summary: "queued".to_string(),
|
||||
details: None,
|
||||
path: None,
|
||||
range: None,
|
||||
},
|
||||
)),
|
||||
))
|
||||
.await
|
||||
.expect("writer queue should accept first message");
|
||||
|
||||
let request = JSONRPCMessage::Request(JSONRPCRequest {
|
||||
id: RequestId::Integer(7),
|
||||
method: "config/read".to_string(),
|
||||
params: Some(json!({ "includeLayers": false })),
|
||||
trace: None,
|
||||
});
|
||||
|
||||
let enqueue_result = timeout(
|
||||
Duration::from_millis(100),
|
||||
enqueue_incoming_message(&transport_event_tx, &writer_tx, connection_id, request),
|
||||
)
|
||||
.await
|
||||
.expect("enqueue should not block while writer queue is full");
|
||||
assert!(enqueue_result);
|
||||
|
||||
let queued_outgoing = writer_rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("writer queue should still contain original message");
|
||||
let queued_json =
|
||||
serde_json::to_value(queued_outgoing.message).expect("serialize queued message");
|
||||
assert_eq!(
|
||||
queued_json,
|
||||
json!({
|
||||
"method": "configWarning",
|
||||
"params": {
|
||||
"summary": "queued",
|
||||
"details": null,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,7 @@ impl ClientTracker {
|
||||
})
|
||||
.await
|
||||
}
|
||||
ClientEvent::Ack => Ok(()),
|
||||
ClientEvent::ClientMessageChunk { .. } | ClientEvent::Ack { .. } => Ok(()),
|
||||
ClientEvent::Ping => {
|
||||
if let Some(client) = self.clients.get_mut(&client_key) {
|
||||
client.last_activity_at = Instant::now();
|
||||
@@ -1,6 +1,7 @@
|
||||
mod client_tracker;
|
||||
mod enroll;
|
||||
mod protocol;
|
||||
mod segment;
|
||||
mod websocket;
|
||||
|
||||
use crate::transport::remote_control::websocket::RemoteControlChannels;
|
||||
@@ -35,14 +36,14 @@ pub(super) struct QueuedServerEnvelope {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RemoteControlHandle {
|
||||
pub struct RemoteControlHandle {
|
||||
enabled_tx: Arc<watch::Sender<bool>>,
|
||||
status_tx: Arc<watch::Sender<RemoteControlStatusChangedNotification>>,
|
||||
state_db_available: bool,
|
||||
}
|
||||
|
||||
impl RemoteControlHandle {
|
||||
pub(crate) fn set_enabled(&self, enabled: bool) {
|
||||
pub fn set_enabled(&self, enabled: bool) {
|
||||
let requested_enabled = enabled;
|
||||
let enabled = enabled && self.state_db_available;
|
||||
if requested_enabled && !self.state_db_available {
|
||||
@@ -55,14 +56,12 @@ impl RemoteControlHandle {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_receiver(
|
||||
&self,
|
||||
) -> watch::Receiver<RemoteControlStatusChangedNotification> {
|
||||
pub fn status_receiver(&self) -> watch::Receiver<RemoteControlStatusChangedNotification> {
|
||||
self.status_tx.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn start_remote_control(
|
||||
pub async fn start_remote_control(
|
||||
remote_control_url: String,
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
@@ -121,5 +120,7 @@ pub(crate) async fn start_remote_control(
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod segment_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -47,10 +47,20 @@ pub enum ClientEvent {
|
||||
ClientMessage {
|
||||
message: JSONRPCMessage,
|
||||
},
|
||||
ClientMessageChunk {
|
||||
segment_id: usize,
|
||||
segment_count: usize,
|
||||
message_size_bytes: usize,
|
||||
message_chunk_base64: String,
|
||||
},
|
||||
/// Backend-generated acknowledgement for all server envelopes addressed to
|
||||
/// `client_id` and `stream_id` whose envelope `seq_id` is less than or equal
|
||||
/// to this ack's `seq_id`. This cursor is stream-scoped.
|
||||
Ack,
|
||||
/// to this ack's `seq_id`. Chunk acknowledgements carry `segment_id` so the
|
||||
/// sender can retain only the still-unacked wire chunks on reconnect.
|
||||
Ack {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
segment_id: Option<usize>,
|
||||
},
|
||||
Ping,
|
||||
ClientClosed,
|
||||
}
|
||||
@@ -85,6 +95,12 @@ pub enum ServerEvent {
|
||||
ServerMessage {
|
||||
message: Box<OutgoingMessage>,
|
||||
},
|
||||
ServerMessageChunk {
|
||||
segment_id: usize,
|
||||
segment_count: usize,
|
||||
message_size_bytes: usize,
|
||||
message_chunk_base64: String,
|
||||
},
|
||||
#[allow(dead_code)]
|
||||
Ack,
|
||||
Pong {
|
||||
@@ -92,6 +108,15 @@ pub enum ServerEvent {
|
||||
},
|
||||
}
|
||||
|
||||
impl ServerEvent {
|
||||
pub(crate) fn segment_id(&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::ServerMessageChunk { segment_id, .. } => Some(*segment_id),
|
||||
Self::ServerMessage { .. } | Self::Ack | Self::Pong { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) struct ServerEnvelope {
|
||||
@@ -0,0 +1,449 @@
|
||||
use super::protocol::ClientEnvelope;
|
||||
use super::protocol::ClientEvent;
|
||||
use super::protocol::ClientId;
|
||||
use super::protocol::ServerEnvelope;
|
||||
use super::protocol::ServerEvent;
|
||||
use super::protocol::StreamId;
|
||||
use base64::DecodeSliceError;
|
||||
use base64::Engine;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Write;
|
||||
use tokio::time::Instant;
|
||||
use tracing::warn;
|
||||
|
||||
pub(super) const REMOTE_CONTROL_SEGMENT_TARGET_BYTES: usize = 100 * 1024;
|
||||
pub(super) const REMOTE_CONTROL_SEGMENT_MAX_BYTES: usize = 150 * 1024;
|
||||
pub(super) const REMOTE_CONTROL_REASSEMBLED_MAX_BYTES: usize = 100 * 1024 * 1024;
|
||||
pub(super) const REMOTE_CONTROL_SEGMENT_COUNT_MAX: usize = 1024;
|
||||
const REMOTE_CONTROL_SEGMENT_ASSEMBLY_MAX_COUNT: usize = 128;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientSegmentAssembly {
|
||||
stream_id: StreamId,
|
||||
metadata: ClientSegmentMetadata,
|
||||
raw: Vec<u8>,
|
||||
next_segment_id: usize,
|
||||
last_chunk_seen_at: Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct ClientSegmentMetadata {
|
||||
seq_id: u64,
|
||||
segment_count: usize,
|
||||
message_size_bytes: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct ClientSegmentReassembler {
|
||||
assemblies: HashMap<ClientId, ClientSegmentAssembly>,
|
||||
}
|
||||
|
||||
pub(super) enum ClientSegmentObservation {
|
||||
Forward(Box<ClientEnvelope>),
|
||||
Pending,
|
||||
Dropped,
|
||||
}
|
||||
|
||||
impl ClientSegmentReassembler {
|
||||
pub(super) fn observe(&mut self, envelope: ClientEnvelope) -> ClientSegmentObservation {
|
||||
let ClientEvent::ClientMessageChunk {
|
||||
segment_id,
|
||||
segment_count,
|
||||
message_size_bytes,
|
||||
message_chunk_base64,
|
||||
} = &envelope.event
|
||||
else {
|
||||
return ClientSegmentObservation::Forward(Box::new(envelope));
|
||||
};
|
||||
let segment_id = *segment_id;
|
||||
let segment_count = *segment_count;
|
||||
let message_size_bytes = *message_size_bytes;
|
||||
|
||||
let Some(metadata) = ClientSegmentMetadata::from_envelope(&envelope) else {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"dropping segmented remote-control client envelope without seq_id"
|
||||
);
|
||||
return ClientSegmentObservation::Dropped;
|
||||
};
|
||||
let Some(stream_id) = envelope.stream_id.clone() else {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"dropping segmented remote-control client envelope without stream_id"
|
||||
);
|
||||
return ClientSegmentObservation::Dropped;
|
||||
};
|
||||
if self.should_ignore_chunk(&envelope.client_id, &stream_id, metadata.seq_id, segment_id) {
|
||||
return ClientSegmentObservation::Dropped;
|
||||
}
|
||||
if segment_count == 0
|
||||
|| segment_count > REMOTE_CONTROL_SEGMENT_COUNT_MAX
|
||||
|| segment_id >= segment_count
|
||||
|| message_size_bytes == 0
|
||||
|| message_size_bytes > REMOTE_CONTROL_REASSEMBLED_MAX_BYTES
|
||||
|| message_chunk_base64.is_empty()
|
||||
{
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"dropping invalid segmented remote-control client envelope"
|
||||
);
|
||||
self.remove_assembly(&envelope.client_id, &stream_id);
|
||||
return ClientSegmentObservation::Dropped;
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
match self.assemblies.get(&envelope.client_id) {
|
||||
Some(assembly) if assembly.stream_id != stream_id => {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"resetting segmented remote-control client envelope after stream change"
|
||||
);
|
||||
self.assemblies.insert(
|
||||
envelope.client_id.clone(),
|
||||
ClientSegmentAssembly {
|
||||
stream_id: stream_id.clone(),
|
||||
metadata: metadata.clone(),
|
||||
raw: Vec::new(),
|
||||
next_segment_id: 0,
|
||||
last_chunk_seen_at: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
self.evict_assemblies_if_full();
|
||||
self.assemblies.insert(
|
||||
envelope.client_id.clone(),
|
||||
ClientSegmentAssembly {
|
||||
stream_id: stream_id.clone(),
|
||||
metadata: metadata.clone(),
|
||||
raw: Vec::new(),
|
||||
next_segment_id: 0,
|
||||
last_chunk_seen_at: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
let result = {
|
||||
let Some(assembly) = self.assemblies.get_mut(&envelope.client_id) else {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"dropping segmented remote-control client envelope without assembly"
|
||||
);
|
||||
return ClientSegmentObservation::Dropped;
|
||||
};
|
||||
if metadata.seq_id < assembly.metadata.seq_id {
|
||||
AssemblyUpdate::Ignore
|
||||
} else if assembly.metadata != metadata {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"resetting segmented remote-control client envelope after metadata mismatch"
|
||||
);
|
||||
AssemblyUpdate::Drop
|
||||
} else if segment_id < assembly.next_segment_id {
|
||||
AssemblyUpdate::Pending
|
||||
} else if segment_id != assembly.next_segment_id {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"dropping out-of-order segmented remote-control client envelope"
|
||||
);
|
||||
AssemblyUpdate::Drop
|
||||
} else {
|
||||
assembly.last_chunk_seen_at = now;
|
||||
let chunk_start = assembly.raw.len();
|
||||
let decoded_chunk_len = base64::decoded_len_estimate(message_chunk_base64.len());
|
||||
let chunk_end = usize::min(
|
||||
message_size_bytes,
|
||||
chunk_start.saturating_add(decoded_chunk_len),
|
||||
);
|
||||
assembly.raw.resize(chunk_end, 0);
|
||||
match base64::engine::general_purpose::STANDARD.decode_slice(
|
||||
message_chunk_base64.as_bytes(),
|
||||
&mut assembly.raw[chunk_start..],
|
||||
) {
|
||||
Ok(decoded_chunk_len) => {
|
||||
assembly.raw.truncate(chunk_start + decoded_chunk_len);
|
||||
assembly.next_segment_id += 1;
|
||||
if assembly.next_segment_id < segment_count {
|
||||
AssemblyUpdate::Pending
|
||||
} else if assembly.raw.len() != message_size_bytes {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"dropping reassembled remote-control client envelope with mismatched size"
|
||||
);
|
||||
AssemblyUpdate::Drop
|
||||
} else {
|
||||
match serde_json::from_slice::<JSONRPCMessage>(&assembly.raw) {
|
||||
Ok(message) => AssemblyUpdate::Complete(message),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"dropping invalid reassembled remote-control client envelope: {err}"
|
||||
);
|
||||
AssemblyUpdate::Drop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(DecodeSliceError::OutputSliceTooSmall) => {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"dropping segmented remote-control client envelope after size overflow"
|
||||
);
|
||||
AssemblyUpdate::Drop
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
client_id = envelope.client_id.0.as_str(),
|
||||
"dropping segmented remote-control client envelope with invalid base64: {err}"
|
||||
);
|
||||
AssemblyUpdate::Drop
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
AssemblyUpdate::Pending => ClientSegmentObservation::Pending,
|
||||
AssemblyUpdate::Ignore => ClientSegmentObservation::Dropped,
|
||||
AssemblyUpdate::Drop => {
|
||||
self.remove_assembly(&envelope.client_id, &stream_id);
|
||||
ClientSegmentObservation::Dropped
|
||||
}
|
||||
AssemblyUpdate::Complete(message) => {
|
||||
self.remove_assembly(&envelope.client_id, &stream_id);
|
||||
ClientSegmentObservation::Forward(Box::new(ClientEnvelope {
|
||||
event: ClientEvent::ClientMessage { message },
|
||||
..envelope
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn invalidate_stream(&mut self, client_id: &ClientId, stream_id: &StreamId) {
|
||||
self.remove_assembly(client_id, stream_id);
|
||||
}
|
||||
|
||||
pub(super) fn invalidate_client(&mut self, client_id: &ClientId) {
|
||||
self.assemblies.remove(client_id);
|
||||
}
|
||||
|
||||
pub(super) fn should_ignore_chunk(
|
||||
&self,
|
||||
client_id: &ClientId,
|
||||
stream_id: &StreamId,
|
||||
seq_id: u64,
|
||||
segment_id: usize,
|
||||
) -> bool {
|
||||
self.assemblies.get(client_id).is_some_and(|assembly| {
|
||||
assembly.stream_id == *stream_id
|
||||
&& (seq_id < assembly.metadata.seq_id
|
||||
|| (seq_id == assembly.metadata.seq_id
|
||||
&& segment_id < assembly.next_segment_id))
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_assembly(&mut self, client_id: &ClientId, stream_id: &StreamId) {
|
||||
if self
|
||||
.assemblies
|
||||
.get(client_id)
|
||||
.is_some_and(|assembly| &assembly.stream_id == stream_id)
|
||||
{
|
||||
self.assemblies.remove(client_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn evict_assemblies_if_full(&mut self) {
|
||||
while self.assemblies.len() >= REMOTE_CONTROL_SEGMENT_ASSEMBLY_MAX_COUNT {
|
||||
let Some(client_id) = self
|
||||
.assemblies
|
||||
.iter()
|
||||
.min_by_key(|(_, assembly)| assembly.last_chunk_seen_at)
|
||||
.map(|(client_id, _)| client_id.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.assemblies.remove(&client_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AssemblyUpdate {
|
||||
Pending,
|
||||
Ignore,
|
||||
Drop,
|
||||
Complete(JSONRPCMessage),
|
||||
}
|
||||
|
||||
impl ClientSegmentMetadata {
|
||||
fn from_envelope(envelope: &ClientEnvelope) -> Option<Self> {
|
||||
let ClientEvent::ClientMessageChunk {
|
||||
segment_count,
|
||||
message_size_bytes,
|
||||
..
|
||||
} = &envelope.event
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
Some(Self {
|
||||
seq_id: envelope.seq_id?,
|
||||
segment_count: *segment_count,
|
||||
message_size_bytes: *message_size_bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn split_server_envelope_for_transport(
|
||||
envelope: ServerEnvelope,
|
||||
) -> io::Result<Vec<ServerEnvelope>> {
|
||||
if !matches!(envelope.event, ServerEvent::ServerMessage { .. }) {
|
||||
return Ok(vec![envelope]);
|
||||
}
|
||||
|
||||
let envelope_size_bytes = serialized_len(&envelope)?;
|
||||
if envelope_size_bytes <= REMOTE_CONTROL_SEGMENT_MAX_BYTES {
|
||||
return Ok(vec![envelope]);
|
||||
}
|
||||
|
||||
let ServerEvent::ServerMessage { message } = envelope.event.clone() else {
|
||||
unreachable!("server message variant checked above");
|
||||
};
|
||||
let raw = serde_json::to_vec(message.as_ref()).map_err(io::Error::other)?;
|
||||
let message_size_bytes = raw.len();
|
||||
if message_size_bytes > REMOTE_CONTROL_REASSEMBLED_MAX_BYTES {
|
||||
warn!("dropping remote-control server envelope that exceeds reassembled size limit");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let minimal_segment_count =
|
||||
usize::min(message_size_bytes.max(1), REMOTE_CONTROL_SEGMENT_COUNT_MAX);
|
||||
let minimal_chunk = &raw[..usize::min(raw.len(), 1)];
|
||||
if serialized_chunk_len(
|
||||
&envelope,
|
||||
/*segment_id*/ 0,
|
||||
minimal_segment_count,
|
||||
message_size_bytes,
|
||||
minimal_chunk,
|
||||
)? > REMOTE_CONTROL_SEGMENT_MAX_BYTES
|
||||
{
|
||||
warn!("dropping remote-control server envelope that cannot fit within segment size limit");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut segment_count = usize::max(
|
||||
2,
|
||||
message_size_bytes.div_ceil(REMOTE_CONTROL_SEGMENT_TARGET_BYTES),
|
||||
);
|
||||
loop {
|
||||
let chunk_size = usize::max(1, message_size_bytes.div_ceil(segment_count));
|
||||
segment_count = message_size_bytes.div_ceil(chunk_size);
|
||||
let segments_fit = raw
|
||||
.chunks(chunk_size)
|
||||
.enumerate()
|
||||
.all(|(segment_id, chunk)| {
|
||||
serialized_chunk_len(
|
||||
&envelope,
|
||||
segment_id,
|
||||
segment_count,
|
||||
message_size_bytes,
|
||||
chunk,
|
||||
)
|
||||
.is_ok_and(|size| size <= REMOTE_CONTROL_SEGMENT_MAX_BYTES)
|
||||
});
|
||||
if segments_fit {
|
||||
return raw
|
||||
.chunks(chunk_size)
|
||||
.enumerate()
|
||||
.map(|(segment_id, chunk)| {
|
||||
build_chunk_envelope(
|
||||
&envelope,
|
||||
segment_id,
|
||||
segment_count,
|
||||
message_size_bytes,
|
||||
chunk,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
if chunk_size == 1 {
|
||||
warn!(
|
||||
"dropping remote-control server envelope that cannot fit within segment size limit"
|
||||
);
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let next_segment_count = segment_count + 1;
|
||||
let next_chunk_size = usize::max(1, message_size_bytes.div_ceil(next_segment_count));
|
||||
segment_count = if next_chunk_size == chunk_size {
|
||||
message_size_bytes
|
||||
} else {
|
||||
next_segment_count
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_chunk_len(
|
||||
envelope: &ServerEnvelope,
|
||||
segment_id: usize,
|
||||
segment_count: usize,
|
||||
message_size_bytes: usize,
|
||||
chunk: &[u8],
|
||||
) -> io::Result<usize> {
|
||||
serialized_len(&build_chunk_envelope(
|
||||
envelope,
|
||||
segment_id,
|
||||
segment_count,
|
||||
message_size_bytes,
|
||||
chunk,
|
||||
)?)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CountingWriter {
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Write for CountingWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.len += buf.len();
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_len(value: &impl serde::Serialize) -> io::Result<usize> {
|
||||
let mut writer = CountingWriter::default();
|
||||
serde_json::to_writer(&mut writer, value).map_err(io::Error::other)?;
|
||||
Ok(writer.len)
|
||||
}
|
||||
|
||||
fn build_chunk_envelope(
|
||||
envelope: &ServerEnvelope,
|
||||
segment_id: usize,
|
||||
segment_count: usize,
|
||||
message_size_bytes: usize,
|
||||
chunk: &[u8],
|
||||
) -> io::Result<ServerEnvelope> {
|
||||
if segment_count > REMOTE_CONTROL_SEGMENT_COUNT_MAX {
|
||||
return Err(io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"remote-control segment count exceeds maximum",
|
||||
));
|
||||
}
|
||||
Ok(ServerEnvelope {
|
||||
event: ServerEvent::ServerMessageChunk {
|
||||
segment_id,
|
||||
segment_count,
|
||||
message_size_bytes,
|
||||
message_chunk_base64: base64::engine::general_purpose::STANDARD.encode(chunk),
|
||||
},
|
||||
client_id: envelope.client_id.clone(),
|
||||
stream_id: envelope.stream_id.clone(),
|
||||
seq_id: envelope.seq_id,
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user