Compare commits

..
Author SHA1 Message Date
Hunter Bown 9272789447 fix: skills: the ambient index caps descriptions at 400 chars with no config knob and no signal in the prompt or to the model (#7073)
Produced by CodeWhale running in a Boat VM. Untested.
2026-10-24 22:29:54 -07:00
2901 changed files with 216383 additions and 422328 deletions

No files matched your search

-23
View File
@@ -1,23 +0,0 @@
[[hooks]]
name = "shell guard (staging, release freeze)"
event = "tool_call_before"
command = 'cd "$DEEPSEEK_WORKSPACE" && echo "4c8e10ebe99524e6a5b2f0fbb090a8f4881f95b2bf732f020c566a610f949aa4 scripts/agent-guard.py" | shasum -a 256 -c -s && exec python3 scripts/agent-guard.py'
timeout_secs = 10
continue_on_error = true
condition = { type = "tool_name", name = "bash" }
[[hooks]]
name = "edit guard (CODEWHALE_RELEASE_FREEZE=1)"
event = "tool_call_before"
command = '[ -z "$CODEWHALE_RELEASE_FREEZE" ] || { cd "$DEEPSEEK_WORKSPACE" && echo "4c8e10ebe99524e6a5b2f0fbb090a8f4881f95b2bf732f020c566a610f949aa4 scripts/agent-guard.py" | shasum -a 256 -c -s && exec python3 scripts/agent-guard.py; }'
timeout_secs = 10
continue_on_error = true
[hooks.condition]
type = "any"
conditions = [
{ type = "tool_category", category = "file_write" },
{ type = "tool_name", name = "write_file" },
{ type = "tool_name", name = "edit_file" },
{ type = "tool_name", name = "fim_edit" },
]
+3 -3
View File
@@ -73,14 +73,14 @@ filter = 'binary(telemetry_kill_switch_dispatch)'
test-group = 'telemetry-contract'
[[profile.default.overrides]]
filter = 'package(codewhale-engine) & kind(lib) & test(/^fleet::manager::tests::/)'
filter = 'package(codewhale-tui) & kind(lib) & test(/^fleet::manager::tests::/)'
test-group = 'fleet-manager-lifecycle'
[[profile.default.overrides]]
# DSH preparation also launches the isolated host against the shared hermetic
# home. OCR/PDF consumers also launch real hosts outside extension_host's
# namespace. Keep their process lifecycle checks in this existing serial lane.
filter = '(package(codewhale-engine) | package(codewhale-tui)) & kind(lib) & (test(/^extension_host::/) | test(/^tools::image_ocr::host_tests::/) | test(/^tools::pdf::host_tests::/) | test(/^core::engine::.*extension/) | test(/^plugins::install::dsh::/) | test(/^plugins::tests::dsh_/) | test(/^runtime_api::tests::dsh_package_preview_then_exact_install_over_http$/) | test(/^commands::contract::tests::plugin_adapter_dsh_preview_runs_from_live_runtime$/) | test(/^commands::groups::plugins::tests::dsh_import_reviews_without_installing_then_installs_the_exact_bundle$/))'
filter = 'package(codewhale-tui) & kind(lib) & (test(/^extension_host::/) | test(/^tools::image_ocr::host_tests::/) | test(/^tools::pdf::host_tests::/) | test(/^core::engine::.*extension/) | test(/^plugins::install::dsh::/) | test(/^plugins::tests::dsh_/) | test(/^runtime_api::tests::dsh_package_preview_then_exact_install_over_http$/) | test(/^commands::contract::tests::plugin_adapter_dsh_preview_runs_from_live_runtime$/) | test(/^commands::groups::plugins::tests::dsh_import_reviews_without_installing_then_installs_the_exact_bundle$/))'
test-group = 'extension-host'
[[profile.default.overrides]]
@@ -116,5 +116,5 @@ path = "junit.xml"
report-name = "codewhale-native-host"
[[profile.ci.overrides]]
filter = 'package(codewhale-engine) & kind(lib) & (test(/^extension_host::tests::compiled_native_host_cannot_read_secrets_or_write_outside_its_data_dir$/) | test(/^extension_host::tests::compiled_native_host_memory_cap_is_enforced$/))'
filter = 'package(codewhale-tui) & kind(lib) & (test(/^extension_host::tests::compiled_native_host_cannot_read_secrets_or_write_outside_its_data_dir$/) | test(/^extension_host::tests::compiled_native_host_memory_cap_is_enforced$/))'
junit.store-success-output = true
+6 -12
View File
@@ -3,9 +3,9 @@
# produces different compiled binaries on Windows vs Linux/macOS.
# Cover nested mode/approval/personality prompts — a top-level *.md glob alone
# left modes/*.md on CRLF under Windows autocrlf and blew the agent token budget.
crates/engine/src/prompts/**/*.md text eol=lf
crates/engine/src/prompts/*.md text eol=lf
crates/engine/src/prompts/*.txt text eol=lf
crates/tui/src/prompts/**/*.md text eol=lf
crates/tui/src/prompts/*.md text eol=lf
crates/tui/src/prompts/*.txt text eol=lf
# The rest of the include_str!() inputs. `* text=auto` below converts these to
# CRLF on a Windows checkout, which is invisible for the ones that are only
@@ -58,12 +58,11 @@ scripts/fixtures/dsh-web-app/** text eol=lf
# the generator's LF output (protocol/tests.rs), and its dist bundle is
# include_bytes!()'d and digest-checked at launch; a CRLF Windows checkout
# failed the former and would embed different bytes.
crates/engine/extension-host/src/protocol.generated.ts text eol=lf
crates/engine/extension-host/dist/** text eol=lf
crates/tui/extension-host/src/protocol.generated.ts text eol=lf
crates/tui/extension-host/dist/** text eol=lf
# Reviewed extension-host fixtures carry exact source SHA-256 receipts.
# A Windows checkout must not rewrite their admitted source closure to CRLF.
crates/engine/tests/fixtures/extension_host/** text eol=lf
crates/engine/extension-host/src/dsh/dsh-message-compat.js text eol=lf
crates/tui/tests/fixtures/extension_host/** text eol=lf
# Rustfmt writes LF; keep Rust sources stable across Windows/Linux/macOS.
*.rs text eol=lf
@@ -80,8 +79,3 @@ scripts/release/*.sh text eol=lf
# Preserve the qualified upstream font license, including its trailing space.
web/public/brand/fonts/OFL.txt text eol=lf whitespace=-blank-at-eol
# Generated protocol and SDK exports are compared byte for byte; keep LF on Windows checkouts.
crates/app-server-protocol/schema/** text eol=lf
crates/app-server-protocol/METHODS.md text eol=lf
npm/runtime-sdk/generated/** text eol=lf
+77
View File
@@ -0,0 +1,77 @@
# Scoped contribution-gate allowlist.
#
# Maintainers and collaborators bypass the gate automatically. Use this file
# for external contributors who are allowed through the automated front door.
# Seed active contributors here before switching the gate workflows to enforce mode.
#
# Supported entries:
# pr:username
# issue:username
# all:username
all:hmbown
all:reidliu41
all:ousamabenyounes
all:ljm3790865
all:HUQIANTAO
all:xyuai
all:merchloubna70-dot
all:h3c-hexin
all:axobase001
all:donglovejava
all:Oliver-ZPLiu
all:idling11
all:angziii
all:aboimpinto
all:encyc
all:Duducoco
all:cyq1017
all:zlh124
all:THINKER-ONLY
all:nightt5879
all:Liu-Vince
all:JiarenWang
all:wdw8276
all:pengyou200902
all:linzhiqin2003
all:LING71671
all:JasonOA888
all:Inference1
all:hongqitai
all:gordonlu
all:gaord
all:zhuangbiaowei
all:yuanchenglu
all:Vishnu1837
all:sximelon
all:Sskift
all:New2Niu
all:shenjackyuanjie
all:AdityaVG13
all:mvanhorn
all:MengZ-super
all:membphis
all:LeoAlex0
all:Lee-take
all:lbcheng888
all:Implementist
all:jrcjrcc
all:yusufgurdogan
all:kunpeng-ai-lab
all:elowen53
all:CrepuscularIRIS
all:chnjames
all:ChaceLyee2101
all:AresNing
pr:dmitri-0
pr:sparkofspike
pr:angel-hair
pr:wuisabel-gif
all:whp233
all:musichen
all:M-Maciej
all:serephus
all:Pinvou
all:SparkofSpike
all:goransh-walia
all:xiechimon
all:yrk111222
-8
View File
@@ -242,8 +242,6 @@ huangxianzhan = huangxianzhan <[email protected]>
[email protected] = huangxianzhan <[email protected]>
zhuowp = zhuowp <[email protected]>
[email protected] = zhuowp <[email protected]>
z27014 = zhuowp <[email protected]>
[email protected] = zhuowp <[email protected]>
[email protected] = 秋月凉梦 <[email protected]>
asto18089 = asto18089 <[email protected]>
@@ -272,9 +270,3 @@ Henrik Bakke Dukefoss = Henrik Bakke Dukefoss <[email protected]
[email protected] = Henrik Bakke Dukefoss <[email protected]>
[email protected] = Dhruv Singal <[email protected]>
[email protected] = Boyce <[email protected]>
Boyce = Boyce <[email protected]>
Felixkw12 = Felix Wunderlich <[email protected]>
[email protected] = Felix Wunderlich <[email protected]>
Felix Wunderlich = Felix Wunderlich <[email protected]>
[email protected] = luochen211 <[email protected]>
+45 -51
View File
@@ -1,68 +1,62 @@
# Berkontribusi pada Codewhale
Codewhale berkembang berkat orang yang menggunakannya, melaporkan masalah,
menerjemahkan dokumentasi, menambahkan penyedia model, dan mengirim perbaikan.
Anda dapat berkontribusi tanpa menulis kode. Semua masukan diterima.
Terima kasih atas minat Anda untuk berkontribusi pada Codewhale! Dokumen ini memberikan panduan dan instruksi untuk berkontribusi.
[Roadmap](../docs/ROADMAP.md) menjelaskan tujuan proyek dan pekerjaan yang masih
terbuka. [Panduan utama](../CONTRIBUTING.md) memuat prosedur pengembangan,
pengujian, dan peninjauan yang lengkap dan terbaru.
---
## Pilih tempat yang sesuai
## Memulai (Getting Started)
| Yang ingin Anda sampaikan | Tempatnya |
| --- | --- |
| Bug, crash, atau perilaku yang tidak diharapkan | [Laporan bug](https://github.com/codewhale-hq/CodeWhale/issues/new?template=bug_report.yml) |
| Ide atau permintaan fitur | [Permintaan fitur](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feature_request.yml) |
| Masukan tentang tampilan, tulisan, atau pengalaman penggunaan | [Formulir masukan](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feedback.yml) |
| Pertanyaan | [Tanya jawab](https://github.com/codewhale-hq/CodeWhale/discussions/categories/q-a) atau [Discord](https://discord.gg/37gfS3ksug) |
| Masalah keamanan | [Laporan keamanan privat](https://github.com/codewhale-hq/CodeWhale/security/advisories/new), bukan issue publik |
| Perbaikan untuk issue yang sudah ada | Pull request yang menautkan issue tersebut |
| Terjemahan, penyedia model, atau perbaikan dokumentasi | Pull request; tidak perlu membuka issue terlebih dahulu |
### Prasyarat
Jika ragu, buka issue dan jelaskan apa yang Anda coba lakukan, apa yang terjadi,
dan apa yang Anda harapkan. Pengelola akan membantu mengarahkannya.
- Rust 1.88 atau lebih baru (edisi 2024)
- Pengelola paket Cargo
- Git
## Mulai fitur baru dengan issue
### Menyiapkan Lingkungan Pengembangan
Untuk fitur baru, buka issue sebelum mulai menulis kode. Jelaskan kebutuhan dan
hambatannya; rancangan solusi boleh disertakan, tetapi tidak wajib. Jika ingin
mengerjakannya sendiri, centang pilihan untuk membuka pull request pada formulir.
1. Fork dan klon repositori:
```bash
git clone https://github.com/USERNAME_ANDA/CodeWhale.git
cd CodeWhale
```
Pengelola dapat membahas cakupan dan perilaku fitur sebelum Anda menghabiskan
waktu untuk implementasi. Pull request fitur tanpa issue tetap dibaca, tetapi
pembahasannya biasanya membutuhkan waktu lebih lama.
2. Kompilasi proyek:
```bash
cargo build
```
## Pekerjaan yang dapat langsung Anda kirim
3. Jalankan pengujian (tests):
```bash
cargo test --workspace --all-features
```
Anda dapat langsung mengirim pull request untuk terjemahan, penyedia dan model,
perbaikan dokumentasi, atau perbaikan bug. Jika bug belum memiliki issue,
jelaskan cara memunculkannya, hasil yang terjadi, dan hasil yang diharapkan di
pull request.
4. Jalankan dalam mode pengembangan:
```bash
cargo run --bin codewhale
```
Untuk pekerjaan yang sudah direncanakan, pilih issue di
[milestone](../docs/ROADMAP.md#releases) atau yang berlabel
[help wanted](https://github.com/codewhale-hq/CodeWhale/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).
Beri komentar bahwa Anda ingin mengerjakannya agar pekerjaan tidak tumpang tindih.
[Lihat proyek terbuka](../CONTRIBUTING.md#standing-projects) untuk tautan ke
panduan terjemahan dan penyedia model.
---
## Siapkan perubahan Anda
## Alur Kerja Pengembangan
Ikuti [petunjuk persiapan pengembangan](../CONTRIBUTING.md#getting-started)
untuk persyaratan Rust, perintah build, dan cara menjalankan versi sumber.
Versi di `main` dapat lebih baru daripada unduhan rilis; sertakan keluaran
`codewhale --version` ketika melaporkan masalah.
### Gaya Kode (Code Style)
Jalankan pemeriksaan yang sesuai dengan bagian yang Anda ubah, mengikuti
[panduan pengujian lokal](../CONTRIBUTING.md#fast-local-loop). Sebelum mengirim
pull request, baca perubahan Anda sendiri dan jelaskan apa yang berubah serta
bagaimana Anda memeriksanya. Jangan sertakan kunci API atau data pribadi.
- Jalankan `cargo fmt` sebelum melakukan commit untuk memastikan format kode konsisten.
- Jalankan `cargo clippy` dan atasi seluruh peringatan.
- Ikuti konvensi penamaan Rust (`snake_case` untuk fungsi/variabel, `CamelCase` untuk tipe data/struct).
- Tambahkan komentar dokumentasi untuk API publik.
## Peninjauan dan kredit
### Pengujian
Pull request Anda digabungkan sebagai kontribusi Anda sendiri. Pengelola
membantu menyelesaikan konflik akibat perubahan di `main`; pemeriksaan yang
merah karena masalah lain bukan tanggung jawab Anda. Jika perubahan belum siap,
pengelola menjelaskan alasannya. Kontribusi dicatat di changelog dan
[daftar kontributor](../docs/CONTRIBUTORS.md).
- Tulis pengujian untuk fungsionalitas baru.
- Pastikan seluruh pengujian yang ada lulus: `cargo test --workspace --all-features`.
- Tempatkan unit test di samping kode yang diuji (`#[cfg(test)]`), serta tambahkan pengujian integrasi di bawah direktori `tests/` pada crate yang bersangkutan.
---
## Pengajuan Pull Request (PR)
1. Buat branch baru dari `main` untuk fitur atau perbaikan Anda.
2. Pastikan `cargo fmt`, `cargo clippy`, dan pengujian lulus sebelum mengajukan PR.
3. Tulis judul dan deskripsi PR yang jelas yang menjelaskan tujuan perubahan.
4. Setiap kontributor akan mendapatkan kredit dalam changelog dan `docs/CONTRIBUTORS.md`.
-3
View File
@@ -3,9 +3,6 @@ contact_links:
- name: Report a security vulnerability
url: https://github.com/codewhale-hq/CodeWhale/security/advisories/new
about: Please report security problems privately, not in a public issue. See SECURITY.md.
- name: Roadmap and where to help
url: https://github.com/codewhale-hq/CodeWhale/blob/main/docs/ROADMAP.md
about: Where Codewhale is going, what each release covers, and good places to start. 路线图。
- name: Ask a question
url: https://github.com/codewhale-hq/CodeWhale/discussions
about: How-to questions and ideas you want to talk through. 提问和讨论。
+1 -3
View File
@@ -7,8 +7,6 @@ body:
value: |
Thanks for the idea. English or 中文 are both fine. Short is good.
This is the best place to start a feature, even if you plan to build it yourself: agreeing on the shape here makes the pull request small and quick to merge. The [roadmap](https://github.com/codewhale-hq/CodeWhale/blob/main/docs/ROADMAP.md) shows what is already planned.
If someone already asked for this, add a 👍 to their issue instead.
Want to talk it through first? Use [Discussions](https://github.com/codewhale-hq/CodeWhale/discussions) or [Discord](https://discord.gg/37gfS3ksug).
@@ -63,4 +61,4 @@ body:
attributes:
label: Contributing
options:
- label: I would like to open a pull request for this, once we agree on the shape. 我愿意在方案确定后提交 PR。
- label: I would like to open a pull request for this. 我愿意提交 PR。
-25
View File
@@ -1,25 +0,0 @@
name: Feedback
description: Something looked, read or felt off, or you have a thought to share. 反馈与感受。
labels: ["needs-triage"]
body:
- type: markdown
attributes:
value: |
All feedback is welcome, and you do not need a fix or a plan. English or 中文 are both fine.
First impressions, confusing wording, an awkward step, a screen that looks wrong, something you liked: it all helps.
For something broken, the [bug report](https://github.com/codewhale-hq/CodeWhale/issues/new?template=bug_report.yml) form asks for the details we need to reproduce it.
- type: textarea
id: feedback
attributes:
label: What would you like us to know? 想告诉我们什么?
description: What you were doing, and what you noticed. A screenshot helps for anything visual.
validations:
required: true
- type: input
id: version
attributes:
label: Codewhale version (optional) 版本
description: Output of `codewhale --version`, if you have it.
+11 -9
View File
@@ -4,20 +4,22 @@
<!-- What does this change, and why? A few sentences is enough. -->
## Where this came from
## Issue
<!-- Closes #N if this finishes an issue, Refs #N if related. Translations,
provider and model updates, docs fixes and bug fixes need no issue: just say
which it is. For a new feature with no issue yet, opening one first is the
faster path (CONTRIBUTING.md explains why); we still read this PR either way.
Please do not write a closing word inside a negation ("this does not close #N"):
GitHub closes the issue anyway. -->
<!-- CI needs one line here. Replace N with a number:
- Closes #N this PR finishes the issue (Fixes / Resolves also work)
- Refs #N related or partial work; the issue stays open
- No-Issue: <one-line reason> for typos, chores, dependency bumps
Only write a closing word when you mean it. GitHub closes the issue even in
"this does not close ..." and CI rejects that wording.
Put issue numbers here rather than in `feat:` commit messages: CI requires a
`feat:` commit that mentions #N to also add #N to CHANGELOG.md. -->
## How I tested it
<!-- Commands you ran and what they showed. Test what you changed, for example
`scripts/dev-test.sh tui <filter>`. A few quick checks run on your PR; if one
fails for a reason unrelated to your change, a maintainer will deal with it.
`scripts/dev-test.sh tui <filter>`; the PR checks run fmt, clippy, the Linux
suite and a Windows build for you (CONTRIBUTING.md lists them).
For visible UI changes, add a screenshot or recording. -->
## Checklist
+16 -7
View File
@@ -5,10 +5,8 @@ repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo}"
template=".github/ISSUE_TEMPLATE/agent-task.yml"
retired_workflows=(
".github/workflows/v0868-milestone-sync.yml"
".github/workflows/agent-task-labels.yml"
)
workflow=".github/workflows/agent-task-labels.yml"
retired_workflow=".github/workflows/v0868-milestone-sync.yml"
fail() {
echo "agent-task metadata contract failed: $*" >&2
@@ -16,9 +14,8 @@ fail() {
}
[[ -f "${template}" ]] || fail "missing ${template}"
for retired in "${retired_workflows[@]}"; do
[[ ! -e "${retired}" ]] || fail "retired workflow still exists: ${retired}"
done
[[ -f "${workflow}" ]] || fail "missing ${workflow}"
[[ ! -e "${retired_workflow}" ]] || fail "retired milestone sync still exists"
if grep -nE '^(title|labels):.*v[0-9]+\.[0-9]+\.[0-9]+' "${template}"; then
fail "agent-task defaults must not pin a release version"
@@ -26,5 +23,17 @@ fi
grep -qF 'labels: ["agent-ready"]' "${template}" \
|| fail "agent-task template must opt into agent-ready without a version label"
grep -qF 'types: [opened]' "${workflow}" \
|| fail "agent-task labeling must run only when an issue is opened"
grep -qF "labels.has('agent-ready')" "${workflow}" \
|| fail "agent-task labeling must be gated by the explicit agent-ready label"
if grep -nE 'listMilestones|issues\.update|^[[:space:]]*milestone:' "${workflow}"; then
fail "agent-task automation must never read or assign milestones"
fi
if grep -nE 'types:.*(labeled|milestoned)' "${workflow}"; then
fail "agent-task automation must not react to later label or milestone edits"
fi
echo "Agent-task metadata is release-neutral and milestone-safe."
+27 -58
View File
@@ -69,7 +69,7 @@ assert.match(
"Engine acceptance must build the canonical executable first",
);
const npmSmokeJob = ci.match(/^ npm-wrapper-smoke:\n([\s\S]*?)(?=^ \S)/m)?.[1];
assert.ok(npmSmokeJob, "CI must retain the npm-wrapper job");
assert.ok(npmSmokeJob, "CI must retain the required npm-wrapper job");
assert.match(
namedStep(npmSmokeJob, "Build wrapper binaries"),
/run: cargo build --release --locked -p codewhale-cli --bin codewhale/,
@@ -79,19 +79,19 @@ assert.match(
/run: node scripts\/release\/npm-wrapper-smoke\.js/,
);
const npmSmokeSteps = npmSmokeJob.split(/(?=^ - )/m).slice(1);
// Exercise the workflow's actual Boolean guards. The smoke is manual-only
// (workflow_dispatch); pull requests, pushes and schedules skip the job.
// Exercise the workflow's actual Boolean guards. A successful location echo
// must never substitute for the build/install smoke on a heavy pull request.
const npmSmokeCases = [
// name, event, heavy, OS, trusted, cache success, execute, Linux deps, CNB
["own PR", "pull_request", true, "ubuntu-latest", true, true, false, false, false],
["fork PR", "pull_request", true, "ubuntu-latest", false, true, false, false, false],
["PR cache failure", "pull_request", true, "ubuntu-latest", false, false, false, false, false],
["own PR", "pull_request", true, "ubuntu-latest", true, true, true, true, false],
["fork PR", "pull_request", true, "ubuntu-latest", false, true, true, true, false],
["PR cache failure", "pull_request", true, "ubuntu-latest", false, false, true, true, false],
["light PR", "pull_request", false, "ubuntu-latest", true, true, false, false, false],
["manual Ubuntu", "workflow_dispatch", true, "ubuntu-latest", true, true, true, true, false],
["main Ubuntu", "push", true, "ubuntu-latest", true, true, false, false, false],
["main Ubuntu", "push", true, "ubuntu-latest", true, true, false, false, true],
["manual macOS", "workflow_dispatch", true, "macos-latest", true, true, true, false, false],
["main Windows", "push", true, "windows-latest", true, true, false, false, false],
["main cache failure", "push", true, "windows-latest", true, false, false, false, false],
["main Windows", "push", true, "windows-latest", true, true, true, false, false],
["main cache failure", "push", true, "windows-latest", true, false, true, false, false],
["light main", "push", false, "ubuntu-latest", true, true, false, false, false],
["schedule", "schedule", true, "ubuntu-latest", true, true, false, false, false],
];
@@ -121,7 +121,6 @@ for (const [label, event, heavy, os, trusted, cache, execute, linuxDeps, cnb] of
else if (name === "Linux smoke location") expected = cnb;
else if (name === sccacheInstallStep) expected = installed;
else if (name === "Enable sccache" || name === "sccache stats") expected = installed && cache;
if (!jobEnabled) expected = false;
assert.equal(
Boolean(jobEnabled && vm.runInNewContext(guard, context)),
expected,
@@ -129,14 +128,15 @@ for (const [label, event, heavy, os, trusted, cache, execute, linuxDeps, cnb] of
);
}
}
// The matrix itself: a manual full-CI dispatch covers Ubuntu and Windows; the
// release artifacts cover macOS.
// The matrix itself: pull requests keep the single required Ubuntu context,
// main pushes skip the hosted-macOS leg (5-job concurrency cap), and a manual
// full-CI dispatch still covers all three platforms.
const npmSmokeMatrix = npmSmokeJob.match(/^ os: \$\{\{ fromJSON\((.+)\) \}\}$/m)?.[1];
assert.ok(npmSmokeMatrix, "the wrapper job must keep an event-keyed OS matrix");
for (const [event, expected] of [
["pull_request", ["ubuntu-latest"]],
["push", ["ubuntu-latest", "windows-latest"]],
["workflow_dispatch", ["ubuntu-latest", "windows-latest"]],
["workflow_dispatch", ["ubuntu-latest", "macos-latest", "windows-latest"]],
]) {
assert.deepEqual(
JSON.parse(vm.runInNewContext(npmSmokeMatrix, { github: { event_name: event } })),
@@ -362,7 +362,6 @@ const expectedTargets = [
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-musl",
"aarch64-linux-android",
"aarch64-unknown-linux-ohos",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
@@ -396,7 +395,7 @@ const builtAssetNames = [
...valuesForKey(artifacts, "shim_artifact"),
...valuesForKey(artifacts, "compat_tui_artifact"),
];
assert.equal(builtAssetNames.length, 24);
assert.equal(builtAssetNames.length, 21);
assert.deepEqual(
[...new Set(builtAssetNames)].sort(),
[
@@ -418,44 +417,6 @@ const bundleInvocations = [...bundles.matchAll(
assert.deepEqual(bundleInvocations.sort(), [...BUNDLE_ASSET_NAMES].sort());
assert.match(artifacts, /aarch64-pc-windows-msvc/);
assert.match(artifacts, /aarch64-linux-android/);
// HarmonyOS bytes cannot be shipped unsigned: the device refuses to execute an
// unsigned ELF, so the leg must self-sign and prove the signature before
// staging. Assert the order too — signing after staging would publish the
// unsigned copy.
assert.match(artifacts, /aarch64-unknown-linux-ohos/);
const ohosSdk = namedStep(artifacts, "Install OpenHarmony native SDK");
assert.match(ohosSdk, /components: 'native;toolchains'/, "the SDK must include the binary signer");
assert.match(ohosSdk, /cache: false/, "a release must download the SDK, not publish a new cache entry");
// A step header glued onto the previous line because a `run:` block lost its
// trailing newline parses as valid YAML (the header becomes script text) and
// silently drops the step. Every `- name:` in these workflows must start its
// own line.
for (const [file, source] of [
["release-artifacts.yml", artifacts],
["release.yml", release],
["nightly.yml", nightly],
]) {
for (const match of source.matchAll(/- name:/g)) {
const lineStart = source.lastIndexOf("\n", match.index) + 1;
assert.match(
source.slice(lineStart, match.index),
/^\s*$/,
`${file}: a step header must start its own line, not follow other content`,
);
}
}
const ohosBuild = namedStep(artifacts, "Cross-compile and code-sign the OpenHarmony binary");
assert.match(ohosBuild, /matrix\.platform == 'ohos-arm64'/);
assert.match(ohosBuild, /binary-sign-tool\.jar/);
assert.match(ohosBuild, /-selfSign 1/);
assert.match(ohosBuild, /code signature is self-sign/);
assert.match(ohosBuild, /scripts\/ohos-env\.sh/);
assert.ok(
ohosBuild.indexOf("codesign") === -1 ||
ohosBuild.indexOf("binary-sign-tool.jar") < ohosBuild.indexOf("display-sign"),
"the signature must be applied before it is verified",
);
assert.match(artifacts, /codew-windows-arm64\.exe/);
assert.match(artifacts, /CodeWhaleSetup\.exe/);
assert.match(artifacts, /assemble-release-assets\.js --verify release-assets/);
@@ -530,7 +491,7 @@ const parityRustCache = [...parity[1].matchAll(/uses: Swatinem\/rust-cache@[\s\S
assert.equal(parityRustCache.length, 1, "parity must pin exactly one rust-cache");
assert.doesNotMatch(parityRustCache[0], /github\.(event|ref|sha)|inputs\./);
assert.equal(allReleaseAssetNames().length, 38);
assert.equal(allReleaseAssetNames().length, 34);
assert.match(release, /^ artifacts:\n/m);
assert.match(release, /uses: \.\/\.github\/workflows\/release-artifacts\.yml/);
assert.doesNotMatch(release, /^ (build|bundle|windows-installer):/m);
@@ -659,7 +620,7 @@ assert.doesNotMatch(
assert.match(runbook, /release[- ]candidate/i);
assert.match(runbook, /expected_sha/);
assert.match(runbook, /38/);
assert.match(runbook, /34/);
assert.match(runbook, /does not create a tag/i);
assert.match(runbook, /explicit.*approval/i);
assert.match(runbook, /last[- ]useful[- ]log/i, "runbook must document the last-useful-log rule (#5496)");
@@ -789,10 +750,18 @@ assert.equal(
2,
"both glibc recovery branches must name codewhale-cli",
);
// The archive installer never overwrites an existing command: it validates the
// retired TUI path against the consolidated bytes and leaves upgrades to
// `codewhale update`, which migrates `codewhale-tui` beside the canonical pair.
assert.match(
archiveInstaller,
/legacy_tui="\$BIN_DIR\/codewhale-tui"[\s\S]*check_destination "\$SCRIPT_DIR\/codewhale" "\$legacy_tui"/,
"archive installs must validate the retired TUI path against consolidated bytes",
);
assert.doesNotMatch(
archiveInstaller,
/install_binary "\$SCRIPT_DIR\/[^"]+" "\$BIN_DIR\/codewhale-tui"/,
"archive installs must not write the retired codewhale-tui command",
/install_binary "\$SCRIPT_DIR\/codewhale" "\$legacy_tui"/,
"archive installs must not overwrite an existing retired TUI command",
);
assert.doesNotMatch(
cliDispatcher,
@@ -905,5 +874,5 @@ assert.ok(
);
console.log(
"Workflow contracts OK: 6-target/12-asset single-runtime nightly and exact-head 8-target/38-asset release candidate.",
"Workflow contracts OK: 6-target/12-asset single-runtime nightly and exact-head 7-target/34-asset release candidate.",
);
+67
View File
@@ -0,0 +1,67 @@
name: Agent task labels
on:
issues:
types: [opened]
permissions:
issues: write
contents: read
jobs:
labels:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Add area hints to agent tasks
uses: actions/github-script@v9
with:
script: |
const issue = context.payload.issue;
if (!issue) return;
const labels = new Set((issue.labels || []).map(label => label.name));
if (!labels.has('agent-ready')) {
core.info(`#${issue.number} is not an agent task; leaving it unchanged.`);
return;
}
// Milestones are the canonical release roadmap and are assigned
// explicitly during triage. Never infer one from a title or label:
// old version labels must remain harmless historical metadata.
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = `${title}\n${body}`;
if (/\bworkflow\b|whaleflow|workflow-runtime/.test(text)) labels.add('workflow-runtime');
// `tui` means "touches the terminal UI layer", not "mentions the TUI
// in passing". A bare \btui\b over-fired on every issue that merely
// referenced the UI — the v0.9.2 localization issues all describe TUI
// locale packs in prose and were labelled on that alone.
//
// Anchor on signals that survive the single-binary refactor (#4747):
// the crate path and the rendering framework stay put when
// codewhale-tui is lib-ized, whereas the `codewhale-tui` *binary*
// name goes away. Do not reintroduce a bare word match here.
if (/crates\/tui\b|ratatui|\bcodewhale-tui\b/.test(text)) labels.add('tui');
if (/fleet|subagent|sub-agent/.test(text)) labels.add('subagents');
if (/catalog|openrouter|model.?picker|provider.?lake/.test(text)) labels.add('reliability');
const existing = await github.paginate(github.rest.issues.listLabelsForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const existingNames = new Set(existing.map(label => label.name));
const currentNames = new Set((issue.labels || []).map(label => label.name));
const toAdd = [...labels].filter(
name => existingNames.has(name) && !currentNames.has(name)
);
if (toAdd.length === 0) return;
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: toAdd,
});
+216
View File
@@ -0,0 +1,216 @@
name: Approve gated contributor
on:
issue_comment:
types: [created]
permissions: {}
jobs:
approve:
# Cheap pre-filter before any token is minted: this workflow fires on
# every issue comment, but only a maintainer's lgtm/lgtmi command does
# anything. Expression string comparisons are case-insensitive; the
# script below still performs the exact trimmed-command match.
if: >-
contains(github.event.comment.body, 'lgtm')
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
issues: write
pull-requests: write
# Job-level so skipped runs never enter the group: a workflow-level group
# let any unrelated comment queue and cancel a pending approval run.
concurrency:
group: contribution-gate-approval
cancel-in-progress: false
steps:
- name: Open allowlist update PR
uses: actions/github-script@v9
with:
script: |
const comment = context.payload.comment;
const issue = context.payload.issue;
const owner = context.repo.owner;
const repo = context.repo.repo;
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
const command = (comment.body || '').trim().toLowerCase();
const scopeByCommand = new Map([
['/lgtm', 'pr'],
['lgtm', 'pr'],
['/lgtmi', 'issue'],
['lgtmi', 'issue'],
]);
const scope = scopeByCommand.get(command);
// Answer the maintainer's command with a reaction, never a comment
// (founder, 2026-09-22). The run log carries the detail, and the
// allowlist PR body links back here, so the thread still shows it.
async function react(content, message) {
core.notice(message);
await github.rest.reactions.createForIssueComment({
owner,
repo,
comment_id: comment.id,
content,
});
}
if (!scope) return;
if (!privileged.has(comment.author_association)) return;
if (scope === 'pr' && !issue.pull_request) {
await react('confused', '`/lgtm` grants PR access and must be used on a pull request. Use `/lgtmi` to grant issue access.');
return;
}
if (scope === 'issue' && issue.pull_request) {
await react('confused', '`/lgtmi` grants issue access and must be used on an issue. Use `/lgtm` to grant PR access.');
return;
}
const path = '.github/APPROVED_CONTRIBUTORS';
const targetLogin = issue.user.login;
const normalizedLogin = targetLogin.toLowerCase();
const entry = `${scope}:${normalizedLogin}`;
const branchSlug = normalizedLogin.replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'contributor';
const defaultContent = [
'# Scoped contribution-gate allowlist.',
'#',
'# Maintainers and collaborators bypass the gate automatically. Use this file',
'# for external contributors who are allowed through the automated front door.',
'# Seed active contributors here before switching the gate workflows to enforce mode.',
'#',
'# Supported entries:',
'# pr:username',
'# issue:username',
'# all:username',
'',
].join('\n');
function parseAllowlist(content) {
return new Set(
content
.split(/\r?\n/)
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
.filter(Boolean)
);
}
const { data: repoData } = await github.rest.repos.get({ owner, repo });
const defaultBranch = repoData.default_branch;
const { data: baseRef } = await github.rest.git.getRef({
owner,
repo,
ref: `heads/${defaultBranch}`,
});
const baseSha = baseRef.object.sha;
const { data: baseCommit } = await github.rest.git.getCommit({
owner,
repo,
commit_sha: baseSha,
});
let content = defaultContent;
try {
const { data } = await github.rest.repos.getContent({
owner,
repo,
path,
ref: defaultBranch,
});
if (!Array.isArray(data) && data.type === 'file') {
content = Buffer.from(data.content, data.encoding || 'base64').toString('utf8');
}
} catch (error) {
if (error.status !== 404) throw error;
}
const existing = parseAllowlist(content);
if (existing.has(entry) || existing.has(`all:${normalizedLogin}`)) {
await react('eyes', `@${targetLogin} is already approved for ${scope} contributions in \`${path}\`.`);
return;
}
const openPrs = [];
for (let page = 1; ; page++) {
const { data: pagePrs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
per_page: 100,
page,
});
openPrs.push(...pagePrs);
if (pagePrs.length < 100) break;
}
const repoFullName = `${owner}/${repo}`.toLowerCase();
const pendingPr = openPrs.find(openPr => {
const sameRepo = (openPr.head?.repo?.full_name || '').toLowerCase() === repoFullName;
const body = openPr.body || '';
return sameRepo && body.includes(`Adds \`${entry}\` to \`${path}\`.`);
});
if (pendingPr) {
await react('eyes', `@${targetLogin} already has a pending allowlist update PR for ${scope} contributions: ${pendingPr.html_url}`);
return;
}
const nextContent = `${content.trimEnd()}\n${entry}\n`;
const { data: blob } = await github.rest.git.createBlob({
owner,
repo,
content: nextContent,
encoding: 'utf-8',
});
const { data: tree } = await github.rest.git.createTree({
owner,
repo,
base_tree: baseCommit.tree.sha,
tree: [
{
path,
mode: '100644',
type: 'blob',
sha: blob.sha,
},
],
});
const branchName = `contribution-gate/${scope}-${branchSlug}-${Date.now()}`;
await github.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${branchName}`,
sha: baseSha,
});
const { data: commit } = await github.rest.git.createCommit({
owner,
repo,
message: `chore: approve @${targetLogin} for ${scope} contributions`,
tree: tree.sha,
parents: [baseSha],
});
await github.rest.git.updateRef({
owner,
repo,
ref: `heads/${branchName}`,
sha: commit.sha,
});
const { data: pr } = await github.rest.pulls.create({
owner,
repo,
title: `chore: approve @${targetLogin} for ${scope} contributions`,
head: branchName,
base: defaultBranch,
body: [
`Adds \`${entry}\` to \`${path}\`.`,
'',
`Requested by @${comment.user.login} in #${issue.number}.`,
].join('\n'),
});
await react('rocket', `Created allowlist update PR: ${pr.html_url}`);
+154
View File
@@ -0,0 +1,154 @@
name: Auto-close harvested PRs
# When a commit on main contains a "Harvested from PR #N" line in its
# message, close PR #N with a templated thank-you that links back to
# the merged commit. Solves the long-standing problem where contributor
# PRs whose code lands via maintainer cherry-pick stay open and
# `CONFLICTING` forever, even though their fix is credited in the
# CHANGELOG.
#
# The expected commit-message convention is documented in
# CONTRIBUTING.md. Two patterns are recognised:
#
# * `Harvested from PR #1234 by @username` (preferred)
# * `harvested from #1234` (case-insensitive fallback)
#
# The first match's PR number is closed; multiple PRs can be closed
# per commit by repeating the line. The match runs on the commit
# body only, not on the subject line, so the subject can describe
# the change naturally without baking a number into it.
on:
push:
branches: [main]
permissions:
contents: read
pull-requests: write
issues: write
# Only one auto-close run at a time so two near-simultaneous main
# pushes can't both try to close the same PR (the second would just
# fail with "Pull request is already closed", harmless but noisy).
concurrency:
group: auto-close-harvested
cancel-in-progress: false
jobs:
close:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
# We need at least the commits that this push introduced.
# fetch-depth: 0 is the simplest correct option; the
# alternative (fetching just `before..after`) is fragile
# when force-pushes happen.
fetch-depth: 0
- name: Close PRs referenced by harvested-from lines
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BEFORE_SHA: ${{ github.event.before }}
AFTER_SHA: ${{ github.event.after }}
shell: bash
run: |
set -euo pipefail
# The first push to a fresh branch has BEFORE_SHA = 0000…0000.
# In that case fall back to the latest commit only — we don't
# want to scan the entire history.
if [[ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" || -z "${BEFORE_SHA:-}" ]]; then
RANGE="${AFTER_SHA}"
RANGE_ARGS=("-1" "${AFTER_SHA}")
else
RANGE="${BEFORE_SHA}..${AFTER_SHA}"
RANGE_ARGS=("${RANGE}")
fi
echo "Scanning commit range: ${RANGE}"
# `git log --format=%H%n%B%n--END--` separates commits with a
# sentinel so multi-line bodies don't get mangled.
mapfile -t commits < <(git log "${RANGE_ARGS[@]}" --format="%H")
if [[ ${#commits[@]} -eq 0 ]]; then
echo "No commits in range; nothing to do."
exit 0
fi
declare -A processed_prs=()
for sha in "${commits[@]}"; do
body="$(git log -1 --format=%B "${sha}")"
# Two patterns, both case-insensitive on the keyword:
# "Harvested from PR #1234 by @username" (preferred form)
# "harvested from #1234" (short form)
mapfile -t pr_numbers < <(
printf '%s\n' "${body}" \
| grep -oiE 'harvested from (pr )?#[0-9]+' \
| grep -oE '#[0-9]+' \
| tr -d '#' \
| sort -u || true
)
if [[ ${#pr_numbers[@]} -eq 0 ]]; then
continue
fi
short_sha="${sha:0:12}"
subject="$(git log -1 --format=%s "${sha}")"
for pr in "${pr_numbers[@]}"; do
key="${pr}-${sha}"
if [[ -n "${processed_prs[${key}]:-}" ]]; then
continue
fi
processed_prs[${key}]=1
# Idempotency: skip if the PR is already closed.
state="$(gh pr view "${pr}" --json state --jq .state 2>/dev/null || echo "MISSING")"
if [[ "${state}" == "CLOSED" || "${state}" == "MERGED" ]]; then
echo "PR #${pr} is already ${state}; skipping."
continue
fi
if [[ "${state}" == "MISSING" ]]; then
echo "::warning::PR #${pr} not found or inaccessible; skipping."
continue
fi
author="$(gh pr view "${pr}" --json author --jq '.author.login' 2>/dev/null || echo "")"
greeting="Hi"
if [[ -n "${author}" ]]; then
greeting="Thanks @${author}"
fi
# NOTE: this block intentionally avoids `<<EOF` heredocs.
# YAML's `|` block scalar requires consistent indentation,
# but heredoc bodies have to start at column 0 — those two
# constraints can't coexist in the same file. We assemble
# the body with `printf` + `\n` so every line of the
# message lives at the same indent as the surrounding
# shell code.
commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${sha}"
contributing_url="https://github.com/${GITHUB_REPOSITORY}/blob/main/CONTRIBUTING.md"
body_text="$(printf '%s\n' \
"${greeting} — your contribution landed in [\`${short_sha}\`](${commit_url}) on \`main\`:" \
"" \
"> ${subject}" \
"" \
"Closing this PR now that the code is on \`main\`. Credit lives in the commit message and (where applicable) the \`CHANGELOG.md\` entry for the next release. Apologies for not closing this at the time of the merge — the auto-close workflow is new in v0.8.31." \
"" \
"If you want to land more work and would prefer your future PRs merge cleanly without a harvest step, the [\`CONTRIBUTING.md\`](${contributing_url}) doc has a short note on what makes a contribution mergeable as-is." \
)"
echo "Closing PR #${pr} (harvested in ${short_sha})"
if ! gh pr close "${pr}" \
--repo "${GITHUB_REPOSITORY}" \
--comment "${body_text}"; then
echo "::warning::Failed to close PR #${pr}; continuing"
fi
done
done
echo "Auto-close pass complete."
+2 -2
View File
@@ -66,8 +66,8 @@ jobs:
echo "::error::Could not parse workspace version from Cargo.toml" >&2
exit 1
fi
if ! echo "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "::error::Workspace version '$v' is not valid semver (expected X.Y.Z or X.Y.Z-prerelease)" >&2
if ! echo "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Workspace version '$v' is not valid semver (expected X.Y.Z)" >&2
exit 1
fi
echo "version=$v" >> "$GITHUB_OUTPUT"
-133
View File
@@ -1,133 +0,0 @@
name: Catalog refresh
on:
schedule:
- cron: '17 5 * * *'
workflow_dispatch:
permissions: {}
concurrency:
group: catalog-refresh
cancel-in-progress: false
jobs:
refresh:
name: Refresh the model catalog
if: github.repository == 'codewhale-hq/Codewhale'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
env:
BRANCH: catalog-refresh
steps:
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510
id: app-token
with:
app-id: ${{ vars.CODEWHALE_APP_ID }}
private-key: ${{ secrets.CODEWHALE_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: main
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
- name: Fetch models.dev once
run: |
curl --fail --silent --show-error --location --max-time 60 \
--user-agent "CodeWhale-catalog-automation (+https://github.com/codewhale-hq/Codewhale)" \
--output "$RUNNER_TEMP/models-dev-catalog.json" \
https://models.dev/catalog.json
- name: Decide whether the pin is due
id: pin
run: |
if python3 scripts/catalog_models_dev.py seed age --max-days 7; then
echo "due=false" >> "$GITHUB_OUTPUT"
else
echo "due=true" >> "$GITHUB_OUTPUT"
fi
- name: Propose new models, re-lock and render
env:
CODEWHALE_MODELS_DEV_PATH: ${{ runner.temp }}/models-dev-catalog.json
run: |
python3 scripts/catalog_models_dev.py seed discover --write --snapshot "$CODEWHALE_MODELS_DEV_PATH" | tee "$RUNNER_TEMP/discover.txt"
python3 scripts/catalog_models_dev.py seed lock > "$RUNNER_TEMP/lock-report.txt"
python3 scripts/catalog_models_dev.py seed render
(cd web && node scripts/derive-facts.mjs)
- name: Decide whether anything material changed
id: change
run: |
material=$(git diff -U0 -- \
crates/config/assets/models_dev.bundled.json \
crates/config/assets/catalog_corrections.json \
scripts/catalog/models_dev_seed.toml \
| grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -v '"upstream":' || true)
if [ -n "$material" ] || [ "${{ steps.pin.outputs.due }}" = "true" ]; then
echo "open=true" >> "$GITHUB_OUTPUT"
else
echo "open=false" >> "$GITHUB_OUTPUT"
echo "No catalog change and the pin is under 7 days old; nothing to propose."
fi
- name: Run the catalog checks
id: checks
if: steps.change.outputs.open == 'true'
run: |
set +e
{
python3 scripts/catalog_models_dev.py seed render --check
python3 scripts/catalog_models_dev.py seed age --max-days 30
python3 -m unittest scripts/catalog_models_dev_test.py
(cd web && node scripts/check-facts.mjs)
} > "$RUNNER_TEMP/checks.txt" 2>&1
status=$?
if [ "$status" -eq 0 ]; then echo "ok=true" >> "$GITHUB_OUTPUT"; else echo "ok=false" >> "$GITHUB_OUTPUT"; fi
tail -n 40 "$RUNNER_TEMP/checks.txt"
- name: Push the branch and open or update the pull request
if: steps.change.outputs.open == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
CHECKS_OK: ${{ steps.checks.outputs.ok }}
APP_ID: ${{ vars.CODEWHALE_APP_ID }}
run: |
git config user.name 'codewhale-agent[bot]'
git config user.email "${APP_ID}+codewhale-agent[bot]@users.noreply.github.com"
git checkout -B "$BRANCH"
git add crates/config/assets/models_dev.bundled.json crates/config/assets/catalog_corrections.json \
scripts/catalog/models_dev_seed.toml scripts/catalog/models_dev_seed.lock.json web/lib/facts.generated.ts
git commit -m "chore(catalog): refresh the Models.dev offline seed" \
-m "Generated by .github/workflows/catalog-refresh.yml from models.dev. No LLM wrote these rows."
git push --force-with-lease="$BRANCH" \
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:refs/heads/$BRANCH"
{
echo "Generated by the scheduled catalog refresh from models.dev. No LLM wrote these rows; review them like any other change."
echo
echo "New models from the vendor allowlist. Merging this PR publishes them on the public /models page; rows carry an \`unreviewed\` source until someone checks them against the vendor's docs:"
echo
echo '```text'
cat "$RUNNER_TEMP/discover.txt"
echo '```'
echo
echo "Checks run in the job: \`seed render --check\`, \`seed age --max-days 30\`, \`scripts/catalog_models_dev_test.py\`, \`web/scripts/check-facts.mjs\`: $([ "$CHECKS_OK" = true ] && echo passed || echo FAILED, see the job log; this PR is a draft)."
echo
echo "To stop proposing a model, add its id to \`[discover].skip\` in \`scripts/catalog/models_dev_seed.toml\`."
} > "$RUNNER_TEMP/pr-body.md"
title="chore(catalog): refresh the Models.dev offline seed"
existing=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty')
if [ -n "$existing" ]; then
gh pr edit "$existing" --title "$title" --body-file "$RUNNER_TEMP/pr-body.md"
if [ "$CHECKS_OK" = true ]; then
gh pr ready "$existing"
else
gh pr ready --undo "$existing"
fi
else
draft=""
[ "$CHECKS_OK" = true ] || draft="--draft"
gh pr create --base main --head "$BRANCH" --title "$title" --body-file "$RUNNER_TEMP/pr-body.md" $draft
fi
+44 -24
View File
@@ -155,7 +155,7 @@ jobs:
crates/*|docs/HOOKS.md|docs/KEYBINDINGS.md|docs/TELEMETRY.md|docs/FLEET.md|docs/FLEET_WORKFLOW_TUTORIAL.md|docs/zh_hans/FLEET.md|docs/2512.24601v2.pdf|docs/cloud-facts/*|docs/examples/*)
heavy=true
;;
docs/*|*.md|packaging/aur/*|.github/PULL_REQUEST_TEMPLATE.md|.github/ISSUE_TEMPLATE/*|.github/scripts/agent-task-metadata.test.sh|.github/workflows/auto-tag.yml|.github/workflows/stale.yml|.github/workflows/triage.yml|scripts/release/check-versions.sh|scripts/release/check-ohos-deps.sh|scripts/release/install-dogfood.sh|scripts/release/install-dogfood.test.sh|scripts/release/prepare-release.sh|scripts/release/prepare-release.test.sh|scripts/dev-cache.sh|scripts/dev-cache.test.sh|scripts/dev-cargo.sh|scripts/dev-test.sh)
docs/*|*.md|packaging/aur/*|.github/PULL_REQUEST_TEMPLATE.md|.github/ISSUE_TEMPLATE/*|.github/scripts/agent-task-metadata.test.sh|.github/workflows/agent-task-labels.yml|.github/workflows/auto-tag.yml|.github/workflows/stale.yml|.github/workflows/triage.yml|scripts/release/check-versions.sh|scripts/release/check-ohos-deps.sh|scripts/release/install-dogfood.sh|scripts/release/install-dogfood.test.sh|scripts/release/prepare-release.sh|scripts/release/prepare-release.test.sh|scripts/dev-cache.sh|scripts/dev-cache.test.sh|scripts/dev-cargo.sh|scripts/dev-test.sh)
;;
*)
heavy=true
@@ -172,7 +172,7 @@ jobs:
# push to main still runs it unconditionally as the pre-release
# safety net for anything this filter misses.
case "${path}" in
crates/app-server/*|crates/engine/src/runtime_threads*|crates/tui/src/main.rs|scripts/mobile-smoke.sh|.github/workflows/ci.yml|Cargo.lock|Cargo.toml)
crates/app-server/*|crates/tui/src/runtime_api*|crates/tui/src/runtime_mobile.html|crates/tui/src/runtime_threads*|crates/tui/src/main.rs|scripts/mobile-smoke.sh|.github/workflows/ci.yml|Cargo.lock|Cargo.toml)
mobile=true
;;
esac
@@ -215,11 +215,12 @@ jobs:
# missing receipt actually matters.
run: ./scripts/release/check-versions.sh --range-audit-advisory
- name: Check this PR's feature release-note receipts
# Advisory, like the range audit above: a missing CHANGELOG receipt
# shows as a warning and maintainers add it while landing.
# The range audit above is advisory because previous-tag..HEAD blames
# every open PR for receipts other merges forgot. This is the same
# check scoped to the PR's own commits, so it blocks: a `feat:` commit
# that references #N must add #N to CHANGELOG.md in the same PR.
# Locally: scripts/preflight.sh.
if: github.event_name == 'pull_request'
continue-on-error: true
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: ./scripts/release/check-feature-release-notes.sh "${PR_BASE_SHA}" HEAD
@@ -268,9 +269,9 @@ jobs:
node --test scripts/release/ensure-release-assets-absent.test.js
node --test scripts/release/verify-release-inventory.test.js
- name: Run runtime web client tests
# crates/app-server/tests/runtime_web_client.test.mjs exercises the embedded
# crates/tui/tests/runtime_web_client.test.mjs exercises the embedded
# web client's event/snapshot state machine; it ran nowhere before.
run: node --test crates/app-server/tests/runtime_web_client.test.mjs
run: node --test crates/tui/tests/runtime_web_client.test.mjs
plugin-conversion:
name: Plugin conversion
@@ -319,6 +320,21 @@ jobs:
(cd "integrations/${bridge}" && npm test)
done
- name: Build computer-use test desktop
# Spawn assertions have short request deadlines; keep the cold image
# build outside those deadlines and fail image preparation explicitly.
timeout-minutes: 10
run: >-
docker build --tag codewhale-cu-linux
--file crates/tui/plugins/computer-use/docker/Dockerfile
crates/tui/plugins/computer-use
- name: Run computer-use plugin suites
# The bundled plugin is dependency-free too; its suites cover the
# manifest contract, the registry, the exec/ssh transport, the four
# platform backends, and the MCP stdio protocol. No GUI input runs.
run: (cd crates/tui/plugins/computer-use && npm test)
- name: Run extension host suites and check the committed bundle
# The TypeScript extension host (experimental, [features]
# extension_host). Tests exercise both the committed bundle and
@@ -326,7 +342,7 @@ jobs:
# testing; then reject any bundle drift. Node 22 matches the host floor.
run: |
set -euo pipefail
cd crates/engine/extension-host
cd crates/tui/extension-host
npm ci --ignore-scripts
npm test
npm run typecheck
@@ -343,7 +359,7 @@ jobs:
# suites only; the Rust host tests run on Node.
run: |
set -euo pipefail
cd crates/engine/extension-host
cd crates/tui/extension-host
npm run test:bun
extension-host-runtimes:
@@ -358,7 +374,7 @@ jobs:
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: crates/engine/extension-host
working-directory: crates/tui/extension-host
shell: bash
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
@@ -383,7 +399,7 @@ jobs:
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: extension-host-runtime-${{ matrix.os }}
path: crates/engine/extension-host/dist/builtin-modules.json
path: crates/tui/extension-host/dist/builtin-modules.json
if-no-files-found: error
vscode-extension:
@@ -421,7 +437,7 @@ jobs:
safety-gate:
name: Safety gate
needs: changes
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
if: needs.changes.outputs.heavy == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
@@ -469,7 +485,7 @@ jobs:
# codewhale-runtime owns `safe_label` (its hostile-authority test)
# since RS-2; add the next package when another safety module moves.
run: |
sh scripts/with-hermetic-test-home.sh cargo nextest run -p codewhale-engine -p codewhale-tui -p codewhale-runtime --lib --locked --no-tests=fail -E 'test(auto_review) | test(authority) | test(sandbox)'
sh scripts/with-hermetic-test-home.sh cargo nextest run -p codewhale-tui -p codewhale-runtime --lib --locked --no-tests=fail -E 'test(auto_review) | test(authority) | test(sandbox)'
sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-execpolicy --locked
lint:
@@ -725,7 +741,7 @@ jobs:
# fail the build there while stable stays green (#6543).
name: MSRV check (1.89)
needs: changes
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
if: needs.changes.outputs.heavy == 'true'
timeout-minutes: 45
runs-on: ubuntu-latest
steps:
@@ -774,7 +790,6 @@ jobs:
test:
name: Test
needs: changes
if: github.event_name == 'workflow_dispatch'
# Contexts "Test (ubuntu-latest)" / "Test (macos-latest)" /
# "Test (windows-latest)" derive from job name + matrix.os and are
# independent of runs-on. For light changes the macOS/Windows legs only
@@ -896,7 +911,7 @@ jobs:
set -euo pipefail
bun_binary="$(bun -p 'process.execPath')"
host_binary="$(node -p "require('path').join(process.env.RUNNER_TEMP, 'codewhale-extension-host' + (process.platform === 'win32' ? '.exe' : ''))")"
compiled_info="$(node crates/engine/extension-host/compile-host.mjs --bun "$bun_binary" --output "$host_binary")"
compiled_info="$(node crates/tui/extension-host/compile-host.mjs --bun "$bun_binary" --output "$host_binary")"
printf 'CODEWHALE_COMPILED_HOST_TEST_BINARY=%s\n' "$host_binary" >> "$GITHUB_ENV"
node - "$host_binary" "$bun_binary" "$compiled_info" >> "$GITHUB_ENV" <<'JS'
const fs = require('node:fs'), crypto = require('node:crypto');
@@ -908,7 +923,7 @@ jobs:
console.log('CODEWHALE_COMPILED_HOST_TEST_SHA256=' + digest(process.argv[2]));
console.log('CODEWHALE_COMPILED_HOST_TEST_BUN=' + process.argv[3]);
console.log('CODEWHALE_COMPILED_HOST_TEST_RUNTIME_SHA256=' + digest(process.argv[3]));
console.log('CODEWHALE_COMPILED_HOST_TEST_BUNDLE_SHA256=' + digest('crates/engine/extension-host/dist/codewhale-extension-host.mjs'));
console.log('CODEWHALE_COMPILED_HOST_TEST_BUNDLE_SHA256=' + digest('crates/tui/extension-host/dist/codewhale-extension-host.mjs'));
JS
# The required hermetic full invocation executes the shared actual
# compiled Native secret/history/write scenario on each OS; Windows
@@ -954,12 +969,12 @@ jobs:
run: cargo build -p codewhale-cli --bin codewhale --all-features --locked
env:
RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
- name: Windows Terminal clipboard-to-draft acceptance
- name: Windows Terminal clipboard acceptance
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
timeout-minutes: 5
shell: pwsh
run: node scripts/release/windows-terminal-clipboard.mjs target/debug/codewhale.exe "${{ runner.temp }}/windows-clipboard"
- name: Retain Windows Terminal clipboard-to-draft evidence
- name: Retain Windows Terminal clipboard evidence
if: ${{ !cancelled() && matrix.os == 'windows-latest' && needs.changes.outputs.heavy == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
@@ -1149,7 +1164,7 @@ jobs:
python3 scripts/compiled-host-native-receipt.py \
--junit target/nextest/ci/junit.xml \
--compiled-image "$CODEWHALE_COMPILED_HOST_TEST_BINARY" \
--bundle crates/engine/extension-host/dist/codewhale-extension-host.mjs \
--bundle crates/tui/extension-host/dist/codewhale-extension-host.mjs \
--bun "$CODEWHALE_COMPILED_HOST_TEST_BUN" --source-sha "$GITHUB_SHA" \
--image-platform "$CODEWHALE_COMPILED_HOST_TEST_PLATFORM" --image-arch "$CODEWHALE_COMPILED_HOST_TEST_ARCH" \
--host-sha256 "$CODEWHALE_COMPILED_HOST_TEST_SHA256" \
@@ -1282,7 +1297,7 @@ jobs:
npm-wrapper-smoke:
name: npm wrapper smoke
needs: changes
if: github.event_name == 'workflow_dispatch'
if: github.event_name != 'schedule'
# Same ternary rationale as the Test job: light legs only echo, so keep
# them off macOS/Windows runners. On pull_request the matrix is
# ubuntu-only, so the required "npm wrapper smoke (ubuntu-latest)"
@@ -1298,7 +1313,7 @@ jobs:
runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }}
strategy:
matrix:
os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || github.event_name == 'workflow_dispatch' && '["ubuntu-latest","windows-latest"]' || '["ubuntu-latest","windows-latest"]') }}
os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || github.event_name == 'workflow_dispatch' && '["ubuntu-latest","macos-latest","windows-latest"]' || '["ubuntu-latest","windows-latest"]') }}
steps:
- name: Skip npm wrapper smoke for light change
if: needs.changes.outputs.heavy != 'true'
@@ -1358,8 +1373,13 @@ jobs:
mobile-smoke:
name: Mobile runtime smoke
needs: changes
# Manual (workflow_dispatch) only; the release candidate builds android-arm64.
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
# Not a required PR context. Pull requests run it only when the mobile
# runtime surface changed (see the `mobile` filter above); every push to
# main runs it unconditionally as the pre-release safety net.
if: >-
github.event_name != 'schedule' &&
needs.changes.outputs.heavy == 'true' &&
(github.event_name != 'pull_request' || needs.changes.outputs.mobile == 'true')
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
+42 -91
View File
@@ -1,119 +1,70 @@
name: Codewhale PR Review
# Account-backed, advisory findings. Failed execution stays visibly failed;
# do not make this optional review job a required merge check.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [main, master]
workflow_dispatch:
inputs:
pr-number:
description: Pull request number
description: Same-repository PR to review at its current revision
required: true
type: string
permissions: {}
concurrency:
group: codewhale-review-${{ github.event.pull_request.number || github.event.issue.number || inputs.pr-number }}
cancel-in-progress: false
group: codewhale-review-${{ github.event.pull_request.number || inputs.pr-number }}
cancel-in-progress: true
jobs:
installation:
if: >-
github.actor != 'dependabot[bot]' &&
((github.event_name == 'workflow_dispatch') ||
(github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.head.repo.full_name == github.repository) ||
((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
(github.event.issue.pull_request || github.event.pull_request) &&
github.event.comment.user.type == 'User' && contains(github.event.comment.body, '@codewhale')))
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510
id: app-token
with:
app-id: ${{ vars.CODEWHALE_APP_ID }}
private-key: ${{ secrets.CODEWHALE_APP_PRIVATE_KEY }}
permission-contents: read
permission-pull-requests: write
review:
needs: installation
if: >-
github.actor != 'dependabot[bot]' &&
((github.event_name == 'workflow_dispatch') ||
(github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.head.repo.full_name == github.repository) ||
((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
(github.event.issue.pull_request || github.event.pull_request) &&
github.event.comment.user.type == 'User' && contains(github.event.comment.body, '@codewhale')))
codewhale-review:
name: Codewhale review
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
contents: read
pull-requests: read
outputs:
findings: ${{ steps.review.outputs.findings }}
steps:
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
with:
node-version: '22'
- uses: codewhale-hq/Codewhale@b0b2b9a335cc1d24abe8a087189bed080d1cc3db
id: review
with:
version: 'v0.10.2'
provider: 'deepseek'
model: 'deepseek-flash'
reasoning-effort: 'off'
max-output-tokens: '16384'
pr-number: ${{ inputs.pr-number }}
trigger-phrase: '@codewhale'
allow-forks: 'true'
post: 'false'
env:
DEEPSEEK_API_KEY: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'issue_comment' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.DEEPSEEK_API_KEY || '' }}
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
if: steps.review.outputs.findings != ''
with:
name: codewhale-findings-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.review.outputs.findings }}
retention-days: 1
if-no-files-found: error
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
if: always() && steps.review.outputs.receipt != ''
with:
name: codewhale-review-receipt-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.review.outputs.receipt }}
retention-days: 14
if-no-files-found: error
publish:
needs: review
if: needs.review.outputs.findings != ''
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
env:
HAS_APP_KEY: ${{ secrets.CODEWHALE_APP_PRIVATE_KEY != '' }}
steps:
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
# The local action is trusted base/default-branch source. Candidate PR
# source is fetched as Git objects by the action and is never executed.
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha || github.sha }}
persist-credentials: false
- uses: actions/[email protected]
with:
node-version: '22'
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333
with:
name: codewhale-findings-${{ github.run_id }}-${{ github.run_attempt }}
path: findings
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510
- name: Mint Codewhale Agent token
if: env.HAS_APP_KEY == 'true' && vars.CODEWHALE_APP_ID != '' && (github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.draft))
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.CODEWHALE_APP_ID }}
private-key: ${{ secrets.CODEWHALE_APP_PRIVATE_KEY }}
permission-contents: read
permission-pull-requests: write
- uses: codewhale-hq/Codewhale/scripts/github-action/publish@b0b2b9a335cc1d24abe8a087189bed080d1cc3db
id: publish
- name: Review with the account model
id: review
uses: ./
with:
findings: findings/findings.json
version: ${{ vars.CODEWHALE_REVIEW_VERSION || 'v0.10.0' }}
provider: codewhale
model: ${{ vars.CODEWHALE_REVIEW_MODEL }}
pr-number: ${{ inputs.pr-number }}
trigger-phrase: '@codewhale'
allow-forks: 'true'
github-token: ${{ steps.app-token.outputs.token }}
reviewer-login: 'codewhale-agent[bot]'
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
if: always() && steps.publish.outputs.receipt != ''
github-token: ${{ steps.app-token.outputs.token || github.token }}
max-chars: ${{ vars.CODEWHALE_REVIEW_MAX_CHARS || '200000' }}
max-passes: ${{ vars.CODEWHALE_REVIEW_MAX_PASSES || '1' }}
max-output-tokens: ${{ vars.CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS }}
env:
CODEWHALE_API_KEY: ${{ (github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.CODEWHALE_API_KEY || '' }}
- name: Save review outcome
if: always() && steps.review.outputs.receipt != ''
uses: actions/upload-artifact@v7
with:
name: codewhale-publication-receipt-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.publish.outputs.receipt }}
name: codewhale-review-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.review.outputs.receipt }}
retention-days: 14
if-no-files-found: error
+57
View File
@@ -0,0 +1,57 @@
name: DCO
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
jobs:
dco:
name: Check Signed-off-by
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Report missing Signed-off-by (dry-run advisory)
shell: bash
run: |
set -euo pipefail
base_rev=$(git merge-base origin/${{ github.base_ref }} HEAD 2>/dev/null || echo "HEAD~1")
echo "✅ merge-base: $base_rev"
echo ""
bad=()
while IFS= read -r commit; do
msg=$(git log --format=%B -n1 "$commit")
if ! echo "$msg" | grep -qi "^Signed-off-by:"; then
bad+=("$commit")
fi
done < <(git rev-list "${base_rev}..HEAD")
if [ ${#bad[@]} -eq 0 ]; then
echo "✅ All commits have Signed-off-by."
exit 0
fi
echo "⚠️ Advisory — the following commit(s) are missing Signed-off-by:"
for c in "${bad[@]}"; do
echo " • $c $(git log --format=%s -n1 "$c")"
done
echo ""
echo "This check is advisory and does not block merging."
echo "Please amend commits with:"
echo " git commit --amend -s"
echo "See CONTRIBUTING.md for details."
echo ""
echo "::warning title=DCO: missing Signed-off-by::Some commits are missing Signed-off-by — amend with git commit --amend -s"
# Dry-run: always pass, but surface the warning
exit 0
+90
View File
@@ -0,0 +1,90 @@
name: Contribution intake - issues
on:
issues:
types: [opened, reopened]
permissions:
contents: read
issues: write
jobs:
gate:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# Labels only, never comments (founder, 2026-09-22): the intake note used
# to thank other bots, and a comment is noise a label does not make.
# Maintainers still see who needs triage; `/lgtmi` still skips it.
- name: Label new external issues for triage
uses: actions/github-script@v9
with:
script: |
const issue = context.payload.issue;
const owner = context.repo.owner;
const repo = context.repo.repo;
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
async function label(name, color, description) {
try {
await github.rest.issues.createLabel({ owner, repo, name, color, description });
} catch (error) {
if (error.status !== 422) throw error; // 422: label already exists
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issue.number,
labels: [name],
});
}
if (privileged.has(issue.author_association)) return;
// `user.type` is set by GitHub for app and bot accounts and cannot
// be spoofed by a login that merely ends in "[bot]".
if (issue.user.type === 'Bot') {
await label('bot-authored', 'ededed', 'Opened by a bot or app account');
return;
}
function parseAllowlist(content) {
return new Set(
content
.split(/\r?\n/)
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
.filter(Boolean)
);
}
async function readAllowlist() {
try {
const { data } = await github.rest.repos.getContent({
owner,
repo,
path: '.github/APPROVED_CONTRIBUTORS',
ref: context.payload.repository.default_branch,
});
if (Array.isArray(data) || data.type !== 'file') return new Set();
return parseAllowlist(
Buffer.from(data.content, data.encoding || 'base64').toString('utf8')
);
} catch (error) {
if (error.status === 404) return new Set();
throw error;
}
}
const allowlist = await readAllowlist();
const login = issue.user.login.toLowerCase();
if (
allowlist.has(`all:${login}`) ||
allowlist.has(`issue:${login}`)
) {
return;
}
await label(
'needs-triage',
'fbca04',
'New external report awaiting maintainer triage; repro, logs and version output help'
);
-75
View File
@@ -1,75 +0,0 @@
name: Main landing gate
# Why this exists
#
# Until 2026-11-08 `ci.yml` ran only on `pull_request` and `workflow_dispatch`,
# so nothing validated a push to `main` and a landing that was red on arrival stayed red
# until the next release candidate run, and every open PR then inherits the
# failure: on 2026-10-29 four commits landed red behind `[skip ci]`
# (`PersistRequest::ClearCheckpoint` became dead, `clippy` failed on
# `codewhale-skills`, and the blocking-calls budget went stale for a file that
# had moved crates) and all nine open PRs looked broken for reasons that had
# nothing to do with their authors' code.
#
# This job is deliberately narrow. It runs `scripts/preflight.sh --check`,
# which is the checked-in local preflight for exactly the class of breakage that
# reaches `main` unnoticed: rustfmt, the CHANGELOG slice, README locale stamps,
# the dead-code and blocking-calls budgets, generated-asset drift (including
# the website's generated facts, agent docs and feature registry), and the
# feature receipts. Measured at 45 s on a warm Linux runner.
#
# Since 2026-11-08 (founder) the `full-ci` job also dispatches `ci.yml` on every
# push to main, so every gate and the three-OS suite run on each landing. The
# preflight job stays narrow so a red landing still shows up in minutes.
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: main-landing-${{ github.sha }}
cancel-in-progress: true
jobs:
preflight:
name: Landing preflight
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
# The preflight diffs against the push's base commit to scope feature
# receipts, and the changelog/budget checks read history.
fetch-depth: 0
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: rustfmt
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
- name: Preflight
env:
BEFORE_SHA: ${{ github.event.before }}
run: bash scripts/preflight.sh --check
full-ci:
name: Dispatch full CI
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: write
contents: read
steps:
- name: Run ci.yml with every gate on the pushed commit
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run ci.yml --repo "${GITHUB_REPOSITORY}" --ref main -f expected_sha="${GITHUB_SHA}"
+3 -2
View File
@@ -23,7 +23,7 @@ jobs:
run: |
python3 - <<'PY'
import json, os, re
snapshot = json.load(open('crates/plugins/assets/first-party-marketplace.json'))
snapshot = json.load(open('crates/tui/assets/first-party-marketplace.json'))
revision = snapshot['revision']
assert re.fullmatch(r'[0-9a-f]{40}', revision), 'Invalid marketplace revision'
if os.environ['EVENT_NAME'] in ('schedule', 'workflow_dispatch'):
@@ -45,10 +45,11 @@ jobs:
run: node marketplace/scripts/check-marketplace.mjs
- name: Check bundled catalog snapshot
run: python3 core/scripts/sync-marketplace.py --marketplace marketplace --check
- name: Check current skill mirrors
- name: Check current skill and Computer Use mirrors
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
run: |
node marketplace/scripts/check-marketplace.mjs --core core
node marketplace/scripts/check-cu-sync.mjs --core core
- name: Explain a drift failure
if: failure()
run: |
+1 -1
View File
@@ -86,7 +86,7 @@ jobs:
timeout-minutes: 25
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6.1.0
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '17'
+110
View File
@@ -0,0 +1,110 @@
name: Contribution gate - pull requests
on:
pull_request_target:
types: [opened, reopened]
permissions:
contents: read
issues: write
pull-requests: write
env:
# Keep new gates observable first. Switch to "enforce" only after maintainers
# have seeded active contributors and reviewed the dry-run signal.
CONTRIBUTION_GATE_MODE: dry-run
jobs:
gate:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Gate unapproved external pull requests
uses: actions/github-script@v9
with:
script: |
const pr = context.payload.pull_request;
const owner = context.repo.owner;
const repo = context.repo.repo;
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
const gateMode = (process.env.CONTRIBUTION_GATE_MODE || 'dry-run').trim().toLowerCase();
const enforceGate = gateMode === 'enforce';
if (!['dry-run', 'enforce'].includes(gateMode)) {
core.warning(`Unknown CONTRIBUTION_GATE_MODE "${gateMode}"; defaulting to dry-run.`);
}
async function label(name, color, description) {
try {
await github.rest.issues.createLabel({ owner, repo, name, color, description });
} catch (error) {
if (error.status !== 422) throw error; // 422: label already exists
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: [name],
});
}
if (privileged.has(pr.author_association)) return;
// `user.type` is set by GitHub for app and bot accounts and cannot
// be spoofed by a login that merely ends in "[bot]".
if (pr.user.type === 'Bot') {
await label('bot-authored', 'ededed', 'Opened by a bot or app account');
return;
}
function parseAllowlist(content) {
return new Set(
content
.split(/\r?\n/)
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
.filter(Boolean)
);
}
async function readAllowlist() {
try {
const { data } = await github.rest.repos.getContent({
owner,
repo,
path: '.github/APPROVED_CONTRIBUTORS',
ref: context.payload.repository.default_branch,
});
if (Array.isArray(data) || data.type !== 'file') return new Set();
return parseAllowlist(
Buffer.from(data.content, data.encoding || 'base64').toString('utf8')
);
} catch (error) {
if (error.status === 404) return new Set();
throw error;
}
}
const allowlist = await readAllowlist();
const login = pr.user.login.toLowerCase();
if (
allowlist.has(`all:${login}`) ||
allowlist.has(`pr:${login}`)
) {
return;
}
// Labels only (founder, 2026-09-22). The label description carries
// the contributor-facing explanation a comment used to.
await label(
'contribution-gate',
'c5def5',
'Author not yet in .github/APPROVED_CONTRIBUTORS; a maintainer grants access with /lgtm'
);
if (!enforceGate) return;
await github.rest.pulls.update({
owner,
repo,
pull_number: pr.number,
state: 'closed',
});
+109
View File
@@ -0,0 +1,109 @@
name: PR closes an issue
# 342 open issues, 329 of them touched within the month: nothing here is rotting,
# the drain is just clogged. Only 8 of 35 open PRs carried a closing keyword, so
# work ships and its issue stays open, and nobody can tell which of the 342 are
# already done. That is how 121 issues end up on one milestone.
#
# This check asks every PR to close an issue, reference one with `Refs #N`, or
# say why it has none (`No-Issue:`). The opt-out is one line, so this is a
# prompt, not a wall. It also fails a negated closing keyword ("does not close
# #N"), which GitHub would otherwise act on and close the issue.
on:
pull_request:
types: [opened, edited, reopened, synchronize]
permissions:
contents: read
pull-requests: read
jobs:
link:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Automated dependency bumps (dependabot and any other GitHub-verified
# bot account) are machine-generated and can never carry a closing
# keyword; failing them here would require hand-editing every bot body,
# which defeats the automation. The gate stays strict for every human
# PR. `user.type` is set by GitHub for verified bot accounts, so a PR
# author cannot spoof it to dodge the check.
- name: Require an issue link or an explicit opt-out
if: github.event.pull_request.user.type != 'Bot'
env:
# Fetched live rather than read from the event payload. A rerun
# replays the payload the run started with, so a body-only fix could
# never turn this check green: the obvious operator move — add the
# missing line, rerun the failed check — re-read the old body and
# failed again with no hint why. Reading the current body makes a
# rerun mean what everyone already assumes it means.
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# Through a variable, never interpolated into the script body:
# a PR body is attacker-controlled text.
PR_BODY=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json body --jq '.body // ""')
# Body only, deliberately. GitHub resolves closing keywords from the
# PR description; a "Closes #123" in the title auto-closes nothing.
# Accepting the title here would pass PRs that never close an issue,
# which is the exact false-assurance this check exists to prevent.
text="${PR_BODY:-}"
# GitHub resolves a closing keyword wherever it appears, negated or
# not: PR #6371 said "does not close #6184", GitHub put #6184 in
# closingIssuesReferences, and the P1 closed on merge. A keyword a
# few words after a negation is always that mistake, so fail first.
# The reference takes all three forms GitHub closes on: #N,
# owner/repo#N and a full issue URL.
keyword='(close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]*:?[[:space:]]*([[:alnum:]_.-]+/[[:alnum:]_.-]+#|#|https?://github\.com/[[:alnum:]_.-]+/[[:alnum:]_.-]+/issues/)[0-9]+'
negation="(\\b(not|never|no longer|without)|n't)([[:space:]]+[[:alnum:]'-]+){0,3}[[:space:]]+"
# `|| true`, not `| head`: under pipefail a SIGPIPE'd grep would turn a
# hit into a miss.
negated=$(grep -m1 -oiE "${negation}${keyword}" <<<"$text" || true)
if [ -n "$negated" ]; then
cat >&2 <<MSG
Negated closing keyword found: "${negated}"
GitHub closes the issue anyway: it ignores the "not". Reword it as
Refs #1234 (the PR relates to the issue; nothing closes)
and keep Closes / Fixes / Resolves for the issue this PR really finishes.
MSG
exit 1
fi
# GitHub's own closing-keyword set, plus the #N it must attach to.
if grep -qiE "\\b${keyword}" <<<"$text"; then
echo "Closing keyword found — this PR will close its issue on merge."
exit 0
fi
# Opt-outs. `Refs #N` links the issue without closing it; `No-Issue:`
# takes a one-line reason for work that has no issue at all.
if grep -qiE '^[[:space:]]*Refs?:?[[:space:]]+#[0-9]+' <<<"$text"; then
echo "Linked without closing — $(grep -iE '^[[:space:]]*Refs?:?[[:space:]]+#[0-9]+' <<<"$text" | head -1)"
exit 0
fi
if grep -qiE '^[[:space:]]*No-Issue:[[:space:]]*\S' <<<"$text"; then
reason=$(grep -iE '^[[:space:]]*No-Issue:' <<<"$text" | head -1)
echo "Opted out — ${reason}"
exit 0
fi
cat >&2 <<'MSG'
This PR neither links an issue nor says why it doesn't.
Add one of these lines to the PR body:
Closes #1234 (only when this PR finishes the issue; Fixes / Resolves also close)
Refs #1234 (related or partial work; the issue stays open)
No-Issue: <one-line why> (chores, docs typos, revert, dependency bump)
Write a closing keyword only when you mean it: GitHub closes the issue
on merge even inside "does not close #1234".
MSG
exit 1
+6 -135
View File
@@ -31,10 +31,6 @@ on:
required: false
default: 7
type: number
secrets:
CODEWHALE_RELEASE_SIGNING_KEY:
description: Ed25519 release key (PEM). Absent means the release ships unsigned.
required: false
permissions:
contents: read
@@ -91,8 +87,9 @@ jobs:
# Both cold macOS artifact builds on 0.10.1 exhausted 90 minutes inside
# compilation. Keep the full CI build budget; packaging stays capped below.
timeout-minutes: 210
# FreeBSD is a source-build target with no prebuilt asset, no npm binary and no CI check
# (see packaging/freebsd/README.md and docs/INSTALL.md#freebsd).
# FreeBSD is a source-build target validated via `cargo check --target x86_64-unknown-freebsd -p codewhale-cli --locked`
# (see packaging/freebsd/README.md and docs/INSTALL.md#freebsd). The 7×1 prebuilt matrix stays 7 targets;
# FreeBSD has no prebuilt asset, no npm binary, and no matrix bloat — it builds from source.
strategy:
fail-fast: false
matrix:
@@ -153,17 +150,6 @@ jobs:
cli_artifact: codewhale-windows-arm64.exe
shim_artifact: codew-windows-arm64.exe
compat_tui_artifact: codewhale-tui-windows-arm64.exe
# Native HarmonyOS and OpenHarmony. The artifact is useless unsigned:
# the device refuses to execute an unsigned ELF (see
# docs/HarmonyOS.md), so this leg self-signs the binary before staging.
- os: ubuntu-latest
target: aarch64-unknown-linux-ohos
platform: ohos-arm64
cli_binary: codewhale
shim_binary: codew
cli_artifact: codewhale-ohos-arm64
shim_artifact: codew-ohos-arm64
compat_tui_artifact: codewhale-tui-ohos-arm64
runs-on: ${{ matrix.os }}
needs: pin
steps:
@@ -193,23 +179,6 @@ jobs:
with:
cache-bin: false
prefix-key: v1-${{ runner.os }}-${{ runner.arch }}-stable
- name: Add swap so the aarch64 dist build is not killed for memory
if: runner.os == 'Linux' && contains(matrix.target, 'aarch64')
shell: bash
run: |
set -euo pipefail
dir=/mnt
if ! sudo test -w /mnt; then dir=/; fi
avail=$(df -Pk "$dir" | awk 'NR==2 {print int($4 / 1048576)}')
if [ "$avail" -ge 30 ]; then size=16; elif [ "$avail" -ge 14 ]; then size=8; else size=0; fi
if [ "$size" -gt 0 ]; then
sudo fallocate -l "${size}G" "$dir/cw-swapfile"
sudo chmod 600 "$dir/cw-swapfile"
sudo mkswap "$dir/cw-swapfile" >/dev/null
sudo swapon "$dir/cw-swapfile"
fi
free -g
df -h "$dir"
- name: Build static Linux binaries (musl)
if: endsWith(matrix.target, '-unknown-linux-musl')
shell: bash
@@ -265,92 +234,9 @@ jobs:
echo "BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android=--target=aarch64-linux-android24 --sysroot=${ndk}/toolchains/llvm/prebuilt/linux-x86_64/sysroot"
} >> "${GITHUB_ENV}"
- name: Build
if: ${{ !endsWith(matrix.target, '-unknown-linux-musl') && matrix.platform != 'ohos-arm64' }}
if: ${{ !endsWith(matrix.target, '-unknown-linux-musl') }}
shell: bash
run: cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli
- name: Install OpenHarmony native SDK
if: matrix.platform == 'ohos-arm64'
id: ohos-sdk
# Pin the action: it downloads a published OpenHarmony SDK and verifies
# the archive checksum, so a broken mirror fails here instead of
# producing a binary linked against host headers.
uses: openharmony-rs/setup-ohos-sdk@b312caf8a43bb836aa24c08f65f97e1f09a89cad
with:
version: '6.1'
# `toolchains` carries binary-sign-tool.jar, which signs the binary
# below; a release without it would ship an ELF the device refuses to
# execute.
components: 'native;toolchains'
# One download per release is cheaper than letting a tag save a 715 MB
# cache entry that main's jobs then compete with.
cache: false
mirror: true
- name: Install Java for the OpenHarmony binary signer
if: matrix.platform == 'ohos-arm64'
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v4
with:
distribution: temurin
java-version: '17'
- name: Cross-compile and code-sign the OpenHarmony binary
if: matrix.platform == 'ohos-arm64'
shell: bash
env:
OHOS_NATIVE_SDK: ${{ steps.ohos-sdk.outputs.ohos_sdk_native }}
run: |
set -euo pipefail
test -x "${OHOS_NATIVE_SDK}/llvm/bin/clang"
test -d "${OHOS_NATIVE_SDK}/sysroot"
# ohos-env.sh also puts the SDK's cmake on PATH, which aws-lc-sys
# needs; without it the build dies inside its CMake probe.
. ./scripts/ohos-env.sh
cargo build --profile dist --locked --target "${{ matrix.target }}" -p codewhale-cli
bin="target/${{ matrix.target }}/dist/${{ matrix.cli_binary }}"
tools="$(dirname "${OHOS_NATIVE_SDK}")/toolchains/lib"
# SDK 6.1 publishes the native `binary-sign-tool` and no jar, while
# DevEco ships the Java build; both take the same arguments.
if [[ -f "${tools}/binary-sign-tool.jar" ]]; then
signer=(java -jar "${tools}/binary-sign-tool.jar")
elif [[ -f "${tools}/binary-sign-tool" ]]; then
chmod +x "${tools}/binary-sign-tool"
signer=("${tools}/binary-sign-tool")
else
echo "no binary-sign-tool in ${tools}; the device refuses an unsigned ELF" >&2
ls -la "${tools}" >&2
exit 1
fi
"${signer[@]}" sign -inFile "${bin}" -outFile "${bin}" -selfSign 1
# Capture then match. `tool | grep -q` cannot be used under
# `pipefail`: grep exits at the first match, the tool takes SIGPIPE,
# and llvm-readelf reports that as exit 74, which reads as a failed
# verification of a good binary.
signature="$("${signer[@]}" display-sign -inFile "${bin}")"
case "${signature}" in
*'code signature is self-sign'*) ;;
*)
echo 'the staged binary is not self-signed; the device would refuse to execute it' >&2
exit 1
;;
esac
kind="$(file -b "${bin}")"
case "${kind}" in
*'ARM aarch64'*) ;;
*)
echo "not an aarch64 ELF: ${kind}" >&2
exit 1
;;
esac
headers="$("${OHOS_NATIVE_SDK}/llvm/bin/llvm-readelf" -l "${bin}")"
case "${headers}" in
*'/lib/ld-musl-aarch64.so.1'*) ;;
*)
echo "unexpected ELF interpreter for ${bin}" >&2
exit 1
;;
esac
- name: Materialize codew command alias
shell: bash
run: |
@@ -384,12 +270,12 @@ jobs:
bin_dir="target/${{ matrix.target }}/dist"
"${bin_dir}/${{ matrix.cli_binary }}" --version
"${bin_dir}/${{ matrix.shim_binary }}" --version
- name: Windows Terminal clipboard-to-draft acceptance
- name: Windows Terminal clipboard acceptance
if: matrix.platform == 'windows-x64'
timeout-minutes: 5
shell: pwsh
run: node scripts/release/windows-terminal-clipboard.mjs "target/${{ matrix.target }}/dist/codewhale.exe" "${{ runner.temp }}/windows-clipboard"
- name: Retain Windows Terminal clipboard-to-draft evidence
- name: Retain Windows Terminal clipboard evidence
if: ${{ !cancelled() && matrix.platform == 'windows-x64' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
@@ -640,20 +526,6 @@ jobs:
pattern: '*'
- name: Assemble exact authoritative release inventory
run: node scripts/release/assemble-release-assets.js intermediate-artifacts release-assets
- name: Sign the checksum manifest when the release key is configured
env:
CODEWHALE_RELEASE_SIGNING_KEY: ${{ secrets.CODEWHALE_RELEASE_SIGNING_KEY }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [[ -z "${CODEWHALE_RELEASE_SIGNING_KEY}" ]]; then
echo "CODEWHALE_RELEASE_SIGNING_KEY is not configured; this release is unsigned and automatic updates wait for each user's OK."
exit 0
fi
bash scripts/release/sign-release-manifest.sh "${VERSION}" \
release-assets/codewhale-artifacts-sha256.txt \
release-assets/codewhale-artifacts-sha256.txt.sig
node scripts/release/assemble-release-assets.js --verify release-assets
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: codewhale-release-assets
@@ -685,7 +557,6 @@ jobs:
node --test scripts/release/assemble-release-assets.test.js
node --test npm/codewhale/test/artifacts.test.js npm/codewhale/test/release-assets.test.js
- name: Render AUR metadata from candidate Linux archives
if: ${{ !contains(inputs.version, '-') }}
run: bash packaging/aur/render.sh release-assets "${RUNNER_TEMP}/codewhale-bin"
- name: Smoke packed npm wrapper against candidate assets
env:
-2
View File
@@ -137,5 +137,3 @@ jobs:
compiled_host_run_id: ${{ vars.CODEWHALE_COMPILED_HOST_QUALIFICATION_RUN_ID || '' }}
compiled_host_targets: ${{ vars.CODEWHALE_COMPILED_HOST_TARGETS || 'linux-x64,macos-arm64,windows-x64' }}
retention_days: 7
secrets:
CODEWHALE_RELEASE_SIGNING_KEY: ${{ secrets.CODEWHALE_RELEASE_SIGNING_KEY }}
+1 -1
View File
@@ -120,7 +120,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
+17 -33
View File
@@ -6,7 +6,7 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Release version without v; dispatch from the matching existing vX.Y.Z or vX.Y.Z-prerelease tag ref'
description: 'Release version, without v; dispatch from the matching existing vX.Y.Z tag ref'
required: true
type: string
@@ -34,7 +34,6 @@ jobs:
tag: ${{ steps.release.outputs.tag }}
sha: ${{ steps.release.outputs.sha }}
version: ${{ steps.release.outputs.version }}
prerelease: ${{ steps.release.outputs.prerelease }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
@@ -48,8 +47,8 @@ jobs:
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
if ! [[ "${INPUT_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Release version '${INPUT_VERSION}' must use X.Y.Z or X.Y.Z-prerelease." >&2
if ! [[ "${INPUT_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Release version '${INPUT_VERSION}' must use X.Y.Z." >&2
exit 1
fi
tag="v${INPUT_VERSION}"
@@ -61,8 +60,8 @@ jobs:
tag="${GITHUB_REF_NAME}"
fi
if ! [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Release tag '${tag}' must use vX.Y.Z or vX.Y.Z-prerelease." >&2
if ! [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Release tag '${tag}' must use vX.Y.Z." >&2
exit 1
fi
if ! git rev-parse --verify "refs/tags/${tag}^{commit}" >/dev/null 2>&1; then
@@ -77,16 +76,10 @@ jobs:
exit 1
fi
version="${tag#v}"
{
echo "tag=${tag}"
echo "sha=${sha}"
echo "version=${version}"
if [[ "${version}" == *-* ]]; then
echo "prerelease=true"
else
echo "prerelease=false"
fi
echo "version=${tag#v}"
} >> "${GITHUB_OUTPUT}"
- name: Validate tagged release metadata
shell: bash
@@ -153,12 +146,10 @@ jobs:
compiled_host_run_id: ${{ vars.CODEWHALE_COMPILED_HOST_QUALIFICATION_RUN_ID || '' }}
compiled_host_targets: ${{ vars.CODEWHALE_COMPILED_HOST_TARGETS || 'linux-x64,macos-arm64,windows-x64' }}
retention_days: 14
secrets:
CODEWHALE_RELEASE_SIGNING_KEY: ${{ secrets.CODEWHALE_RELEASE_SIGNING_KEY }}
docker-build:
needs: [artifacts, resolve]
if: ${{ !cancelled() && needs.artifacts.result == 'success' && needs.resolve.outputs.prerelease != 'true' }}
if: ${{ !cancelled() && needs.artifacts.result == 'success' }}
name: Docker ${{ matrix.platform }}
timeout-minutes: 60
strategy:
@@ -211,7 +202,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -289,7 +280,7 @@ jobs:
docker:
timeout-minutes: 30
needs: [docker-build, release, resolve]
if: ${{ !cancelled() && needs.docker-build.result == 'success' && needs.release.result == 'success' && needs.resolve.outputs.prerelease != 'true' }}
if: ${{ !cancelled() && needs.docker-build.result == 'success' && needs.release.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
@@ -309,7 +300,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -394,7 +385,7 @@ jobs:
release:
timeout-minutes: 30
needs: [artifacts, docker-build, resolve]
if: ${{ !cancelled() && needs.artifacts.result == 'success' && (needs.resolve.outputs.prerelease == 'true' || needs.docker-build.result == 'success') }}
if: ${{ !cancelled() && needs.artifacts.result == 'success' && needs.docker-build.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
@@ -440,7 +431,7 @@ jobs:
name: Codewhale ${{ needs.resolve.outputs.tag }}
files: artifacts/*
draft: true
prerelease: ${{ needs.resolve.outputs.prerelease == 'true' }}
prerelease: false
body_path: release-body.md
overwrite_files: false
fail_on_unmatched_files: true
@@ -458,7 +449,7 @@ jobs:
# also works when GITHUB_TOKEN publication suppresses release events.
cnb:
needs: [release, resolve]
if: ${{ !cancelled() && needs.release.result == 'success' && needs.resolve.outputs.prerelease != 'true' }}
if: ${{ !cancelled() && needs.release.result == 'success' }}
uses: ./.github/workflows/sync-cnb.yml
with:
release_tag: ${{ needs.resolve.outputs.tag }}
@@ -468,7 +459,7 @@ jobs:
gitcode:
needs: [release, resolve]
if: ${{ !cancelled() && needs.release.result == 'success' && vars.GITCODE_MIRROR == 'enabled' && needs.resolve.outputs.prerelease != 'true' }}
if: ${{ !cancelled() && needs.release.result == 'success' && vars.GITCODE_MIRROR == 'enabled' }}
uses: ./.github/workflows/sync-gitcode.yml
with:
release_tag: ${{ needs.resolve.outputs.tag }}
@@ -519,19 +510,12 @@ jobs:
# public GitHub Release and therefore needs the same read token as
# the explicit asset gate above.
GH_TOKEN: ${{ github.token }}
PRERELEASE: ${{ needs.resolve.outputs.prerelease }}
run: |
set -euo pipefail
if [[ "${PRERELEASE}" == "true" ]]; then
npm publish --access public --tag next
else
npm publish --access public
fi
run: npm publish --access public
homebrew:
timeout-minutes: 20
needs: [release, resolve]
if: ${{ !cancelled() && needs.release.result == 'success' && needs.resolve.outputs.prerelease != 'true' }}
if: ${{ !cancelled() && needs.release.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
@@ -590,7 +574,7 @@ jobs:
sync-release-record:
timeout-minutes: 15
needs: [release, resolve]
if: ${{ !cancelled() && needs.release.result == 'success' && needs.resolve.outputs.prerelease != 'true' }}
if: ${{ !cancelled() && needs.release.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
+2 -1
View File
@@ -38,7 +38,8 @@ jobs:
- { name: root, directory: . }
- { name: web, directory: web }
- { name: telemetry-ingest, directory: telemetry-ingest }
- { name: extension-host, directory: crates/engine/extension-host }
- { name: extension-host, directory: crates/tui/extension-host }
- { name: embedded-computer-use, directory: crates/tui/plugins/computer-use }
- { name: vscode, directory: extensions/vscode }
- { name: feishu-bridge, directory: integrations/feishu-bridge }
- { name: telegram-bridge, directory: integrations/telegram-bridge }
+7
View File
@@ -27,6 +27,11 @@ jobs:
const blob = `${issue.title || ''}\n${issue.body || ''}`;
const patterns = [
/\bcrypto\b/i,
/\bairdrop\b/i,
/\bnft\b/i,
/\bpresale\b/i,
/\busdt\b/i,
/\btg\s*@/i,
/\btelegram\s+@/i,
/\bt\.me\//i,
@@ -35,6 +40,8 @@ jobs:
/\bguest\s+post/i,
/\bbacklink/i,
/\bbuy\s+followers/i,
/\bjoin\s+our\s+(community|server|group)/i,
/\bpromot[ei]\s+your\b/i,
];
const hit = patterns.find(p => p.test(blob));
if (!hit) return;
+29 -83
View File
@@ -1,14 +1,11 @@
name: Issue and PR triage
name: Issue triage
on:
issues:
types: [opened, reopened]
pull_request_target:
types: [opened, reopened, synchronize]
permissions:
issues: write
pull-requests: write
contents: read
jobs:
@@ -16,95 +13,44 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Auto-label by title, body and changed paths
- name: Auto-label by title and body
uses: actions/github-script@v9
with:
script: |
const pr = context.payload.pull_request;
const issue = context.payload.issue || pr;
const issue = context.payload.issue;
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = `${title}\n${body}`;
const labels = new Set();
if (pr) {
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100,
});
const pathRules = [
[/^crates\/(tui|tui-kit|palette)\//, 'area:tui'],
[/^crates\/(tools\/|engine\/src\/tools\/)/, 'area:tools'],
[/^crates\/(engine|core|runtime|agent|agent-contract|protocol)\/(?!src\/tools\/)/, 'area:core'],
[/^crates\/mcp\//, 'area:mcp'],
[/^crates\/(state|snapshot|memory)\//, 'area:state'],
[/^crates\/(sandbox|execpolicy)\//, 'area:sandbox'],
[/^crates\/(plugins|skills|skill-install|hooks)\//, 'area:plugins'],
[/^(crates\/(app-server|app-server-protocol)|npm\/runtime-sdk)\//, 'area:runtime-api'],
[/^crates\/models\//, 'area:providers'],
[/^(crates\/localization|docs\/zh_hans)\//, 'area:localization'],
[/^(npm\/(codewhale|deepseek-tui)|scripts\/release)\//, 'area:install'],
[/^(\.github|scripts\/ci)\//, 'area:ci'],
[/^apps\/desktop\//, 'area:desktop'],
[/^(apps\/web|web)\//, 'area:web'],
[/^apps\/mobile\//, 'area:mobile'],
];
const areas = new Set();
let docsOnly = files.length > 0;
for (const f of files) {
if (!/^(docs\/|[^/]+\.md$)/.test(f.filename)) docsOnly = false;
for (const [re, name] of pathRules) if (re.test(f.filename)) areas.add(name);
}
if (areas.size <= 4) areas.forEach(name => labels.add(name));
if (docsOnly) labels.add('documentation');
if (/^feat(\(|!|:)/.test(title)) labels.add('enhancement');
if (/^fix(\(|!|:)/.test(title)) labels.add('bug');
if (/^docs(\(|!|:)/.test(title)) labels.add('documentation');
if (/^(chore|refactor|style|build)(\(|!|:)/.test(title)) labels.add('cleanup');
if (/^perf(\(|!|:)/.test(title)) labels.add('performance');
if (!['OWNER', 'MEMBER', 'COLLABORATOR'].includes(pr.author_association) && pr.user.type !== 'Bot') {
labels.add('contributor:external');
}
} else {
// Type — matched against the TITLE only.
//
// These ran against title+body and mislabelled long, well-specified
// issues. Any body containing an acceptance criterion like "fails
// when a pack drifts" matched the bug rule, and a "How to add a
// locale" heading matched the question rule, so planned work landed
// tagged `bug, question`. A title states the type; a body just
// discusses it. `error`/`fail` are dropped entirely — they describe
// what almost every issue talks about, not what any issue *is*.
if (/\b(bug|crash|panic|broken|stack ?trace|regression)\b/.test(title)) labels.add('bug');
if (/\b(feat(?:ure)?|request|enhancement|wishlist|proposal|please add|would be nice|support for)\b/.test(title)) labels.add('enhancement');
if (/\b(docs?|readme|documentation|typo|grammar|wording|spelling)\b/.test(title)) labels.add('documentation');
if (/\b(question|how (?:do|to)|why does|what does|is it possible)\b/.test(title)) labels.add('question');
// Type — matched against the TITLE only.
//
// These ran against title+body and mislabelled long, well-specified
// issues. Any body containing an acceptance criterion like "fails
// when a pack drifts" matched the bug rule, and a "How to add a
// locale" heading matched the question rule, so planned work landed
// tagged `bug, question`. A title states the type; a body just
// discusses it. `error`/`fail` are dropped entirely — they describe
// what almost every issue talks about, not what any issue *is*.
if (/\b(bug|crash|panic|broken|stack ?trace|regression)\b/.test(title)) labels.add('bug');
if (/\b(feat(?:ure)?|request|enhancement|wishlist|proposal|please add|would be nice|support for)\b/.test(title)) labels.add('enhancement');
if (/\b(docs?|readme|documentation|typo|grammar|wording|spelling)\b/.test(title)) labels.add('documentation');
if (/\b(question|how (?:do|to)|why does|what does|is it possible)\b/.test(title)) labels.add('question');
// Locale — title contains CJK (Chinese, Japanese, Korean) characters
if (/[぀-ヿ㐀-鿿가-힯]/.test(issue.title || '')) labels.add('lang:zh');
// Locale — title contains CJK (Chinese, Japanese, Korean) characters
if (/[぀-ヿ㐀-鿿가-힯]/.test(issue.title || '')) labels.add('lang:zh');
// Areas (path-driven hints)
if (/crates\/tui|\btui\b|ratatui|composer|sidebar/.test(text)) labels.add('area:tui');
if (/crates\/core|engine|turn ?loop|agent ?loop/.test(text)) labels.add('area:core');
if (/crates\/mcp|\bmcp\b/.test(text)) labels.add('area:mcp');
if (/crates\/state|sqlite|sessions?|persistence/.test(text)) labels.add('area:state');
if (/crates\/execpolicy|approval|sandbox|seatbelt|landlock/.test(text)) labels.add('area:sandbox');
if (/crates\/tools|tool[ _]call|tool[ _]registry/.test(text)) labels.add('area:tools');
if (/install|cargo install|npm install|scoop|homebrew|prebuilt|binary/.test(text)) labels.add('area:install');
if (/\bdesktop app\b|electron/.test(text)) labels.add('area:desktop');
if (/app\.codewhale\.net|\bweb app\b|codewhale web/.test(text)) labels.add('area:web');
if (/\bmobile\b|\bios\b|\bandroid\b/.test(text)) labels.add('area:mobile');
if (/screen ?reader|accessib|a11y|voiceover|\bnvda\b|keyboard navigation/.test(text)) labels.add('accessibility');
if (/windows/.test(text)) labels.add('os:windows');
if (/freebsd/.test(text)) labels.add('os:freebsd');
if (/\bohos\b|harmonyos/.test(text)) labels.add('os:ohos');
if (/macos|darwin|apple silicon/.test(text)) labels.add('os:macos');
if (/\blinux\b|ubuntu|debian|fedora|arch ?linux/.test(text)) labels.add('os:linux');
}
// Areas (path-driven hints)
if (/crates\/tui|\btui\b|ratatui|composer|sidebar/.test(text)) labels.add('area:tui');
if (/crates\/core|engine|turn ?loop|agent ?loop/.test(text)) labels.add('area:core');
if (/crates\/mcp|\bmcp\b/.test(text)) labels.add('area:mcp');
if (/crates\/state|sqlite|sessions?|persistence/.test(text)) labels.add('area:state');
if (/crates\/execpolicy|approval|sandbox|seatbelt|landlock/.test(text)) labels.add('area:execpolicy');
if (/crates\/tools|tool[ _]call|tool[ _]registry/.test(text)) labels.add('area:tools');
if (/install|cargo install|npm install|scoop|homebrew|prebuilt|binary/.test(text)) labels.add('area:install');
if (/windows/.test(text)) labels.add('os:windows');
if (/macos|darwin|apple silicon/.test(text)) labels.add('os:macos');
if (/\blinux\b|ubuntu|debian|fedora|arch ?linux/.test(text)) labels.add('os:linux');
if (labels.size === 0) return;
-319
View File
@@ -1,319 +0,0 @@
name: Web maintainer
on:
schedule:
- cron: '43 6 * * *'
workflow_dispatch:
inputs:
lane:
description: Which lane to run
required: true
default: all
type: choice
options:
- all
- derived
- copy-freshness
- link-health
- docs-drift
permissions: {}
concurrency:
group: web-maintainer
cancel-in-progress: false
env:
WEB_MAINTAINER_CLI_VERSION: v0.10.2
jobs:
plan:
name: Plan
if: github.repository == 'codewhale-hq/Codewhale' && github.ref == 'refs/heads/main' && vars.WEB_MAINTAINER_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
outputs:
lanes: ${{ steps.plan.outputs.lanes }}
derived: ${{ steps.plan.outputs.derived }}
steps:
- id: plan
env:
INPUT_LANE: ${{ inputs.lane || 'all' }}
run: |
derived=false
lanes='[]'
case "$INPUT_LANE" in
all) derived=true; lanes='["copy-freshness","link-health","docs-drift"]' ;;
derived) derived=true ;;
copy-freshness|link-health|docs-drift) lanes="[\"${INPUT_LANE}\"]" ;;
*) echo "::error::unknown lane"; exit 1 ;;
esac
echo "derived=${derived}" >> "$GITHUB_OUTPUT"
echo "lanes=${lanes}" >> "$GITHUB_OUTPUT"
echo "Web maintainer plan: derived=${derived} lanes=${lanes}" >> "$GITHUB_STEP_SUMMARY"
derived:
name: Derived files
needs: plan
if: needs.plan.result == 'success' && needs.plan.outputs.derived == 'true' && vars.WEB_MAINTAINER_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: main
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
- name: Install web dependencies
working-directory: web
run: npm ci --no-audit --no-fund
- name: Regenerate derived files
working-directory: web
env:
GH_TOKEN: ${{ github.token }}
run: |
node scripts/sync-latest-release.mjs
node scripts/derive-facts.mjs
node scripts/derive-changelog.mjs
node scripts/derive-install.mjs
node scripts/render-terminal-capture.mjs
- name: Check what changed
id: drift
run: |
mkdir -p "$RUNNER_TEMP/out"
if [ -n "$(git ls-files --others --exclude-standard)" ]; then
echo "::error::regeneration produced untracked files"
git ls-files --others --exclude-standard
exit 1
fi
git diff --name-status | node web/maintainer/check-allowlist.mjs web/maintainer/derived.txt derived
if [ -n "$(git diff --summary)" ]; then
echo "::error::regeneration created, deleted or changed modes"
exit 1
fi
git diff --binary > "$RUNNER_TEMP/out/change.patch"
if [ -s "$RUNNER_TEMP/out/change.patch" ]; then
echo "drift=true" >> "$GITHUB_OUTPUT"
git diff --stat >> "$GITHUB_STEP_SUMMARY"
else
echo "drift=false" >> "$GITHUB_OUTPUT"
echo "Derived files are current." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Run the web checks on the regenerated tree
if: steps.drift.outputs.drift == 'true'
working-directory: web
run: |
npm run check
npx vitest run
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: steps.drift.outputs.drift == 'true'
with:
name: web-maintainer-derived
path: ${{ runner.temp }}/out/change.patch
retention-days: 3
if-no-files-found: error
lane:
name: Lane ${{ matrix.lane }}
needs: plan
if: needs.plan.result == 'success' && needs.plan.outputs.lanes != '[]' && vars.WEB_MAINTAINER_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
strategy:
fail-fast: false
max-parallel: 3
matrix:
lane: ${{ fromJSON(needs.plan.outputs.lanes) }}
env:
LANE: ${{ matrix.lane }}
steps:
- name: Require the DeepSeek key
env:
KEY: ${{ secrets.WEB_MAINTAINER_DEEPSEEK_API_KEY }}
run: |
if [ -z "$KEY" ]; then
echo "::error::WEB_MAINTAINER_DEEPSEEK_API_KEY is not set. Create a dedicated DeepSeek key with a provider-side monthly cap and add it as that repository secret, or unset WEB_MAINTAINER_ENABLED. See web/maintainer/README.md."
exit 1
fi
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: main
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
- name: Prove the command sandbox works or stop
run: |
sudo apt-get install -y --no-install-recommends bubblewrap util-linux >/dev/null
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 >/dev/null || true
probe="$GITHUB_WORKSPACE/.sandbox-probe"
rm -f "$probe" /usr/local/sandbox-probe
if ! timeout 10 bash -c 'exec 3<>/dev/tcp/api.deepseek.com/443'; then
echo "::error::the runner cannot reach api.deepseek.com, so the network denial cannot be proven."
exit 1
fi
if ! /usr/bin/bwrap --unshare-all --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp \
--bind "$GITHUB_WORKSPACE" "$GITHUB_WORKSPACE" --chdir "$GITHUB_WORKSPACE" -- \
bash -c 'touch .sandbox-probe && ! touch /usr/local/sandbox-probe 2>/dev/null && ! timeout 10 bash -c "exec 3<>/dev/tcp/api.deepseek.com/443" 2>/dev/null'; then
echo "::error::bubblewrap did not confine a child on this runner (writes outside the workspace or outbound network were not denied). Refusing to give a model a shell."
exit 1
fi
if [ ! -e "$probe" ] || [ -e /usr/local/sandbox-probe ]; then
echo "::error::sandbox probe left unexpected state. Refusing to give a model a shell."
exit 1
fi
rm -f "$probe"
echo "Sandbox probe passed: workspace-only writes, no outbound network." >> "$GITHUB_STEP_SUMMARY"
- name: Stage trusted tooling and the pinned CLI
run: |
mkdir -p "$RUNNER_TEMP/trusted" "$RUNNER_TEMP/home"
cp web/maintainer/allowlist.txt web/maintainer/check-allowlist.mjs "$RUNNER_TEMP/trusted/"
cp "web/maintainer/lanes/${LANE}.md" "$RUNNER_TEMP/trusted/prompt.md"
CODEWHALE_VERSION="$WEB_MAINTAINER_CLI_VERSION" CODEWHALE_INSTALL_DIR="$RUNNER_TEMP/bin" sh web/public/install.sh
"$RUNNER_TEMP/bin/codewhale" --version
- name: Probe outbound links
if: matrix.lane == 'link-health'
run: node web/maintainer/link-report.mjs "$GITHUB_WORKSPACE/.git/maintainer-input/link-report.json"
- name: Record the git state the model must not change
run: |
git config --local --list | sort > "$RUNNER_TEMP/git-config.before"
find .git/hooks -type f ! -name '*.sample' > "$RUNNER_TEMP/hooks.before"
test ! -s "$RUNNER_TEMP/hooks.before"
- name: Run the lane on DeepSeek
timeout-minutes: 25
env:
WEB_MAINTAINER_DEEPSEEK_API_KEY: ${{ secrets.WEB_MAINTAINER_DEEPSEEK_API_KEY }}
run: |
env -i \
PATH="$PATH" \
HOME="$RUNNER_TEMP/home" \
CODEWHALE_HOME="$RUNNER_TEMP/home/.codewhale" \
CODEWHALE_SECRET_BACKEND=file \
CODEWHALE_MEMORY=false \
CODEWHALE_TELEMETRY=false \
DEEPSEEK_API_KEY="$WEB_MAINTAINER_DEEPSEEK_API_KEY" \
"$RUNNER_TEMP/bin/codewhale" \
--no-project-config \
--skip-onboarding \
--workspace "$GITHUB_WORKSPACE" \
exec \
--provider deepseek \
--model deepseek-flash \
--reasoning-effort low \
--auto \
--sandbox workspace-write \
--max-turns 40 \
--output-format stream-json \
-- "$(cat "$RUNNER_TEMP/trusted/prompt.md")" \
> "$RUNNER_TEMP/exec.jsonl"
echo "Model run finished: $(wc -l < "$RUNNER_TEMP/exec.jsonl") stream events." >> "$GITHUB_STEP_SUMMARY"
- name: Enforce the path allowlist
id: scope
run: |
if git status --porcelain --untracked-files=all --ignored | grep -qv '^ M '; then
echo "::error::the lane left untracked, ignored, staged or non-edit entries"
git status --porcelain --untracked-files=all --ignored | grep -v '^ M '
exit 1
fi
git config --local --list | sort | diff - "$RUNNER_TEMP/git-config.before" || { echo "::error::the lane changed .git/config"; exit 1; }
find .git/hooks -type f ! -name '*.sample' | diff - "$RUNNER_TEMP/hooks.before" || { echo "::error::the lane added git hooks"; exit 1; }
git diff --name-status | node "$RUNNER_TEMP/trusted/check-allowlist.mjs" "$RUNNER_TEMP/trusted/allowlist.txt" "$LANE"
if [ -n "$(git diff --summary)" ]; then
echo "::error::the lane created, deleted or changed modes"
exit 1
fi
mkdir -p "$RUNNER_TEMP/out"
git diff --binary > "$RUNNER_TEMP/out/change.patch"
if [ ! -s "$RUNNER_TEMP/out/change.patch" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "Lane ${LANE}: no change proposed." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$(wc -c < "$RUNNER_TEMP/out/change.patch")" -gt 262144 ]; then
echo "::error::patch exceeds 256 KiB"
exit 1
fi
echo "changed=true" >> "$GITHUB_OUTPUT"
{ echo "Lane ${LANE} proposes:"; echo '```text'; git diff --stat; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
- name: Fresh checkout for verification
if: steps.scope.outputs.changed == 'true'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: main
path: verify
persist-credentials: false
- name: Run the web checks on the proposal in a clean tree without secrets
if: steps.scope.outputs.changed == 'true'
env:
PATCH: ${{ runner.temp }}/out/change.patch
run: |
cd verify
git apply --index --whitespace=nowarn "$PATCH"
cd web
npm ci --no-audit --no-fund
npm run check
npx vitest run
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: steps.scope.outputs.changed == 'true'
with:
name: web-maintainer-${{ matrix.lane }}
path: ${{ runner.temp }}/out/change.patch
retention-days: 3
if-no-files-found: error
publish:
name: Open pull requests
needs: [plan, derived, lane]
if: always() && needs.plan.result == 'success' && vars.WEB_MAINTAINER_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
continue-on-error: true
with:
pattern: web-maintainer-*
path: ${{ runner.temp }}/patches
- name: Look for proposed patches
id: patches
run: |
if [ -n "$(find "$RUNNER_TEMP/patches" -name change.patch -size +0c 2>/dev/null)" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No lane proposed a change; no pull request opened." >> "$GITHUB_STEP_SUMMARY"
fi
- if: steps.patches.outputs.found == 'true'
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3
id: app-token
with:
app-id: ${{ vars.CODEWHALE_APP_ID }}
private-key: ${{ secrets.CODEWHALE_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- if: steps.patches.outputs.found == 'true'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: main
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false
- name: Open or update one pull request per lane
if: steps.patches.outputs.found == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_ID: ${{ vars.CODEWHALE_APP_ID }}
ARTIFACTS_DIR: ${{ runner.temp }}/patches
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
mkdir -p "$RUNNER_TEMP/trusted"
cp web/maintainer/allowlist.txt web/maintainer/derived.txt web/maintainer/check-allowlist.mjs web/maintainer/publish-prs.sh "$RUNNER_TEMP/trusted/"
TRUSTED_DIR="$RUNNER_TEMP/trusted" bash "$RUNNER_TEMP/trusted/publish-prs.sh"
-40
View File
@@ -1,40 +0,0 @@
name: Windows focus
on:
workflow_dispatch:
inputs:
expected_sha:
description: Exact 40-character commit selected by --ref
required: true
type: string
filter:
description: nextest expression, for example 'test(compaction_lock) | test(persistence_actor)'
required: true
default: all()
type: string
permissions:
contents: read
jobs:
focused:
name: Focused tests (windows-latest)
runs-on: windows-latest
timeout-minutes: 90
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUSTFLAGS: -Dwarnings
RUST_MIN_STACK: 8388608
RUST_BACKTRACE: full
FILTER: ${{ inputs.filter }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.expected_sha }}
fetch-depth: 1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- name: Run focused tests
shell: bash
run: cargo nextest run -p codewhale-engine -p codewhale-plugins -p codewhale-tui -p codewhale-app-server --lib --locked --profile ci -E "$FILTER"
+1 -13
View File
@@ -1,7 +1,6 @@
# Build artifacts
/target
/target-*
/vendor/codewhale-ratatui/target/
.entire/
.cursor/
.plans/
@@ -91,11 +90,9 @@ project_overhaul_prompt.md
# Local runtime state
# Ignore everything under any .codewhale/ (snapshots, auto-generated
# instructions.md, etc.) at any depth EXCEPT the committed repo authority policy
# and this repository's own project hooks.
# instructions.md, etc.) at any depth EXCEPT the committed repo authority policy.
**/.codewhale/*
!**/.codewhale/constitution.json
!/.codewhale/hooks.toml
.deepseek/
**/session_*.json
*.db
@@ -183,12 +180,3 @@ orca-evidence/
# Portable pet build entry points are source.
!pet/**/*.sh
!pet/verify.sh
# Private keys and certificates never belong in the tree.
*.pem
*.key
*.p12
*.p8
id_rsa*
id_ed25519*
!docs/cloud-facts/fixtures/test-only-signing-key.pem
+81 -14
View File
@@ -1,10 +1,13 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.12.0.schema.json
# HunterBown.CodeWhale — winget singleton manifest for CodeWhale (single binary)
# PackageIdentifier follows the publisher convention used by the Homebrew formula (codewhale).
# Canonical maintenance instructions live in packaging/winget/README.md; the generator mirrors these exact bytes to .winget/.
# Winget installs only `codewhale` + `codew`; compatibility release filenames are not installed commands.
PackageIdentifier: HunterBown.CodeWhale
PackageVersion: 0.10.2
PackageLocale: en-US
PackageVersion: 0.9.6
DefaultLocale: en-US
ManifestType: singleton
ManifestVersion: 1.12.0
ManifestVersion: 1.6.0
Publisher: Hmbown
PublisherUrl: https://github.com/Hmbown
@@ -16,7 +19,10 @@ LicenseUrl: https://github.com/codewhale-hq/CodeWhale/blob/main/LICENSE
Copyright: Copyright (c) Hmbown
ShortDescription: Terminal coding agent for supported hosted and local models
Description: |
CodeWhale runs in your terminal, connects to hosted and local models, and edits your workspace with approval controls.
CodeWhale is a terminal coding agent that runs on your machine. The v0.9.5+ single-binary
release ships one `codewhale` binary per target (plus the `codew` shim) across Linux x64 (musl),
Linux arm64, Android arm64, macOS x64/arm64, and Windows x64/arm64. See https://github.com/codewhale-hq/CodeWhale
for provider setup, Fleet workflows, and the full install guide (docs/INSTALL.md).
Author: Hmbown
Moniker: codewhale
Tags:
@@ -28,18 +34,79 @@ Tags:
- ai
- coding-agent
- rust
ReleaseNotesUrl: https://github.com/codewhale-hq/CodeWhale/releases/tag/v0.10.2
MinimumOSVersion: 10.0.0.0
ReleaseNotes: https://github.com/codewhale-hq/CodeWhale/releases/tag/v0.9.6
ReleaseNotesUrl: https://github.com/codewhale-hq/CodeWhale/releases/tag/v0.9.6
InstallationNotes: |
The winget package installs the Windows binaries. For the NSIS installer (CodeWhaleSetup.exe)
the installer adds %LOCALAPPDATA%\Programs\CodeWhale\bin to the user PATH. For the portable
ZIP, winget extracts codewhale.exe and codew.exe side-by-side and adds the install location to PATH.
Verify checksums with codewhale-artifacts-sha256.txt from the same GitHub Release.
Documentations:
- DocumentLabel: Install guide
DocumentUrl: https://github.com/codewhale-hq/CodeWhale/blob/main/docs/INSTALL.md
- DocumentLabel: Releases
DocumentUrl: https://github.com/codewhale-hq/CodeWhale/releases
InstallerType: portable
Commands:
- codewhale
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.VCRedist.2015+.x64
Installers:
# Preferred: NSIS installer for Windows x64 (per-user, no elevation, adds user PATH).
- Architecture: x64
InstallerUrl: https://github.com/codewhale-hq/Codewhale/releases/download/v0.10.2/codewhale-tui-windows-x64.exe
InstallerSha256: 578280bec8d6e0ee9cfe97ee29de817535689f4078d8afd47384ff1941b1ee6c
InstallerType: nullsoft
Scope: user
InstallerUrl: https://github.com/codewhale-hq/CodeWhale/releases/download/v0.9.6/CodeWhaleSetup.exe
InstallerSha256: 040afa01a70ddae4ea7783e563bacd962a512b4d288ee5c5c5111ef2760755a7
ProductCode: CodeWhale
UpgradeBehavior: install
FileExtensions:
- toml
ReleaseDate: 2026-08-12
# Portable ZIP fallback — same single binary (codewhale.exe + codew.exe). Used when NSIS is blocked
# by policy or for winget's portable install flow. NestedInstallerType is portable (no installer).
- Architecture: x64
InstallerType: zip
Scope: user
InstallerUrl: https://github.com/codewhale-hq/CodeWhale/releases/download/v0.9.6/codewhale-windows-x64.zip
InstallerSha256: a3e0ed9b5810904ca4f0d76074aca749dfd13b71126de17bb12357d3506587b1
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: codewhale-windows-x64/codewhale.exe
PortableCommandAlias: codewhale
- RelativeFilePath: codewhale-windows-x64/codew.exe
PortableCommandAlias: codew
ReleaseDate: 2026-08-12
- Architecture: x64
InstallerType: zip
Scope: user
InstallerUrl: https://github.com/codewhale-hq/CodeWhale/releases/download/v0.9.6/codewhale-windows-x64-portable.zip
InstallerSha256: f803fba6f174ccd710e81b4e986daedcbe7a62fda0744f8c64342aa7751d1206
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: codewhale-windows-x64-portable/codewhale.exe
PortableCommandAlias: codewhale
- RelativeFilePath: codewhale-windows-x64-portable/codew.exe
PortableCommandAlias: codew
ReleaseDate: 2026-08-12
- Architecture: arm64
InstallerType: zip
Scope: user
InstallerUrl: https://github.com/codewhale-hq/CodeWhale/releases/download/v0.9.6/codewhale-windows-arm64.zip
InstallerSha256: c1ed644838550922c563fca7b00646482d3a3bb781aa92affc9059d14ca6d942
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: codewhale-windows-arm64/codewhale.exe
PortableCommandAlias: codewhale
- RelativeFilePath: codewhale-windows-arm64/codew.exe
PortableCommandAlias: codew
ReleaseDate: 2026-08-12
- Architecture: arm64
InstallerType: zip
Scope: user
InstallerUrl: https://github.com/codewhale-hq/CodeWhale/releases/download/v0.9.6/codewhale-windows-arm64-portable.zip
InstallerSha256: ac2acd52090c88fd0babf94a30951b97c4eb1f45f522ca70e0fee12d2b728102
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: codewhale-windows-arm64-portable/codewhale.exe
PortableCommandAlias: codewhale
- RelativeFilePath: codewhale-windows-arm64-portable/codew.exe
PortableCommandAlias: codew
ReleaseDate: 2026-08-12
+38 -45
View File
@@ -55,13 +55,10 @@ base prompt". Two more corollaries earned here:
- Search for behavior and symbols before reviving work from an old branch. If a
lane is obsolete, preserve its intent and evidence rather than merging stale
code mechanically.
- Agent-owned work lands on current `main`. Each writing agent works in its
own short-lived git worktree cut from `origin/main`, rebases and pushes
`main` the same day, then removes the worktree and its build directory. No
long-lived branches, nested clones or integration lanes, and never a new
worktree to hide dirty, stale or conflicting work: that work is still yours
to finish. In a shared checkout, stage by explicit path and retry an
`index.lock` failure. Verified main slices may
- Agent-owned work uses the canonical checkout on current `main`. Do not
create branches, worktrees, nested clones or task-specific integration lanes
to handle dirty, stale or conflicting work. When agents share the checkout,
partition by file and retry an `index.lock` failure. Verified main slices may
be committed and pushed normally under the rule below; tag, release,
deployment and destructive cleanup still require their explicit
authorization.
@@ -80,11 +77,11 @@ base prompt". Two more corollaries earned here:
- Commit by explicit path; never `git add -A`.
- Work that fails its check and cannot be fixed now is not pushed to `main`.
Record it and report it instead.
- **Maintainers land small verified slices directly on `main`.** Run the
focused local check (`scripts/preflight.sh --check`), put the real counts in
the commit message, fetch and rebase onto `origin/main` and retry on
rejection. Never force-push or rewrite a shared ref. Contributor PRs are
always welcome and merge as themselves (founder, 2026-11-07).
- **Maintainers do not open PR branches** (founder, 2026-10-08: more PRs means
more CI and slower work). Verified slices land directly on `main` and are
pushed: run the focused gate, put the real counts in the commit message,
fetch and rebase onto `origin/main` and retry on rejection. Never force-push
or rewrite a shared ref. Contributor PRs still merge as PRs.
- When the task is local-only, stay fully offline: no browsing, GitHub or remote
Git operations, downloads, dependency installation, provider calls, or
source/diff transmission. Record the missing external receipt and keep working
@@ -105,19 +102,19 @@ base prompt". Two more corollaries earned here:
- **Write down what a design does not do**, beside the behaviour it owns — a
short known-limitations note in the owning module. A stated limit stops the
next reader from assuming a capability that was never built.
- **Be generous in public** (founder, 2026-11-07). Thank people specifically
for their reports, diagnosis, fixes and testing; link the carrying commit or
PR and distinguish candidate work from a shipped release. Status, answers
and useful questions are welcome; repetitive progress posts and bot noise
are not. Evidence stays in commits and PR bodies; ownership stays in Linear.
A contributor PR is never closed because CI is slow, flaky or red for a
reason the PR did not cause; when a PR is closed, say why in one specific,
thankful sentence. Review bots stay off unless the founder enables them.
- **Substantive contributor replies are authorized for the 0.10.2 release**
(founder, 2026-10-13). Thank people specifically for their reports, diagnosis,
fixes and testing; link the carrying commit or PR and distinguish candidate
work from a shipped release. Status, answers and useful questions are welcome;
repetitive progress posts and bot noise are not. Evidence stays in commits
and PR bodies; ownership stays in Linear. Outside this release, the previous
no-comment rule remains, except a one-sentence reason when closing or
superseding a human contribution. Review workflows remain disabled unless
the founder separately authorizes enabling them.
- **A user feature lands with its registry row.** A new command, `[features]`
flag, provider or user-visible feature adds or updates its row in
`docs/features.toml` in the same change;
`cargo test -p codewhale-tui --test feature_registry` fails on flag drift.
- **Public planned-work issues are written for contributors.** An issue opened for planned or release work has the parts in CONTRIBUTING.md § "What a planned-work issue gives you": status, ways to help with files, a local test command and a done-when, what maintainers already hold, and how to claim. Keep it current as pieces land or are claimed.
- **Write `close`/`fix`/`resolve #N` only when you mean it.** GitHub closes the
issue on merge even inside "does not close #N"; use `Refs #N` otherwise.
@@ -126,18 +123,17 @@ base prompt". Two more corollaries earned here:
An external contributor's branch goes stale because *we* land things, not
because they did anything wrong. Treat their time as more expensive than ours.
**The goal is the contributor's PR merging as itself.** Review it, run or
rerun only the checks it needs, fix small problems on their branch, and resolve
conflicts that `main` caused. Landing their commits as ours (harvesting) is the
rare fallback for a branch we cannot push to and that cannot merge in
reasonable time; ask on the PR first, because done casually it reads as taking
the work even when credit is preserved.
**The goal is the contributor's PR merging as itself.** Review it, help it
rebase, or fix it on their branch — that is the default path. Closing their PR
and re-landing the work as our own commit (`auto-close-harvested`) is the
fallback for a branch that truly cannot merge in reasonable time; done
casually it reads as taking the work even when credit is preserved.
- **Never make a contributor rebase around our churn.** If their PR conflicts
only because main moved, a maintainer resolves it.
- Landing mechanics — merge-base diffing, mid-function conflicts, fork-push
refusal, runs awaiting first-contributor approval — live in `cw-land`.
Follow them instead of improvising.
refusal, the contribution gate — live in `cw-land`. Follow them instead of
improvising.
- **Preserve credit in the mechanical sense, not just the polite one.** Commit
authorship and `Co-authored-by` trailers must use the contributor's own
GitHub-linked address. `AUTHOR_MAP` and `.mailmap` are project conventions —
@@ -173,11 +169,11 @@ the work even when credit is preserved.
- The model-facing subagent tool is `agent`; `agent_open`/`agent_eval`/
`agent_close`/`delegate_to_agent` are removed surfaces. If the shape must
move, move the code and its existing guard test with it.
- `BASE_PROMPT` in `crates/engine/src/prompts/text.rs` is the sole base prompt
- `BASE_PROMPT` in `crates/tui/src/prompts/text.rs` is the sole base prompt
by convention. Same rule: move the code, not the prose, if that changes.
- There is exactly one turn loop: `Engine::run_turn` in
`crates/engine/src/core/engine/turn_loop.rs`. Note that `crates/engine/src/core/`
is a module inside the engine crate (`codewhale-engine`) — it is not `crates/core`, which owns
`crates/tui/src/core/engine/turn_loop.rs`. Note that `crates/tui/src/core/`
is a module inside the TUI crate — it is not `crates/core`, which owns
request construction, bounded fragments, and thread/session types and
runs no turns. A guard test (`crates/core/tests/single_turn_loop.rs`)
fails on a second loop; changing the shape means changing the guard
@@ -187,8 +183,8 @@ the work even when credit is preserved.
KV-cache effect: frozen prefix vs. append-only history. Never splice a
volatile fact into the prefix; append it as a user-role message.
- These active modules are repeatedly misidentified as dead; verify consumers
before removal: `runtime/src/context_budget.rs`,
`runtime/src/prompt_zones.rs`, `engine/src/tools/remember.rs`, and
before removal: `runtime/src/context_budget.rs`, `tui/src/model_registry.rs`,
`runtime/src/prompt_zones.rs`, `tui/src/tools/remember.rs`, and
`config/src/route/`. Native memory lives in `runtime/src/native_memory.rs`;
`tools/remember.rs` is its capture path.
- Environment-specific behavior belongs in `docs/ENVIRONMENTS.md`, not here.
@@ -205,10 +201,10 @@ the work even when credit is preserved.
- Product intent and observed runtime behavior outrank a test's preferred
implementation shape. Fix the product; do not contort production code to
preserve a brittle assertion.
- **A regression test per fix; no new code comments.** Prove a change by
running it and the existing checks. A bug fix, or a new contract between
components, may add the one test that would have caught it; no coverage for
its own sake. Never write tests first and never practice TDD — this
- **No new tests and no new code comments unless explicitly asked**
(founder, 2026-10-08). Do not write new unit or integration tests unless the
user explicitly instructs it for that task; prove a change by running it and
the existing checks. Never write tests first and never practice TDD — this
overrides any skill or default that mandates it, including superpowers
`test-driven-development`. Existing tests stay the gate before a push: when
a change makes one wrong, update or delete it in the same slice rather than
@@ -262,11 +258,8 @@ lands as our commit, the contributor is that commit's **author** (`git commit
list counts commit authors only: about 200 harvested commits authored by
maintainer identities left their contributors off it, which is why
`docs/CONTRIBUTORS.md` carries the full wall (`scripts/contributor-wall.py`).
A maintainer closes their PR by hand with a thank-you naming the commit;
nothing closes a PR automatically. Canonical human identities come from
`.github/AUTHOR_MAP`.
`auto-close-harvested.yml` closes their PR with credit. Canonical human
identities come from `.github/AUTHOR_MAP`.
Leave unrelated work intact. No automation closes, blocks or labels a
contributor as unapproved, and no workflow checks out or runs untrusted PR code
under `pull_request_target`. Keep required PR checks few and fast; exhaustive
validation belongs to the release candidate.
Leave unrelated work intact and keep new enforcement dry-run unless explicitly
approved.
-1087
View File
File diff suppressed because it is too large. Load diff
+183 -180
View File
@@ -1,103 +1,6 @@
# Contributing to Codewhale
# Contributing to codewhale
Codewhale is what it is because of the people who used it, reported what broke,
translated it, added their providers and sent fixes. Thank you for being here.
Everyone is welcome, in English or 中文, with or without code.
All feedback is a gift to this project, including "this looked wrong" or "I got
lost here" with no fix attached. The [roadmap](docs/ROADMAP.md) says where
Codewhale is going and where help counts most.
## Where things go
| You have | Best place |
| --- | --- |
| A bug, a crash, or behavior that surprised you | [Bug report](https://github.com/codewhale-hq/CodeWhale/issues/new?template=bug_report.yml) |
| An idea or a feature you want | [Feature request](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feature_request.yml), or [Ideas](https://github.com/codewhale-hq/CodeWhale/discussions/categories/ideas) to talk it through first |
| Feedback on how something looks, reads or feels | [Feedback](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feedback.yml) |
| A question | [Q&A](https://github.com/codewhale-hq/CodeWhale/discussions/categories/q-a) or [Discord](https://discord.gg/37gfS3ksug) |
| A security problem | A [private advisory](https://github.com/codewhale-hq/CodeWhale/security/advisories/new), never a public issue ([policy](.github/SECURITY.md)) |
| A fix for an open issue | A pull request that links the issue |
| A translation, a provider or model, a docs fix | A pull request, no issue needed ([standing projects](#standing-projects)) |
| A new feature, already written or not | An issue first ([why](#new-features-start-as-an-issue)) |
If you are not sure, open an issue. Nothing is filed in the wrong place for
long: a maintainer moves it.
## New features start as an issue
Issues are the best place for feature requests, ideas and bugs. A pull request
is the best place for work we have already agreed on.
A feature that first appears as a pull request takes longer to land than one
that starts as an issue, even when we want it. The questions that decide a
feature are about the product, not the diff: does it belong in the engine or in
a plugin, what does it do in each permission mode, how does it show up in
receipts, what is it called in 19 languages. Answering those in a review means
rewriting code you already finished. Answering them in an issue costs you a few
sentences.
So for a new feature:
1. Open a [feature request](https://github.com/codewhale-hq/CodeWhale/issues/new?template=feature_request.yml).
Say what you are trying to do and what gets in the way. A proposed design is
welcome and optional.
2. Tick "I would like to open a pull request" if you want to build it.
3. A maintainer answers with the shape that fits, or with what is still
undecided. Then the pull request is small and merges quickly.
A feature pull request that arrives without an issue is still read, and it is
never closed for that reason. We will most likely ask the same questions there
and it will take longer.
## Standing projects
These are always open, and a pull request needs no issue first:
- **Translations.** You can improve the terminal language packs, the translated
READMEs in `docs/i18n/`, the website, and the translated docs in
`docs/zh_hans/` and `docs/id/`. The [localization matrix](docs/LOCALIZATION.md)
shows what each language has and how to add one, and the
[Chinese localization group](https://github.com/codewhale-hq/CodeWhale/issues/6804)
is recruiting.
- **Providers and models.** You can add a missing provider or a new model, or
correct a price, a context size or a provider quirk.
[Providers](docs/PROVIDERS.md) lists what ships, and
`python3 scripts/check-provider-registry.py` checks your change.
- **Documentation.** You can fix a typo, an unclear step, a missing example or
an instruction that no longer matches the product.
- **Bug fixes.** If there is no issue yet, describe the bug in the pull request
by saying what you ran, what happened and what you expected.
- **Planned work.** You can take anything in a
[milestone](docs/ROADMAP.md#releases) or anything labelled
[help wanted](https://github.com/codewhale-hq/CodeWhale/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).
Comment on the issue to claim it so two people do not build the same thing.
Large changes to planned work are easier in layers; see
[Layered and EPIC-Sized Work](#layered-and-epic-sized-work).
### What a planned-work issue gives you
Every issue a maintainer opens for planned work, and every release tracking issue, has the same parts, so you can pick one up without asking first:
- **Status:** what is on `main` already, with commit links, and what is still missing.
- **Ways to help:** each piece open to contributors, with the files to read, the command to test it locally (usually `scripts/dev-test.sh <crate> <filter>`) and what "done" means. Pieces that need no Rust, such as trying a feature and reporting exactly what happened, are listed first. A precise report is credited like code.
- **Being built by maintainers right now:** the parts someone already holds, so you don't build the same thing twice.
- **How to take a piece:** comment to claim it, then open a pull request that says `Refs #<issue>`.
If an issue for planned work is missing one of these, say so on it. That is a bug in the issue.
## How we work with you
- We reply by naming what you reported, diagnosed or fixed.
- Your pull request merges as itself, with you as the author
([details](#how-your-contribution-lands)).
- You do not rebase around our changes, and a red check you did not cause is
ours to fix.
- We credit you in the changelog and in
[the contributors list](docs/CONTRIBUTORS.md).
- If something is not ready or not a fit, we say what is blocked and what would
change the answer.
Thank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.
## Getting Started
@@ -326,9 +229,9 @@ Co-author a *person* freely; the address must be their GitHub-linked one
When a commit harvests code from a community PR (see "How Your Contribution
Lands" below), include a `Harvested from PR #N by @author` line in the commit
body. A maintainer then closes the referenced PR by hand with a thank-you and
a link to the commit, so the contributor gets a clear signal that their work
shipped.
body. An auto-close workflow watches for this pattern and closes the
referenced PR with credit so the contributor gets a clear signal that
their work shipped.
## What happens after you open a pull request
@@ -339,23 +242,26 @@ maintainer to approve the first run for a brand-new GitHub account.
| Check | What it runs | When it is red |
| --- | --- | --- |
| `Lint` (required) | `cargo fmt --check`, clippy on every target, and the fast drift checks (locales, READMEs, provider registry, dead-code and blocking-call budgets) | Run `cargo fmt --all` and the clippy command in [Pre-push verification](#pre-push-verification); the log names the file and the check |
| `Test (ubuntu-latest)` | The whole workspace suite on Linux with `cargo nextest`, plus doctests | Re-run the named test locally, for example `scripts/dev-test.sh tui <test name>` |
| `Test (windows-latest)` (required) | Compiles every workspace target for Windows with warnings denied, and the installer PATH helper tests | Usually a `cfg(windows)` path or an import only Windows sees; the log has the compiler error |
| `npm wrapper smoke (ubuntu-latest)` (required) | Builds `codewhale` and installs it through the npm wrapper | The log shows the failing install or entrypoint step |
| `Safety gate` | The sandbox, authorization and auto-review tests | Treat it as a real regression in a trust boundary |
| `Version drift` | Version files, the `feat:` release-note rule, release helper tests and contributor credit | The credit step only reports: a maintainer adds your credit when the PR merges |
| `PR closes an issue` | Your description has `Closes #N`, `Refs #N` or `No-Issue: <reason>` | Edit the description, then re-run the check |
| `DCO` | Lists commits without `Signed-off-by` | Advisory; it never blocks |
| `Contribution gate` | Labels PRs from authors not yet in `.github/APPROVED_CONTRIBUTORS` | A label, not a judgement; see [Contribution Gate](#contribution-gate) |
Smaller checks cover the VS Code extension, integrations, the extension host
and plugin conversion, plus workflow lint when you change `.github/workflows`.
A documentation-only change skips the Rust builds but still reports the
required checks, so it can merge.
Smaller checks cover MSRV, the VS Code extension, integrations, the extension
host and plugin conversion, plus workflow lint when you change
`.github/workflows`. A documentation-only change skips the Rust builds but
still reports the required checks, so it can merge. The Linux test suite took
45 minutes on a recent pull request; the other checks run alongside it.
Your pull request does not run the test suites, the safety gate, MSRV, the npm
wrapper smoke, the slow runtime-contract and persistence-backlog measurements,
the dependency audits or the OpenHarmony build. Maintainers run those once per
release, on the exact commit being released. If that run finds a problem your
change introduced, we fix it on `main`; we will not ask you to chase it.
A red check that your change did not cause is not your job to fix. We rerun it
or fix it on `main`, and we never close a PR because CI is slow or flaky. Your
PR may be a draft, and the issue line in its description is optional.
Your pull request does not run the macOS and Windows test suites, the slow
runtime-contract and persistence-backlog measurements, the dependency audits
or the OpenHarmony build. Maintainers run those once per release, on the exact
commit being released. If that run finds a problem your change introduced, we
fix it on `main`; we will not ask you to chase it.
## How Your Contribution Lands
@@ -374,10 +280,9 @@ Before merging, a maintainer may:
### Fallback — harvest
Rarely, a branch cannot merge in reasonable time, for example when it is in a
fork we cannot push to and the conflicts cannot be resolved there. We will ask
on your PR first. If it still cannot merge, a maintainer may **harvest** the
useful commits into `main` instead. This is **not a rejection**: your code
landed, and your credit is kept mechanically.
fork we cannot push to and the conflicts cannot be resolved there. Then a
maintainer may **harvest** the useful commits into `main` instead. This is
**not a rejection**: your code landed, and your credit is kept mechanically.
- You stay the author of the harvested commit, with your GitHub-linked address
from `.github/AUTHOR_MAP` or
@@ -386,8 +291,8 @@ landed, and your credit is kept mechanically.
- The commit body includes `Harvested from PR #N by @your-handle`. That line is
the contract: it is your credit and the signal that your work shipped.
- The `CHANGELOG.md` entry for the next release credits you by handle.
- A maintainer closes your PR by hand with a thank-you and a link to the commit
on `main`.
- `auto-close-harvested.yml` closes your PR with a thank-you and a link to the
commit on `main`.
When a maintainer closes a harvested PR by hand, the closing comment
follows this template (the pattern set on PR #2634):
@@ -477,54 +382,96 @@ because `main` moved while it was in review, say so and a maintainer resolves
it on your branch. If your branch is in a fork we cannot push to, we ask on the
PR first; harvesting is the last resort.
## Labels
## Contribution Gate
Issues and pull requests are labelled by `.github/workflows/triage.yml`, which
only ever adds labels that already exist. Pull requests get their labels from
the paths they change and their conventional-commit title prefix; issues get
them from the title and form answers.
Codewhale uses a maintainer-managed contribution gate for the community front
door. Maintainers and collaborators bypass this gate automatically. The gate
workflows run in dry-run mode and only add labels, so maintainers can observe
the signal before changing contributor flow.
| Family | Meaning |
| --- | --- |
| `area:*` | The subsystem or client: `tui`, `core`, `tools`, `mcp`, `state`, `sandbox`, `plugins`, `providers`, `runtime-api`, `install`, `localization`, `ci`, `desktop`, `web`, `mobile`, `remote-control`, `subagents` |
| `os:*` | Platform-specific behaviour: `windows`, `macos`, `linux`, `freebsd`, `ohos` |
| `priority:p0` / `p1` / `p2` | Set by a maintainer; `release-blocker` still marks work that must land before a release |
| `bug`, `enhancement`, `documentation`, `question`, `cleanup`, `performance`, `security` | What kind of change it is |
| `accessibility` | Screen readers, keyboard use, contrast, motion |
| `contributor:external` | Opened by someone outside the maintainer team |
| `lang:zh` | Written in Chinese |
| `needs-triage`, `needs-info`, `agent-ready`, `agent-in-progress`, `needs-human`, `keep-open`, `pinned`, `stale` | Workflow state; other workflows read these names |
The maintainer posture is documented in
[docs/AGENT_ETHOS.md](docs/AGENT_ETHOS.md): automation should reduce load while
keeping good-faith contributors seen, credited, and able to keep helping.
Release targeting uses milestones, not version labels.
Issues are never auto-closed by the contribution gate. Unapproved external
issues receive a triage label and remain open for maintainers. Codewhale
depends on real edge cases from real users, so issue intake should stay warm
and open.
## Working with an agent
Pull requests are different because they can touch code, CI, release plumbing,
auth, sandboxing, provider policy, and other trust-boundary surfaces. The PR
gate can be switched from dry-run to enforcement when maintainers decide they
need that safety control, but it should be treated as a review-load control,
not a judgment on contributor quality. Before enabling PR enforcement, seed the
allowlist broadly enough for active external contributors who should not be
interrupted by the rollout.
Using an AI assistant is welcome and needs no disclosure. Codewhale is used to
work on Codewhale, and many fixes here began as a person pointing an agent at
an issue.
The allowlist is scoped:
What makes agent-assisted work easy to merge is the same as for any other: a
specific reproduction, a small diff, the checks you ran, and a description of
the trade-off. Keep each pull request to one friction point, and put a list of
ideas for improvement in an issue.
- `pr:username` allows pull requests.
- `issue:username` allows issues.
- `all:username` allows both.
Do not point an agent at auth, credentials, sandbox policy, publishing and
release plumbing, provider policy, telemetry, sponsorship, branding or global
prompts without a maintainer's agreement on the issue first.
A maintainer can approve someone by commenting `/lgtm` on a pull request for PR
access, or `/lgtmi` on an issue for issue access. The exact bare commands
`lgtm` and `lgtmi` are also accepted for compatibility, but the prefixed forms
are preferred because they are harder to trigger accidentally in ordinary review
discussion.
Maintainers write some issues with the
[Agent task](.github/ISSUE_TEMPLATE/agent-task.yml) form so that an agent with
no other context can finish them; those carry the `agent-ready` label and are
open to anyone. Agents and maintainers follow
[docs/AGENT_ETHOS.md](docs/AGENT_ETHOS.md).
Approvals do not edit `main` directly. The approval workflow opens a small
allowlist update PR so the new entry is reviewable before it takes effect.
## How maintainers ship
If the PR gate fires on a good contributor incorrectly, use the same approval
flow to restore them: comment `/lgtm`, merge the generated allowlist PR, then
reopen the affected pull request. If GitHub will not allow the closed PR to be
reopened, ask the contributor to resubmit after the allowlist PR is merged.
Maintainers, and the agents working for them, land small verified slices
directly on `main` with the checks they ran in the commit message. Every push
to `main` runs a fast preflight and the full CI suite on Linux, macOS and
Windows. A release also needs a green `release-candidate.yml` run on the exact
commit being released; the sequence is in
## Agent-Assisted Improvements
Codewhale is allowed to help improve Codewhale, but the contribution still has
to be shaped for human review. The recommended workflow is the recursive self-improvement prompt
in the private `codewhale-ops` repo: run it
from a fresh fork or branch, let the agent find exactly one small friction point,
and stop after one patch. DeepSeek V4 Pro is the reference path for this loop
today, but any configured provider works — the review shape matters more than
the provider.
Agents and maintainers should follow the stewardship posture in
[docs/AGENT_ETHOS.md](docs/AGENT_ETHOS.md): use automation for evidence,
verification, and narrow patches while keeping the final community decision
human-reviewed.
The useful output is not "ideas for improvement." The useful output is a
specific reproduction, a minimal diff, focused checks, and a PR description that
explains the trade-off. Do not use an agent to touch auth, credentials, sandbox
policy, publishing/release plumbing, provider policy, telemetry, sponsorship,
branding, or global prompts without prior maintainer sign-off.
## How maintainers and agents ship
Maintainers, and the agents working for them, do not open pull requests. They
land verified work directly on `main` with the local checks they ran and their
counts in the commit message. A push to `main` starts no GitHub build or test
workflow: GitHub only mirrors `main` to CNB and GitCode and closes harvested
PRs. CNB then runs its own Linux gates on the mirror and reports them on CNB,
not on GitHub. Do not write `[skip ci]`; nothing needs skipping, and it would
also skip the mirrors and the harvest auto-close.
The exhaustive gate runs once, at the final release step, from one dispatch on
the frozen candidate commit:
```bash
git fetch origin main
candidate_sha="$(git rev-parse origin/main)"
gh workflow run ci.yml --repo codewhale-hq/CodeWhale --ref main \
-f expected_sha="${candidate_sha}"
```
It stops if `main` no longer resolves to that commit. Otherwise it runs every
CI gate on Linux, macOS and Windows, the budget measurements, the
documentation build, `cargo-deny`, the cargo and npm audits and the
OpenHarmony build. Publishing also needs a green `release-candidate.yml` run
on the same commit, which `release.yml` checks; the full sequence is in
[docs/RELEASE_RUNBOOK.md](docs/RELEASE_RUNBOOK.md).
Contributor pull requests still merge as pull requests, as described in
@@ -533,7 +480,7 @@ Contributor pull requests still merge as pull requests, as described in
## Project Structure
Codewhale is a Cargo workspace with one Engine implementation in
`crates/engine/src/core/engine/`. The public `codewhale` executable links the
`crates/tui/src/core/engine/`. The public `codewhale` executable links the
TUI/runtime library; interactive sessions, noninteractive runs and the Runtime
API share that Engine.
@@ -567,37 +514,93 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the runtime data flow and
[the build guide](docs/BUILD_PERFORMANCE.md) for crate dependencies and local
verification.
## Writing the pull request
## Submitting Changes
- Use the [pull request template](.github/PULL_REQUEST_TEMPLATE.md): what and
why, where it came from, and how you tested it.
- Link the issue: `Closes #N` (or `Fixes` / `Resolves`) when the pull request
finishes it, `Refs #N` for related or partial work. Never write a closing
keyword inside a negation such as "does not close #N": GitHub closes the
issue anyway.
- Keep it to one change. If you find related work, open an issue for it.
- If you add a layer, module or abstraction, say which one it replaces.
- Update the docs your change makes wrong.
- A draft is fine, and so is asking for direction before it is finished.
1. Create a feature branch from `main`:
```bash
git checkout -b feat/your-feature
```
2. Make your changes and commit them
3. Run the pre-push verification commands (see
[Pre-push verification](#pre-push-verification) above for the exact
gate and the stricter release clippy form)
4. Push your branch and create a Pull Request
5. Describe your changes clearly in the PR description
## Pull Request Guidelines
- Use the [pull request template](.github/PULL_REQUEST_TEMPLATE.md) when opening
a PR — what and why, the issue line, and how you tested it
- The PR description needs one issue line, checked by CI
(`.github/workflows/pr-issue-link.yml`): `Closes #N` (or `Fixes` /
`Resolves`) when the PR finishes the issue, `Refs #N` for related or partial
work, or `No-Issue: <one-line reason>`. Never write a negated closing
keyword such as "does not close #N": GitHub closes the issue anyway, so CI
rejects it
- If you add a new layer, module, or abstraction, say which one it replaces
or deletes
- Keep PRs focused on a single change
- Update documentation if needed
- Add tests for new functionality
- Get the pull-request checks green before requesting review, or say in the
PR why one is red
## Shape of a Typical PR
A well-structured PR follows a consistent pattern. Recent exemplars include:
- **#386** — `/init` command: new `crates/tui/src/commands/groups/project/init.rs` module, project-type detection,
AGENTS.md generation, command registration in `commands/mod.rs`, localization strings.
- **#389** — Inline LSP diagnostics: LSP subsystem in `crates/tui/src/lsp/`, engine hooks in
`crates/tui/src/core/engine/lsp_hooks.rs`, config toggle, test coverage.
- **#387** — Self-update: new `crates/cli/src/update.rs` module, CLI subcommand registration,
HTTP download + SHA256 verification + atomic binary replacement.
- **#393** — `/share` session URL: new `crates/tui/src/commands/groups/project/share.rs`, HTML rendering,
`gh gist create` integration, command registration.
- **#343/#346** — (v0.8.5) Runtime thread/turn timeline and durable task manager refactors.
Typically each PR touches 1–3 new files, modifies 2–5 existing files for wiring
(registries, dispatch matches, localization), and adds or updates tests. Changes
are scoped to a single feature or fix — if you discover related work that needs
doing, open a separate issue rather than expanding the PR scope.
Before submitting, run the commands in
[Pre-push verification](#pre-push-verification).
## Reporting Issues
When reporting issues, please use one of the issue templates:
- [Bug report](.github/ISSUE_TEMPLATE/bug_report.yml) — for reproducible problems
or regressions
- [Feature request](.github/ISSUE_TEMPLATE/feature_request.yml) — for ideas and
improvements
The forms ask for what a report needs (`codewhale --version`, OS, how you got
Codewhale, and steps to reproduce). Questions go to
[Discussions](https://github.com/codewhale-hq/CodeWhale/discussions) or
[Discord](https://discord.gg/37gfS3ksug).
## Security
Please do **not** open a public issue for a vulnerability. See
[SECURITY.md](.github/SECURITY.md) for private reporting.
If you discover a security vulnerability, please do **not** open a public issue.
See [SECURITY.md](.github/SECURITY.md) for the responsible disclosure process and
contact information.
## Code of Conduct
Be respectful and inclusive. We welcome contributors of all backgrounds and
experience levels. See [CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md).
experience levels. See [CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md) for the full
code of conduct.
## License
By contributing to Codewhale, you agree that your contributions will be
licensed under the MIT License.
By contributing to codewhale, you agree that your contributions will be licensed under the MIT License.
## Questions?
Ask in [Discussions](https://github.com/codewhale-hq/CodeWhale/discussions) or
on [Discord](https://discord.gg/37gfS3ksug), or add the maintainer, Hunter, on
WeChat (`hunterbown`) and ask to join the Whale Brothers group. Everyone is
welcome.
Feel free to open an issue for any questions about contributing.
Generated
+130 -1591
View File
File diff suppressed because it is too large. Load diff
+3 -21
View File
@@ -1,10 +1,8 @@
[workspace]
members = [
"crates/account-cli",
"crates/agent",
"crates/agent-contract",
"crates/app-server",
"crates/app-server-protocol",
"crates/build-support",
"crates/cli",
"crates/cloud-facts",
@@ -12,8 +10,6 @@ members = [
"crates/commands",
"crates/config",
"crates/core",
"crates/engine",
"crates/error-taxonomy",
"crates/execpolicy",
"crates/hooks",
"crates/lane",
@@ -21,29 +17,17 @@ members = [
"crates/mcp",
"crates/memory",
"crates/models",
"crates/net",
"crates/notify",
"crates/palette",
"crates/paths",
"crates/plugins",
"crates/protocol",
"crates/release",
"crates/runtime",
"crates/sandbox",
"crates/secrets",
"crates/skill-install",
"crates/skills",
"crates/sanitize",
"crates/state",
"crates/telemetry",
"crates/tools",
"crates/tui",
"crates/tui-kit",
"crates/util",
"crates/dependencies",
"crates/snapshot",
"crates/lsp",
"crates/repl",
"crates/workflow",
"crates/workflow-js",
"tests/portable-session",
@@ -55,7 +39,7 @@ default-members = ["crates/cli"]
resolver = "2"
[workspace.package]
version = "0.10.3"
version = "0.10.2"
edition = "2024"
# The floor is what CI's MSRV job builds (1.89): Rust 1.88 stabilized the
# `let_chains` the codebase relies on, but a locked dependency
@@ -73,8 +57,6 @@ repository = "https://github.com/codewhale-hq/CodeWhale"
warnings = "deny"
[workspace.dependencies]
agent-client-protocol = { version = "=5.2.0", default-features = false }
agent-client-protocol-schema = { version = "=1.17.0", default-features = false }
anyhow = "1.0.100"
async-trait = "0.1.89"
axum = { version = "0.8.5", features = ["json", "ws"] }
@@ -93,7 +75,7 @@ reqwest = { version = "0.13.1", default-features = false, features = ["json", "r
# multi-thread engine over channels (see crates/workflow-js).
rquickjs = { version = "0.14", features = ["futures"] }
rustls = { version = "0.23.36", default-features = false, features = ["ring", "std", "tls12"] }
rusqlite = { version = "0.41.1", features = ["bundled"] }
rusqlite = { version = "0.40.2", features = ["bundled"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
# DSH bundle import: event-level parsing so aliases and explicit tags can be refused.
@@ -107,7 +89,7 @@ toml_edit = "0.25.12"
sha2 = "0.11"
tower-http = { version = "0.7", features = ["cors"] }
tracing = "0.1"
gix = { version = "=0.90.0", default-features = false, features = ["sha1", "sha256"] }
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
uuid = { version = "1.11", features = ["v4"] }
mimalloc = { version = "0.1", default-features = false }
+26 -17
View File
@@ -1,20 +1,29 @@
# Extension packager dependency security
# Extension packager dependency acceptance
The VS Code extension's packaging toolchain uses `@vscode/vsce` →
`@azure/identity` → `@azure/msal-node`. MSAL Node versions before 7.1.0 are
affected by [GHSA-xcx6-jx8p-x3h5](https://github.com/AzureAD/microsoft-authentication-library-for-js/security/advisories/GHSA-xcx6-jx8p-x3h5).
The extension manifest overrides Azure Identity's MSAL Node dependency to
`^7.1.2`; its lockfile resolves 7.1.2. Keep the override until Azure Identity
requires a patched version itself.
The npm audit of `extensions/vscode` reports GHSA-xcx6-jx8p-x3h5, a high
severity CWE-346 advisory in `@azure/msal-node` 6.0.1. It reaches the lockfile
through `@vscode/vsce` 4.0.0 → `@azure/identity` 4.13.3 → `@azure/msal-node`, all
of which are build-only tooling. There is no upstream-consistent fix: every
published `@vscode/vsce` and `@azure/identity` release still declares
`@azure/msal-node: ^6.0.0`, so only a semver-breaking `overrides` pin or a major
downgrade of the packager would clear the report, and either changes the
packaging tool's behaviour.
`npm run audit:dependencies -- --report <path>` audits the complete lockfile
and keeps the raw report. Every reported vulnerability blocks the check.
Incomplete reports, count mismatches and critical findings also fail. There
is no advisory exception.
This is a reviewed acceptance of an unfixed advisory, not a mitigation. The
vulnerable package is a `devDependency`: `npm run package` invokes
`vsce package --no-dependencies`, so no dependency of it ships in the vsix and
the vulnerable authorization-code path is never executed when packaging. The
extension's published contents and runtime dependency set are unchanged.
After changing this dependency, materialize the lockfile with `npm ci`, run
the extension's existing `npm test` checks and build a local package with
`npm run package`. Packaging uses `vsce package --no-dependencies`, which
excludes development dependencies from the VSIX. A local package build does
not establish that a publishing authentication flow works; verify that flow
only when publication is separately authorized.
`npm run audit:dependencies` audits the complete lockfile and keeps the raw
report. Only this exact advisory, in `@azure/msal-node`, reachable through that
one chain, can be classified as accepted. Any other advisory, a second
`@azure/msal-node` installation, a critical finding, an incomplete audit, or a
count mismatch fails the check. CI stores the raw report, including the accepted
findings.
After an upstream fix is available, upgrade and verify it, then remove this
acceptance and its classification in the same slice. Never broaden the admitted
advisory list to get a passing check.
References: [upstream advisory](https://github.com/advisories/GHSA-xcx6-jx8p-x3h5).
+13 -21
View File
@@ -72,9 +72,8 @@ two copies end up on `PATH` and whichever comes first wins.
## Quickstart
1. Commit or stash your work, then run `codewhale` from the root of a git
repository. By default (**Accept Edits**) it edits files in the repository
without asking and asks before each command; `Shift+Tab` steps the
permission dial when you want it to ask more or less.
repository. By default it applies file edits there without asking and
shows you the diff.
2. Run `/provider` (or press `F3`), pick a provider, paste its API key, and
choose a model. DeepSeek is the default provider. With no key, pick a local
runtime instead. If no provider is configured and Ollama is already serving
@@ -103,14 +102,12 @@ codewhale exec --auto --sandbox workspace-write "fix the failing tests and expla
## Modes and permissions
A fresh install starts in Work mode with the Accept Edits permission. One dial
switches both: `Shift+Tab` steps Ask → Accept Edits → Plan → Auto-Review →
Full Access.
A fresh install starts in Work mode with the Ask permission.
| | Switch with | Options |
| --- | --- | --- |
| Mode | the Plan stop on `Shift+Tab`, or `/mode` | **Plan** reads and searches but changes nothing. **Work** (every other stop) edits files and runs commands; start a goal with `/goal` when a request needs more than one turn. |
| Permission | `Shift+Tab` | **Ask** asks before every file edit and every shell command that is not read-only. **Accept Edits** applies file edits in your git repository without asking and still asks before commands. **Auto-Review** lets rules and a reviewer model approve routine calls and refuses high-risk ones; switch to Ask to approve those yourself. **Full Access** never asks, and commands can reach anything on this computer; repository rules and managed policy still apply. |
| Mode | `Tab` (when the input is empty) or `/mode` | **Plan** reads and searches but changes nothing. **Work** edits files and runs commands. **Operate** has the same access as Work, treats your request as a goal, and keeps going until the goal is verified or it needs input from you. |
| Permission | `Shift+Tab` | **Ask** applies file edits in your repository without asking and asks before shell commands that are not read-only. **Auto-Review** lets rules and a reviewer model approve routine calls and refuses high-risk ones; switch to Ask to approve those yourself. **Full Access** never asks and turns off Codewhale's sandbox; repository rules and managed policy still apply. |
See the [modes and permissions guide](docs/MODES.md) for the details.
@@ -194,10 +191,10 @@ Connect [MCP servers](docs/MCP.md), add [skills](docs/SKILLS.md) and
[plugins](docs/PLUGINS.md), and run [hooks](docs/HOOKS.md) on session and tool
events. Code mode (experimental, on by default) lets the model write one
`execute_tools` program that calls several MCP and plugin tools; each call
still meets your approval rules and keeps its receipt. The independently installed Computer
still meets your approval rules and keeps its receipt. The bundled Computer
Use plugin (experimental, macOS only) can observe
and operate other apps. It stays off until you review its access and enable it
([guide](https://github.com/codewhale-hq/codewhale-cu-plugin)).
([guide](crates/tui/plugins/computer-use/README.md)).
## Ways to run it
@@ -206,7 +203,7 @@ and operate other apps. It stays off until you review its access and enable it
| `codewhale` | The interactive terminal app |
| `codewhale exec "…"` | One task without the interface. On its own it answers without using tools; `--auto` enables tools with Full Access by default, `--sandbox workspace-write` restricts them, and `--output-format stream-json` streams events as JSON |
| `codewhale web` | A local web UI on `127.0.0.1` ([guide](docs/WEB.md)) |
| `codewhale review --pr N` | An advisory review of a GitHub pull request. The [GitHub bot setup](docs/GITHUB_ACTION.md#set-up-your-own-bot) generates a pinned workflow for automatic reviews and `@codewhale review`, using your own model key and optional App identity |
| `codewhale review --pr N` | An advisory review of a GitHub pull request. It posts nothing unless you add `--post`; the [GitHub Action](docs/GITHUB_ACTION.md) runs the same review in CI |
| `codewhale app-server --http` | The [Runtime API](docs/RUNTIME_API.md), a local HTTP API for threads, events, approvals, and each turn's file changes |
| `codewhale serve --acp` | The Agent Client Protocol over stdio, for editors such as Zed |
@@ -258,19 +255,14 @@ that. More in [authorization order](docs/AUTHORIZATION_ORDER.md),
## Community
Codewhale is what it is because of its community, and all feedback is
welcome: a bug, an idea, or something that looked or felt wrong.
[Open an issue](https://github.com/codewhale-hq/CodeWhale/issues/new/choose) for any
of these; an issue is also the best place to start a new feature. Pull requests
are welcome for fixes, translations, providers and planned work. The
[contributing guide](CONTRIBUTING.md) shows where each kind of help goes, and
the [roadmap](docs/ROADMAP.md) shows where the project is headed. Contributors
keep credit for the work that lands, and the
[repository layout](CONTRIBUTING.md#project-structure) shows where things live.
If a provider is missing or a workflow is awkward,
[open an issue](https://github.com/codewhale-hq/CodeWhale/issues/new/choose) or
[send a pull request](CONTRIBUTING.md). Contributors keep credit for the work
that lands, and the [repository layout](CONTRIBUTING.md#project-structure)
shows where things live.
Chat on [Discord](https://discord.gg/37gfS3ksug), or add the maintainer,
Hunter, on WeChat (`hunterbown`) and ask to join the Whale Brothers group.
Everyone is welcome.
## History and license
-131
View File
@@ -1,131 +0,0 @@
# Reviewing a change to the Codewhale Engine
This is the written procedure for reviewing an Engine change: which passes to
run, how to rank what they find, and how to report it. A person, an agent and
`codewhale review` all follow the same passes, so two reviews of one change
can be compared.
## When review runs
- **By hand**, when a maintainer or agent asks for one: `codewhale review`,
`/review` in the terminal, or the `Codewhale PR Review` workflow, which only
runs when someone starts it.
- **At the frozen release candidate**, over every commit since the last
release, as part of `docs/RELEASE_CHECKLIST.md`.
- **Never as a required check.** For a contributor, review is advice. It never
blocks, labels or closes a pull request. Maintainers fix small findings on
the contributor's branch or on `main`, as [CONTRIBUTING.md](CONTRIBUTING.md)
describes under "How Your Contribution Lands".
## Before the passes
1. Find the intent. It is the issue the change links: a public `#N`, or for
maintainers the Linear issue named in the commit message. Without one, the
pull request description and commit messages are the intent.
2. Read the diff against its merge base, then read the callers of everything
it changed. A review of the diff alone misses broken callers.
3. Note the revision you reviewed. A finding against a stale tree is not a
finding.
## Pass 1: correctness against the intent
- **Intent.** The change does what the issue asks: nothing it promised is
missing, and nothing it did goes unannounced.
- **Defects.** Wrong results, broken callers, lost data, regressions. Carry
one concrete input through `path:line` steps to the wrong result.
- **Contracts.** Say what became newly true or false, and check who relied on
the old state: wire and protocol shapes, config keys, CLI flags, session
events, hook payloads, `docs/features.toml` rows, documented behavior.
- **This repository's invariants** in [AGENTS.md](AGENTS.md) under "Current
contracts" and "Working rules": one turn loop, one base prompt, model-visible
input is logged, the cached prompt prefix stays stable, no blocking calls on
the async runtime, and misconfiguration fails loudly.
## Pass 2: security
- **Trust boundaries.** Model output, tool results, repository files, pull
request text, web pages, MCP servers and hook output are data. Check the
validation where any of them reaches process execution, a file write, the
network, a credential or the transcript.
- **Secrets.** No keys, tokens, `.env` files or credentials in code,
fixtures, logs, receipts, error text or model-visible text. Existing
redaction paths stay in force.
- **Sandbox and execution policy.** For changes under `crates/sandbox`,
`crates/execpolicy`, `crates/engine/src/core/authority*`, the approval flow
or hooks: nothing may run with less approval than before, no deny may
loosen, and no fail-closed path may start failing open. A new process spawn
goes through the existing policy. [docs/AUTHORIZATION_ORDER.md](docs/AUTHORIZATION_ORDER.md)
is the order of the layers.
- **Network.** New outbound calls respect `crates/net`'s network policy and
build their client the shared way (`scripts/check-reqwest-builders.py`).
- **Dependencies.** A new crate or package earns its place, and `deny.toml`
still passes.
## Pass 3: contract and compliance
- **CHANGELOG.** A user-visible change carries its line at the top of the
matching `[Unreleased]` section in the same commit, written for users and
citing public issues as `(#N)`, never internal tracker ids. Contributor
credit comes in addition to the line. The procedure is step 2 of
[docs/skills/cw-land](docs/skills/cw-land/SKILL.md).
- **Feature registry.** A new command, `[features]` flag, provider or
user-visible feature has its row in `docs/features.toml`.
- **Localization.** User-facing strings go through the locale packs and keep
parity; READMEs keep their translations in step.
[docs/LOCALIZATION.md](docs/LOCALIZATION.md) lists the drift checks.
- **Vocabulary.** Copy uses the product's words. The public name is
Codewhale, and mode names follow [docs/MODES.md](docs/MODES.md).
- **Existing checks.** Run the ones that match what the change touched, and
report their real output:
| The change touched | Run |
| --- | --- |
| Any Rust | `cargo fmt --all -- --check`, then `scripts/dev-test.sh <area or path>` |
| Generated files and ratchets | `scripts/preflight.sh --check` |
| `#[allow(dead_code)]` | `python3 scripts/check-dead-code-budget.py` |
| Async code | `python3 scripts/check-blocking-calls-budget.py` |
| TUI strings | `python3 scripts/check-tui-locale-parity.py`, `scripts/check-tui-product-vocabulary.sh` |
| User-facing English copy | `python3 scripts/check-lexicon.py` |
| READMEs | `python3 scripts/check-readme-translations.py`, `scripts/check-readme-locales.sh` |
| The provider registry | `python3 scripts/check-provider-registry.py` |
| `config.example.toml` | `python3 scripts/check-config-example.py` |
| A feature flag or command | `cargo test -p codewhale-tui --test feature_registry` |
| `CHANGELOG.md` or credit | `sh scripts/sync-changelog.sh`, `python3 scripts/check-contributor-credit.py` |
| Versions | `bash scripts/release/check-versions.sh` |
Do not ask for new tests or code comments; this repository adds neither unless
an issue asks. Recommend a regression test only for a specific failure you can
explain.
## Severity
Use the three levels `codewhale review` reports:
- **blocker**: a demonstrated severe failure, such as lost data, a security
hole, a broken build or install, a crash or wrong result on a common path,
or a broken public contract.
- **concern**: a concrete, narrower defect with a named trigger.
- **nit**: a demonstrated low-impact defect, such as a misleading message or a
doc the change made wrong.
Every finding carries its evidence: **reproduced** (a command you ran and its
output), **traced** (one input carried through `path:line` steps) or
**suspected** (anything less). A suspected finding is a question for a person,
never a blocker. Do not raise severity to express uncertainty. A defect the
change did not introduce is labeled pre-existing and does not count against
the change.
## Nits
Group nits into one list instead of one comment each. Leave formatting to
`cargo fmt` and clippy. When nits are all a review found, say the change is
fine to merge and leave the list for the author to take or leave.
## Report
- Findings ranked blocker, then concern, then nit, each with `path:line`, the
trigger, the impact, the smallest fix and its evidence.
- What you verified (commands and results), kept apart from what you only
read, and what you did not review.
- On a contributor's pull request, thank them for the specific thing they did,
keep every finding actionable, and offer to fix small ones on their branch.
+25 -75
View File
@@ -46,24 +46,6 @@ poll-based render loop; the file header records the change.
Upstream: <https://github.com/openai/codex>
The App Server request-ID, RPC envelope and initialization contracts in
`crates/app-server-protocol/src/{rpc,lifecycle}.rs` are adapted from official
Codex `rust-v0.166.0`, commit `6ff1e1ca3b30fc8b914720681fcf5fe62e95f856`.
The changes and exact source paths are recorded in that crate's `UPSTREAM.md`.
The crate retains `LICENSE-APACHE`, `LICENSE-MIT` and the upstream `NOTICE`;
its combined license expression is `MIT AND Apache-2.0`.
Upstream NOTICE, retained unchanged:
```text
OpenAI Codex
Copyright 2025 OpenAI
This project includes code derived from [Ratatui](https://github.com/ratatui/ratatui), licensed under the MIT license.
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023-2025 The Ratatui Developers
```
```
Copyright 2025 OpenAI
@@ -86,12 +68,12 @@ The full Apache License 2.0 text ships in this repository at
## Cordis, schemastery and cosmokit (Shigma, via DeepSeek Harness) — MIT
The embedded extension host bundle
(`crates/engine/extension-host/dist/codewhale-extension-host.mjs`) contains these
(`crates/tui/extension-host/dist/codewhale-extension-host.mjs`) contains these
packages, as published by DeepSeek Harness:
- `@deepseek-ai/cordis` 4.1.0
- `@deepseek-ai/schemastery` 3.19.0
- `@deepseek-ai/cosmokit` 1.9.0
- `@deepseek-ai/cordis` 4.0.4
- `@deepseek-ai/schemastery` 3.18.4
- `@deepseek-ai/cosmokit` 1.8.5
Upstream: <https://github.com/deepseek-ai/deepseek-harness> (`vendor/cordis`,
`vendor/schemastery`, `vendor/cosmokit`)
@@ -120,18 +102,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## DeepSeek Harness utility packages (`dsh-util-values`, `dsh-tools`, message compatibility) — MIT
## DeepSeek Harness utility packages (`dsh-util-values`, `dsh-tools`) — MIT
The same bundle contains `@deepseek-ai/dsh-util-values` 0.2.2-rc.1, and
`crates/engine/extension-host/src/dsh/dsh-tools-compat.js` contains verbatim
excerpts of `@deepseek-ai/dsh-tools` 0.2.2-rc.1 (`lib/index.js` and its shared
`lib/shared-DTdn5dXO.js` chunk), under the same licence text.
`crates/engine/extension-host/src/dsh/dsh-message-compat.js` contains verbatim
excerpts of `@deepseek-ai/dsh-brand` 0.2.2-rc.1,
`@deepseek-ai/dsh-llm` 0.2.2-rc.1, `@deepseek-ai/dsh-session` 0.2.2-rc.1,
`@deepseek-ai/dsh-system-prompt` 0.2.2-rc.1,
`@deepseek-ai/dsh-user-questions` 0.2.2-rc.1 and
`@deepseek-ai/dsh-util-crypto` 0.2.2-rc.1, under the same licence text.
The same bundle contains `@deepseek-ai/dsh-util-values` 0.1.7-alpha.2, and
`crates/tui/extension-host/src/dsh/dsh-tools-compat.js` contains verbatim
excerpts of `@deepseek-ai/dsh-tools` 0.1.7-alpha.2 (`lib/index.js`, lines 8-888
and 2528-2558), under the same licence text.
Upstream: <https://github.com/deepseek-ai/deepseek-harness>
(`dsh-util-values` is published from `packages/util/values`)
@@ -166,13 +142,13 @@ materialises (`~/.codewhale/extension-host/<sha256>/LICENSES.txt`).
## Model Context Protocol SDK and bundled dependencies
The pinned builtin MCP bundle (`crates/engine/extension-host/dist/builtin/mcp.mjs`)
The pinned builtin MCP bundle (`crates/tui/extension-host/dist/builtin/mcp.mjs`)
contains the following packages. The MCP packages are labeled Apache-2.0 and
carry the upstream licensing-transition statement and full Apache-2.0 text
reproduced below. The other packages are labeled MIT. Their exact distributed
notices are preserved.
### `@modelcontextprotocol/client` 2.4.0
### `@modelcontextprotocol/client` 2.3.1
```text
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
@@ -393,7 +369,7 @@ CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
the full license text.
```
### `@modelcontextprotocol/core` 2.4.0
### `@modelcontextprotocol/core` 2.3.1
```text
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
@@ -749,9 +725,9 @@ SOFTWARE.
The host and its offline composition reviewer also contain these packages.
Vendored composition adaptations are recorded in
`crates/engine/extension-host/src/dsh/upstream/UPSTREAM.json`.
`crates/tui/extension-host/src/dsh/upstream/UPSTREAM.json`.
### `@deepseek-ai/cordis-plugin-group` 1.1.0
### `@deepseek-ai/cordis-plugin-group` 1.0.2
```text
MIT License
@@ -777,7 +753,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
### `@deepseek-ai/cordis-plugin-include` 1.1.0
### `@deepseek-ai/cordis-plugin-include` 1.0.7
```text
MIT License
@@ -803,7 +779,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
### `@deepseek-ai/cordis-plugin-loader` 1.1.0
### `@deepseek-ai/cordis-plugin-loader` 1.0.3
```text
MIT License
@@ -889,26 +865,21 @@ THE SOFTWARE.
The embedded extension host includes these pinned DeepSeek Harness packages:
- `@deepseek-ai/dsh-agent-presets` 0.1.6-alpha.1
- `@deepseek-ai/dsh-hook-protocol` 0.2.2-rc.1
- `@deepseek-ai/dsh-hooks-claude-code` 0.2.2-rc.1
- `@deepseek-ai/dsh-hooks-codex` 0.2.2-rc.1
- `@deepseek-ai/dsh-persona` 0.2.2-rc.1
- `@deepseek-ai/dsh-system-prompt` 0.2.2-rc.1
- `@deepseek-ai/dsh-hook-protocol` 0.1.6-alpha.1
- `@deepseek-ai/dsh-hooks-claude-code` 0.1.6-alpha.1
- `@deepseek-ai/dsh-hooks-codex` 0.1.6-alpha.1
- `@deepseek-ai/dsh-persona` 0.1.6-alpha.1
- `@deepseek-ai/dsh-system-prompt` 0.1.6-alpha.1
Upstream: <https://github.com/deepseek-ai/deepseek-harness>, tag
`dsh-v0.2.2-rc.1`, commit `66491b05e3f05ad35bcdd4f6cd7ed26041d9e3db`.
The legacy `dsh-agent-presets` adapter alone retains commit
`0d1f50007f9bca3f52b06e1c3074fa14d5fb0720`; the newer preset runtime packages
are not bundled. The reviewed composition loader is refreshed to the rc.1
source: `cordis-plugin-loader` 1.1.0, `cordis-plugin-include` 1.1.0 and
`cordis-plugin-group` 1.1.0 (MIT, Shigma).
Upstream: <https://github.com/deepseek-ai/deepseek-harness>, commit
`0d1f50007f9bca3f52b06e1c3074fa14d5fb0720`.
Preset discovery and metadata, hook matching and dialect codecs, and additive
persona templates run through receipt-backed inputs. Core retains selection,
prompt assembly, sessions, approvals and writable roots. Original persona source
hashes are recorded in `crates/engine/extension-host/src/dsh/persona.UPSTREAM.json`.
hashes are recorded in `crates/tui/extension-host/src/dsh/persona.UPSTREAM.json`.
The identical upstream license text for all six packages is also carried in
`crates/engine/extension-host/dist/LICENSES.txt`.
`crates/tui/extension-host/dist/LICENSES.txt`.
```
MIT License
@@ -934,27 +905,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
### `semver` 7.8.5
```text
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
```
## Optional compiled extension host
Node remains the default extension-host runtime. An explicitly installed
+6 -23
View File
@@ -1,31 +1,20 @@
name: Codewhale
description: Review pull requests automatically or on an authorized @codewhale review request, using your chosen model.
branding:
icon: code
color: blue
name: Codewhale review
description: Review a pinned pull request with a checksummed Codewhale release and an explicit outcome receipt.
inputs:
version:
description: Exact released CLI tag (vX.Y.Z). Must support review --json.
required: true
provider:
description: Explicit BYOK provider (orcarouter, deepseek, anthropic, openrouter, zai, modelstudio-token-plan), or codewhale for a separately verified account machine key.
required: true
description: codewhale for an account machine key, or an explicit BYOK provider.
default: codewhale
model:
description: Exact model ID. Account mode requires provider/model from the account catalog.
required: true
reasoning-effort:
description: Optional Engine reasoning setting (auto, off, low, medium, high, max).
github-token:
description: GitHub token with contents read and pull-requests read; legacy post:true also requires pull-requests write.
description: GitHub token with contents read and pull-requests write.
default: ${{ github.token }}
pr-number:
description: PR number for workflow_dispatch; pull_request events use the event snapshot.
trigger-phrase:
description: Bot mention used for a standalone @name review line in a new PR comment.
default: '@codewhale'
allow-forks:
description: Allow inert fork review only on a write-authorized issue_comment or manual run from the default branch.
default: 'false'
max-chars:
description: Maximum diff characters per complete pass.
default: '200000'
@@ -38,7 +27,7 @@ inputs:
description: Model and publication deadline, in seconds (30–1200).
default: '600'
post:
description: Legacy direct COMMENT publication. Set false to export findings for a separate publisher job.
description: Publish a COMMENT review; never approve or request changes.
default: 'true'
outputs:
outcome:
@@ -47,9 +36,6 @@ outputs:
receipt:
description: Absolute path to the sanitized JSON execution receipt (also written on failures).
value: ${{ steps.review.outputs.receipt }}
findings:
description: Absolute path to validated findings JSON for a complete post:false review; contains repository-sensitive review text.
value: ${{ steps.review.outputs.findings }}
pr-url:
description: URL of the pull request, when resolved.
value: ${{ steps.review.outputs.pr-url }}
@@ -63,10 +49,7 @@ runs:
CW_VERSION: ${{ inputs.version }}
CW_PROVIDER: ${{ inputs.provider }}
CW_MODEL: ${{ inputs.model }}
CW_REASONING_EFFORT: ${{ inputs.reasoning-effort }}
CW_PR_NUMBER: ${{ inputs.pr-number }}
CW_TRIGGER_PHRASE: ${{ inputs.trigger-phrase }}
CW_ALLOW_FORKS: ${{ inputs.allow-forks }}
CW_MAX_CHARS: ${{ inputs.max-chars }}
CW_MAX_PASSES: ${{ inputs.max-passes }}
CW_MAX_OUTPUT_TOKENS: ${{ inputs.max-output-tokens }}
+27 -47
View File
@@ -82,7 +82,6 @@ default_text_model = "deepseek-flash"
# ─────────────────────────────────────────────────────────────────────────────────
# Thinking Mode (DeepSeek V4 reasoning effort)
# ─────────────────────────────────────────────────────────────────────────────────
# "auto" — the route chooses per request (default)
# "off" — disables chain-of-thought (thinking.type = disabled)
# "low" — compat-maps to "high" server-side
# "medium" — compat-maps to "high" server-side
@@ -92,7 +91,7 @@ default_text_model = "deepseek-flash"
# Ctrl+T in the TUI cycles the reasoning tier. The header shows the current
# tier as a ⚡ chip. (Shift+Tab cycles the permission posture — Ask /
# Auto-Review / Full Access — not the reasoning tier.)
reasoning_effort = "auto"
reasoning_effort = "max"
# NOTE: `show_thinking`, `thinking_default_expanded`, `thinking_preview_lines`,
# `help_expand_groups`, `pin_last_prompt`, and `cost_currency` live in
@@ -169,25 +168,11 @@ sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-acc
# "muse", or "all". Env: CODEWHALE_PROJECT_INSTRUCTION_IMPORTS (comma-separated).
# project_instruction_imports = ["claude"]
#
# Standing instructions are budgeted per scope, in estimated tokens, and every
# trim or skip is reported to the model and in /context.
# `global` covers ~/.codewhale/AGENTS.md (or ~/.agents/AGENTS.md), the global
# instructions.md fallbacks, ~/.claude/CLAUDE.md (last resort, claude_fallback
# only) and the `instructions = [...]` array. `project` covers the
# repository-root -> workspace AGENTS.md chain, co-read foreign files, and
# .codewhale/rules/*.md. A scope is capped at the smaller of its absolute token
# budget and its share of the usable window (context window minus the output
# reservation and a 10% safety reserve). Each file keeps at most its first
# 100 KiB. User config only: a cloned repository cannot change it.
# [instruction_budget]
# global_budget_tokens = 100000 # positive; absolute cap for the global scope
# project_budget_tokens = 100000 # positive; absolute cap for the project scope
# global_budget_percent = 25 # 1..=100, share of the usable window
# project_budget_percent = 25 # 1..=100, share of the usable window
# static_prefix_guard_percent = 80 # 1..=100; guard must leave >=10 points
# file_max_bytes = 8388608 # 1..=8388608; larger files are skipped
# policy = "trim" # trim | refuse
# claude_fallback = false # opt in: AGENTS.md wins; else CLAUDE.md
# Everything that reaches the model as standing project instruction authority —
# the repository-root -> workspace AGENTS.md chain, the global fallback layer,
# .codewhale/rules/*.md, and any imported foreign files — shares one 48 KiB
# aggregate budget. Instructions claim it first and are trimmed from the
# broadest scope inward, so the nearest-scope file is the last thing dropped.
# prompt_suggestion = true # opt-in: show ghost-text follow-up question in composer after each turn
# Optional tab/window title shown as `[title] …` in front of the terminal
@@ -525,11 +510,6 @@ show_whats_new = true
# 0 disables the timeout entirely; values above 86400 (24h) are clamped.
# user_input_timeout_seconds = 300
#
# Fail-safe ceiling, in seconds, for how long a durable task (runtime API
# server) may sit on an unanswered approval or question before it fails with
# human_wait_timeout. 0 or omitted uses 1800; values are clamped to 1..=86400.
# task_human_wait_cap_seconds = 1800
#
# Bound an interactive approval card's wait (#6101). When the window elapses
# the card resolves to deny (fail-closed) and the transcript says the bound
# denied the call; omitted or 0 waits indefinitely.
@@ -897,13 +877,12 @@ base_url = "https://api.deepseek.com/beta"
# api_key = "YOUR_XAI_API_KEY"
# auth_mode = "oauth" # or "device_code" / "grok_cli"
# base_url = "https://api.x.ai/v1"
# model = "grok-4.7" # or grok-4.6, grok-4.5, grok-4.3, grok-build
# model = "grok-4.6" # or grok-4.5, grok-4.3, grok-build
# Mistral AI — la Plateforme (https://console.mistral.ai/)
# OpenAI-compatible Chat Completions route.
# Provider aliases: mistral, mistral-ai, mistralai, la-plateforme
# Env var aliases: MISTRAL_API_KEY, MISTRAL_BASE_URL, MISTRAL_MODEL
# Vibe Code subscription setup: docs/MISTRAL_VIBE.md
[providers.mistral]
# api_key = "YOUR_MISTRAL_API_KEY"
# base_url = "https://api.mistral.ai/v1"
@@ -924,7 +903,7 @@ base_url = "https://api.deepseek.com/beta"
# (https://bailian.console.aliyun.com/)
#
# Token Plan Personal and Team share the same AP-Southeast (Singapore) endpoint.
# Available text/coding models: qwen3.8-max, qwen4-max-preview, qwen3.7-plus,
# Available text/coding models: qwen3.8-max, qwen3.8-max-preview, qwen3.7-plus,
# qwen3.7-max, qwen3.6-flash, deepseek-v4-pro, deepseek-v4-flash-0731, glm-5.2
#
# Provider aliases: modelstudio-token-plan, modelstudio_token_plan,
@@ -944,8 +923,7 @@ base_url = "https://api.deepseek.com/beta"
# (https://bailian.console.aliyun.com/)
#
# Coding Plan uses a separate international endpoint (coding-intl.dashscope).
# Same model ids as the Token Plan except qwen4-max-preview, which is
# Token Plan only.
# Same model catalog as the Token Plan.
#
# Provider aliases: modelstudio-coding-plan, modelstudio_coding_plan,
# alibaba-coding-plan, dashscope-coding-plan
@@ -1187,9 +1165,9 @@ base_url = "https://api.deepseek.com/beta"
[stream]
open_timeout_secs = 45 # response headers, including connect (0 = env/default, 5-300)
chunk_timeout_secs = 900 # per-SSE-chunk idle bound (0 = default, 1-3600)
max_resumes = 3 # proven pre-dispatch connection recovery (0 = off, 0-10)
max_transparent_retries = 2 # compatibility value; interrupted streams never reissue
max_stream_errors = 5 # compatibility value; uncertain stream errors end the stream
max_resumes = 3 # whole-request reissues after failed streams (0 = off, 0-10)
max_transparent_retries = 2 # retry before any content (0 = off, 0-10)
max_stream_errors = 5 # recoverable errors per stream (0 = default, 1-50)
max_duration_secs = 1800 # one stream, not the whole turn (0 = default, 10-86400)
max_content_mb = 10 # one stream's content (0 = default, 1-512)
connect_timeout_secs = 30 # TCP/TLS setup (0 = default, 1-300)
@@ -1289,18 +1267,20 @@ code_mode = true # execute_tools composes MCP/plugin/native calls; false defers
# a prompt under Full Access, under Bypass, or with a
# matching session grant, which is bound to the
# plugin's reviewed build (an update asks again).
# Native code requires a verified OS sandbox: Seatbelt
# on macOS, bubblewrap on Linux, and LPAC/Job on Windows.
# An unavailable Native sandbox refuses activation.
# Unix reads are limited to the selected runtime and
# libraries, canonical host bundle, captured Core-managed
# plugin stores and Native data. Writes stay in Native
# data and private scratch; repository reads and direct
# network are denied. Native plugins share one trusted
# cohort, not separate per-plugin OS sandboxes.
# On macOS a detached child can outlive process-group
# cancellation while retaining the sandbox restrictions.
# Builtin host policy is separate and diagnosed separately.
# Sandbox: on macOS the host runs under Seatbelt, and
# on Linux under bubblewrap (/usr/bin/bwrap) when a
# launch-time probe shows it works. Either way: no
# direct network, no reads of ~/.codewhale secrets/
# credentials/config/sessions, and writes only to its
# data dir and temp dirs (plus, on macOS, the Darwin
# user cache, ~/.cargo/registry, ~/.cargo/git and the
# npm cache, which every Seatbelt-sandboxed command
# gets). If bwrap is missing or cannot start (for
# example blocked unprivileged user namespaces), the
# host runs UNSANDBOXED with your user permissions and
# /plugin, `codewhale doctor` and the start diagnostic
# say so with bwrap's own error. On Windows it always
# runs with your user permissions.
# Toggling this changes the plugin activation policy,
# so every plugin must be re-reviewed afterwards.
@@ -1764,7 +1744,7 @@ base_url = "https://integrate.api.nvidia.com/v1"
# # Optional owner-selected total child limit. Zero leaves work uncapped.
# max_children = 0
# # Maximum concurrently live agents inside one run (others wait for a slot).
# max_concurrent = 64
# max_concurrent = 16
# # Optional structural Workflow depth limit; zero leaves it uncapped.
# # This is separate from Runtime child delegation below.
# max_depth = 0
-28
View File
@@ -1,28 +0,0 @@
[package]
name = "codewhale-account-cli"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Codewhale account, cloud computer and dispatch commands for the CLI"
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
clap.workspace = true
chrono.workspace = true
codewhale-config = { path = "../config", version = "0.10.3" }
codewhale-release = { path = "../release", version = "0.10.3" }
codewhale-runtime = { path = "../runtime", version = "0.10.3" }
codewhale-secrets = { path = "../secrets", version = "0.10.3" }
console = "0.16.3"
reqwest = { workspace = true, features = ["blocking"] }
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
toml.workspace = true
webbrowser = "1.0"
zeroize = "1.8.2"
-688
View File
@@ -1,688 +0,0 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use chrono::{DateTime, Utc};
use codewhale_secrets::account::{AccountDeviceAccess, AccountSessionState, RuntimeAccountInfo};
use super::*;
pub fn account_api_base(api_base: Option<&str>) -> Result<String> {
let requested_base = machine::resolve_api_base(
api_base,
std::env::var(machine::MACHINE_API_BASE_ENV).ok().as_deref(),
std::env::var(CLOUD_API_BASE_ENV).ok().as_deref(),
DEFAULT_API_BASE,
);
Ok(validate_api_base(&requested_base)?.display)
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountStatus {
pub profile: String,
pub api_base: String,
pub account: RuntimeAccountInfo,
pub user: Option<CloudUser>,
pub revision: String,
pub import_allowed: bool,
}
pub struct AccountAuthLease {
pub bundle: AuthBundle,
pub snapshot: AccountSessionSnapshot,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountModel {
pub id: String,
pub label: String,
pub provider: String,
pub model: String,
pub protocol: String,
pub billing_mode: String,
pub is_default: bool,
}
pub struct AccountModels {
pub api_base: String,
pub account_id: String,
pub session_id: String,
pub expires_at: i64,
pub default_model: Option<String>,
pub models: Vec<AccountModel>,
pub credential: Option<String>,
pub snapshot: AccountSessionSnapshot,
}
#[derive(Clone)]
pub struct AccountClient {
transport: ReqwestTransport,
store: AccountSessionStore,
profile: String,
api_base: String,
}
impl AccountClient {
pub fn new(profile: Option<&str>, api_base: Option<&str>) -> Result<Self> {
let requested_base = account_api_base(api_base)?;
let base = validate_api_base(&requested_base)?;
let profile = normalized_profile(profile);
let secrets = cloud_session_secrets()?;
Ok(Self {
transport: ReqwestTransport::new(base.url)?,
store: AccountSessionStore::new(secrets, Some(&profile), &base.display),
profile,
api_base: base.display,
})
}
fn client(&self) -> CloudClient<'_, ReqwestTransport> {
CloudClient {
transport: &self.transport,
account_store: self.store.clone(),
}
}
fn revoke_session_later(&self, previous: Option<AccountSessionSnapshot>, bundle: AuthBundle) {
let client = self.clone();
let _ = std::thread::Builder::new()
.name("cw-account-revoke".into())
.spawn(move || {
let _ = match previous {
Some(previous) => client.client().revoke_previous_session(&previous, &bundle),
None => client.client().revoke_session_bundle(&bundle),
};
});
}
pub fn snapshot(&self) -> Result<AccountSessionSnapshot> {
Ok(self.store.snapshot()?)
}
pub fn status(&self) -> Result<AccountStatus> {
let snapshot = self.snapshot()?;
let user = snapshot.load()?.and_then(|stored| stored.bundle.user);
Ok(AccountStatus {
profile: self.profile.clone(),
api_base: self.api_base.clone(),
account: snapshot.runtime_info_at(Utc::now())?,
user,
revision: snapshot.fingerprint(),
import_allowed: snapshot.is_pristine(),
})
}
pub fn claim(&self) -> Result<AccountSessionSnapshot> {
Ok(self.store.begin_login()?)
}
pub fn claim_if_unchanged(
&self,
expected_revision: &str,
) -> Result<Option<AccountSessionSnapshot>> {
self.store.with_transaction(|transaction| {
if transaction.snapshot().fingerprint() != expected_revision {
return Ok(None);
}
Ok(Some(transaction.renew_revision()?))
})
}
pub fn cancel_claim(&self, expected_revision: &str) -> Result<bool> {
self.store.with_transaction(|transaction| {
if transaction.snapshot().fingerprint() != expected_revision {
return Ok(false);
}
transaction.renew_revision()?;
Ok(true)
})
}
pub fn import(
&self,
bundle: AuthBundle,
expected_revision: &str,
replace_existing: bool,
) -> Result<bool> {
self.import_with_receipt(bundle, expected_revision, replace_existing)
.map(|receipt| receipt.is_some())
}
pub fn import_with_receipt(
&self,
bundle: AuthBundle,
expected_revision: &str,
replace_existing: bool,
) -> Result<Option<AccountSessionSnapshot>> {
validate_auth_bundle(&bundle)?;
let expected = self.snapshot()?;
if expected.fingerprint() != expected_revision {
return Ok(None);
}
if !replace_existing && !expected.is_pristine() {
return Ok(None);
}
let Some(committed) = self.store.save_if_unchanged(&expected, bundle.clone())? else {
return Ok(None);
};
self.revoke_session_later(Some(expected), bundle);
Ok(Some(committed))
}
pub fn fresh_auth(&self, rejected_token: Option<&str>) -> Result<Option<AccountAuthLease>> {
let snapshot = self.snapshot()?;
let Some(stored) = snapshot.load()? else {
return Ok(None);
};
if matches!(
snapshot.runtime_info_at(Utc::now())?.state,
AccountSessionState::Expired | AccountSessionState::Revoked
) {
return Ok(None);
}
let refresh = rejected_token == Some(stored.bundle.access_token.as_str())
|| !stored
.bundle
.session
.as_ref()
.and_then(|session| DateTime::parse_from_rfc3339(&session.expires_at).ok())
.is_some_and(|expires| expires.timestamp() > Utc::now().timestamp() + 120);
let (bundle, snapshot) = if refresh {
let Some(renewed) = self.client().refresh_snapshot(&snapshot)? else {
return Ok(None);
};
renewed
} else {
(stored.bundle, snapshot)
};
let current = self.store.with_transaction(|transaction| {
Ok::<_, anyhow::Error>(transaction.matches(&snapshot))
})?;
Ok(current.then_some(AccountAuthLease { bundle, snapshot }))
}
pub fn logout(&self) -> Result<bool> {
self.client().logout()
}
pub fn logout_if_unchanged(&self, snapshot: &AccountSessionSnapshot) -> Result<bool> {
self.client().logout_from_snapshot(snapshot)
}
pub fn connect_models(&self) -> Result<AccountModels> {
self.connect_models_with_cancel(&AtomicBool::new(false))
}
pub fn connect_models_with_cancel(&self, cancelled: &AtomicBool) -> Result<AccountModels> {
if cancelled.load(Ordering::Acquire) {
bail!("Codewhale account model connection cancelled");
}
let lease = self
.fresh_auth(None)?
.ok_or_else(|| anyhow!("Sign in to Codewhale before connecting account models"))?;
let (response, snapshot) = self.client().execute_from_snapshot(
HttpMethod::Get,
"/api/account/model-api",
None,
lease.snapshot,
)?;
let catalog: serde_json::Value = expect_json(response, &[200])?;
let data = catalog
.get("data")
.and_then(serde_json::Value::as_array)
.filter(|rows| rows.len() <= 8192)
.ok_or_else(|| anyhow!("The Codewhale account model catalog was invalid"))?;
let mut models = Vec::new();
for row in data {
let facts = &row["codewhale"];
if facts["usable"].as_bool() != Some(true) {
continue;
}
if facts["protocol"].as_str() != Some("chat-completions")
|| facts["billingMode"].as_str() != Some("byok_external")
{
continue;
}
let id = model_text(row, "id")?;
let provider = model_text(facts, "provider")?;
let model = model_text(facts, "model")?;
if id != format!("{provider}/{model}") {
bail!("The Codewhale account model catalog was invalid");
}
models.push(AccountModel {
id,
label: facts["label"]
.as_str()
.map(|label| printable_max(label, 200))
.filter(|label| !label.is_empty())
.unwrap_or_else(|| model.clone()),
provider,
model,
protocol: model_text(facts, "protocol")?,
billing_mode: model_text(facts, "billingMode")?,
is_default: facts["default"].as_bool() == Some(true),
});
}
let stored = snapshot
.load()?
.ok_or_else(|| anyhow!("The signed-in account changed. Refresh and try again."))?;
let account_id = stored
.bundle
.user
.as_ref()
.map(|user| user.id.clone())
.filter(|id| !id.is_empty())
.ok_or_else(|| anyhow!("Sign in again before connecting account models"))?;
let session = stored
.bundle
.session
.as_ref()
.filter(|session| !session.id.is_empty())
.ok_or_else(|| anyhow!("Sign in again before connecting account models"))?;
let session_id = session.id.clone();
let access_expires = DateTime::parse_from_rfc3339(&session.expires_at)
.context("The account session expiry was invalid")?
.timestamp();
let refresh_expires = DateTime::parse_from_rfc3339(&session.refresh_expires_at)
.context("The account session expiry was invalid")?
.timestamp();
let default_model = catalog["codewhale"]["defaultModel"]
.as_str()
.filter(|id| models.iter().any(|model| model.id == *id))
.map(str::to_string)
.or_else(|| {
models
.iter()
.find(|model| model.is_default)
.map(|model| model.id.clone())
});
let mut result = AccountModels {
api_base: format!("{}/v1", self.api_base),
account_id: account_id.clone(),
session_id: session_id.clone(),
expires_at: 0,
default_model,
models,
credential: None,
snapshot,
};
if result.models.is_empty() {
let current = self.store.with_transaction(|transaction| {
Ok::<_, anyhow::Error>(transaction.matches(&result.snapshot))
})?;
if !current {
bail!("The signed-in account changed. Refresh and try again.");
}
return Ok(result);
}
if cancelled.load(Ordering::Acquire) {
bail!("Codewhale account model connection cancelled");
}
let device_id = self.store.device_id()?;
let mut issued_id: Option<String> = None;
let connected = self.store.with_transaction(|transaction| {
if !transaction.matches(&result.snapshot) || access_expires <= Utc::now().timestamp() {
bail!("The signed-in account changed. Refresh and try again.");
}
if cancelled.load(Ordering::Acquire) {
bail!("Codewhale account model connection cancelled");
}
let current = transaction.snapshot();
if let Some(access) = current.device_access()? {
if access.device_id != device_id {
bail!("The Codewhale device connection changed. Reconnect your account.");
}
machine::MachineKey::parse(&access.credential)?;
if access.expires_at > Utc::now().timestamp() {
let checked = self.transport.execute(CloudRequest {
method: HttpMethod::Get,
path: "/v1/models".into(),
bearer: Some(access.credential.clone()),
body: None,
})?;
match checked.status {
200 => return Ok((access, current)),
401 => {}
_ => return Err(response_error(&checked)),
}
}
}
if cancelled.load(Ordering::Acquire) {
bail!("Codewhale account model connection cancelled");
}
let response = self.transport.execute(CloudRequest {
method: HttpMethod::Post,
path: "/api/account/device-link".into(),
bearer: Some(stored.bundle.access_token.clone()),
body: Some(serde_json::to_vec(&serde_json::json!({
"deviceId": device_id,
"deviceName": "Codewhale",
"expectedAccountId": account_id,
}))?),
})?;
let linked: LinkedDevice = expect_json(response, &[201])?;
if linked.api_key.id.len() == 24
&& linked
.api_key
.id
.bytes()
.all(|byte| byte.is_ascii_hexdigit())
{
issued_id = Some(linked.api_key.id.clone());
}
let key = machine::MachineKey::parse(&linked.secret)?;
let expires_at = DateTime::parse_from_rfc3339(&linked.api_key.expires_at)
.context("The account device expiry was invalid")?
.timestamp();
if !linked.session_bound
|| linked.api_key.scopes.len() != 1
|| linked.api_key.scopes[0] != "models:infer"
|| linked.api_key.revoked_at.is_some()
|| linked.api_key.context.kind != "device"
|| linked.api_key.context.device_id != device_id
|| linked.api_key.context.session_id != session_id
|| key.head().strip_prefix("cwc_key_") != Some(linked.api_key.id.as_str())
|| expires_at <= Utc::now().timestamp()
|| expires_at > refresh_expires
{
bail!("The Codewhale device connection could not be verified");
}
let access = AccountDeviceAccess {
device_id: device_id.clone(),
credential: linked.secret,
api_key_id: linked.api_key.id,
account_id: account_id.clone(),
session_id: session_id.clone(),
expires_at,
};
transaction.set_device_access(access.clone())?;
Ok((access, transaction.snapshot()))
});
let (access, snapshot) = match connected {
Ok(connected) => connected,
Err(error) => {
if let Some(id) = issued_id {
let _ = self.transport.execute(CloudRequest {
method: HttpMethod::Delete,
path: format!("/api/account/api-keys/{id}"),
bearer: Some(stored.bundle.access_token),
body: None,
});
}
return Err(error);
}
};
result.expires_at = access.expires_at.min(access_expires);
result.credential = Some(access.credential);
result.snapshot = snapshot;
Ok(result)
}
pub fn start_login(&self, timeout: Duration) -> Result<AccountLogin> {
self.start_login_with_cancel(timeout, Arc::new(AtomicBool::new(false)))
}
pub fn start_login_with_cancel(
&self,
timeout: Duration,
cancelled: Arc<AtomicBool>,
) -> Result<AccountLogin> {
if timeout.is_zero() || timeout > Duration::from_secs(MAX_LOGIN_TIMEOUT_SECONDS) {
bail!("Invalid Codewhale account login timeout");
}
let expected = self.store.with_transaction(|transaction| {
if cancelled.load(Ordering::Acquire) {
bail!("Codewhale account login cancelled");
}
Ok::<_, anyhow::Error>(transaction.renew_revision()?)
})?;
let started = Instant::now();
if cancelled.load(Ordering::Acquire) {
bail!("Codewhale account login cancelled");
}
let device = self.client().start_device()?;
validate_device_code(&device.device_code)?;
validate_user_code(&device.user_code)?;
let verification_uri = validate_verification_url(
&device.verification_uri,
&self.api_base,
&device.user_code,
false,
)?;
let verification_uri_complete = validate_verification_url(
&device.verification_uri_complete,
&self.api_base,
&device.user_code,
true,
)?;
let lifetime = timeout.min(Duration::from_secs(
device.expires_in.clamp(1, MAX_LOGIN_TIMEOUT_SECONDS),
));
let interval = if device.interval == 0 {
codewhale_config::device_code::DEFAULT_POLL_INTERVAL_SECS
} else {
device.interval.min(10)
};
Ok(AccountLogin {
client: self.clone(),
expected,
prompt: AccountLoginPrompt {
user_code: device.user_code.clone(),
verification_uri,
verification_uri_complete,
expires_in: lifetime.as_secs(),
interval,
},
device,
cancelled: AccountLoginCancellation(cancelled),
deadline: started + lifetime,
next_poll: started,
pending_bundle: None,
finished: false,
})
}
}
fn model_text(value: &serde_json::Value, key: &str) -> Result<String> {
value[key]
.as_str()
.filter(|text| {
!text.is_empty()
&& text.len() <= 512
&& !text.chars().any(char::is_control)
&& !text.chars().any(char::is_whitespace)
})
.map(str::to_string)
.ok_or_else(|| anyhow!("The Codewhale account model catalog was invalid"))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct LinkedDevice {
secret: String,
api_key: LinkedDeviceKey,
session_bound: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct LinkedDeviceKey {
id: String,
scopes: Vec<String>,
expires_at: String,
revoked_at: Option<String>,
context: LinkedDeviceContext,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct LinkedDeviceContext {
kind: String,
device_id: String,
session_id: String,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountLoginPrompt {
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: String,
pub expires_in: u64,
pub interval: u64,
}
#[derive(Clone)]
pub struct AccountLoginCancellation(Arc<AtomicBool>);
impl AccountLoginCancellation {
pub fn cancel(&self) {
self.0.store(true, Ordering::Release);
}
}
pub enum AccountLoginPoll {
Pending { retry_after: Duration },
Complete { user: CloudUser },
Cancelled,
Expired,
}
pub struct AccountLogin {
client: AccountClient,
expected: AccountSessionSnapshot,
device: DeviceStart,
prompt: AccountLoginPrompt,
cancelled: AccountLoginCancellation,
deadline: Instant,
next_poll: Instant,
pending_bundle: Option<AuthBundle>,
finished: bool,
}
impl AccountLogin {
pub fn prompt(&self) -> &AccountLoginPrompt {
&self.prompt
}
pub fn cancellation(&self) -> AccountLoginCancellation {
self.cancelled.clone()
}
pub fn poll(&mut self) -> Result<AccountLoginPoll> {
let result = self.poll_once();
if !matches!(result, Ok(AccountLoginPoll::Pending { .. })) {
self.finished = true;
}
if matches!(
result,
Ok(AccountLoginPoll::Cancelled | AccountLoginPoll::Expired)
) {
self.revoke_pending();
}
result
}
fn revoke_pending(&mut self) {
if let Some(bundle) = self.pending_bundle.take() {
self.client.revoke_session_later(None, bundle);
}
}
fn poll_once(&mut self) -> Result<AccountLoginPoll> {
if self.finished || self.cancelled.0.load(Ordering::Acquire) {
return Ok(AccountLoginPoll::Cancelled);
}
let now = Instant::now();
if now >= self.deadline {
return Ok(AccountLoginPoll::Expired);
}
if now < self.next_poll {
return Ok(AccountLoginPoll::Pending {
retry_after: (self.next_poll - now).min(self.deadline - now),
});
}
if !self.client.store.with_transaction(|transaction| {
Ok::<_, anyhow::Error>(transaction.matches(&self.expected))
})? {
return Ok(AccountLoginPoll::Cancelled);
}
if let Some(bundle) = self.pending_bundle.as_mut() {
if bundle.user.is_none() {
let response = self.client.transport.execute(CloudRequest {
method: HttpMethod::Get,
path: "/api/me".into(),
bearer: Some(bundle.access_token.clone()),
body: None,
})?;
bundle.user = Some(expect_json::<MeResponse>(response, &[200])?.user);
}
} else {
match self.client.client().poll_device_once(&self.device)? {
DevicePollOutcome::Complete(bundle) => self.pending_bundle = Some(bundle),
DevicePollOutcome::Pending | DevicePollOutcome::SlowDown { .. } => {
self.next_poll = Instant::now() + Duration::from_secs(self.prompt.interval);
return Ok(AccountLoginPoll::Pending {
retry_after: self
.next_poll
.min(self.deadline)
.saturating_duration_since(Instant::now()),
});
}
}
}
let bundle = self
.pending_bundle
.as_ref()
.ok_or_else(|| anyhow!("The Codewhale account authorization was unavailable"))?;
if bundle.user.is_none() {
return Ok(AccountLoginPoll::Pending {
retry_after: Duration::ZERO,
});
}
let Some(user) = bundle
.user
.as_ref()
.filter(|user| !user.id.trim().is_empty())
else {
bail!("The Codewhale service returned an account without an ID");
};
let user = user.clone();
validate_auth_bundle(bundle)?;
if Instant::now() >= self.deadline {
return Ok(AccountLoginPoll::Expired);
}
let bundle = self
.pending_bundle
.take()
.ok_or_else(|| anyhow!("The Codewhale account authorization was unavailable"))?;
let committed = self.client.store.with_transaction(|transaction| {
if self.cancelled.0.load(Ordering::Acquire)
|| !transaction.matches(&self.expected)
|| Instant::now() >= self.deadline
{
return Ok::<_, anyhow::Error>(false);
}
transaction.replace(bundle.clone())?;
Ok(true)
})?;
if !committed {
self.client.revoke_session_later(None, bundle);
return Ok(AccountLoginPoll::Cancelled);
}
self.client
.revoke_session_later(Some(self.expected.clone()), bundle);
Ok(AccountLoginPoll::Complete { user })
}
}
impl Drop for AccountLogin {
fn drop(&mut self) {
if !self.finished {
self.cancelled.cancel();
}
self.revoke_pending();
}
}
+4 -2
View File
@@ -14,6 +14,8 @@ workspace = true
test-support = []
[dependencies]
codewhale-execpolicy = { path = "../execpolicy", version = "0.10.3" }
codewhale-protocol = { path = "../protocol", version = "0.10.3" }
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.10.2" }
codewhale-protocol = { path = "../protocol", version = "0.10.2" }
codewhale-workflow = { path = "../workflow", version = "0.10.2" }
serde = { workspace = true, features = ["derive"] }
+30 -10
View File
@@ -20,8 +20,8 @@
//! ledger / executor / worker_runtime / roster / profile / identity /
//! members, and nothing from tools.
use codewhale_protocol::fleet::{FleetEffectivePermissions, migrate_legacy_role_token};
use codewhale_protocol::fleet::{PermissionCeiling, ShellCeiling};
use codewhale_protocol::fleet::FleetEffectivePermissions;
use codewhale_workflow::{PermissionCeiling, ShellCeiling};
use crate::worker_profile::{ShellPolicy, ToolScope, WorkerRuntimeProfile};
@@ -40,7 +40,7 @@ pub const FLEET_ROLE_SCHEMA_VALUES: [&str; 8] = [
"custom",
];
/// Human-readable hint listing every token [`FleetRole::parse`] accepts,
/// Human-readable hint listing every token [`FleetRole::from_str`] accepts,
/// for spawn-time error messages. Keep in sync with
/// [`migrate_legacy_role_token`] and the `from_str` match arms; those two are
/// the only role parser (#2649 was a second table in the spawn tool drifting
@@ -114,18 +114,38 @@ impl<'de> serde::Deserialize<'de> for FleetRole {
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
Self::parse(&raw)
Self::from_str(&raw)
.ok_or_else(|| serde::de::Error::unknown_variant(&raw, &FLEET_ROLE_SCHEMA_VALUES))
}
}
/// Explicit boundary migration for pre-Fleet serialized role tokens.
///
/// Call this only at load / parse edges. Runtime code must use Fleet role
/// names via [`FleetRole::as_str`]. Returns `None` for tokens that are
/// already canonical or unknown — callers should prefer [`FleetRole::from_str`]
/// for full acceptance (canonical + legacy).
#[must_use]
pub fn migrate_legacy_role_token(token: &str) -> Option<&'static str> {
match token.trim().to_ascii_lowercase().as_str() {
"worker" | "general-purpose" | "general_purpose" | "default" => Some("general"),
"scout" | "exploration" | "explorer" => Some("explore"),
"plan" | "planning" | "awaiter" => Some("planner"),
"review" | "code-review" | "code_review" => Some("reviewer"),
"builder" | "implementer" | "implementation" => Some("implement"),
"verifier" | "verify" | "verification" | "validator" | "tester" => Some("test"),
"consultant" | "oracle" => Some("advisor"),
_ => None,
}
}
impl FleetRole {
/// Parse a Fleet role from user input or a serialized boundary.
///
/// Accepts Fleet role names and, at this parse boundary only, legacy
/// aliases (`scout` → explore, `plan` → planner, …).
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
pub fn from_str(s: &str) -> Option<Self> {
let normalized = s.trim().to_ascii_lowercase();
// Boundary migration first, then canonical Fleet names.
let token = migrate_legacy_role_token(&normalized).unwrap_or(normalized.as_str());
@@ -202,7 +222,7 @@ impl FleetRole {
/// anything else passes through trimmed.
#[must_use]
pub fn public_role_label(token: &str) -> String {
FleetRole::parse(token).map_or_else(
FleetRole::from_str(token).map_or_else(
|| token.trim().to_string(),
|role| role.as_str().to_string(),
)
@@ -268,7 +288,7 @@ pub const NETWORK_TOOL_DENYLIST: &[&str] = &[
// contract was considered and rejected: `rlm_open` chooses its source from
// *input fields* (`file_path` / `content` / `url` / `session_object`), not
// from the action name, and the action-policy seam
// (`codewhale_execpolicy::canonical_action`) resolves names, not field shapes —
// (`crate::tools::canonical_action`) resolves names, not field shapes —
// it cannot prove a source is local before execution. So this fails closed.
// A network-denied member loses `rlm` loading and evaluation entirely,
// including the purely local `file_path` form, and keeps only the bounded
@@ -639,7 +659,7 @@ impl ChildAuthority {
/// model-facing spawn vocabulary.
///
/// Deliberately *not* folded into [`migrate_legacy_role_token`]. That table
/// feeds [`FleetRole::parse`], which is also the closed vocabulary the
/// feeds [`FleetRole::from_str`], which is also the closed vocabulary the
/// `agent` tool validates its `type` against, and — decisively — the test the
/// spawn parser uses to decide whether a `role` string is a posture alias or a
/// roster profile key. Teaching `from_str` about `manager` or `smoke-runner`
@@ -685,7 +705,7 @@ fn member_role_alias(token: &str) -> Option<FleetRole> {
/// Resolution order, and nothing else:
/// 1. an empty label means *unspecified*, which is the documented general
/// default a Fleet task without a `role` has always had;
/// 2. [`FleetRole::parse`] — the declared closed vocabulary plus its
/// 2. [`FleetRole::from_str`] — the declared closed vocabulary plus its
/// documented legacy aliases ([`VALID_ROLE_ALIASES`]);
/// 3. `member_role_alias` — the org-chart/slot spellings above;
/// 4. **fail closed.**
@@ -703,7 +723,7 @@ pub fn runtime_role_for_member(role: &str) -> FleetRole {
if token.is_empty() {
return FleetRole::Worker;
}
FleetRole::parse(&token)
FleetRole::from_str(&token)
.or_else(|| member_role_alias(&token))
.unwrap_or(FleetRole::Scout)
}
+1 -66
View File
@@ -463,7 +463,7 @@ impl WorkerRuntimeProfile {
// wins via `derive_child`, same as every other role.
reasoning_effort: matches!(role, FleetRole::Consultant).then(|| "high".to_string()),
denied_tools: Vec::new(),
max_spawn_depth: codewhale_protocol::fleet::DEFAULT_SPAWN_DEPTH,
max_spawn_depth: codewhale_config::DEFAULT_SPAWN_DEPTH,
spawn_depth: 0,
wall_time_secs: None,
wall_deadline_ms: None,
@@ -579,69 +579,6 @@ impl Default for WorkerRuntimeProfile {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChildActivationIdentity {
pub owner_session_id: String,
pub session_boot_id: String,
pub public_child_id: String,
pub child_id: String,
pub run_id: String,
pub generation: u32,
pub initial_submission_id: String,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ChildActivationDelivery {
#[default]
Parent,
Caller,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct FrozenChildConfiguration {
pub persona: Option<String>,
pub denied_tools: Vec<String>,
pub delivery: ChildActivationDelivery,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChildInitialAdmission {
pub submission_id: String,
pub turn_id: String,
pub message_index: usize,
pub prefix_sha256: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ChildActivationStopReason {
Completed,
Aborted,
Error,
MaxTokens,
Refusal,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChildActivationOutput {
pub transcript_message_index: usize,
pub content_sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChildActivationManifest {
pub identity: ChildActivationIdentity,
pub configuration: Option<FrozenChildConfiguration>,
pub admission: Option<ChildInitialAdmission>,
#[serde(default)]
pub output_captured: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<ChildActivationStopReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<ChildActivationOutput>,
}
/// Unified pre-launch manifest for a child agent (#414).
///
/// Everything needed to provision, launch, and resume a child — prompt, role,
@@ -649,8 +586,6 @@ pub struct ChildActivationManifest {
/// from this single persisted record. No field is derived ad-hoc at launch time.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChildLaunchManifest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub activation: Option<ChildActivationManifest>,
pub owner_session: String,
pub child_id: String,
pub profile: WorkerRuntimeProfile,
+1 -1
View File
@@ -11,5 +11,5 @@ description = "Model/provider registry and fallback strategy for Codewhale"
workspace = true
[dependencies]
codewhale-config = { path = "../config", version = "0.10.3" }
codewhale-config = { path = "../config", version = "0.10.2" }
serde.workspace = true
+6 -6
View File
@@ -1131,7 +1131,7 @@ mod tests {
for (provider, id) in [
(ProviderKind::Zai, "GLM-5.2"),
(ProviderKind::Stepfun, "step-5-preview"),
(ProviderKind::Stepfun, "step-3.7-flash"),
(ProviderKind::Minimax, "MiniMax-M2.1"),
(ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
(ProviderKind::Openmodel, "deepseek-v4-flash"),
@@ -1245,12 +1245,12 @@ mod tests {
let default = registry.resolve_ok(None, Some(ProviderKind::Xai));
assert_eq!(default.resolved.provider, ProviderKind::Xai);
assert_eq!(default.resolved.id, "grok-4.7");
assert_eq!(default.resolved.id, "grok-4.6");
assert!(default.used_fallback);
let alias = registry.resolve_ok(Some("grok"), Some(ProviderKind::Xai));
assert_eq!(alias.resolved.provider, ProviderKind::Xai);
assert_eq!(alias.resolved.id, "grok-4.7");
assert_eq!(alias.resolved.id, "grok-4.6");
assert!(!alias.used_fallback);
let fast = registry.resolve_ok(
@@ -1268,12 +1268,12 @@ mod tests {
let default = registry.resolve_ok(None, Some(ProviderKind::Meta));
assert_eq!(default.resolved.provider, ProviderKind::Meta);
assert_eq!(default.resolved.id, "muse-spark-1.3");
assert_eq!(default.resolved.id, "muse-spark-1.2");
assert!(default.used_fallback);
let alias = registry.resolve_ok(Some("muse-spark"), Some(ProviderKind::Meta));
assert_eq!(alias.resolved.provider, ProviderKind::Meta);
assert_eq!(alias.resolved.id, "muse-spark-1.3");
assert_eq!(alias.resolved.id, "muse-spark-1.2");
assert!(!alias.used_fallback);
assert_eq!(model_family("muse-spark-1.2"), ModelFamily::Meta);
}
@@ -1307,7 +1307,7 @@ mod tests {
let stepfun = registry.resolve_ok(None, Some(ProviderKind::Stepfun));
assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
assert_eq!(stepfun.resolved.id, "step-5-preview");
assert_eq!(stepfun.resolved.id, "step-3.7-flash");
for (alias, expected) in [
("minimax", "MiniMax-M3"),
-32
View File
@@ -1,32 +0,0 @@
[package]
name = "codewhale-app-server-protocol"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license = "MIT AND Apache-2.0"
repository.workspace = true
description = "Client wire contracts and schema exports for Codewhale App Server"
[lints]
workspace = true
[features]
schema = ["dep:schemars"]
[dependencies]
chrono.workspace = true
serde.workspace = true
serde_json = { workspace = true, features = ["preserve_order"] }
schemars = { version = "1.2.1", optional = true, features = ["derive", "preserve_order"] }
[dev-dependencies]
jsonschema.workspace = true
[[bin]]
name = "codewhale-codex-contract"
path = "src/bin/contract.rs"
[[bin]]
name = "codewhale-app-server-schema"
path = "src/bin/export.rs"
required-features = ["schema"]
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 OpenAI
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-285
View File
@@ -1,285 +0,0 @@
# Codex App Server method inventory
Generated from `contract/codex-v2.json` (contract `codewhale-codex-app-server-v2` version 1) and the pinned Codex `rust-v0.166.0` export (commit `6ff1e1ca3b30fc8b914720681fcf5fe62e95f856`) by `cargo run -p codewhale-app-server-protocol --bin codewhale-codex-contract -- --write`. Edit the manifest, never this file; `tests/contract.rs` fails when they drift. The method sets are the unmodified export members under `tests/pinned/codex/`.
- **A**: required for 0.10.3 Codex parity.
- **B**: service methods, required for 0.10.3.
- **C**: out of scope, for the reason given.
- **undecided**: the 0.10.3 parity bar does not settle it yet. The last column carries a recommended tier for the founder to confirm; until then it is handled exactly like Tier C.
Status: `implemented` means a Codex-shaped handler is on main (response goldens are not yet checked); `partial` means the Codex method name dispatches to the legacy shape; `missing` means no handler; `method-not-found` means the dispatcher answers `-32601`; `never-sent` means the server never emits it. The stable export is the scope: methods that exist only in the experimental export are not listed. A complete inventory or typed shape is not a handler: status, not tier, says what main dispatches.
## Dispatch rules
| Tier | Rule |
| --- | --- |
| A | selected for 0.10.3; the typed params/result in src/v2.rs are the wire contract |
| B | selected for 0.10.3 (service methods); the typed params/result in src/v2.rs are the wire contract |
| C | not selected; a client request answers methodNotFound (-32601); the server never sends a Tier C request or notification |
| undecided | not selected until the founder decides; until then it is handled exactly like Tier C (methodNotFound, never sent), and the row keeps its recommendation |
## Client requests
| Method | Tier | Status | Basis | Recommended | Params | Result |
| --- | --- | --- | --- | --- | --- | --- |
| `initialize` | A | implemented | connection lifecycle (CW-7704; on main) | | `InitializeParams` | `InitializeResponse` |
| `thread/start` | A | implemented | thread lifecycle | | `ThreadStartParams` | `ThreadStartResponse` |
| `thread/resume` | A | partial | thread lifecycle | | `ThreadResumeParams` | `ThreadResumeResponse` |
| `thread/fork` | A | partial | thread lifecycle | | `ThreadForkParams` | `ThreadForkResponse` |
| `thread/archive` | A | partial | thread lifecycle | | `ThreadArchiveParams` | `ThreadArchiveResponse` |
| `thread/delete` | B | missing | S9 services: thread delete, revert, shellCommand, compact | | `ThreadDeleteParams` | `ThreadDeleteResponse` |
| `thread/unsubscribe` | A | missing | S5 subscriptions and paging | | `ThreadUnsubscribeParams` | `ThreadUnsubscribeResponse` |
| `thread/name/set` | A | implemented | thread lifecycle | | `ThreadSetNameParams` | `ThreadSetNameResponse` |
| `thread/goal/set` | A | partial | S8 goals | | `ThreadGoalSetParams` | `ThreadGoalSetResponse` |
| `thread/goal/get` | A | partial | S8 goals | | `ThreadGoalGetParams` | `ThreadGoalGetResponse` |
| `thread/goal/clear` | A | partial | S8 goals | | `ThreadGoalClearParams` | `ThreadGoalClearResponse` |
| `thread/metadata/update` | undecided | missing | thread metadata beyond name/goal | B | ThreadMetadataUpdateParams (untyped) | ThreadMetadataUpdateResponse (untyped) |
| `thread/attachment/add` | undecided | missing | Codex desktop attachments | C | ThreadAttachmentAddParams (untyped) | ThreadAttachmentAddResponse (untyped) |
| `thread/attachment/list` | undecided | missing | Codex desktop attachments | C | ThreadAttachmentListParams (untyped) | ThreadAttachmentListResponse (untyped) |
| `thread/attachmentOwner/list` | undecided | missing | Codex desktop attachments | C | ThreadAttachmentOwnerListParams (untyped) | ThreadAttachmentOwnerListResponse (untyped) |
| `thread/attachment/remove` | undecided | missing | Codex desktop attachments | C | ThreadAttachmentRemoveParams (untyped) | ThreadAttachmentRemoveResponse (untyped) |
| `thread/section/move` | undecided | missing | Codex desktop sidebar sections; Codewhale projects are CW-7878 | C | ThreadSectionMoveParams (untyped) | ThreadSectionMoveResponse (untyped) |
| `thread/unarchive` | A | partial | thread lifecycle | | `ThreadUnarchiveParams` | `ThreadUnarchiveResponse` |
| `thread/compact/start` | B | missing | S9 services: thread delete, revert, shellCommand, compact | | `ThreadCompactStartParams` | `ThreadCompactStartResponse` |
| `thread/shellCommand` | B | missing | S9 services: thread delete, revert, shellCommand, compact | | `ThreadShellCommandParams` | `ThreadShellCommandResponse` |
| `thread/approveGuardianDeniedAction` | undecided | missing | Codex Guardian auto-review override; Codewhale has its own Auto-Review posture | C | ThreadApproveGuardianDeniedActionParams (untyped) | ThreadApproveGuardianDeniedActionResponse (untyped) |
| `thread/revert` | B | missing | S9 services: thread delete, revert, shellCommand, compact | | `ThreadRevertParams` | `ThreadRevertResponse` |
| `thread/list` | A | implemented | thread lifecycle | | `ThreadListParams` | `ThreadListResponse` |
| `threadSection/list` | undecided | missing | Codex desktop sidebar sections; Codewhale projects are CW-7878 | C | ThreadSectionListParams (untyped) | ThreadSectionListResponse (untyped) |
| `threadSection/create` | undecided | missing | Codex desktop sidebar sections; Codewhale projects are CW-7878 | C | ThreadSectionCreateParams (untyped) | ThreadSectionCreateResponse (untyped) |
| `threadSection/update` | undecided | missing | Codex desktop sidebar sections; Codewhale projects are CW-7878 | C | ThreadSectionUpdateParams (untyped) | ThreadSectionUpdateResponse (untyped) |
| `threadSection/delete` | undecided | missing | Codex desktop sidebar sections; Codewhale projects are CW-7878 | C | ThreadSectionDeleteParams (untyped) | ThreadSectionDeleteResponse (untyped) |
| `thread/loaded/list` | A | implemented | thread lifecycle | | `ThreadLoadedListParams` | `ThreadLoadedListResponse` |
| `thread/read` | A | implemented | thread lifecycle | | `ThreadReadParams` | `ThreadReadResponse` |
| `thread/turns/list` | A | missing | S5 subscriptions and paging | | `ThreadTurnsListParams` | `ThreadTurnsListResponse` |
| `thread/items/list` | A | missing | S5 subscriptions and paging | | `ThreadItemsListParams` | `ThreadItemsListResponse` |
| `thread/inject_items` | undecided | missing | raw history injection | C | ThreadInjectItemsParams (untyped) | ThreadInjectItemsResponse (untyped) |
| `skills/list` | B | missing | S9 services: skills | | `SkillsListParams` | `SkillsListResponse` |
| `skills/extraRoots/set` | undecided | missing | skills configuration writes | B | SkillsExtraRootsSetParams (untyped) | SkillsExtraRootsSetResponse (untyped) |
| `hooks/list` | B | missing | S9 services: hooks | | `HooksListParams` | `HooksListResponse` |
| `marketplace/add` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | MarketplaceAddParams (untyped) | MarketplaceAddResponse (untyped) |
| `marketplace/remove` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | MarketplaceRemoveParams (untyped) | MarketplaceRemoveResponse (untyped) |
| `marketplace/upgrade` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | MarketplaceUpgradeParams (untyped) | MarketplaceUpgradeResponse (untyped) |
| `plugin/list` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | PluginListParams (untyped) | PluginListResponse (untyped) |
| `plugin/installed` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | PluginInstalledParams (untyped) | PluginInstalledResponse (untyped) |
| `plugin/reconcile` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | PluginReconcileParams (untyped) | PluginReconcileResponse (untyped) |
| `plugin/read` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | PluginReadParams (untyped) | PluginReadResponse (untyped) |
| `plugin/skill/read` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | PluginSkillReadParams (untyped) | PluginSkillReadResponse (untyped) |
| `plugin/share/save` | C | method-not-found | OpenAI-hosted plugin sharing; no Codewhale service | | PluginShareSaveParams (untyped) | PluginShareSaveResponse (untyped) |
| `plugin/share/updateTargets` | C | method-not-found | OpenAI-hosted plugin sharing; no Codewhale service | | PluginShareUpdateTargetsParams (untyped) | PluginShareUpdateTargetsResponse (untyped) |
| `plugin/share/list` | C | method-not-found | OpenAI-hosted plugin sharing; no Codewhale service | | PluginShareListParams (untyped) | PluginShareListResponse (untyped) |
| `plugin/share/checkout` | C | method-not-found | OpenAI-hosted plugin sharing; no Codewhale service | | PluginShareCheckoutParams (untyped) | PluginShareCheckoutResponse (untyped) |
| `plugin/share/delete` | C | method-not-found | OpenAI-hosted plugin sharing; no Codewhale service | | PluginShareDeleteParams (untyped) | PluginShareDeleteResponse (untyped) |
| `app/read` | C | method-not-found | ChatGPT Apps connectors; no Codewhale service | | AppsReadParams (untyped) | AppsReadResponse (untyped) |
| `app/list` | C | method-not-found | ChatGPT Apps connectors; no Codewhale service | | AppsListParams (untyped) | AppsListResponse (untyped) |
| `app/installed` | C | method-not-found | ChatGPT Apps connectors; no Codewhale service | | AppsInstalledParams (untyped) | AppsInstalledResponse (untyped) |
| `fs/readFile` | B | missing | S9 services: fs | | `FsReadFileParams` | `FsReadFileResponse` |
| `fs/writeFile` | B | missing | S9 services: fs | | `FsWriteFileParams` | `FsWriteFileResponse` |
| `fs/createDirectory` | B | missing | S9 services: fs | | `FsCreateDirectoryParams` | `FsCreateDirectoryResponse` |
| `fs/getMetadata` | B | missing | S9 services: fs | | `FsGetMetadataParams` | `FsGetMetadataResponse` |
| `fs/readDirectory` | B | missing | S9 services: fs | | `FsReadDirectoryParams` | `FsReadDirectoryResponse` |
| `fs/remove` | B | missing | S9 services: fs | | `FsRemoveParams` | `FsRemoveResponse` |
| `fs/copy` | B | missing | S9 services: fs | | `FsCopyParams` | `FsCopyResponse` |
| `fs/watch` | B | missing | S9 services: fs | | `FsWatchParams` | `FsWatchResponse` |
| `fs/unwatch` | B | missing | S9 services: fs | | `FsUnwatchParams` | `FsUnwatchResponse` |
| `skills/config/write` | undecided | missing | skills configuration writes | B | SkillsConfigWriteParams (untyped) | SkillsConfigWriteResponse (untyped) |
| `plugin/install` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | PluginInstallParams (untyped) | PluginInstallResponse (untyped) |
| `plugin/uninstall` | undecided | missing | Codex plugin marketplace; Codewhale plugins are DSH (CW-7051) | C | PluginUninstallParams (untyped) | PluginUninstallResponse (untyped) |
| `turn/start` | A | missing | S4a full-tool turns | | `TurnStartParams` | `TurnStartResponse` |
| `turn/steer` | A | missing | S4a full-tool turns | | `TurnSteerParams` | `TurnSteerResponse` |
| `turn/interrupt` | A | missing | S4a full-tool turns | | `TurnInterruptParams` | `TurnInterruptResponse` |
| `review/start` | B | missing | S9 services: review | | `ReviewStartParams` | `ReviewStartResponse` |
| `model/list` | B | missing | S9 services: models | | `ModelListParams` | `ModelListResponse` |
| `account/gatewayOAuth/read` | C | method-not-found | OpenAI gateway OAuth; no Codewhale service | | | |
| `account/gatewayOAuth/login` | C | method-not-found | OpenAI gateway OAuth; no Codewhale service | | | |
| `account/gatewayOAuth/cancel` | C | method-not-found | OpenAI gateway OAuth; no Codewhale service | | | |
| `modelProvider/capabilities/read` | undecided | missing | provider, feature and permission catalogs | B | ModelProviderCapabilitiesReadParams (untyped) | ModelProviderCapabilitiesReadResponse (untyped) |
| `experimentalFeature/list` | undecided | missing | Codex experimental feature flags | C | ExperimentalFeatureListParams (untyped) | ExperimentalFeatureListResponse (untyped) |
| `permissionProfile/list` | undecided | missing | provider, feature and permission catalogs | B | PermissionProfileListParams (untyped) | PermissionProfileListResponse (untyped) |
| `experimentalFeature/enablement/set` | undecided | missing | Codex experimental feature flags | C | ExperimentalFeatureEnablementSetParams (untyped) | ExperimentalFeatureEnablementSetResponse (untyped) |
| `mcpServer/oauth/login` | undecided | missing | MCP actions beyond status | B | McpServerOauthLoginParams (untyped) | McpServerOauthLoginResponse (untyped) |
| `config/mcpServer/reload` | undecided | missing | MCP actions beyond status | B | | |
| `mcpServerStatus/list` | B | missing | S9 services: MCP status | | `ListMcpServerStatusParams` | `ListMcpServerStatusResponse` |
| `mcpServer/resource/read` | undecided | missing | MCP actions beyond status | B | McpResourceReadParams (untyped) | McpResourceReadResponse (untyped) |
| `mcpServer/tool/call` | undecided | missing | MCP actions beyond status | B | McpServerToolCallParams (untyped) | McpServerToolCallResponse (untyped) |
| `windowsSandbox/setupStart` | undecided | missing | Codex Windows sandbox setup; Codewhale has its own AppContainer sandbox | C | WindowsSandboxSetupStartParams (untyped) | WindowsSandboxSetupStartResponse (untyped) |
| `windowsSandbox/readiness` | undecided | missing | Codex Windows sandbox setup; Codewhale has its own AppContainer sandbox | C | | |
| `account/login/start` | undecided | missing | OpenAI account sign-in and usage; Codewhale signs in through /auth | C | LoginAccountParams (untyped) | LoginAccountResponse (untyped) |
| `account/login/cancel` | undecided | missing | OpenAI account sign-in and usage; Codewhale signs in through /auth | C | CancelLoginAccountParams (untyped) | CancelLoginAccountResponse (untyped) |
| `account/logout` | undecided | missing | OpenAI account sign-in and usage; Codewhale signs in through /auth | C | | |
| `account/rateLimits/read` | undecided | missing | OpenAI account sign-in and usage; Codewhale signs in through /auth | C | | |
| `account/rateLimitResetCredit/consume` | C | method-not-found | OpenAI billing and workspace messaging; no Codewhale service | | ConsumeAccountRateLimitResetCreditParams (untyped) | ConsumeAccountRateLimitResetCreditResponse (untyped) |
| `account/usage/read` | undecided | missing | OpenAI account sign-in and usage; Codewhale signs in through /auth | C | | |
| `account/workspaceMessages/read` | C | method-not-found | OpenAI billing and workspace messaging; no Codewhale service | | | |
| `account/sendAddCreditsNudgeEmail` | C | method-not-found | OpenAI billing and workspace messaging; no Codewhale service | | SendAddCreditsNudgeEmailParams (untyped) | SendAddCreditsNudgeEmailResponse (untyped) |
| `feedback/upload` | C | method-not-found | uploads feedback to OpenAI; no Codewhale service | | FeedbackUploadParams (untyped) | FeedbackUploadResponse (untyped) |
| `feedback/save` | C | method-not-found | uploads feedback to OpenAI; no Codewhale service | | FeedbackSaveParams (untyped) | FeedbackSaveResponse (untyped) |
| `command/exec` | B | missing | S9 services: command/exec | | `CommandExecParams` | `CommandExecResponse` |
| `command/exec/write` | B | missing | S9 services: command/exec | | `CommandExecWriteParams` | `CommandExecWriteResponse` |
| `command/exec/terminate` | B | missing | S9 services: command/exec | | `CommandExecTerminateParams` | `CommandExecTerminateResponse` |
| `command/exec/resize` | B | missing | S9 services: command/exec | | `CommandExecResizeParams` | `CommandExecResizeResponse` |
| `config/read` | B | missing | S9 services: config | | `ConfigReadParams` | `ConfigReadResponse` |
| `externalAgentConfig/detect` | undecided | missing | import config from other agents; Codewhale external agents are CW-7879 | C | ExternalAgentConfigDetectParams (untyped) | ExternalAgentConfigDetectResponse (untyped) |
| `externalAgentConfig/import` | undecided | missing | import config from other agents; Codewhale external agents are CW-7879 | C | ExternalAgentConfigImportParams (untyped) | ExternalAgentConfigImportResponse (untyped) |
| `externalAgentConfig/import/recordHistory` | undecided | missing | import config from other agents; Codewhale external agents are CW-7879 | C | ExternalAgentConfigImportHistoryRecordParams (untyped) | ExternalAgentConfigImportHistoryRecordResponse (untyped) |
| `externalAgentConfig/import/readHistories` | undecided | missing | import config from other agents; Codewhale external agents are CW-7879 | C | | |
| `config/value/write` | B | missing | S9 services: config | | `ConfigValueWriteParams` | `ConfigWriteResponse` |
| `config/batchWrite` | B | missing | S9 services: config | | `ConfigBatchWriteParams` | `ConfigWriteResponse` |
| `configRequirements/read` | undecided | missing | managed config requirements | B | | |
| `account/read` | undecided | missing | OpenAI account sign-in and usage; Codewhale signs in through /auth | C | GetAccountParams (untyped) | GetAccountResponse (untyped) |
| `getConversationSummary` | C | method-not-found | deprecated v1 API (upstream marks it DEPRECATED; TypeScript-only in the export) | | GetConversationSummaryParams (untyped) | GetConversationSummaryResponse (untyped) |
| `gitDiffToRemote` | C | method-not-found | deprecated v1 API (upstream marks it DEPRECATED; TypeScript-only in the export) | | GitDiffToRemoteParams (untyped) | GitDiffToRemoteResponse (untyped) |
| `getAuthStatus` | C | method-not-found | deprecated v1 API (upstream marks it DEPRECATED; TypeScript-only in the export) | | GetAuthStatusParams (untyped) | GetAuthStatusResponse (untyped) |
| `fuzzyFileSearch` | undecided | missing | file search | B | FuzzyFileSearchParams (untyped) | FuzzyFileSearchResponse (untyped) |
## Client notifications
| Method | Tier | Status | Basis | Recommended | Params | Result |
| --- | --- | --- | --- | --- | --- | --- |
| `initialized` | A | implemented | connection lifecycle (CW-7704; on main) | | | |
## Server requests
| Method | Tier | Status | Basis | Recommended | Params | Result |
| --- | --- | --- | --- | --- | --- | --- |
| `item/commandExecution/requestApproval` | A | missing | S6 interactions (CW-7704: bidirectional approvals, input, client tools) | | `CommandExecutionRequestApprovalParams` | `CommandExecutionRequestApprovalResponse` |
| `item/fileChange/requestApproval` | A | missing | S6 interactions (CW-7704: bidirectional approvals, input, client tools) | | `FileChangeRequestApprovalParams` | `FileChangeRequestApprovalResponse` |
| `item/tool/requestUserInput` | A | missing | S6 interactions (CW-7704: bidirectional approvals, input, client tools) | | `ToolRequestUserInputParams` | `ToolRequestUserInputResponse` |
| `mcpServer/elicitation/request` | undecided | missing | MCP elicitation | A | McpServerElicitationRequestParams (untyped) | McpServerElicitationRequestResponse (untyped) |
| `item/permissions/requestApproval` | A | missing | S6 interactions (CW-7704: bidirectional approvals, input, client tools) | | `PermissionsRequestApprovalParams` | `PermissionsRequestApprovalResponse` |
| `item/tool/call` | A | missing | S6 interactions (CW-7704: bidirectional approvals, input, client tools) | | `DynamicToolCallParams` | `DynamicToolCallResponse` |
| `account/chatgptAuthTokens/refresh` | C | never-sent | ChatGPT externally managed auth tokens; no Codewhale service | | ChatgptAuthTokensRefreshParams (untyped) | ChatgptAuthTokensRefreshResponse (untyped) |
| `attestation/generate` | C | never-sent | OpenAI request attestation; no Codewhale service | | AttestationGenerateParams (untyped) | AttestationGenerateResponse (untyped) |
| `applyPatchApproval` | C | never-sent | deprecated v1 approvals for legacy turns; superseded by item/*/requestApproval | | ApplyPatchApprovalParams (untyped) | ApplyPatchApprovalResponse (untyped) |
| `execCommandApproval` | C | never-sent | deprecated v1 approvals for legacy turns; superseded by item/*/requestApproval | | ExecCommandApprovalParams (untyped) | ExecCommandApprovalResponse (untyped) |
## Server notifications
| Method | Tier | Status | Basis | Recommended | Params | Result |
| --- | --- | --- | --- | --- | --- | --- |
| `error` | A | missing | S3 projection: errors | | `ErrorNotification` | |
| `thread/started` | A | missing | S3 projection: thread lifecycle | | `ThreadStartedNotification` | |
| `thread/status/changed` | A | missing | S3 projection: thread lifecycle | | `ThreadStatusChangedNotification` | |
| `thread/readState/changed` | undecided | missing | thread state beyond the Tier A set | A | ThreadReadStateChangedNotification (untyped) | |
| `thread/archived` | A | missing | S3 projection: thread lifecycle | | `ThreadArchivedNotification` | |
| `thread/deleted` | B | missing | pairs with a Tier B service | | `ThreadDeletedNotification` | |
| `thread/unarchived` | A | missing | S3 projection: thread lifecycle | | `ThreadUnarchivedNotification` | |
| `thread/closed` | A | missing | S3 projection: thread lifecycle | | `ThreadClosedNotification` | |
| `thread/reverted` | B | missing | pairs with a Tier B service | | `ThreadRevertedNotification` | |
| `skills/changed` | B | missing | pairs with a Tier B service | | `SkillsChangedNotification` | |
| `thread/name/updated` | A | missing | S3 projection: thread lifecycle | | `ThreadNameUpdatedNotification` | |
| `thread/attachment/updated` | undecided | missing | Codex desktop attachments, predictions, projects and environments | C | ThreadAttachmentUpdatedNotification (untyped) | |
| `thread/goal/updated` | A | missing | S3 projection with S8 goals | | `ThreadGoalUpdatedNotification` | |
| `thread/prediction/updated` | undecided | missing | Codex desktop attachments, predictions, projects and environments | C | ThreadPredictionUpdatedNotification (untyped) | |
| `thread/goal/cleared` | A | missing | S3 projection with S8 goals | | `ThreadGoalClearedNotification` | |
| `thread/queue/changed` | undecided | missing | thread state beyond the Tier A set | A | ThreadQueueChangedNotification (untyped) | |
| `project/changed` | undecided | missing | Codex desktop attachments, predictions, projects and environments | C | ProjectChangedNotification (untyped) | |
| `thread/project/updated` | undecided | missing | Codex desktop attachments, predictions, projects and environments | C | ThreadProjectUpdatedNotification (untyped) | |
| `thread/environment/connected` | undecided | missing | Codex desktop attachments, predictions, projects and environments | C | EnvironmentConnectionNotification (untyped) | |
| `thread/environment/disconnected` | undecided | missing | Codex desktop attachments, predictions, projects and environments | C | EnvironmentConnectionNotification (untyped) | |
| `thread/settings/updated` | undecided | missing | thread state beyond the Tier A set | A | ThreadSettingsUpdatedNotification (untyped) | |
| `thread/tokenUsage/updated` | A | missing | S3 projection: thread lifecycle | | `ThreadTokenUsageUpdatedNotification` | |
| `turn/started` | A | missing | S3 projection: turn/* | | `TurnStartedNotification` | |
| `hook/started` | undecided | missing | hook runs; hooks/list is Tier B | B | HookStartedNotification (untyped) | |
| `turn/completed` | A | missing | S3 projection: turn/* | | `TurnCompletedNotification` | |
| `hook/completed` | undecided | missing | hook runs; hooks/list is Tier B | B | HookCompletedNotification (untyped) | |
| `turn/diff/updated` | A | missing | S3 projection: turn/* | | `TurnDiffUpdatedNotification` | |
| `turn/plan/updated` | A | missing | S3 projection: turn/* | | `TurnPlanUpdatedNotification` | |
| `item/started` | A | missing | S3 projection: item/* | | `ItemStartedNotification` | |
| `item/autoApprovalReview/started` | undecided | missing | Codex Guardian; Codewhale has its own Auto-Review posture | C | ItemGuardianApprovalReviewStartedNotification (untyped) | |
| `item/autoApprovalReview/completed` | undecided | missing | Codex Guardian; Codewhale has its own Auto-Review posture | C | ItemGuardianApprovalReviewCompletedNotification (untyped) | |
| `autoApprovalReview/strictReviewRequired` | undecided | missing | Codex Guardian; Codewhale has its own Auto-Review posture | C | StrictReviewRequiredNotification (untyped) | |
| `item/completed` | A | missing | S3 projection: item/* | | `ItemCompletedNotification` | |
| `rawResponseItem/completed` | C | never-sent | internal-only upstream (Codex Cloud, exact upstream usage); TypeScript-only in the export | | RawResponseItemCompletedNotification (untyped) | |
| `rawResponse/completed` | C | never-sent | internal-only upstream (Codex Cloud, exact upstream usage); TypeScript-only in the export | | RawResponseCompletedNotification (untyped) | |
| `item/agentMessage/delta` | A | missing | S3 projection: item/* | | `AgentMessageDeltaNotification` | |
| `item/plan/delta` | A | missing | S3 projection: item/* | | `PlanDeltaNotification` | |
| `command/exec/outputDelta` | B | missing | pairs with a Tier B service | | `CommandExecOutputDeltaNotification` | |
| `process/outputDelta` | undecided | missing | background processes | B | ProcessOutputDeltaNotification (untyped) | |
| `process/exited` | undecided | missing | background processes | B | ProcessExitedNotification (untyped) | |
| `item/commandExecution/outputDelta` | A | missing | S3 projection: item/* | | `CommandExecutionOutputDeltaNotification` | |
| `item/commandExecution/terminalInteraction` | A | missing | S3 projection: item/* | | `TerminalInteractionNotification` | |
| `item/fileChange/outputDelta` | A | missing | S3 projection: item/* | | `FileChangeOutputDeltaNotification` | |
| `item/fileChange/patchUpdated` | A | missing | S3 projection: item/* | | `FileChangePatchUpdatedNotification` | |
| `serverRequest/resolved` | A | missing | S6 interactions (CW-7704) | | `ServerRequestResolvedNotification` | |
| `item/mcpToolCall/progress` | A | missing | S3 projection: item/* | | `McpToolCallProgressNotification` | |
| `mcpServer/oauthLogin/completed` | undecided | missing | MCP server events | B | McpServerOauthLoginCompletedNotification (untyped) | |
| `mcpServer/startupStatus/updated` | undecided | missing | MCP server events | B | McpServerStatusUpdatedNotification (untyped) | |
| `mcpServer/event/stream/notification` | undecided | missing | MCP server events | B | McpServerEventStreamNotification (untyped) | |
| `account/updated` | undecided | missing | OpenAI account state | C | AccountUpdatedNotification (untyped) | |
| `account/gatewayOAuth/changed` | C | never-sent | pairs with a Tier C method | | GatewayOAuthChangedNotification (untyped) | |
| `account/rateLimits/updated` | undecided | missing | OpenAI account state | C | AccountRateLimitsUpdatedNotification (untyped) | |
| `app/list/updated` | C | never-sent | pairs with a Tier C method | | AppListUpdatedNotification (untyped) | |
| `remoteControl/status/changed` | undecided | missing | remote control status | C | RemoteControlStatusChangedNotification (untyped) | |
| `externalAgentConfig/import/progress` | undecided | missing | pairs with externalAgentConfig/import | C | ExternalAgentConfigImportProgressNotification (untyped) | |
| `externalAgentConfig/import/completed` | undecided | missing | pairs with externalAgentConfig/import | C | ExternalAgentConfigImportCompletedNotification (untyped) | |
| `fs/changed` | B | missing | pairs with a Tier B service | | `FsChangedNotification` | |
| `item/reasoning/summaryTextDelta` | A | missing | S3 projection: item/* | | `ReasoningSummaryTextDeltaNotification` | |
| `item/reasoning/summaryPartAdded` | A | missing | S3 projection: item/* | | `ReasoningSummaryPartAddedNotification` | |
| `item/reasoning/textDelta` | A | missing | S3 projection: item/* | | `ReasoningTextDeltaNotification` | |
| `thread/compacted` | B | missing | pairs with a Tier B service | | `ContextCompactedNotification` | |
| `model/rerouted` | undecided | missing | OpenAI model routing, verification, moderation and safety | C | ModelReroutedNotification (untyped) | |
| `model/verification` | undecided | missing | OpenAI model routing, verification, moderation and safety | C | ModelVerificationNotification (untyped) | |
| `modelProvider/authRecoveryStarted` | undecided | missing | provider auth recovery | B | AuthRecoveryNotification (untyped) | |
| `modelProvider/authRecoveryCompleted` | undecided | missing | provider auth recovery | B | AuthRecoveryNotification (untyped) | |
| `turn/moderationMetadata` | undecided | missing | OpenAI model routing, verification, moderation and safety | C | TurnModerationMetadataNotification (untyped) | |
| `model/safetyBuffering/updated` | undecided | missing | OpenAI model routing, verification, moderation and safety | C | ModelSafetyBufferingUpdatedNotification (untyped) | |
| `warning` | undecided | missing | generic warnings | A | WarningNotification (untyped) | |
| `guardianWarning` | undecided | missing | Codex Guardian; Codewhale has its own Auto-Review posture | C | GuardianWarningNotification (untyped) | |
| `deprecationNotice` | undecided | missing | generic warnings | A | DeprecationNoticeNotification (untyped) | |
| `configWarning` | undecided | missing | generic warnings | A | ConfigWarningNotification (untyped) | |
| `fuzzyFileSearch/sessionUpdated` | undecided | missing | pairs with fuzzyFileSearch | B | FuzzyFileSearchSessionUpdatedNotification (untyped) | |
| `fuzzyFileSearch/sessionCompleted` | undecided | missing | pairs with fuzzyFileSearch | B | FuzzyFileSearchSessionCompletedNotification (untyped) | |
| `thread/realtime/started` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeStartedNotification (untyped) | |
| `thread/realtime/itemAdded` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeItemAddedNotification (untyped) | |
| `thread/realtime/item/started` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeItemStartedNotification (untyped) | |
| `thread/realtime/item/transcript/delta` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeItemTranscriptDeltaNotification (untyped) | |
| `thread/realtime/item/completed` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeItemCompletedNotification (untyped) | |
| `thread/realtime/transcript/delta` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeTranscriptDeltaNotification (untyped) | |
| `thread/realtime/transcript/done` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeTranscriptDoneNotification (untyped) | |
| `thread/realtime/outputAudio/delta` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeOutputAudioDeltaNotification (untyped) | |
| `thread/realtime/sdp` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeSdpNotification (untyped) | |
| `thread/realtime/error` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeErrorNotification (untyped) | |
| `thread/realtime/closed` | undecided | missing | realtime audio sessions; no Codewhale equivalent | C | ThreadRealtimeClosedNotification (untyped) | |
| `windows/worldWritableWarning` | undecided | missing | Codex Windows sandbox | C | WindowsWorldWritableWarningNotification (untyped) | |
| `windowsSandbox/setupCompleted` | undecided | missing | Codex Windows sandbox | C | WindowsSandboxSetupCompletedNotification (untyped) | |
| `account/login/completed` | undecided | missing | OpenAI account state | C | AccountLoginCompletedNotification (untyped) | |
## Totals
| Kind | A | B | C | undecided |
| --- | --- | --- | --- | --- |
| Client requests | 19 | 25 | 19 | 46 |
| Client notifications | 1 | 0 | 0 | 0 |
| Server requests | 5 | 0 | 4 | 1 |
| Server notifications | 27 | 6 | 4 | 50 |
Typed definitions generated into `src/v2.rs`: 319.
## Errors
Codex envelopes carry no `jsonrpc` member. ACP keeps its own JSON-RPC 2.0 envelope and is not this contract.
| Name | Code | Meaning |
| --- | --- | --- |
| `PARSE_ERROR` | -32700 | the line or message is not JSON |
| `INVALID_REQUEST` | -32600 | not a valid request envelope, or a request before initialize |
| `METHOD_NOT_FOUND` | -32601 | Tier C or undecided method, or a name the pinned export does not have |
| `INVALID_PARAMS` | -32602 | params do not decode into the selected method's typed params |
| `INTERNAL_ERROR` | -32603 | the Engine failed while serving a valid request |
| `OVERLOADED` | -32001 | upstream Codex transport overload code; the outgoing queue is full |
| `THREAD_NOT_FOUND` | -32004 | the named thread does not exist (existing App Server code) |
| `RUNTIME_UNAVAILABLE` | -32005 | the Engine could not be reached (existing App Server code) |
| `CAPABILITY_UNSUPPORTED` | -32020 | the method exists and is selected, but this thread's executor or placement cannot serve it; `data` is `CapabilityRefusalData`; the request has no side effect and never falls back to local execution |
## Codewhale extensions
These ride on Codex shapes without changing any pinned field. The pinned `Thread` schema allows additional properties, so a Codex client ignores the fields. Codewhale notifications live under `codewhale/`, because Codex already uses `thread/environment/*` for its own environments. Extension shapes are declared under `extensions.definitions` in the manifest and generated with the Codex types; none carries a credential.
| Name | Kind | Tier | Status | Shape |
| --- | --- | --- | --- | --- |
| `Thread.executor` | field | A | defined, unpopulated | `ThreadExecutor` `{ kind, agentId, agentName? }`: a thread driven by an external agent over ACP; absent for a native thread. Never an environment value or a credential. |
| `Thread.environment` | field | B | defined, unpopulated | `ThreadPlacement` `{ id, providerKind }` (named apart from Codex `ThreadEnvironment`, which is a different type): where the thread runs; absent means this computer. A thread placed in an environment this build cannot serve refuses with `CAPABILITY_UNSUPPORTED` (`environment_not_supported`) and never runs locally. |
| `codewhale/environment/*` | server notifications | B | reserved, not implemented | environment state changes for placement |
| `ThreadItem.placement` | field | B | reserved, not implemented | where a child or job ran |
| `approval.decided.responder` | Runtime journal field | A | implemented | the client that answered (`desktop`, `phone`, `web`, `tui`, `acp`), as declared by that client |
-6
View File
@@ -1,6 +0,0 @@
OpenAI Codex
Copyright 2025 OpenAI
This project includes code derived from [Ratatui](https://github.com/ratatui/ratatui), licensed under the MIT license.
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023-2025 The Ratatui Developers
-57
View File
@@ -1,57 +0,0 @@
# Codewhale App Server protocol
This crate owns the existing Runtime image, event-envelope, capability and
dynamic-tool contracts, plus the shared RPC envelope and initialization shapes.
Engine, App Server, hooks and the SDK consume that one
family; the former `codewhale_protocol::runtime` module is removed.
The runtime contracts reuse serde and the existing workspace Chrono dependency. The optional `schema` feature uses
the existing locked schemars dependency and shared JSON Schema to TypeScript
renderer. It selects input/output schema contracts from the actual owner. Runtime output
fields reflect serialization while accepted input defaults stay optional.
DynamicToolSpecOutput is the serialized view of the same DynamicToolSpec Rust
type used in thread-created output. Runtime TypeScript uses JSON-serializable
object members (which may omit undefined values); root/array undefined remains
excluded. The existing Native host generator keeps its prior text and
present-value normalization.
Generate outputs with:
```sh
cargo run -p codewhale-app-server-protocol --features schema --bin codewhale-app-server-schema -- --write
```
The command writes `schema/json/runtime.schema.json`,
`schema/typescript/runtime.d.ts`, and the matching packaged SDK declaration.
`--check` compares all three without writing. Normal runtime startup does not
generate files or enable schema machinery.
`contract/codex-v2.json` is the frozen, versioned Codex v2 contract manifest:
every pinned stable Codex method with its tier (A, B, C or undecided), status,
basis and, for selected methods, the upstream variant, params and result types;
the dispatch rule for each tier; the error codes; and the Codewhale extensions.
`src/codegen` reads it with the unmodified pinned export and writes
`src/methods.rs`, `src/v2.rs`, `METHODS.md`,
`schema/json/codex-v2.schema.json` and `schema/typescript/codex-v2.d.ts`:
```sh
cargo run -p codewhale-app-server-protocol --bin codewhale-codex-contract -- --write
```
`--check` compares without writing, and `tests/contract.rs` fails on any drift,
on any disagreement between the manifest and the pinned method sets, and on
any selected shape whose typed Rust value does not round-trip an instance the
pinned schema accepts. `v2::ClientRequestParams`, `v2::ServerNotification`,
`v2::ServerRequestParams`, `v2::ClientResponse` and `v2::ServerRequestResponse`
are the typed envelopes; Codex envelopes carry no `jsonrpc` member. A typed
shape is not a handler: `METHODS.md` status says what main dispatches.
The selected future Codex wire target is official `rust-v0.166.0`, commit
`6ff1e1ca3b30fc8b914720681fcf5fe62e95f856`. Its four-variant envelope and initialize/initialized lifecycle serve JSONL
stdio and WebSocket text messages over the private owner channel. The private
framing retains kernel peer and attach checks, bounded headers and messages, and
ACP's separate JSON-RPC 2.0 envelope. TCP WebSocket listener, exact thread/turn/item
dispatch, subscriptions, interactions and their complete generated SDK remain separate required adoption
slices; legacy compatibility methods do not establish full Codex wire parity.
The existing Runtime journal and native Engine receipt/store authority remain
where they are; no serialized observation creates publication authority.
-11
View File
@@ -1,11 +0,0 @@
# Upstream contract provenance
The request-ID, request/notification/error data shapes in src/rpc.rs and initialization data shapes in src/lifecycle.rs are adapted from official openai/codex rust-v0.166.0, immutable commit6ff1e1ca3b30fc8b914720681fcf5fe62e95f856, codex-rs/app-server-protocol/src/rpc.rs and src/protocol/v1.rs. Apache-2.0 LICENSE-APACHE and upstream NOTICE are retained with the original Codewhale MIT license in LICENSE-MIT. The crate combines MIT Codewhale implementation with Apache-2.0 contract adaptations.
Changed files: these adaptations remove Codex-specific derive/export dependencies and replace the absolute-path wrapper with PathBuf on the server-created response, whose producer validates absolute scope. RpcCall normalizes existing optional jsonrpc compatibility input and requires present request IDs to be strings or i64. It preserves notification ID absence. Correlated results and errors require RequestId. The existing compatibility uncorrelated parse-error helper uses a distinct null-ID error record; that response does not satisfy the pinned correlated Codex error envelope and remains explicit phase4 closure work. No private Engine source observation, receipt or accounting producer proof appears in this protocol crate.
The exact pinned wire golden fixtures under tests/pinned are unmodified members of the local stable precomputed schema export. This is a staged owner/lifecycle adoption, not complete wire/parity acceptance.
The method sets in tests/pinned/codex/{ClientRequest,ClientNotification,ServerRequest,ServerNotification}.ts are unmodified TypeScript members of the same commit's stable precomputed export (`codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst`, compressed SHA256 `c3802deddc224896967bccd29e353e93e62af768b44dbd5d583e35825fb3e02a`). Member SHA256: ClientRequest.ts `d52eab2b624ee8bab88a121f78a5465faaa6a2701bbd0d2979dc7e5937741b36`, ClientNotification.ts `96692c191ff9ac32c2688b46a0a04f23d15646937adc352f2adcc8211c6d17a0`, ServerRequest.ts `30880f1a8da876eec30600ba27560d05ad0db5c645e14564c9c79a2429274f06`, ServerNotification.ts `df9b5ab2f11010ac336fb7c5b6bb386a7c389cd5dbe742ac5c804fcc69bd40d9`. The TypeScript members are used because they carry the full wire enums: the export's JSON schemas omit three deprecated v1 client requests and two internal-only notifications. Variant names in src/methods.rs follow the upstream `common.rs` variants.
The selected v2 contract is generated from two further unmodified pins of the same commit. `tests/pinned/codex/codex_app_server_protocol.schemas.json` is the `json_schema/codex_app_server_protocol.schemas.json` member of the stable precomputed export (expanded export SHA256 `7ae41cc257d2a4d9f93d1e99f24560802439d2f2d050ec5685832025ced962a2`; member SHA256 `9b6d53647b00d68a04181b826599025a810cfa78acb3ec4d518c68b4120499c0`). `tests/pinned/codex/common.rs` is `codex-rs/app-server-protocol/src/protocol/common.rs` (Git blob `2936c01855e463275d8306df8583ec0303e5c854`, SHA256 `b7f0a90dcc822962e59890b252a43142a113cb1355f333d8c6b0f704b97f89f7`); it is read only by `tests/contract.rs` to check each selected request's result type and is not compiled. The Rust types in `src/v2.rs`, the TypeScript in `schema/typescript/codex-v2.d.ts` and the selected-closure JSON Schema in `schema/json/codex-v2.schema.json` are derived from the schema member by `src/codegen`; they are Apache-2.0 contract adaptations like the files above.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,638 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"RuntimeImageInput": {
"type": "object",
"additionalProperties": false,
"properties": {
"mime": {
"type": "string"
},
"dataBase64": {
"type": "string"
}
},
"required": [
"mime",
"dataBase64"
],
"description": "Inline bytes only: neither host paths nor remote URLs confer attachment authority."
},
"ThreadLoadedListParams": {
"type": "object",
"properties": {
"cursor": {
"type": [
"string",
"null"
]
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
}
}
},
"ThreadSetNameParams": {
"type": "object",
"properties": {
"threadId": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"threadId",
"name"
]
},
"DynamicToolSpec": {
"type": "object",
"properties": {
"namespace": {
"type": [
"string",
"null"
],
"description": "Optional namespace that groups related tools (e.g. `\"tau_bench\"`).\nWhen present, the runtime may expose the tool as\n`<namespace>::<name>` to the model."
},
"name": {
"type": "string",
"description": "Short tool name. Combined with `namespace` it forms a unique tool id."
},
"description": {
"type": "string",
"description": "Human-readable description exposed to the model."
},
"input_schema": {
"description": "JSON Schema describing the tool's input parameters."
},
"defer_loading": {
"type": "boolean",
"description": "If true, the runtime may defer schema validation / tool loading until\nthe model actually calls the tool.\n\nDefaults to `false` so that older clients omitting this field still\nbehave the same way.",
"default": false
}
},
"required": [
"name",
"description",
"input_schema"
],
"description": "Specification for a dynamic external tool registered by a runtime client.\n\nExample JSON from the spec:\n\n```json\n{\n \"namespace\": \"tau_bench\",\n \"name\": \"get_reservation\",\n \"description\": \"Look up an airline reservation.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"reservation_id\": { \"type\": \"string\" }\n },\n \"required\": [\"reservation_id\"],\n \"additionalProperties\": false\n }\n}\n```"
},
"DynamicToolItemStatus": {
"type": "string",
"enum": [
"in_progress",
"completed",
"failed"
],
"description": "Lifecycle status of a dynamic tool item shown in thread detail and event\npayloads."
},
"DynamicToolCallResult": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "Whether the client-side tool execution succeeded."
},
"content": {
"type": "array",
"items": {
"$ref": "#/$defs/DynamicToolCallContent"
},
"description": "Content fragments returned by the tool.\n\nDefaults to an empty vector when omitted so clients can send a minimal\n`{ \"success\": false }` payload.",
"default": []
}
},
"required": [
"success"
],
"description": "Result submitted by a runtime client after executing a dynamic tool."
},
"DynamicToolCallContent": {
"oneOf": [
{
"type": "object",
"properties": {
"text": {
"type": "string"
},
"type": {
"type": "string",
"const": "input_text"
}
},
"required": [
"type",
"text"
]
},
{
"type": "object",
"properties": {
"image_url": {
"type": "string"
},
"type": {
"type": "string",
"const": "input_image"
}
},
"required": [
"type",
"image_url"
]
}
],
"description": "A single content fragment inside a [`DynamicToolCallResult`]."
},
"TurnEnvironmentParams": {
"type": "object",
"properties": {
"environment_id": {
"type": "string"
},
"cwd": {
"type": "string"
}
},
"required": [
"environment_id",
"cwd"
],
"description": "Environment target selected for a turn's shell/filesystem work.\n\nExample JSON:\n\n```json\n{\n \"environment_id\": \"local\",\n \"cwd\": \"/workspace\"\n}\n```"
},
"RuntimeEventEnvelope": {
"type": "object",
"properties": {
"schema_version": {
"type": "integer",
"format": "uint32",
"minimum": 0,
"default": 1
},
"seq": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"event": {
"type": "string"
},
"kind": {
"type": "string"
},
"thread_id": {
"type": "string"
},
"turn_id": {
"type": [
"string",
"null"
]
},
"item_id": {
"type": [
"string",
"null"
]
},
"timestamp": {
"type": "string"
},
"created_at": {
"type": "string"
},
"payload": true
},
"required": [
"schema_version",
"seq",
"event",
"kind",
"thread_id",
"turn_id",
"item_id",
"timestamp",
"payload"
],
"additionalProperties": true
},
"ThreadLoadedListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "string"
}
},
"nextCursor": {
"type": [
"string",
"null"
]
}
},
"required": [
"data",
"nextCursor"
]
},
"ThreadSetNameResponse": {
"type": "object",
"additionalProperties": false
},
"RuntimeCapabilities": {
"type": "object",
"properties": {
"client_token_intents": {
"type": "boolean",
"description": "Device client tokens have immutable watch/drive intent. Watch cannot\nmutate Runtime state, acquire control, or forward display input.",
"default": false
},
"account_session": {
"type": "boolean",
"default": false
},
"account_model_owner": {
"type": "boolean",
"default": false
},
"session_provider_keys": {
"type": "boolean",
"default": false
},
"threads": {
"type": "boolean"
},
"thread_create_idempotency": {
"type": "boolean",
"default": false
},
"thread_idle_precondition": {
"type": "boolean",
"default": false
},
"thread_patch_undo_turn_id": {
"type": "boolean",
"default": false
},
"thread_scoped_reads": {
"type": "boolean",
"default": false
},
"thread_scoped_git_writes": {
"type": "boolean",
"description": "Git stage, unstage, discard, commit, push and branch accept\n`?thread_id=` and act on that thread's workspace.",
"default": false
},
"thread_scoped_snapshots": {
"type": "boolean",
"description": "`GET /v1/snapshots` and `POST /v1/snapshots/{id}/restore` accept\n`?thread_id=` and act on that thread's workspace.",
"default": false
},
"thread_scoped_terminals": {
"type": "boolean",
"description": "`GET /v1/threads/{id}/terminals` lists the thread's live shells, and\n`/v1/threads/{id}/terminals/{terminal_id}/output|input|resize|kill`\ndrive one by the identity the Engine issued it.",
"default": false
},
"worktrees": {
"type": "boolean",
"description": "`GET`/`POST /v1/worktrees` and `POST /v1/worktrees/remove`.",
"default": false
},
"job_operations": {
"type": "boolean",
"default": false
},
"thread_shell_consent": {
"type": "boolean",
"description": "Explicit per-thread shell opt-in is checked against loaded policy and\ncannot broaden a conversation while it has an active turn.",
"default": false
},
"turns": {
"type": "boolean"
},
"turn_operation_idempotency": {
"type": "boolean",
"description": "`POST /v1/threads/{id}/turns` accepts a durable, thread-scoped\n`operation_key` and returns the original turn for exact retries.",
"default": false
},
"turn_operation_lookup": {
"type": "boolean",
"description": "Read-only exact accepted-turn lookup by thread and operation key.",
"default": false
},
"watch_event_turns": {
"type": "boolean",
"default": false
},
"structured_review_turns": {
"type": "boolean",
"default": false
},
"structured_review_version": {
"type": "integer",
"format": "uint32",
"minimum": 0,
"default": 0
},
"automation_mutation_preconditions": {
"type": "boolean",
"description": "Automation patch/run/pause/resume honor `expected_revision` (409\n`revision_conflict` carrying `current_revision`); create/patch/run/pause/\nresume honor a bounded, persisted `Idempotency-Key`. Delete honors\nneither.",
"default": false
},
"turn_image_inputs": {
"type": "boolean",
"description": "Bounded inline image inputs, persisted and replayed with their turn.",
"default": false
},
"turn_output_token_limit": {
"type": "boolean",
"description": "Per-turn maxOutputTokens is validated and intersected with the route ceiling.",
"default": false
},
"profile_constitution": {
"type": "boolean",
"description": "Account profile snapshots and Engine-owned constitution preview.",
"default": false
},
"turn_steer": {
"type": "boolean"
},
"turn_interrupt": {
"type": "boolean"
},
"event_replay": {
"type": "boolean"
},
"external_tools": {
"type": "boolean"
},
"environments": {
"type": "boolean"
},
"worker_runtime": {
"type": "boolean"
},
"fleet_run_create": {
"type": "boolean",
"default": false
},
"thread_fleet_members": {
"type": "boolean",
"default": false
},
"thread_fleet_member_durable_operations_v1": {
"type": "boolean",
"description": "Member starts are durable per `(store, thread, operation_key)`;\n`GET /v1/threads/{id}/members/operations/{operation_key}` reads them.",
"default": false
},
"fleet_run_start": {
"type": "boolean",
"default": false
},
"fleet_event_replay": {
"type": "boolean",
"default": false
},
"fleet_event_stream": {
"type": "boolean",
"default": false
},
"fleet_local_target": {
"type": "boolean",
"default": false
},
"thread_goals": {
"type": "boolean",
"description": "`GET/PUT/DELETE /v1/threads/{id}/goal` and the `complete`/`block`\nlifecycle actions are available.",
"default": false
},
"memory": {
"type": "boolean",
"description": "`GET /v1/memory` and `GET /v1/memory/{id}` are available for\nbounded inspection of the native memory store. `POST /v1/memory`\nand `DELETE /v1/memory` are also available (auth-gated via the\nstandard route layer) for lifecycle controls.",
"default": false
},
"mcp_server_management": {
"type": "boolean",
"description": "Whether the runtime supports create/update/enable/disable/reconnect/delete\noperations on MCP server configuration via the `POST|GET|PATCH|DELETE\n/v1/apps/mcp/servers` family of endpoints.",
"default": false
},
"skill_lifecycle": {
"type": "boolean",
"description": "Skill lifecycle operations (install, update, uninstall, trust, audit)\nare available via the HTTP API.",
"default": false
},
"skill_detail": {
"type": "boolean",
"description": "`GET /v1/skills/{name}` returns one skill's full body and routing\nmetadata, so a client can compose an activation instruction for its\nnext turn. `GET /v1/skills` rows also carry `invocation`, `aliases`,\nand `bundled_tier` routing fields when this flag is set.",
"default": false
},
"plugin_management": {
"type": "boolean",
"description": "Plugin bundle and marketplace lifecycle operations (list/detail,\ninstall/update/uninstall, trust/enable/disable/revoke, marketplace\nadd/remove/install) are available via the `/v1/apps/plugins` and\n`/v1/apps/marketplaces` endpoint families.",
"default": false
},
"agent_mail": {
"type": "boolean",
"description": "Durable, workspace-scoped cross-task Agent Mail endpoints and events.",
"default": false
},
"event_stream_resume": {
"type": "boolean",
"description": "`GET /v1/threads/{id}/events` puts the durable `seq` on every journal\nframe as the SSE `id:` and resumes from a `Last-Event-ID` header, so a\nbrowser `EventSource` reconnects without a cursor in the query string.",
"default": false
}
},
"required": [
"client_token_intents",
"account_session",
"account_model_owner",
"session_provider_keys",
"threads",
"thread_create_idempotency",
"thread_idle_precondition",
"thread_patch_undo_turn_id",
"thread_scoped_reads",
"thread_scoped_git_writes",
"thread_scoped_snapshots",
"thread_scoped_terminals",
"worktrees",
"job_operations",
"thread_shell_consent",
"turns",
"turn_operation_idempotency",
"turn_operation_lookup",
"watch_event_turns",
"structured_review_turns",
"structured_review_version",
"automation_mutation_preconditions",
"turn_image_inputs",
"turn_output_token_limit",
"profile_constitution",
"turn_steer",
"turn_interrupt",
"event_replay",
"external_tools",
"environments",
"worker_runtime",
"fleet_run_create",
"thread_fleet_members",
"thread_fleet_member_durable_operations_v1",
"fleet_run_start",
"fleet_event_replay",
"fleet_event_stream",
"fleet_local_target",
"thread_goals",
"memory",
"mcp_server_management",
"skill_lifecycle",
"skill_detail",
"plugin_management",
"agent_mail",
"event_stream_resume"
],
"description": "Fixed capability map advertised by `GET /v1/runtime/info`.\n\nAll fields are required on serialization so clients can rely on the shape."
},
"FleetProfileDocument": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"kind": {
"$ref": "#/$defs/FleetProfileDocumentKind"
},
"origin": {
"type": "string"
},
"path": {
"type": [
"string",
"null"
]
},
"text": {
"type": "string"
},
"revision": {
"type": "string"
},
"writable": {
"type": "boolean"
},
"refusal_reason": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"kind",
"origin",
"path",
"text",
"revision",
"writable",
"refusal_reason"
]
},
"FleetProfileDocumentKind": {
"type": "string",
"enum": [
"profile",
"fleet"
]
},
"RuntimeExperimentalCapabilities": {
"type": "object",
"properties": {
"environments": {
"type": "boolean",
"default": false
}
},
"required": [
"environments"
],
"description": "Experimental opt-in flags advertised by `GET /v1/runtime/info`.\n\nFields are additive and default to `false` when omitted by older servers."
},
"DynamicToolCallParams": {
"type": "object",
"properties": {
"thread_id": {
"type": "string"
},
"turn_id": {
"type": "string"
},
"call_id": {
"type": "string"
},
"namespace": {
"type": "string",
"description": "Optional namespace that was registered with the tool."
},
"tool": {
"type": "string",
"description": "Tool name that the model invoked."
},
"arguments": {
"description": "Arguments supplied by the model, validated against `input_schema`."
}
},
"required": [
"thread_id",
"turn_id",
"call_id",
"tool",
"arguments"
],
"description": "Parameters identifying a dynamic tool call request emitted by the runtime.\n\nThis is the typed payload for `tool_call.requested` events and also the\nnatural identifier used when the runtime looks up a pending call."
},
"DynamicToolSpecOutput": {
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "Optional namespace that groups related tools (e.g. `\"tau_bench\"`).\nWhen present, the runtime may expose the tool as\n`<namespace>::<name>` to the model."
},
"name": {
"type": "string",
"description": "Short tool name. Combined with `namespace` it forms a unique tool id."
},
"description": {
"type": "string",
"description": "Human-readable description exposed to the model."
},
"input_schema": {
"description": "JSON Schema describing the tool's input parameters."
},
"defer_loading": {
"type": "boolean",
"description": "If true, the runtime may defer schema validation / tool loading until\nthe model actually calls the tool.\n\nDefaults to `false` so that older clients omitting this field still\nbehave the same way.",
"default": false
}
},
"required": [
"name",
"description",
"input_schema",
"defer_loading"
],
"description": "Specification for a dynamic external tool registered by a runtime client.\n\nExample JSON from the spec:\n\n```json\n{\n \"namespace\": \"tau_bench\",\n \"name\": \"get_reservation\",\n \"description\": \"Look up an airline reservation.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"reservation_id\": { \"type\": \"string\" }\n },\n \"required\": [\"reservation_id\"],\n \"additionalProperties\": false\n }\n}\n```"
}
}
}
@@ -1,410 +0,0 @@
export type AbsolutePathBuf = string;
export type AdditionalFileSystemPermissions = { "entries"?: Array<FileSystemSandboxEntry> | null; "globScanMaxDepth"?: number | null; "read"?: Array<LegacyAppPathString> | null; "write"?: Array<LegacyAppPathString> | null };
export type AdditionalNetworkPermissions = { "enabled"?: boolean | null };
export type AgentMessageDelivery = "async";
export type AgentMessageDeltaNotification = { "delta": string; "itemId": string; "threadId": string; "turnId": string };
export type AgentPath = string;
export type AllowDenyRequirement = "allow" | "deny";
export type AnalyticsConfig = { "enabled"?: boolean | null; [key: string]: unknown };
export type ApprovalsReviewer = "user" | "auto_review" | "guardian_subagent";
export type AskForApproval = "untrusted" | "on-request" | "never" | { "granular": { "mcp_elicitations": boolean; "request_permissions"?: boolean; "rules": boolean; "sandbox_approval": boolean; "skill_approval"?: boolean } };
export type AsyncUserInputQuestion = { "options"?: Array<string> | null; "title": string };
export type AutoCompactTokenLimitScope = "total" | "body_after_prefix";
export type BrowserUseConfig = { "allow_history_access"?: boolean | null; "default_origin_policy"?: BrowserUseOriginPolicyConfig | null; "origins"?: { [key: string]: BrowserUseOriginPolicyConfig } | null };
export type BrowserUseOriginPolicyConfig = { "access"?: AllowDenyRequirement | null; "downloads"?: AllowDenyRequirement | null; "full_cdp_access"?: AllowDenyRequirement | null; "uploads"?: AllowDenyRequirement | null };
export type ByteRange = { "end": number; "start": number };
export type CapabilityRefusal = { "code": CapabilityRefusalCode; "method"?: string | null; "threadId"?: string | null };
export type CapabilityRefusalCode = "unknown_agent" | "not_installed" | "adapter_missing" | "below_min_version" | "needs_sign_in" | "no_acp" | "executor_not_supported" | "environment_not_supported" | "steer_not_supported" | "reopen_not_supported";
export type CapabilityRefusalData = { "codewhaleRefusal": CapabilityRefusal };
export type ClientInfo = { "name": string; "title"?: string | null; "version": string };
export type CodexErrorInfo = CodexErrorInfoVariant0 | CodexErrorInfoVariant1 | CodexErrorInfoVariant2 | CodexErrorInfoVariant3 | CodexErrorInfoVariant4 | CodexErrorInfoVariant5 | unknown;
export type CodexErrorInfoVariant0 = "contextWindowExceeded" | "sessionBudgetExceeded" | "usageLimitExceeded" | "rateLimitExceeded" | "flexUnavailable" | "serverOverloaded" | "modelNotFound" | "policyCapacityUnavailable" | "cyberPolicy" | "misalignmentPolicyViolation" | "tooManyDenials" | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | "other";
export type CodexErrorInfoVariant1 = { "httpConnectionFailed": CodexErrorInfoVariant1HttpConnectionFailed };
export type CodexErrorInfoVariant1HttpConnectionFailed = { "httpStatusCode"?: number | null };
export type CodexErrorInfoVariant2 = { "responseStreamConnectionFailed": CodexErrorInfoVariant2ResponseStreamConnectionFailed };
export type CodexErrorInfoVariant2ResponseStreamConnectionFailed = { "httpStatusCode"?: number | null };
export type CodexErrorInfoVariant3 = { "responseStreamDisconnected": CodexErrorInfoVariant3ResponseStreamDisconnected };
export type CodexErrorInfoVariant3ResponseStreamDisconnected = { "httpStatusCode"?: number | null };
export type CodexErrorInfoVariant4 = { "responseTooManyFailedAttempts": CodexErrorInfoVariant4ResponseTooManyFailedAttempts };
export type CodexErrorInfoVariant4ResponseTooManyFailedAttempts = { "httpStatusCode"?: number | null };
export type CodexErrorInfoVariant5 = { "activeTurnNotSteerable": CodexErrorInfoVariant5ActiveTurnNotSteerable };
export type CodexErrorInfoVariant5ActiveTurnNotSteerable = { "turnKind": NonSteerableTurnKind };
export type CollabAgentState = { "message"?: string | null; "status": CollabAgentStatus };
export type CollabAgentStatus = "pendingInit" | "running" | "interrupted" | "completed" | "errored" | "shutdown" | "notFound";
export type CollabAgentTool = "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent" | "sendMessage" | "followupTask" | "interruptAgent" | "listAgents";
export type CollabAgentToolCallStatus = "inProgress" | "completed" | "failed" | "interrupted";
export type CollaborationMode = { "mode": ModeKind; "settings": Settings };
export type CommandAction = { "command": string; "name": string; "path": LegacyAppPathString; "type": "read" } | { "command": string; "path"?: string | null; "type": "listFiles" } | { "command": string; "path"?: string | null; "query"?: string | null; "type": "search" } | { "command": string; "type": "unknown" };
export type CommandExecOutputDeltaNotification = { "capReached": boolean; "deltaBase64": string; "processId": string; "stream": CommandExecOutputStream };
export type CommandExecOutputStream = "stdout" | "stderr";
export type CommandExecParams = { "command": Array<string>; "cwd"?: string | null; "disableOutputCap"?: boolean; "disableTimeout"?: boolean; "env"?: { [key: string]: string | null } | null; "outputBytesCap"?: number | null; "processId"?: string | null; "sandboxPolicy"?: SandboxPolicy | null; "size"?: CommandExecTerminalSize | null; "streamStdin"?: boolean; "streamStdoutStderr"?: boolean; "timeoutMs"?: number | null; "tty"?: boolean };
export type CommandExecResizeParams = { "processId": string; "size": CommandExecTerminalSize };
export type CommandExecResizeResponse = Record<string, never>;
export type CommandExecResponse = { "exitCode": number; "stderr": string; "stdout": string };
export type CommandExecTerminalSize = { "cols": number; "rows": number };
export type CommandExecTerminateParams = { "processId": string };
export type CommandExecTerminateResponse = Record<string, never>;
export type CommandExecWriteParams = { "closeStdin"?: boolean; "deltaBase64"?: string | null; "processId": string };
export type CommandExecWriteResponse = Record<string, never>;
export type CommandExecutionApprovalDecision = "accept" | "acceptForSession" | { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": Array<string> } } | { "applyNetworkPolicyAmendment": { "network_policy_amendment": NetworkPolicyAmendment } } | "decline" | "cancel";
export type CommandExecutionApprovalKind = "command" | "writeStdin";
export type CommandExecutionOutputDeltaNotification = { "delta": string; "itemId": string; "threadId": string; "turnId": string };
export type CommandExecutionRequestApprovalParams = { "approvalId"?: string | null; "command"?: string | null; "commandActions"?: Array<CommandAction> | null; "cwd"?: LegacyAppPathString | null; "environmentId"?: string | null; "itemId": string; "kind"?: CommandExecutionApprovalKind; "networkApprovalContext"?: NetworkApprovalContext | null; "proposedExecpolicyAmendment"?: Array<string> | null; "proposedNetworkPolicyAmendments"?: Array<NetworkPolicyAmendment> | null; "reason"?: string | null; "startedAtMs": number; "threadId": string; "turnId": string };
export type CommandExecutionRequestApprovalResponse = { "decision": CommandExecutionApprovalDecision };
export type CommandExecutionSource = "agent" | "userShell" | "unifiedExecStartup" | "unifiedExecInteraction";
export type CommandExecutionStatus = "inProgress" | "completed" | "failed" | "declined";
export type ComputerUseConfig = { "default_app_access"?: AllowDenyRequirement | null; "macos"?: ComputerUseMacosConfig | null; "windows"?: ComputerUseWindowsConfig | null };
export type ComputerUseMacosConfig = { "bundle_ids"?: { [key: string]: AllowDenyRequirement } | null };
export type ComputerUseWindowsConfig = { "aumids"?: { [key: string]: AllowDenyRequirement } | null; "exes"?: Array<ComputerUseWindowsExeConfig> | null };
export type ComputerUseWindowsExeConfig = { "access": AllowDenyRequirement; "binary_name"?: string | null; "product_name": string; "publisher_name": string };
export type Config = { "analytics"?: AnalyticsConfig | null; "approval_policy"?: AskForApproval | null; "approvals_reviewer"?: ApprovalsReviewer | null; "browser_use"?: BrowserUseConfig | null; "compact_prompt"?: string | null; "computer_use"?: ComputerUseConfig | null; "desktop"?: { [key: string]: unknown } | null; "developer_instructions"?: string | null; "forced_chatgpt_workspace_id"?: ForcedChatgptWorkspaceIds | null; "forced_login_method"?: ForcedLoginMethod | null; "instructions"?: string | null; "model"?: string | null; "model_auto_compact_token_limit"?: number | null; "model_auto_compact_token_limit_scope"?: AutoCompactTokenLimitScope | null; "model_context_window"?: number | null; "model_provider"?: string | null; "model_reasoning_effort"?: ReasoningEffort | null; "model_reasoning_summary"?: ReasoningSummary | null; "model_verbosity"?: Verbosity | null; "review_model"?: string | null; "sandbox_mode"?: SandboxMode | null; "sandbox_workspace_write"?: SandboxWorkspaceWrite | null; "service_tier"?: string | null; "tools"?: ToolsV2 | null; "web_search"?: WebSearchMode | null; [key: string]: unknown };
export type ConfigBatchWriteParams = { "edits": Array<ConfigEdit>; "expectedVersion"?: string | null; "filePath"?: string | null; "reloadUserConfig"?: boolean };
export type ConfigEdit = { "keyPath": string; "mergeStrategy": MergeStrategy; "value": unknown };
export type ConfigLayer = { "config": unknown; "disabledReason"?: string | null; "name": ConfigLayerSource; "version": string };
export type ConfigLayerMetadata = { "name": ConfigLayerSource; "version": string };
export type ConfigLayerSource = { "file": AbsolutePathBuf; "type": "packagedDefaults" } | { "domain": string; "key": string; "type": "mdm" } | { "file": AbsolutePathBuf; "type": "system" } | { "id": string; "name": string; "type": "enterpriseManaged" } | { "file": AbsolutePathBuf; "profile"?: string | null; "type": "user" } | { "dotCodexFolder": AbsolutePathBuf; "type": "project" } | { "type": "sessionFlags" } | { "file": AbsolutePathBuf; "type": "legacyManagedConfigTomlFromFile" } | { "type": "legacyManagedConfigTomlFromMdm" };
export type ConfigReadParams = { "cwd"?: string | null; "includeLayers"?: boolean };
export type ConfigReadResponse = { "config": Config; "layers"?: Array<ConfigLayer> | null; "origins": { [key: string]: ConfigLayerMetadata } };
export type ConfigValueWriteParams = { "expectedVersion"?: string | null; "filePath"?: string | null; "keyPath": string; "mergeStrategy": MergeStrategy; "value": unknown };
export type ConfigWriteResponse = { "filePath": AbsolutePathBuf; "overriddenMetadata"?: OverriddenMetadata | null; "status": WriteStatus; "version": string };
export type ContextCompactedNotification = { "threadId": string; "turnId": string };
export type CyberAccessProgram = "standard" | "daybreakBlue" | "daybreakRed";
export type DynamicToolCallOutputContentItem = { "text": string; "type": "inputText" } | { "imageUrl": string; "type": "inputImage" } | { "audioUrl": string; "type": "inputAudio" };
export type DynamicToolCallParams = { "arguments": unknown; "callId": string; "namespace"?: string | null; "threadId": string; "tool": string; "turnId": string };
export type DynamicToolCallResponse = { "contentItems": Array<DynamicToolCallOutputContentItem>; "success": boolean };
export type DynamicToolCallStatus = "inProgress" | "completed" | "failed";
export type ErrorNotification = { "error": TurnError; "threadId": string; "turnId": string; "willRetry": boolean };
export type FileChangeApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";
export type FileChangeOutputDeltaNotification = { "delta": string; "itemId": string; "threadId": string; "turnId": string };
export type FileChangePatchUpdatedNotification = { "changes": Array<FileUpdateChange>; "itemId": string; "threadId": string; "turnId": string };
export type FileChangeRequestApprovalParams = { "grantRoot"?: string | null; "itemId": string; "reason"?: string | null; "startedAtMs": number; "threadId": string; "turnId": string };
export type FileChangeRequestApprovalResponse = { "decision": FileChangeApprovalDecision };
export type FileSystemAccessMode = "read" | "write" | "deny";
export type FileSystemPath = { "path": LegacyAppPathString; "type": "path" } | { "pattern": string; "type": "glob_pattern" } | { "value": FileSystemSpecialPath; "type": "special" };
export type FileSystemSandboxEntry = { "access": FileSystemAccessMode; "path": FileSystemPath };
export type FileSystemSpecialPath = { "kind": "root" } | { "kind": "minimal" } | { "subpath"?: LegacyAppPathString | null; "kind": "project_roots" } | { "kind": "tmpdir" } | { "kind": "slash_tmp" } | { "path": string; "subpath"?: LegacyAppPathString | null; "kind": "unknown" };
export type FileUpdateChange = { "diff": string; "kind": PatchChangeKind; "path": string };
export type ForcedChatgptWorkspaceIds = string | Array<string>;
export type ForcedLoginMethod = "chatgpt" | "api";
export type FsChangedNotification = { "changedPaths": Array<AbsolutePathBuf>; "watchId": string };
export type FsCopyParams = { "destinationPath": AbsolutePathBuf; "recursive"?: boolean; "sourcePath": AbsolutePathBuf };
export type FsCopyResponse = Record<string, never>;
export type FsCreateDirectoryParams = { "path": AbsolutePathBuf; "recursive"?: boolean | null };
export type FsCreateDirectoryResponse = Record<string, never>;
export type FsGetMetadataParams = { "path": AbsolutePathBuf };
export type FsGetMetadataResponse = { "createdAtMs": number; "isDirectory": boolean; "isFile": boolean; "isSymlink": boolean; "modifiedAtMs": number };
export type FsReadDirectoryEntry = { "fileName": string; "isDirectory": boolean; "isFile": boolean };
export type FsReadDirectoryParams = { "path": AbsolutePathBuf };
export type FsReadDirectoryResponse = { "entries": Array<FsReadDirectoryEntry> };
export type FsReadFileParams = { "path": AbsolutePathBuf };
export type FsReadFileResponse = { "dataBase64": string };
export type FsRemoveParams = { "force"?: boolean | null; "path": AbsolutePathBuf; "recursive"?: boolean | null };
export type FsRemoveResponse = Record<string, never>;
export type FsUnwatchParams = { "watchId": string };
export type FsUnwatchResponse = Record<string, never>;
export type FsWatchParams = { "path": AbsolutePathBuf; "watchId": string };
export type FsWatchResponse = { "path": AbsolutePathBuf };
export type FsWriteFileParams = { "dataBase64": string; "path": AbsolutePathBuf };
export type FsWriteFileResponse = Record<string, never>;
export type FunctionCallOutputBody = string | Array<FunctionCallOutputContentItem>;
export type FunctionCallOutputContentItem = { "text": string; "type": "input_text" } | { "detail"?: ImageDetail | null; "type": "input_image" } & FunctionCallOutputContentItemInputImageKind | { "audio_url": string; "type": "input_audio" } | { "encrypted_content": string; "type": "encrypted_content" };
export type FunctionCallOutputContentItemInputImageKind = { "image_url": string } | { "file_id": string };
export type GitInfo = { "branch"?: string | null; "originUrl"?: string | null; "sha"?: string | null };
export type GrantedPermissionProfile = { "fileSystem"?: AdditionalFileSystemPermissions | null; "network"?: AdditionalNetworkPermissions | null };
export type HookErrorInfo = { "message": string; "path": string };
export type HookEventName = "preModelRequest" | "postModelRequest" | "preToolUse" | "permissionRequest" | "postToolUse" | "preCompact" | "postCompact" | "sessionStart" | "sessionEnd" | "userPromptSubmit" | "subagentStart" | "subagentStop" | "stop" | "interrupt";
export type HookMetadata = { "additionalContextLimit"?: number | null; "currentHash": string; "displayOrder": number; "enabled": boolean; "environmentId"?: string | null; "eventName": HookEventName; "isManaged": boolean; "key": string; "matcher"?: string | null; "pluginId"?: string | null; "source": HookSource; "sourcePath": AbsolutePathBuf; "statusMessage"?: string | null; "timeoutSec": number; "trustStatus": HookTrustStatus } & HookMetadataKind;
export type HookMetadataKind = { "async"?: boolean; "command": string; "handlerType": "command" } | { "server": string; "tool": string; "handlerType": "mcpTool" } | { "handlerType": "prompt" } | { "handlerType": "agent" };
export type HookPromptFragment = { "hookRunId": string; "text": string };
export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "cloudRequirements" | "cloudManagedConfig" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown";
export type HookTrustStatus = "managed" | "untrusted" | "trusted" | "modified";
export type HooksListEntry = { "cwd": string; "errors": Array<HookErrorInfo>; "hooks": Array<HookMetadata>; "warnings": Array<string> };
export type HooksListParams = { "cwds"?: Array<string>; "threadId"?: string | null };
export type HooksListResponse = { "data": Array<HooksListEntry> };
export type ImageDetail = "auto" | "low" | "high" | "original";
export type ImageGenerationFailure = { "limitId": string; "resetsAt"?: number | null; "type": "usageLimitExceeded" };
export type ImageGenerationInputImage = { "path": AbsolutePathBuf; "sha256": string };
export type InitializeCapabilities = { "experimentalApi"?: boolean; "explicitGatewayOauth"?: boolean; "extensions"?: { [key: string]: unknown } | null; "mcpServerOpenaiFormElicitation"?: boolean; "optOutNotificationMethods"?: Array<string> | null; "requestAttestation"?: boolean };
export type InitializeParams = { "capabilities"?: InitializeCapabilities | null; "clientInfo": ClientInfo };
export type InitializeResponse = { "codexHome": AbsolutePathBuf; "platformFamily": string; "platformOs": string; "userAgent": string };
export type InputModality = "text" | "image" | "audio";
export type ItemCompletedNotification = { "completedAtMs": number; "item": ThreadItem; "threadId": string; "turnId": string };
export type ItemStartedNotification = { "item": ThreadItem; "startedAtMs": number; "threadId": string; "turnId": string };
export type LegacyAppPathString = string;
export type ListMcpServerStatusParams = { "cursor"?: string | null; "detail"?: McpServerStatusDetail | null; "limit"?: number | null; "serverName"?: string | null; "threadId"?: string | null };
export type ListMcpServerStatusResponse = { "data": Array<McpServerStatus>; "nextCursor"?: string | null };
export type McpAppDisplayMode = "inline" | "fullscreen";
export type McpAppUi = { "preferredModelDisplayMode": McpAppDisplayMode; "resourceUri": string };
export type McpAuthStatus = "unknown" | "unsupported" | "notLoggedIn" | "bearerToken" | "oAuth";
export type McpServerConnectionStatus = "notStarted" | "starting" | "connected" | "authenticationRequired" | "failed" | "cancelled" | "disabled";
export type McpServerInfo = { "description"?: string | null; "icons"?: Array<unknown> | null; "name": string; "title"?: string | null; "version": string; "websiteUrl"?: string | null };
export type McpServerStatus = { "authStatus": McpAuthStatus; "httpOrigin"?: string | null; "name": string; "pluginId"?: string | null; "resourceTemplates": Array<ResourceTemplate>; "resources": Array<Resource>; "runtimeStatus"?: McpServerConnectionStatus | null; "serverCapabilities"?: unknown; "serverInfo"?: McpServerInfo | null; "toolListMeta"?: unknown; "tools": { [key: string]: Tool }; "toolsError"?: string | null };
export type McpServerStatusDetail = "full" | "toolsAndAuthOnly";
export type McpToolCallAppContext = { "actionName"?: string | null; "appName"?: string | null; "connectorId": string; "linkId"?: string | null; "resourceUri"?: string | null };
export type McpToolCallError = { "message": string };
export type McpToolCallProgressNotification = { "itemId": string; "message": string; "threadId": string; "turnId": string };
export type McpToolCallResult = { "_meta"?: unknown; "content": Array<unknown>; "structuredContent"?: unknown };
export type McpToolCallStatus = "inProgress" | "completed" | "failed";
export type MemoryCitation = { "entries": Array<MemoryCitationEntry>; "threadIds": Array<string> };
export type MemoryCitationEntry = { "lineEnd": number; "lineStart": number; "note": string; "path": string };
export type MergeStrategy = "replace" | "upsert";
export type MessagePhase = "commentary" | "partial_answer" | "final_answer";
export type MisalignmentErrorDetails = { "detailedExplanation"?: string | null; "errorType"?: string | null; "reviewTarget"?: string | null; "steer"?: MisalignmentSteer | null };
export type MisalignmentSteer = { "message": string };
export type ModeKind = "plan" | "default";
export type Model = { "additionalSpeedTiers"?: Array<string>; "availabilityNux"?: ModelAvailabilityNux | null; "availableAccessPrograms"?: ModelAccessPrograms | null; "defaultReasoningEffort": ReasoningEffort; "defaultServiceTier"?: string | null; "description": string; "displayName": string; "hidden": boolean; "id": string; "inputModalities"?: Array<InputModality>; "isDefault": boolean; "model": string; "modelSpecialty"?: string | null; "multiAgentVersion"?: MultiAgentVersion | null; "serviceTiers"?: Array<ModelServiceTier>; "supportedReasoningEfforts": Array<ReasoningEffortOption>; "supportsPersonality"?: boolean; "upgrade"?: string | null; "upgradeInfo"?: ModelUpgradeInfo | null };
export type ModelAccessPrograms = { "cyber": Array<CyberAccessProgram> };
export type ModelAvailabilityNux = { "message": string };
export type ModelListParams = { "cursor"?: string | null; "includeHidden"?: boolean | null; "limit"?: number | null };
export type ModelListResponse = { "data": Array<Model>; "nextCursor"?: string | null };
export type ModelServiceTier = { "description": string; "id": string; "name": string };
export type ModelUpgradeInfo = { "migrationMarkdown"?: string | null; "model": string; "modelLink"?: string | null; "retirementAt"?: number | null; "upgradeCopy"?: string | null };
export type MultiAgentVersion = "disabled" | "v1" | "v2";
export type NetworkAccess = "restricted" | "enabled";
export type NetworkApprovalContext = { "host": string; "protocol": NetworkApprovalProtocol };
export type NetworkApprovalProtocol = "http" | "https" | "socks5Tcp" | "socks5Udp";
export type NetworkPolicyAmendment = { "action": NetworkPolicyRuleAction; "host": string };
export type NetworkPolicyRuleAction = "allow" | "deny";
export type NonSteerableTurnKind = "review" | "compact";
export type OverriddenMetadata = { "effectiveValue": unknown; "message": string; "overridingLayer": ConfigLayerMetadata };
export type PatchApplyStatus = "inProgress" | "completed" | "failed" | "declined";
export type PatchChangeKind = { "type": "add" } | { "type": "delete" } | { "move_path"?: string | null; "type": "update" };
export type PermissionGrantScope = "turn" | "session";
export type PermissionsRequestApprovalParams = { "cwd": LegacyAppPathString; "environmentId"?: string | null; "itemId": string; "permissions": RequestPermissionProfile; "reason"?: string | null; "startedAtMs": number; "threadId": string; "turnId": string };
export type PermissionsRequestApprovalResponse = { "permissions": GrantedPermissionProfile; "scope"?: PermissionGrantScope; "strictAutoReview"?: boolean | null };
export type Personality = "none" | "friendly" | "pragmatic";
export type PlanDeltaNotification = { "delta": string; "itemId": string; "threadId": string; "turnId": string };
export type ReasoningEffort = string;
export type ReasoningEffortOption = { "description": string; "reasoningEffort": ReasoningEffort };
export type ReasoningSummary = "auto" | "concise" | "detailed" | "none";
export type ReasoningSummaryPartAddedNotification = { "itemId": string; "summaryIndex": number; "threadId": string; "turnId": string };
export type ReasoningSummaryTextDeltaNotification = { "delta": string; "itemId": string; "summaryIndex": number; "threadId": string; "turnId": string };
export type ReasoningTextDeltaNotification = { "contentIndex": number; "delta": string; "itemId": string; "threadId": string; "turnId": string };
export type RequestId = string | number;
export type RequestPermissionProfile = { "fileSystem"?: AdditionalFileSystemPermissions | null; "network"?: AdditionalNetworkPermissions | null };
export type Resource = { "_meta"?: unknown; "annotations"?: unknown; "description"?: string | null; "icons"?: Array<unknown> | null; "mimeType"?: string | null; "name": string; "size"?: number | null; "title"?: string | null; "uri": string };
export type ResourceTemplate = { "annotations"?: unknown; "description"?: string | null; "mimeType"?: string | null; "name": string; "title"?: string | null; "uriTemplate": string };
export type ReviewDelivery = "inline";
export type ReviewStartParams = { "delivery"?: ReviewDelivery | null; "target": ReviewTarget; "threadId": string };
export type ReviewStartResponse = { "reviewThreadId": string; "turn": Turn };
export type ReviewTarget = { "type": "uncommittedChanges" } | { "branch": string; "type": "baseBranch" } | { "sha": string; "title"?: string | null; "type": "commit" } | { "instructions": string; "type": "custom" };
export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
export type SandboxPolicy = { "type": "dangerFullAccess" } | { "networkAccess"?: boolean; "type": "readOnly" } | { "networkAccess"?: NetworkAccess; "type": "externalSandbox" } | { "excludeSlashTmp"?: boolean; "excludeTmpdirEnvVar"?: boolean; "networkAccess"?: boolean; "writableRoots"?: Array<AbsolutePathBuf>; "type": "workspaceWrite" };
export type SandboxWorkspaceWrite = { "exclude_slash_tmp"?: boolean; "exclude_tmpdir_env_var"?: boolean; "network_access"?: boolean; "writable_roots"?: Array<string> };
export type ServerRequestResolvedNotification = { "requestId": RequestId; "threadId": string };
export type SessionSource = "cli" | "vscode" | "exec" | "appServer" | "unknown" | { "custom": string } | { "subAgent": SubAgentSource };
export type Settings = { "developer_instructions"?: string | null; "model": string; "reasoning_effort"?: ReasoningEffort | null };
export type SkillDependencies = { "tools": Array<SkillToolDependency> };
export type SkillErrorInfo = { "message": string; "path": string };
export type SkillInterface = { "brandColor"?: string | null; "defaultPrompt"?: string | null; "displayName"?: string | null; "iconLarge"?: AbsolutePathBuf | null; "iconLargeUrl"?: string | null; "iconSmall"?: AbsolutePathBuf | null; "iconSmallUrl"?: string | null; "shortDescription"?: string | null };
export type SkillMetadata = { "dependencies"?: SkillDependencies | null; "description": string; "enabled": boolean; "interface"?: SkillInterface | null; "name": string; "path": LegacyAppPathString; "pluginId"?: string | null; "scope": SkillScope; "shortDescription"?: string | null };
export type SkillScope = "user" | "repo" | "system" | "admin";
export type SkillToolDependency = { "command"?: string | null; "description"?: string | null; "transport"?: string | null; "type": string; "url"?: string | null; "value": string };
export type SkillsChangedNotification = Record<string, never>;
export type SkillsListEntry = { "cwd": string; "errors": Array<SkillErrorInfo>; "skills": Array<SkillMetadata> };
export type SkillsListParams = { "cwds"?: Array<string>; "forceReload"?: boolean };
export type SkillsListResponse = { "data": Array<SkillsListEntry> };
export type SortDirection = "asc" | "desc";
export type SubAgentActivityKind = "started" | "interacted" | "interrupted" | "completed";
export type SubAgentSource = "review" | "compact" | "memory_consolidation" | { "thread_spawn": { "agent_nickname"?: string | null; "agent_path"?: AgentPath | null; "agent_role"?: string | null; "depth": number; "parent_thread_id": ThreadId } } | { "other": string };
export type TerminalInteractionNotification = { "itemId": string; "processId": string; "stdin": string; "threadId": string; "turnId": string };
export type TextElement = { "byteRange": ByteRange; "placeholder"?: string | null };
export type Thread = { "agentNickname"?: string | null; "agentRole"?: string | null; "cliVersion": string; "createdAt": number; "cwd": AbsolutePathBuf; "ephemeral": boolean; "forkedFromId"?: string | null; "gitInfo"?: GitInfo | null; "historyMode"?: ThreadHistoryMode; "id": string; "model"?: string | null; "modelProvider": string; "name"?: string | null; "originator"?: string | null; "parentThreadId"?: string | null; "path"?: string | null; "preview": string; "projectId": string | null; "reasoningEffort"?: ReasoningEffort | null; "recencyAt"?: number | null; "section"?: ThreadSection | null; "sectionEnteredAt"?: number | null; "sessionId": string; "source": SessionSource; "status": ThreadStatus; "threadSource"?: ThreadSource | null; "turns": Array<Turn>; "updatedAt": number; "executor"?: ThreadExecutor | null; "environment"?: ThreadPlacement | null };
export type ThreadActiveFlag = "waitingOnApproval" | "waitingOnUserInput";
export type ThreadArchiveParams = { "threadId": string };
export type ThreadArchiveResponse = Record<string, never>;
export type ThreadArchivedNotification = { "threadId": string };
export type ThreadClosedNotification = { "threadId": string };
export type ThreadCompactStartParams = { "threadId": string };
export type ThreadCompactStartResponse = Record<string, never>;
export type ThreadDeleteParams = { "threadId": string };
export type ThreadDeleteResponse = Record<string, never>;
export type ThreadDeletedNotification = { "threadId": string };
export type ThreadExecutor = { "kind": string; "agentId": string; "agentName"?: string | null };
export type ThreadForkParams = { "approvalPolicy"?: AskForApproval | null; "approvalsReviewer"?: ApprovalsReviewer | null; "baseInstructions"?: string | null; "config"?: { [key: string]: unknown } | null; "cwd"?: string | null; "developerInstructions"?: string | null; "ephemeral"?: boolean; "excludeTurns"?: boolean; "lastTurnId"?: string | null; "model"?: string | null; "modelProvider"?: string | null; "sandbox"?: SandboxMode | null; "serviceTier"?: string | null; "threadId": string; "threadSource"?: ThreadSource | null };
export type ThreadForkResponse = { "approvalPolicy": AskForApproval; "approvalsReviewer": ApprovalsReviewer; "cwd": AbsolutePathBuf; "disabledPluginIds"?: Array<string>; "instructionSources"?: Array<LegacyAppPathString>; "model": string; "modelProvider": string; "reasoningEffort"?: ReasoningEffort | null; "sandbox": SandboxPolicy; "serviceTier"?: string | null; "thread": Thread };
export type ThreadGoal = { "createdAt": number; "objective": string; "status": ThreadGoalStatus; "threadId": string; "timeUsedSeconds": number; "tokenBudget"?: number | null; "tokensUsed": number; "updatedAt": number };
export type ThreadGoalClearParams = { "origin"?: ThreadGoalMutationOrigin | null; "threadId": string };
export type ThreadGoalClearResponse = { "cleared": boolean };
export type ThreadGoalClearedNotification = { "threadId": string };
export type ThreadGoalGetParams = { "threadId": string };
export type ThreadGoalGetResponse = { "goal"?: ThreadGoal | null };
export type ThreadGoalMutationOrigin = "user" | "automatic";
export type ThreadGoalSetParams = { "objective"?: string | null; "origin"?: ThreadGoalMutationOrigin | null; "status"?: ThreadGoalStatus | null; "threadId": string; "tokenBudget"?: number | null };
export type ThreadGoalSetResponse = { "goal": ThreadGoal };
export type ThreadGoalStatus = "active" | "paused" | "blocked" | "usageLimited" | "budgetLimited" | "complete";
export type ThreadGoalUpdatedNotification = { "goal": ThreadGoal; "threadId": string; "turnId"?: string | null };
export type ThreadHistoryMode = "legacy" | "paginated";
export type ThreadId = string;
export type ThreadItem = { "completedAtMs"?: number | null; "startedAtMs"?: number | null } & ThreadItemKind;
export type ThreadItemEntry = { "completedAtMs"?: number | null; "item": ThreadItem; "startedAtMs"?: number | null; "turnId": string };
export type ThreadItemKind = { "clientId"?: string | null; "content": Array<UserInput>; "id": string; "type": "userMessage" } | { "fragments": Array<HookPromptFragment>; "id": string; "type": "hookPrompt" } | { "delivery"?: AgentMessageDelivery | null; "id": string; "memoryCitation"?: MemoryCitation | null; "phase"?: MessagePhase | null; "questions"?: Array<AsyncUserInputQuestion> | null; "text": string; "type": "agentMessage" } | { "id": string; "name": string; "namespace"?: string | null; "output": FunctionCallOutputBody; "type": "functionCallOutput" } | { "id": string; "text": string; "type": "plan" } | { "content"?: Array<string>; "id": string; "summary"?: Array<string>; "type": "reasoning" } | { "aggregatedOutput"?: string | null; "command": string; "commandActions": Array<CommandAction>; "cwd": LegacyAppPathString; "durationMs"?: number | null; "exitCode"?: number | null; "id": string; "pluginId"?: string | null; "processId"?: string | null; "scriptPath"?: string | null; "source"?: CommandExecutionSource; "status": CommandExecutionStatus; "type": "commandExecution" } | { "changes": Array<FileUpdateChange>; "id": string; "status": PatchApplyStatus; "type": "fileChange" } | { "appContext"?: McpToolCallAppContext | null; "arguments": unknown; "durationMs"?: number | null; "error"?: McpToolCallError | null; "id": string; "mcpAppResourceUri"?: string | null; "mcpAppUi"?: McpAppUi | null; "pluginId"?: string | null; "readOnlyHint"?: boolean | null; "result"?: McpToolCallResult | null; "server": string; "status": McpToolCallStatus; "tool": string; "type": "mcpToolCall" } | { "arguments": unknown; "contentItems"?: Array<DynamicToolCallOutputContentItem> | null; "durationMs"?: number | null; "id": string; "namespace"?: string | null; "status": DynamicToolCallStatus; "success"?: boolean | null; "tool": string; "type": "dynamicToolCall" } | { "agentsStates": { [key: string]: CollabAgentState }; "id": string; "model"?: string | null; "prompt"?: string | null; "reasoningEffort"?: ReasoningEffort | null; "receiverThreadIds": Array<string>; "senderThreadId": string; "status": CollabAgentToolCallStatus; "tool": CollabAgentTool; "type": "collabAgentToolCall" } | { "agentPath": string; "agentThreadId": string; "id": string; "kind": SubAgentActivityKind; "model"?: string | null; "reasoningEffort"?: ReasoningEffort | null; "type": "subAgentActivity" } | { "action"?: WebSearchAction | null; "id": string; "query": string; "results"?: Array<unknown> | null; "type": "webSearch" } | { "id": string; "path": LegacyAppPathString; "type": "imageView" } | { "durationMs": number; "id": string; "type": "sleep" } | { "failure"?: ImageGenerationFailure | null; "generationId"?: string | null; "id": string; "inputImages"?: Array<ImageGenerationInputImage>; "revisedPrompt"?: string | null; "savedPath"?: AbsolutePathBuf | null; "status": string; "transparentBackground"?: boolean | null; "type": "imageGeneration" } & ThreadItemKindImageGenerationKind | { "id": string; "review": string; "type": "enteredReviewMode" } | { "id": string; "review": string; "type": "exitedReviewMode" } | { "id": string; "type": "contextCompaction" };
export type ThreadItemKindImageGenerationKind = { "fileId": string } | { "result": string };
export type ThreadItemsListAnchor = { "itemId": string; "type": "item" };
export type ThreadItemsListCursor = string | ThreadItemsListAnchor;
export type ThreadItemsListParams = { "cursor"?: ThreadItemsListCursor | null; "limit"?: number | null; "sortDirection"?: SortDirection | null; "threadId": string; "turnId"?: string | null };
export type ThreadItemsListResponse = { "backwardsCursor"?: string | null; "data": Array<ThreadItemEntry>; "nextCursor"?: string | null };
export type ThreadListCwdFilter = string | Array<string>;
export type ThreadListParams = { "archived"?: boolean | null; "cursor"?: string | null; "cwd"?: ThreadListCwdFilter | null; "excludedThreadIds"?: Array<string> | null; "limit"?: number | null; "modelProviders"?: Array<string> | null; "originators"?: Array<string> | null; "searchTerm"?: string | null; "sectionId"?: string | null; "sortDirection"?: SortDirection | null; "sortKey"?: ThreadSortKey | null; "sourceKinds"?: Array<ThreadSourceKind> | null; "useStateDbOnly"?: boolean };
export type ThreadListResponse = { "backwardsCursor"?: string | null; "data": Array<Thread>; "nextCursor"?: string | null };
export type ThreadLoadedListParams = { "cursor"?: string | null; "limit"?: number | null };
export type ThreadLoadedListResponse = { "data": Array<string>; "nextCursor"?: string | null };
export type ThreadNameUpdatedNotification = { "threadId": string; "threadName"?: string | null };
export type ThreadPlacement = { "id": string; "providerKind": string };
export type ThreadReadParams = { "includeTurns"?: boolean; "threadId": string };
export type ThreadReadResponse = { "thread": Thread };
export type ThreadResumeParams = { "approvalPolicy"?: AskForApproval | null; "approvalsReviewer"?: ApprovalsReviewer | null; "baseInstructions"?: string | null; "config"?: { [key: string]: unknown } | null; "cwd"?: string | null; "developerInstructions"?: string | null; "excludeTurns"?: boolean; "model"?: string | null; "modelProvider"?: string | null; "personality"?: Personality | null; "sandbox"?: SandboxMode | null; "serviceTier"?: string | null; "threadId": string };
export type ThreadResumeResponse = { "approvalPolicy": AskForApproval; "approvalsReviewer": ApprovalsReviewer; "collaborationMode"?: CollaborationMode | null; "cwd": AbsolutePathBuf; "disabledPluginIds"?: Array<string>; "instructionSources"?: Array<LegacyAppPathString>; "itemsBackwardsCursor"?: string | null; "model": string; "modelProvider": string; "reasoningEffort"?: ReasoningEffort | null; "sandbox": SandboxPolicy; "serviceTier"?: string | null; "thread": Thread; "turnsBackwardsCursor"?: string | null };
export type ThreadRevertParams = { "beforeTurnId": string; "threadId": string };
export type ThreadRevertResponse = { "itemsBackwardsCursor"?: string | null; "thread": Thread; "turnsBackwardsCursor"?: string | null };
export type ThreadRevertedNotification = { "threadId": string };
export type ThreadSection = { "appearance"?: ThreadSectionAppearance | null; "id": string; "name": string };
export type ThreadSectionAppearance = { "color"?: string | null; "icon"?: string | null };
export type ThreadSetNameParams = { "name": string; "threadId": string };
export type ThreadSetNameResponse = Record<string, never>;
export type ThreadShellCommandParams = { "command": string; "threadId": string; "timeoutMs"?: number | null };
export type ThreadShellCommandResponse = Record<string, never>;
export type ThreadSortKey = "created_at" | "updated_at" | "recency_at" | "section_position";
export type ThreadSource = string;
export type ThreadSourceKind = "cli" | "vscode" | "exec" | "appServer" | "subAgent" | "subAgentReview" | "subAgentCompact" | "subAgentThreadSpawn" | "subAgentOther" | "unknown";
export type ThreadStartParams = { "approvalPolicy"?: AskForApproval | null; "approvalsReviewer"?: ApprovalsReviewer | null; "baseInstructions"?: string | null; "config"?: { [key: string]: unknown } | null; "cwd"?: string | null; "developerInstructions"?: string | null; "ephemeral"?: boolean | null; "model"?: string | null; "modelProvider"?: string | null; "personality"?: Personality | null; "sandbox"?: SandboxMode | null; "serviceName"?: string | null; "serviceTier"?: string | null; "sessionStartSource"?: ThreadStartSource | null; "threadSource"?: ThreadSource | null };
export type ThreadStartResponse = { "approvalPolicy": AskForApproval; "approvalsReviewer": ApprovalsReviewer; "cwd": AbsolutePathBuf; "disabledPluginIds"?: Array<string>; "instructionSources"?: Array<LegacyAppPathString>; "model": string; "modelProvider": string; "reasoningEffort"?: ReasoningEffort | null; "sandbox": SandboxPolicy; "serviceTier"?: string | null; "thread": Thread };
export type ThreadStartSource = "startup" | "clear";
export type ThreadStartedNotification = { "thread": Thread };
export type ThreadStatus = { "type": "notLoaded" } | { "type": "idle" } | { "type": "systemError" } | { "activeFlags": Array<ThreadActiveFlag>; "type": "active" };
export type ThreadStatusChangedNotification = { "status": ThreadStatus; "threadId": string };
export type ThreadTokenUsage = { "last": TokenUsageBreakdown; "modelContextWindow"?: number | null; "total": TokenUsageBreakdown };
export type ThreadTokenUsageUpdatedNotification = { "threadId": string; "tokenUsage": ThreadTokenUsage; "turnId": string };
export type ThreadTurnsListParams = { "cursor"?: string | null; "itemsView"?: TurnItemsView | null; "limit"?: number | null; "sortDirection"?: SortDirection | null; "threadId": string };
export type ThreadTurnsListResponse = { "backwardsCursor"?: string | null; "data": Array<Turn>; "nextCursor"?: string | null };
export type ThreadUnarchiveParams = { "threadId": string };
export type ThreadUnarchiveResponse = { "thread": Thread };
export type ThreadUnarchivedNotification = { "threadId": string };
export type ThreadUnsubscribeParams = { "threadId": string };
export type ThreadUnsubscribeResponse = { "status": ThreadUnsubscribeStatus };
export type ThreadUnsubscribeStatus = "notLoaded" | "notSubscribed" | "unsubscribed";
export type TokenUsageBreakdown = { "cacheWriteInputTokens"?: number; "cachedInputTokens": number; "inputTokens": number; "outputTokens": number; "reasoningOutputTokens": number; "totalTokens": number };
export type Tool = { "_meta"?: unknown; "annotations"?: unknown; "description"?: string | null; "icons"?: Array<unknown> | null; "inputSchema": unknown; "name": string; "outputSchema"?: unknown; "title"?: string | null };
export type ToolRequestUserInputAnswer = { "answers": Array<string> };
export type ToolRequestUserInputOption = { "description": string; "label": string };
export type ToolRequestUserInputParams = { "autoResolutionMs"?: number | null; "isBlocking": boolean; "itemId": string; "questions": Array<ToolRequestUserInputQuestion>; "threadId": string; "turnId": string };
export type ToolRequestUserInputQuestion = { "header": string; "id": string; "isOther"?: boolean; "isSecret"?: boolean; "options"?: Array<ToolRequestUserInputOption> | null; "question": string };
export type ToolRequestUserInputResponse = { "answers": { [key: string]: ToolRequestUserInputAnswer } };
export type ToolsV2 = { "web_search"?: WebSearchToolConfig | null };
export type Turn = { "completedAt"?: number | null; "durationMs"?: number | null; "error"?: TurnError | null; "id": string; "items": Array<ThreadItem>; "itemsView"?: TurnItemsView; "rootTurnId"?: string | null; "startedAt"?: number | null; "status": TurnStatus };
export type TurnCompletedNotification = { "threadId": string; "turn": Turn };
export type TurnDiffUpdatedNotification = { "diff": string; "threadId": string; "turnId": string };
export type TurnError = { "additionalDetails"?: string | null; "codexErrorInfo"?: CodexErrorInfo | null; "message": string; "misalignment"?: MisalignmentErrorDetails | null };
export type TurnInterruptParams = { "threadId": string; "turnId": string };
export type TurnInterruptResponse = Record<string, never>;
export type TurnItemsView = "notLoaded" | "summary" | "full";
export type TurnPlanStep = { "status": TurnPlanStepStatus; "step": string };
export type TurnPlanStepStatus = "pending" | "inProgress" | "completed";
export type TurnPlanUpdatedNotification = { "explanation"?: string | null; "plan": Array<TurnPlanStep>; "threadId": string; "turnId": string };
export type TurnStartParams = { "approvalPolicy"?: AskForApproval | null; "approvalsReviewer"?: ApprovalsReviewer | null; "clientUserMessageId"?: string | null; "cwd"?: string | null; "disabledPluginIds"?: Array<string> | null; "effort"?: ReasoningEffort | null; "input": Array<UserInput>; "model"?: string | null; "outputSchema"?: unknown; "parentTurnId"?: string | null; "personality"?: Personality | null; "rootTurnId"?: string | null; "sandboxPolicy"?: SandboxPolicy | null; "serviceTier"?: string | null; "serviceTierForTurn"?: string | null; "summary"?: ReasoningSummary | null; "threadId": string; "toolOutput"?: TurnToolOutput | null; "turnTrigger"?: string | null };
export type TurnStartResponse = { "turn": Turn };
export type TurnStartedNotification = { "threadId": string; "turn": Turn };
export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
export type TurnSteerParams = { "clientUserMessageId"?: string | null; "expectedTurnId": string; "input": Array<UserInput>; "threadId": string };
export type TurnSteerResponse = { "turnId": string };
export type TurnToolOutput = { "name": string; "namespace"?: string | null; "output": FunctionCallOutputBody; "retain"?: boolean };
export type UserInput = { "text": string; "text_elements"?: Array<TextElement>; "type": "text" } | { "detail"?: ImageDetail | null; "type": "image" } & UserInputImageKind | { "detail"?: ImageDetail | null; "path": string; "type": "localImage" } | { "url": string; "type": "audio" } | { "path": string; "type": "localAudio" } | { "name": string; "path": string; "type": "skill" } | { "name": string; "path": string; "type": "mention" };
export type UserInputImageKind = { "url": string } | { "fileId": string };
export type Verbosity = "low" | "medium" | "high";
export type WebSearchAction = { "queries"?: Array<string> | null; "query"?: string | null; "type": "search" } | { "url"?: string | null; "type": "openPage" } | { "pattern"?: string | null; "url"?: string | null; "type": "findInPage" } | { "type": "other" };
export type WebSearchContextSize = "low" | "medium" | "high";
export type WebSearchLocation = { "city"?: string | null; "country"?: string | null; "region"?: string | null; "timezone"?: string | null };
export type WebSearchMode = "disabled" | "cached" | "indexed" | "live";
export type WebSearchToolConfig = { "allowed_domains"?: Array<string> | null; "context_size"?: WebSearchContextSize | null; "location"?: WebSearchLocation | null };
export type WriteStatus = "ok" | "okOverridden";
export type ClientRequestMap = {
"initialize": { params: InitializeParams; result: InitializeResponse };
"thread/start": { params: ThreadStartParams; result: ThreadStartResponse };
"thread/resume": { params: ThreadResumeParams; result: ThreadResumeResponse };
"thread/fork": { params: ThreadForkParams; result: ThreadForkResponse };
"thread/archive": { params: ThreadArchiveParams; result: ThreadArchiveResponse };
"thread/delete": { params: ThreadDeleteParams; result: ThreadDeleteResponse };
"thread/unsubscribe": { params: ThreadUnsubscribeParams; result: ThreadUnsubscribeResponse };
"thread/name/set": { params: ThreadSetNameParams; result: ThreadSetNameResponse };
"thread/goal/set": { params: ThreadGoalSetParams; result: ThreadGoalSetResponse };
"thread/goal/get": { params: ThreadGoalGetParams; result: ThreadGoalGetResponse };
"thread/goal/clear": { params: ThreadGoalClearParams; result: ThreadGoalClearResponse };
"thread/unarchive": { params: ThreadUnarchiveParams; result: ThreadUnarchiveResponse };
"thread/compact/start": { params: ThreadCompactStartParams; result: ThreadCompactStartResponse };
"thread/shellCommand": { params: ThreadShellCommandParams; result: ThreadShellCommandResponse };
"thread/revert": { params: ThreadRevertParams; result: ThreadRevertResponse };
"thread/list": { params: ThreadListParams; result: ThreadListResponse };
"thread/loaded/list": { params: ThreadLoadedListParams; result: ThreadLoadedListResponse };
"thread/read": { params: ThreadReadParams; result: ThreadReadResponse };
"thread/turns/list": { params: ThreadTurnsListParams; result: ThreadTurnsListResponse };
"thread/items/list": { params: ThreadItemsListParams; result: ThreadItemsListResponse };
"skills/list": { params: SkillsListParams; result: SkillsListResponse };
"hooks/list": { params: HooksListParams; result: HooksListResponse };
"fs/readFile": { params: FsReadFileParams; result: FsReadFileResponse };
"fs/writeFile": { params: FsWriteFileParams; result: FsWriteFileResponse };
"fs/createDirectory": { params: FsCreateDirectoryParams; result: FsCreateDirectoryResponse };
"fs/getMetadata": { params: FsGetMetadataParams; result: FsGetMetadataResponse };
"fs/readDirectory": { params: FsReadDirectoryParams; result: FsReadDirectoryResponse };
"fs/remove": { params: FsRemoveParams; result: FsRemoveResponse };
"fs/copy": { params: FsCopyParams; result: FsCopyResponse };
"fs/watch": { params: FsWatchParams; result: FsWatchResponse };
"fs/unwatch": { params: FsUnwatchParams; result: FsUnwatchResponse };
"turn/start": { params: TurnStartParams; result: TurnStartResponse };
"turn/steer": { params: TurnSteerParams; result: TurnSteerResponse };
"turn/interrupt": { params: TurnInterruptParams; result: TurnInterruptResponse };
"review/start": { params: ReviewStartParams; result: ReviewStartResponse };
"model/list": { params: ModelListParams; result: ModelListResponse };
"mcpServerStatus/list": { params: ListMcpServerStatusParams; result: ListMcpServerStatusResponse };
"command/exec": { params: CommandExecParams; result: CommandExecResponse };
"command/exec/write": { params: CommandExecWriteParams; result: CommandExecWriteResponse };
"command/exec/terminate": { params: CommandExecTerminateParams; result: CommandExecTerminateResponse };
"command/exec/resize": { params: CommandExecResizeParams; result: CommandExecResizeResponse };
"config/read": { params: ConfigReadParams; result: ConfigReadResponse };
"config/value/write": { params: ConfigValueWriteParams; result: ConfigWriteResponse };
"config/batchWrite": { params: ConfigBatchWriteParams; result: ConfigWriteResponse };
};
export type ClientNotificationMap = {
"initialized": { params: undefined };
};
export type ServerRequestMap = {
"item/commandExecution/requestApproval": { params: CommandExecutionRequestApprovalParams; result: CommandExecutionRequestApprovalResponse };
"item/fileChange/requestApproval": { params: FileChangeRequestApprovalParams; result: FileChangeRequestApprovalResponse };
"item/tool/requestUserInput": { params: ToolRequestUserInputParams; result: ToolRequestUserInputResponse };
"item/permissions/requestApproval": { params: PermissionsRequestApprovalParams; result: PermissionsRequestApprovalResponse };
"item/tool/call": { params: DynamicToolCallParams; result: DynamicToolCallResponse };
};
export type ServerNotificationMap = {
"error": { params: ErrorNotification };
"thread/started": { params: ThreadStartedNotification };
"thread/status/changed": { params: ThreadStatusChangedNotification };
"thread/archived": { params: ThreadArchivedNotification };
"thread/deleted": { params: ThreadDeletedNotification };
"thread/unarchived": { params: ThreadUnarchivedNotification };
"thread/closed": { params: ThreadClosedNotification };
"thread/reverted": { params: ThreadRevertedNotification };
"skills/changed": { params: SkillsChangedNotification };
"thread/name/updated": { params: ThreadNameUpdatedNotification };
"thread/goal/updated": { params: ThreadGoalUpdatedNotification };
"thread/goal/cleared": { params: ThreadGoalClearedNotification };
"thread/tokenUsage/updated": { params: ThreadTokenUsageUpdatedNotification };
"turn/started": { params: TurnStartedNotification };
"turn/completed": { params: TurnCompletedNotification };
"turn/diff/updated": { params: TurnDiffUpdatedNotification };
"turn/plan/updated": { params: TurnPlanUpdatedNotification };
"item/started": { params: ItemStartedNotification };
"item/completed": { params: ItemCompletedNotification };
"item/agentMessage/delta": { params: AgentMessageDeltaNotification };
"item/plan/delta": { params: PlanDeltaNotification };
"command/exec/outputDelta": { params: CommandExecOutputDeltaNotification };
"item/commandExecution/outputDelta": { params: CommandExecutionOutputDeltaNotification };
"item/commandExecution/terminalInteraction": { params: TerminalInteractionNotification };
"item/fileChange/outputDelta": { params: FileChangeOutputDeltaNotification };
"item/fileChange/patchUpdated": { params: FileChangePatchUpdatedNotification };
"serverRequest/resolved": { params: ServerRequestResolvedNotification };
"item/mcpToolCall/progress": { params: McpToolCallProgressNotification };
"fs/changed": { params: FsChangedNotification };
"item/reasoning/summaryTextDelta": { params: ReasoningSummaryTextDeltaNotification };
"item/reasoning/summaryPartAdded": { params: ReasoningSummaryPartAddedNotification };
"item/reasoning/textDelta": { params: ReasoningTextDeltaNotification };
"thread/compacted": { params: ContextCompactedNotification };
};
@@ -1,153 +0,0 @@
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json | undefined }
export type DynamicToolCallContent = { type: 'input_text'; text: string } | { type: 'input_image'; image_url: string }
export type DynamicToolCallParams = {
thread_id: string
turn_id: string
call_id: string
namespace?: string
tool: string
arguments: Json
[key: string]: Json | undefined
}
export type DynamicToolCallResult = {
success: boolean
content?: DynamicToolCallContent[]
[key: string]: Json | undefined
}
export type DynamicToolItemStatus = 'in_progress' | 'completed' | 'failed'
export type DynamicToolSpec = {
namespace?: (string | null)
name: string
description: string
input_schema: Json
defer_loading?: boolean
[key: string]: Json | undefined
}
export type DynamicToolSpecOutput = {
namespace?: string
name: string
description: string
input_schema: Json
defer_loading: boolean
[key: string]: Json | undefined
}
export type FleetProfileDocument = {
id: string
kind: FleetProfileDocumentKind
origin: string
path: (string | null)
text: string
revision: string
writable: boolean
refusal_reason: (string | null)
}
export type FleetProfileDocumentKind = 'profile' | 'fleet'
export type RuntimeCapabilities = {
client_token_intents: boolean
account_session: boolean
account_model_owner: boolean
session_provider_keys: boolean
threads: boolean
thread_create_idempotency: boolean
thread_idle_precondition: boolean
thread_patch_undo_turn_id: boolean
thread_scoped_reads: boolean
thread_scoped_git_writes: boolean
thread_scoped_snapshots: boolean
thread_scoped_terminals: boolean
worktrees: boolean
job_operations: boolean
thread_shell_consent: boolean
turns: boolean
turn_operation_idempotency: boolean
turn_operation_lookup: boolean
watch_event_turns: boolean
structured_review_turns: boolean
structured_review_version: number
automation_mutation_preconditions: boolean
turn_image_inputs: boolean
turn_output_token_limit: boolean
profile_constitution: boolean
turn_steer: boolean
turn_interrupt: boolean
event_replay: boolean
external_tools: boolean
environments: boolean
worker_runtime: boolean
fleet_run_create: boolean
thread_fleet_members: boolean
thread_fleet_member_durable_operations_v1: boolean
fleet_run_start: boolean
fleet_event_replay: boolean
fleet_event_stream: boolean
fleet_local_target: boolean
thread_goals: boolean
memory: boolean
mcp_server_management: boolean
skill_lifecycle: boolean
skill_detail: boolean
plugin_management: boolean
agent_mail: boolean
event_stream_resume: boolean
[key: string]: Json | undefined
}
export type RuntimeEventEnvelope = {
schema_version: number
seq: number
event: string
kind: string
thread_id: string
turn_id: (string | null)
item_id: (string | null)
timestamp: string
created_at?: string
payload: Json
[key: string]: Json | undefined
}
export type RuntimeExperimentalCapabilities = {
environments: boolean
[key: string]: Json | undefined
}
export type RuntimeImageInput = {
mime: string
dataBase64: string
}
export type ThreadLoadedListParams = {
cursor?: (string | null)
limit?: (number | null)
[key: string]: Json | undefined
}
export type ThreadLoadedListResponse = {
data: string[]
nextCursor: (string | null)
[key: string]: Json | undefined
}
export type ThreadSetNameParams = {
threadId: string
name: string
[key: string]: Json | undefined
}
export type ThreadSetNameResponse = Record<string, never>
export type TurnEnvironmentParams = {
environment_id: string
cwd: string
[key: string]: Json | undefined
}
@@ -1,33 +0,0 @@
#[path = "../codegen/mod.rs"]
mod codegen;
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let arguments: Vec<String> = std::env::args().skip(1).collect();
if !matches!(arguments.as_slice(), [mode] if mode == "--write" || mode == "--check") {
return Err("use --write or --check against the contract manifest outputs".into());
}
let write = arguments[0] == "--write";
let package = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let manifest = std::fs::read_to_string(package.join(codegen::MANIFEST_PATH))?;
let pinned = std::fs::read_to_string(package.join(codegen::PINNED_SCHEMA_PATH))?;
let outputs = codegen::generate(&manifest, &pinned)?;
let mut drift = Vec::new();
for (path, text) in outputs.files {
let target = package.join(path);
if write {
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&target, text)?;
} else if std::fs::read_to_string(&target).ok().as_deref() != Some(text.as_str()) {
drift.push(path);
}
}
if drift.is_empty() {
Ok(())
} else {
Err(format!("generated contract drift: {}", drift.join(", ")).into())
}
}
@@ -1,49 +0,0 @@
use std::path::{Path, PathBuf};
fn retain(path: &Path, bytes: &[u8], write: bool) -> std::io::Result<()> {
if write {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, bytes)
} else if std::fs::read(path)? == bytes {
Ok(())
} else {
Err(std::io::Error::other(format!(
"generated contract drift: {}",
path.display()
)))
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let arguments: Vec<String> = std::env::args().skip(1).collect();
if !matches!(arguments.as_slice(), [mode] if mode == "--write" || mode == "--check") {
return Err("use --write or --check against the declared package and SDK outputs".into());
}
let write = arguments[0] == "--write";
let package = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repository = package
.parent()
.and_then(Path::parent)
.ok_or("package repository root")?;
let schema = codewhale_app_server_protocol::export::runtime_schema();
let typescript = codewhale_app_server_protocol::export::runtime_typescript(&schema);
let json = serde_json::to_string_pretty(&schema)? + "\n";
retain(
&package.join("schema/json/runtime.schema.json"),
json.as_bytes(),
write,
)?;
retain(
&package.join("schema/typescript/runtime.d.ts"),
typescript.as_bytes(),
write,
)?;
retain(
&repository.join("npm/runtime-sdk/generated/runtime.d.ts"),
typescript.as_bytes(),
write,
)?;
Ok(())
}
File diff suppressed because it is too large. Load diff
-86
View File
@@ -1,86 +0,0 @@
use crate::rpc::{
ThreadLoadedListParams, ThreadLoadedListResponse, ThreadSetNameParams, ThreadSetNameResponse,
};
use crate::runtime::*;
use schemars::generate::SchemaSettings;
use serde_json::{Value, json};
use std::collections::BTreeMap;
fn output_references(value: &mut Value) {
match value {
Value::Object(object) => {
if object.get("$ref").and_then(Value::as_str) == Some("#/$defs/DynamicToolSpec") {
object.insert(
"$ref".into(),
Value::String("#/$defs/DynamicToolSpecOutput".into()),
);
}
for child in object.values_mut() {
output_references(child);
}
}
Value::Array(values) => {
for child in values {
output_references(child);
}
}
_ => {}
}
}
pub fn runtime_schema() -> Value {
let mut input = SchemaSettings::draft2020_12()
.for_deserialize()
.into_generator();
let _ = input.subschema_for::<RuntimeImageInput>();
let _ = input.subschema_for::<ThreadLoadedListParams>();
let _ = input.subschema_for::<ThreadSetNameParams>();
let _ = input.subschema_for::<DynamicToolSpec>();
let _ = input.subschema_for::<DynamicToolItemStatus>();
let _ = input.subschema_for::<DynamicToolCallResult>();
let _ = input.subschema_for::<DynamicToolCallContent>();
let _ = input.subschema_for::<TurnEnvironmentParams>();
let mut output = SchemaSettings::draft2020_12()
.for_serialize()
.into_generator();
let _ = output.subschema_for::<RuntimeEventEnvelope>();
let _ = output.subschema_for::<ThreadLoadedListResponse>();
let _ = output.subschema_for::<ThreadSetNameResponse>();
let _ = output.subschema_for::<RuntimeCapabilities>();
let _ = output.subschema_for::<FleetProfileDocument>();
let _ = output.subschema_for::<RuntimeExperimentalCapabilities>();
let _ = output.subschema_for::<DynamicToolCallParams>();
let _ = output.subschema_for::<DynamicToolSpec>();
let _ = output.subschema_for::<TurnEnvironmentParams>();
let _ = output.subschema_for::<DynamicToolItemStatus>();
let mut definitions = input.take_definitions(false);
for (name, mut schema) in output.take_definitions(false) {
output_references(&mut schema);
let name = if name == "DynamicToolSpec" {
"DynamicToolSpecOutput".to_string()
} else {
name
};
if let Some(previous) = definitions.get(&name) {
assert_eq!(previous, &schema, "input/output schema conflict for {name}");
} else {
definitions.insert(name, schema);
}
}
json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": definitions})
}
pub fn runtime_typescript(schema: &Value) -> String {
let definitions: BTreeMap<String, crate::schema::Def> = schema["$defs"]
.as_object()
.expect("Runtime schema definitions")
.iter()
.map(|(name, schema)| {
(
name.clone(),
crate::schema::parse_def(name, schema, crate::schema::SchemaMode::Runtime),
)
})
.collect();
crate::schema::typescript_defs(&definitions, crate::schema::SchemaMode::Runtime)
}
-13
View File
@@ -1,13 +0,0 @@
pub mod lifecycle;
#[rustfmt::skip]
pub mod methods;
pub mod rpc;
pub mod runtime;
#[rustfmt::skip]
pub mod v2;
#[cfg(feature = "schema")]
pub mod schema;
#[cfg(feature = "schema")]
pub mod export;
@@ -1,45 +0,0 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeParams {
pub client_info: ClientInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub capabilities: Option<InitializeCapabilities>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientInfo {
pub name: String,
pub title: Option<String>,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeCapabilities {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub explicit_gateway_oauth: bool,
#[serde(default)]
pub experimental_api: bool,
#[serde(default)]
pub request_attestation: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub mcp_server_openai_form_elicitation: bool,
pub opt_out_notification_methods: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResponse {
pub user_agent: String,
pub codex_home: PathBuf,
pub platform_family: String,
pub platform_os: String,
}
File diff suppressed because it is too large. Load diff
-312
View File
@@ -1,312 +0,0 @@
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
pub use crate::v2::{ThreadExecutor, ThreadPlacement as ThreadEnvironment};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RequestId {
String(String),
Integer(i64),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct W3cTraceContext {
pub traceparent: Option<String>,
pub tracestate: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RpcRequest {
pub id: RequestId,
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace: Option<W3cTraceContext>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RpcNotification {
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RpcResponse {
pub id: RequestId,
pub result: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RpcErrorResponse {
pub id: RequestId,
pub error: RpcErrorDetails,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RpcErrorDetails {
pub code: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
pub message: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct RpcCall {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RequestId>,
pub method: String,
#[serde(skip_serializing_if = "Value::is_null")]
pub params: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace: Option<W3cTraceContext>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RpcMessage {
Request(RpcRequest),
Notification(RpcNotification),
Response(RpcResponse),
Error(RpcErrorResponse),
}
impl RpcMessage {
pub fn into_call(self) -> Option<RpcCall> {
match self {
Self::Request(request) => Some(RpcCall {
id: Some(request.id),
method: request.method,
params: request.params.unwrap_or(Value::Null),
trace: request.trace,
}),
Self::Notification(notification) => Some(RpcCall {
id: None,
method: notification.method,
params: notification.params.unwrap_or(Value::Null),
trace: None,
}),
Self::Response(_) | Self::Error(_) => None,
}
}
}
impl<'de> Deserialize<'de> for RpcCall {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
RpcMessage::deserialize(deserializer)?
.into_call()
.ok_or_else(|| serde::de::Error::custom("RPC response is not a call"))
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ThreadLoadedListParams {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ThreadLoadedListResponse {
pub data: Vec<String>,
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ThreadListResponse {
pub data: Vec<ThreadListItem>,
pub next_cursor: Option<String>,
pub backwards_cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ThreadListItem {
pub id: String,
pub session_id: String,
pub preview: String,
pub ephemeral: bool,
pub model_provider: String,
pub created_at: i64,
pub updated_at: i64,
pub status: ThreadListStatus,
pub cwd: String,
pub cli_version: String,
pub source: ThreadListSource,
pub project_id: Option<String>,
pub turns: Vec<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub executor: Option<ThreadExecutor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment: Option<ThreadEnvironment>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ThreadListStatus {
NotLoaded,
Idle,
SystemError,
Active {
#[serde(rename = "activeFlags")]
active_flags: Vec<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ThreadListSource {
Cli,
AppServer,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ThreadStartParams {
pub operation_key: Option<String>,
pub model: Option<String>,
pub model_provider: Option<String>,
pub cwd: Option<String>,
pub ephemeral: Option<bool>,
pub approval_policy: Option<Value>,
pub approvals_reviewer: Option<Value>,
pub sandbox: Option<Value>,
pub config: Option<Value>,
pub base_instructions: Option<String>,
pub developer_instructions: Option<String>,
pub service_name: Option<String>,
pub service_tier: Option<String>,
pub session_start_source: Option<Value>,
pub personality: Option<Value>,
pub thread_source: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ThreadStartResponse {
pub thread: ThreadListItem,
pub model: String,
pub model_provider: String,
pub cwd: String,
pub approval_policy: String,
pub approvals_reviewer: String,
pub sandbox: ThreadSandboxPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ThreadSandboxPolicy {
ReadOnly,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ThreadReadParams {
pub thread_id: String,
#[serde(default)]
pub include_turns: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ThreadReadResponse {
pub thread: ThreadListItem,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ThreadTurn {
pub id: String,
pub items: Vec<ThreadTurnItem>,
pub items_view: ThreadTurnItemsView,
pub status: ThreadTurnStatus,
pub error: Option<ThreadTurnError>,
pub started_at: Option<i64>,
pub completed_at: Option<i64>,
pub duration_ms: Option<i64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ThreadTurnItemsView {
Summary,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum ThreadTurnStatus {
Completed,
Interrupted,
Failed,
InProgress,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ThreadTurnError {
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ThreadTurnItem {
UserMessage {
id: String,
content: Vec<ThreadUserInput>,
},
AgentMessage {
id: String,
text: String,
},
Reasoning {
id: String,
content: Vec<String>,
},
ContextCompaction {
id: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ThreadUserInput {
Text { text: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ThreadSetNameParams {
pub thread_id: String,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ThreadSetNameResponse {}
@@ -1,157 +0,0 @@
use serde::{Deserialize, Serialize};
pub const MAX_WATCH_EVENT_RECEIPT_BYTES: usize = 4096;
const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WatchEventReceipt {
pub version: u32,
pub platform_account_id: String,
pub project_id: String,
pub watch_id: String,
pub watch_revision: u64,
pub definition_sha256: String,
pub source_kind: WatchEventSourceKind,
pub occurrence_sha256: String,
pub event_sha256: String,
pub run_id: String,
pub room_thread_root_id: String,
pub source: WatchEventSource,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum WatchEventSourceKind {
#[serde(rename = "schedule")]
Schedule,
#[serde(rename = "github.pull_request")]
GitHubPullRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum WatchEventSource {
Schedule(WatchScheduleSource),
GitHubPullRequest(WatchPullRequestSource),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WatchScheduleSource {
pub slot: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WatchPullRequestSource {
pub installation_id: String,
pub binding_id: String,
pub repo: String,
pub number: u64,
pub delivery_id: String,
pub event_name: String,
}
fn reference(value: &str) -> bool {
(1..=200).contains(&value.len())
&& value.as_bytes()[0].is_ascii_alphanumeric()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"._:/@#-".contains(&byte))
}
fn fingerprint(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn canonical_utc_slot(value: &str) -> bool {
value.len() == 24
&& chrono::DateTime::parse_from_rfc3339(value).is_ok_and(|parsed| {
parsed.timestamp_subsec_nanos() < 1_000_000_000
&& parsed.to_rfc3339_opts(chrono::SecondsFormat::Millis, true) == value
})
}
impl WatchEventReceipt {
pub fn validate(&self) -> Result<(), &'static str> {
if self.version != 1 || self.watch_revision == 0 || self.watch_revision > MAX_SAFE_INTEGER {
return Err("unsupported watch event version or revision");
}
if [
&self.platform_account_id,
&self.project_id,
&self.watch_id,
&self.run_id,
]
.iter()
.any(|value| !reference(value))
|| [
&self.definition_sha256,
&self.occurrence_sha256,
&self.event_sha256,
]
.iter()
.any(|value| !fingerprint(value))
{
return Err("invalid watch event reference or fingerprint");
}
if !self.room_thread_root_id.is_empty()
&& (self.room_thread_root_id.len() != 35
|| !self.room_thread_root_id.starts_with("pm_")
|| !self.room_thread_root_id.as_bytes()[3..]
.iter()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)))
{
return Err("invalid watch event room thread");
}
match (&self.source_kind, &self.source) {
(WatchEventSourceKind::Schedule, WatchEventSource::Schedule(source)) => {
if source.slot.len() != 24
|| !reference(&source.slot)
|| !canonical_utc_slot(&source.slot)
{
return Err("invalid watch event schedule occurrence");
}
}
(
WatchEventSourceKind::GitHubPullRequest,
WatchEventSource::GitHubPullRequest(source),
) => {
let valid_repo = source.repo.split_once('/').is_some_and(|(owner, name)| {
[owner, name].iter().all(|part| {
(1..=100).contains(&part.len())
&& part
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"_.-".contains(&byte))
})
});
if source.number == 0
|| source.number > MAX_SAFE_INTEGER
|| [
&source.installation_id,
&source.binding_id,
&source.delivery_id,
&source.event_name,
]
.iter()
.any(|value| !reference(value))
|| !valid_repo
{
return Err("invalid watch event pull request occurrence");
}
}
_ => return Err("watch event kind does not match its source"),
}
if serde_json::to_vec(self)
.map_err(|_| "watch event cannot be encoded")?
.len()
> MAX_WATCH_EVENT_RECEIPT_BYTES
{
return Err("watch event receipt exceeds 4 KiB");
}
Ok(())
}
}
-302
View File
@@ -1,302 +0,0 @@
use serde_json::Value;
use std::collections::BTreeMap;
use std::fmt::Write as _;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum SchemaMode {
Host,
Runtime,
}
/// A field's wire type, normalized.
pub enum Ty {
Nullable(Box<Ty>),
String,
Boolean,
Uint,
Integer,
/// A JSON object with any keys (`Map<String, Value>`).
Object,
/// Any JSON value.
Json,
Ref(String),
/// A string tag inside a union member.
Const(String),
Array(Box<Ty>),
}
pub struct Field {
pub name: String,
pub ty: Ty,
pub required: bool,
}
pub enum Def {
Object {
strict: bool,
extra: bool,
fields: Vec<Field>,
},
Enum(Vec<String>),
Union(Vec<Vec<Field>>),
}
pub fn ref_name(schema: &Value) -> Option<String> {
let reference = schema.get("$ref")?.as_str()?;
Some(
reference
.strip_prefix("#/$defs/")
.unwrap_or_else(|| panic!("unexpected $ref `{reference}`"))
.to_string(),
)
}
fn parse_ty(at: &str, schema: &Value, mode: SchemaMode) -> Ty {
let Some(object) = schema.as_object() else {
assert_eq!(schema, &Value::Bool(true), "{at}: unsupported schema");
return Ty::Json;
};
if let Some(name) = ref_name(schema) {
return Ty::Ref(name);
}
if let Some(variants) = object.get("anyOf").and_then(Value::as_array)
&& variants.len() == 2
&& variants
.iter()
.filter(|variant| variant.get("type").and_then(Value::as_str) == Some("null"))
.count()
== 1
{
let present = parse_ty(
at,
variants
.iter()
.find(|variant| variant.get("type").and_then(Value::as_str) != Some("null"))
.expect("one non-null variant"),
mode,
);
return if mode == SchemaMode::Runtime {
Ty::Nullable(Box::new(present))
} else {
present
};
}
if let Some(value) = object.get("const").and_then(Value::as_str) {
return Ty::Const(value.to_string());
}
let types: Vec<&str> = match object.get("type") {
Some(Value::String(ty)) => vec![ty.as_str()],
Some(Value::Array(types)) => types
.iter()
.filter_map(Value::as_str)
.filter(|ty| *ty != "null")
.collect(),
// Only annotations (`default`, `description`): any value.
None if !["enum", "anyOf", "oneOf", "allOf", "properties", "items"]
.iter()
.any(|key| object.contains_key(*key)) =>
{
return Ty::Json;
}
_ => panic!("{at}: unsupported wire schema {schema}"),
};
let value = match types.as_slice() {
["string"] => Ty::String,
["boolean"] => Ty::Boolean,
["integer"] if object.get("minimum").and_then(Value::as_u64) == Some(0) => Ty::Uint,
["integer"] => Ty::Integer,
["object"]
if object.get("additionalProperties") == Some(&Value::Bool(true))
&& !object.contains_key("properties") =>
{
Ty::Object
}
["array"] => Ty::Array(Box::new(parse_ty(
&format!("{at}[]"),
object
.get("items")
.unwrap_or_else(|| panic!("{at}: array without items")),
mode,
))),
_ => panic!("{at}: unsupported wire schema {schema}"),
};
if mode == SchemaMode::Runtime
&& object
.get("type")
.and_then(Value::as_array)
.is_some_and(|types| types.iter().any(|ty| ty == "null"))
{
Ty::Nullable(Box::new(value))
} else {
value
}
}
fn object_fields(at: &str, schema: &Value, mode: SchemaMode) -> Vec<Field> {
assert_eq!(
schema.get("type").and_then(Value::as_str),
Some("object"),
"{at}: expected an object schema, got {schema}"
);
let required: Vec<&str> = schema
.get("required")
.and_then(Value::as_array)
.map(|names| names.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
schema
.get("properties")
.and_then(Value::as_object)
.map(|properties| {
properties
.iter()
.map(|(name, schema)| Field {
name: name.clone(),
ty: parse_ty(&format!("{at}.{name}"), schema, mode),
required: required.contains(&name.as_str()),
})
.collect()
})
.unwrap_or_default()
}
pub fn parse_def(name: &str, schema: &Value, mode: SchemaMode) -> Def {
let members = schema
.get("oneOf")
.or_else(|| schema.get("anyOf"))
.and_then(Value::as_array);
if let Some(members) = members {
// Unit variants, with or without doc comments: a string enum.
let consts: Option<Vec<String>> = members
.iter()
.map(|member| {
member
.get("const")
.and_then(Value::as_str)
.map(str::to_string)
})
.collect();
return match consts {
Some(values) => Def::Enum(values),
None if members.len() == 1 => Def::Object {
strict: members[0].get("additionalProperties") == Some(&Value::Bool(false)),
extra: mode == SchemaMode::Runtime
&& members[0].get("additionalProperties") != Some(&Value::Bool(false)),
fields: object_fields(name, &members[0], mode),
},
None => Def::Union(
members
.iter()
.map(|member| object_fields(name, member, mode))
.collect(),
),
};
}
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
return Def::Enum(
values
.iter()
.map(|value| {
value
.as_str()
.unwrap_or_else(|| panic!("{name}: non-string enum value"))
.to_string()
})
.collect(),
);
}
Def::Object {
strict: schema.get("additionalProperties") == Some(&Value::Bool(false)),
extra: mode == SchemaMode::Runtime
&& schema.get("additionalProperties") != Some(&Value::Bool(false)),
fields: object_fields(name, schema, mode),
}
}
pub fn quote(value: &str) -> String {
assert!(
!value.contains(['\'', '\\']),
"`{value}` needs escaping in TypeScript"
);
format!("'{value}'")
}
fn ts(ty: &Ty) -> String {
match ty {
Ty::Nullable(inner) => format!("({} | null)", ts(inner)),
Ty::String => "string".into(),
Ty::Boolean => "boolean".into(),
Ty::Uint | Ty::Integer => "number".into(),
Ty::Object => "{ [key: string]: Json }".into(),
Ty::Json => "Json".into(),
Ty::Ref(name) => name.clone(),
Ty::Const(value) => quote(value),
Ty::Array(item) => format!("{}[]", ts(item)),
}
}
/// An array's item type, however deeply nested.
pub fn innermost(ty: &Ty) -> &Ty {
match ty {
Ty::Array(item) => innermost(item),
ty => ty,
}
}
fn member(field: &Field) -> String {
let optional = if field.required { "" } else { "?" };
format!("{}{optional}: {}", field.name, ts(&field.ty))
}
pub fn typescript_defs(defs: &BTreeMap<String, Def>, mode: SchemaMode) -> String {
let mut out = String::from(if mode == SchemaMode::Host {
"\nexport type Json = null | boolean | number | string | Json[] | { [key: string]: Json }\n"
} else {
"\nexport type Json = null | boolean | number | string | Json[] | { [key: string]: Json | undefined }\n"
});
for (name, def) in defs {
out.push('\n');
match def {
Def::Object { fields, extra, .. } if fields.is_empty() && !extra => {
if mode == SchemaMode::Host {
let _ = writeln!(out, "export interface {name} {{}}");
} else {
let _ = writeln!(out, "export type {name} = Record<string, never>");
}
}
Def::Object { fields, extra, .. } => {
let declaration = if mode == SchemaMode::Host {
format!("export interface {name}")
} else {
format!("export type {name} =")
};
let _ = writeln!(out, "{declaration} {{");
for field in fields {
let _ = writeln!(out, " {}", member(field));
}
if *extra {
out.push_str(" [key: string]: Json | undefined\n");
}
out.push_str("}\n");
}
Def::Enum(values) => {
let values: Vec<String> = values.iter().map(|v| quote(v)).collect();
let _ = writeln!(out, "export type {name} = {}", values.join(" | "));
}
Def::Union(members) => {
let members: Vec<String> = members
.iter()
.map(|fields| {
// Tags first, then the variant's fields in order.
let (tags, rest): (Vec<&Field>, Vec<&Field>) = fields
.iter()
.partition(|field| matches!(field.ty, Ty::Const(_)));
let parts: Vec<String> = tags.into_iter().chain(rest).map(member).collect();
format!("{{ {} }}", parts.join("; "))
})
.collect();
let _ = writeln!(out, "export type {name} = {}", members.join(" | "));
}
}
}
out
}
File diff suppressed because it is too large. Load diff
@@ -1,633 +0,0 @@
#[path = "../src/codegen/mod.rs"]
mod codegen;
use codewhale_app_server_protocol::methods::{
self, ClientNotificationMethod, ClientRequestMethod, Disposition, ServerNotificationMethod,
ServerRequestMethod, Tier, error_code,
};
use codewhale_app_server_protocol::rpc::RpcCall;
use codewhale_app_server_protocol::v2;
use serde_json::{Map, Value, json};
use std::collections::{BTreeMap, BTreeSet};
const MANIFEST: &str = include_str!("../contract/codex-v2.json");
const PINNED: &str = include_str!("pinned/codex/codex_app_server_protocol.schemas.json");
const COMMON: &str = include_str!("pinned/codex/common.rs");
fn manifest() -> Value {
serde_json::from_str(MANIFEST).unwrap()
}
fn pinned() -> Value {
serde_json::from_str(PINNED).unwrap()
}
#[test]
fn generated_outputs_have_not_drifted_from_the_manifest_and_pinned_export() {
let package = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let read = |path: &str| std::fs::read_to_string(package.join(path)).unwrap();
assert_eq!(read(codegen::MANIFEST_PATH), MANIFEST);
assert_eq!(read(codegen::PINNED_SCHEMA_PATH), PINNED);
let outputs = codegen::generate(MANIFEST, PINNED).unwrap();
let committed: BTreeMap<&str, &str> = BTreeMap::from([
("src/methods.rs", include_str!("../src/methods.rs")),
("src/v2.rs", include_str!("../src/v2.rs")),
("METHODS.md", include_str!("../METHODS.md")),
(
"schema/json/codex-v2.schema.json",
include_str!("../schema/json/codex-v2.schema.json"),
),
(
"schema/typescript/codex-v2.d.ts",
include_str!("../schema/typescript/codex-v2.d.ts"),
),
]);
assert_eq!(outputs.files.len(), committed.len());
for (path, text) in outputs.files {
assert!(
committed[path] == text,
"{path} drifted; run cargo run -p codewhale-app-server-protocol --bin codewhale-codex-contract -- --write"
);
}
}
const KINDS: [(&str, &str, &str); 4] = [
(
"clientRequests",
include_str!("pinned/codex/ClientRequest.ts"),
"ClientRequest",
),
(
"clientNotifications",
include_str!("pinned/codex/ClientNotification.ts"),
"ClientNotification",
),
(
"serverRequests",
include_str!("pinned/codex/ServerRequest.ts"),
"ServerRequest",
),
(
"serverNotifications",
include_str!("pinned/codex/ServerNotification.ts"),
"ServerNotification",
),
];
fn pinned_ts_methods(source: &str) -> Vec<&str> {
source
.split("\"method\": \"")
.skip(1)
.map(|rest| rest.split('"').next().unwrap())
.collect()
}
fn typed(kind: &str) -> Vec<(&'static str, Tier)> {
match kind {
"clientRequests" => ClientRequestMethod::ALL
.iter()
.map(|method| (method.method(), method.tier()))
.collect(),
"clientNotifications" => ClientNotificationMethod::ALL
.iter()
.map(|method| (method.method(), method.tier()))
.collect(),
"serverRequests" => ServerRequestMethod::ALL
.iter()
.map(|method| (method.method(), method.tier()))
.collect(),
_ => ServerNotificationMethod::ALL
.iter()
.map(|method| (method.method(), method.tier()))
.collect(),
}
}
fn disposition(kind: &str, method: &str) -> Option<Disposition> {
match kind {
"clientRequests" => methods::client_request_disposition(method),
"clientNotifications" => methods::client_notification_disposition(method),
"serverRequests" => methods::server_request_disposition(method),
_ => methods::server_notification_disposition(method),
}
}
fn upstream_response(method: &str) -> String {
let marker = format!("=> \"{method}\" {{");
let start = COMMON
.find(&marker)
.unwrap_or_else(|| panic!("pinned common.rs has no {method}"));
let rest = &COMMON[start..];
let end = rest.find("\n },").unwrap();
let body = &rest[..end];
let response = body
.split("response: ")
.nth(1)
.unwrap_or_else(|| panic!("{method} has no response in common.rs"));
let ty = response.split(',').next().unwrap().trim();
ty.rsplit("::").next().unwrap().to_owned()
}
fn pinned_union_params(pinned: &Value, union: &str, method: &str) -> Option<String> {
pinned["definitions"][union]["oneOf"]
.as_array()
.unwrap()
.iter()
.find(|variant| variant["properties"]["method"]["enum"][0] == method)
.unwrap_or_else(|| panic!("pinned {union} has no {method}"))["properties"]["params"]["$ref"]
.as_str()
.map(|reference| reference.rsplit('/').next().unwrap().to_owned())
}
#[test]
fn manifest_inventory_matches_the_pinned_method_sets_and_typed_enums() {
let manifest = manifest();
let pinned = pinned();
let mut totals: BTreeMap<String, usize> = BTreeMap::new();
for (kind, ts, union) in KINDS {
let pinned_methods = pinned_ts_methods(ts);
let pinned_set: BTreeSet<&str> = pinned_methods.iter().copied().collect();
assert_eq!(pinned_methods.len(), pinned_set.len(), "{kind}: duplicate");
let rows = manifest["methods"][kind].as_array().unwrap();
let mut seen = BTreeSet::new();
for row in rows {
let method = row["method"].as_str().unwrap();
let tier = row["tier"].as_str().unwrap();
let status = row["status"].as_str().unwrap();
assert!(seen.insert(method), "{kind}: {method} twice");
assert!(pinned_set.contains(method), "{kind}: {method} not pinned");
assert!(matches!(tier, "A" | "B" | "C" | "undecided"));
assert!(matches!(
status,
"implemented" | "partial" | "missing" | "method-not-found" | "never-sent"
));
assert_eq!(
tier == "C",
matches!(status, "method-not-found" | "never-sent"),
"{kind}: {method} status {status} does not fit tier {tier}"
);
assert_eq!(
row.get("recommended").is_some(),
tier == "undecided",
"{kind}: {method} recommendation belongs to undecided rows only"
);
let expected = match tier {
"A" => Disposition::Selected(Tier::A),
"B" => Disposition::Selected(Tier::B),
"C" => Disposition::C,
_ => Disposition::Undecided,
};
assert_eq!(
disposition(kind, method),
Some(expected),
"{kind}: {method}"
);
*totals.entry(format!("{kind}/{tier}")).or_default() += 1;
if matches!(tier, "A" | "B") {
let params = pinned_union_params(&pinned, union, method);
assert_eq!(
row["params"].as_str().map(str::to_owned),
params,
"{kind}: {method} params disagree with the pinned {union} union"
);
if matches!(kind, "clientRequests" | "serverRequests") {
assert_eq!(
row["response"].as_str().unwrap(),
upstream_response(method),
"{kind}: {method} result disagrees with pinned common.rs"
);
}
}
}
assert_eq!(
seen, pinned_set,
"{kind}: manifest and pinned export differ"
);
let typed_set: BTreeSet<&str> = typed(kind).iter().map(|(method, _)| *method).collect();
let selected: BTreeSet<&str> = rows
.iter()
.filter(|row| matches!(row["tier"].as_str(), Some("A" | "B")))
.map(|row| row["method"].as_str().unwrap())
.collect();
assert_eq!(
typed_set, selected,
"{kind}: typed enum and selected rows differ"
);
}
eprintln!("inventory totals: {totals:?}");
assert_eq!(totals["clientRequests/A"] + totals["clientRequests/B"], 44);
assert_eq!(totals["serverRequests/A"], 5);
assert_eq!(
totals["serverNotifications/A"] + totals["serverNotifications/B"],
33
);
}
#[test]
fn typed_methods_round_trip_their_wire_names() {
for method in ClientRequestMethod::ALL {
assert_eq!(
ClientRequestMethod::from_method(method.method()),
Some(*method)
);
let wire = serde_json::to_value(method).unwrap();
assert_eq!(wire, method.method());
assert_eq!(
serde_json::from_value::<ClientRequestMethod>(wire).unwrap(),
*method
);
}
for method in ServerNotificationMethod::ALL {
assert_eq!(
ServerNotificationMethod::from_method(method.method()),
Some(*method)
);
}
for method in ServerRequestMethod::ALL {
assert_eq!(
ServerRequestMethod::from_method(method.method()),
Some(*method)
);
}
assert!(serde_json::from_value::<ClientRequestMethod>("feedback/upload".into()).is_err());
}
#[derive(Clone, Copy, PartialEq)]
enum Mode {
Full,
Minimal,
}
struct Instances {
definitions: Value,
}
const DEPTH: usize = 5;
impl Instances {
fn new() -> Self {
let mut definitions = pinned()["definitions"].clone();
definitions["codewhale"] = manifest()["extensions"]["definitions"].clone();
Instances { definitions }
}
fn resolve(&self, reference: &str) -> &Value {
let mut node = &self.definitions;
for part in reference.trim_start_matches("#/definitions/").split('/') {
node = &node[part];
}
assert!(!node.is_null(), "unresolved {reference}");
node
}
fn lookup(&self, name: &str) -> Option<(String, &Value)> {
for namespace in ["v2/", "", "codewhale/"] {
let reference = format!("#/definitions/{namespace}{name}");
let mut node = &self.definitions;
for part in reference.trim_start_matches("#/definitions/").split('/') {
node = &node[part];
}
if !node.is_null() && !(namespace.is_empty() && name == "v2") {
return Some((reference, node));
}
}
None
}
fn validator(&self, reference: &str) -> jsonschema::Validator {
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": self.definitions,
"allOf": [{"$ref": reference}],
});
jsonschema::validator_for(&schema).unwrap()
}
fn members<'a>(&self, schema: &'a Value) -> Option<&'a Vec<Value>> {
schema["oneOf"].as_array().or(schema["anyOf"].as_array())
}
fn value(&self, schema: &Value, mode: Mode, depth: usize) -> Value {
if schema == &Value::Bool(true) {
return json!({"any": [1, "x"]});
}
if let Some(reference) = schema["$ref"].as_str() {
return self.value(self.resolve(reference), mode, depth + 1);
}
if let Some(all) = schema["allOf"].as_array() {
return self.value(&all[0], mode, depth);
}
if let Some(members) = self.members(schema) {
let member = members
.iter()
.find(|member| member["type"] != "null")
.unwrap();
return self.member_value(schema, member, mode, depth);
}
if let Some(values) = schema["enum"].as_array() {
return values[0].clone();
}
let ty = match &schema["type"] {
Value::String(ty) => ty.clone(),
Value::Array(list) => list
.iter()
.find(|ty| *ty != "null")
.and_then(Value::as_str)
.unwrap()
.to_owned(),
_ => return json!({"any": [1, "x"]}),
};
match ty.as_str() {
"string" => json!("s"),
"integer" => json!(1),
"boolean" => json!(true),
"array" => {
if mode == Mode::Full && depth < DEPTH {
json!([self.value(&schema["items"], mode, depth + 1)])
} else {
json!([])
}
}
"object" => self.object(schema, mode, depth),
other => panic!("unsupported type {other}"),
}
}
fn object(&self, schema: &Value, mode: Mode, depth: usize) -> Value {
let mut out = Map::new();
let required: BTreeSet<&str> = schema["required"]
.as_array()
.map(|names| names.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
let full = mode == Mode::Full && depth < DEPTH;
if let Some(properties) = schema["properties"].as_object() {
for (name, property) in properties {
if required.contains(name.as_str()) || full {
out.insert(name.clone(), self.value(property, mode, depth + 1));
} else if let Some(default) =
property.get("default").filter(|value| !value.is_null())
{
out.insert(name.clone(), default.clone());
}
}
if full && schema["additionalProperties"] == true {
out.insert("x-additional".into(), json!(1));
}
} else if full {
match &schema["additionalProperties"] {
Value::Object(_) => {
out.insert(
"k".into(),
self.value(&schema["additionalProperties"], mode, depth + 1),
);
}
Value::Bool(true) => {
out.insert("k".into(), json!(1));
}
_ => {}
}
}
Value::Object(out)
}
fn member_value(&self, schema: &Value, member: &Value, mode: Mode, depth: usize) -> Value {
let mut value = self.value(member, mode, depth);
if schema.get("properties").is_some()
&& let (Value::Object(base), Value::Object(extra)) =
(self.object(schema, mode, depth), &mut value)
{
for (key, field) in base {
extra.entry(key).or_insert(field);
}
}
value
}
fn cases(&self, schema: &Value) -> Vec<(String, Value)> {
let mut cases = vec![
("full".to_owned(), self.value(schema, Mode::Full, 0)),
("minimal".to_owned(), self.value(schema, Mode::Minimal, 0)),
];
if let Some(members) = self.members(schema) {
for (index, member) in members.iter().enumerate() {
if member["type"] == "null" {
continue;
}
if let Some(values) = member["enum"].as_array() {
for value in values {
cases.push((format!("member {index} {value}"), value.clone()));
}
} else {
cases.push((
format!("member {index} full"),
self.member_value(schema, member, Mode::Full, 0),
));
cases.push((
format!("member {index} minimal"),
self.member_value(schema, member, Mode::Minimal, 0),
));
}
}
} else if let Some(values) = schema["enum"].as_array() {
for value in values {
cases.push((format!("enum {value}"), value.clone()));
}
}
cases
}
}
#[test]
fn every_selected_shape_round_trips_against_the_pinned_schema() {
let instances = Instances::new();
let mut checked = 0usize;
let mut typed = 0usize;
let mut inline = Vec::new();
for name in v2::TYPE_NAMES {
let Some((reference, schema)) = instances.lookup(name) else {
inline.push(*name);
continue;
};
typed += 1;
let validator = instances.validator(&reference);
for (case, value) in instances.cases(schema) {
assert!(
validator.is_valid(&value),
"{name} {case}: generated instance is not valid upstream: {value}"
);
let again = v2::round_trip(name, value.clone())
.unwrap()
.unwrap_or_else(|error| {
panic!("{name} {case}: typed decode failed: {error}: {value}")
});
assert!(
validator.is_valid(&again),
"{name} {case}: typed output is not valid upstream: {again}"
);
assert_eq!(
again, value,
"{name} {case}: typed round trip changed the value"
);
checked += 1;
}
}
eprintln!(
"typed definitions with a pinned schema: {typed}; inline helper types: {}; round-tripped instances: {checked}",
inline.len()
);
assert!(typed > 250 && checked > typed * 2);
}
#[test]
fn typed_envelopes_match_the_pinned_unions_without_jsonrpc() {
let instances = Instances::new();
let client_request = instances.validator("#/definitions/ClientRequest");
let server_request = instances.validator("#/definitions/ServerRequest");
let server_notification = instances.validator("#/definitions/ServerNotification");
let client_notification = instances.validator("#/definitions/ClientNotification");
let mut counts = BTreeMap::new();
for method in ClientRequestMethod::ALL {
let (_, params_schema) = instances.lookup(method.params_type().unwrap()).unwrap();
let (_, result_schema) = instances.lookup(method.response_type()).unwrap();
for mode in [Mode::Full, Mode::Minimal] {
let params = instances.value(params_schema, mode, 0);
let envelope = json!({"id": 7, "method": method.method(), "params": params});
assert!(client_request.is_valid(&envelope), "{method}: {envelope}");
let call: RpcCall = serde_json::from_value(envelope).unwrap();
let typed: v2::ClientRequestParams =
serde_json::from_value(json!({"method": call.method, "params": call.params}))
.unwrap_or_else(|error| panic!("{method}: {error}"));
assert_eq!(typed.method(), *method);
let encoded = serde_json::to_value(&typed).unwrap();
assert!(encoded.get("jsonrpc").is_none());
assert_eq!(encoded["params"], params);
let result = instances.value(result_schema, mode, 0);
let response = v2::ClientResponse::decode(*method, result.clone()).unwrap();
assert_eq!(response.method(), *method);
assert_eq!(serde_json::to_value(&response).unwrap(), result);
}
*counts.entry("client requests").or_insert(0) += 1;
}
for method in ServerRequestMethod::ALL {
let (_, params_schema) = instances.lookup(method.params_type().unwrap()).unwrap();
let (_, result_schema) = instances.lookup(method.response_type()).unwrap();
for mode in [Mode::Full, Mode::Minimal] {
let params = instances.value(params_schema, mode, 0);
let typed: v2::ServerRequestParams =
serde_json::from_value(json!({"method": method.method(), "params": params}))
.unwrap();
let mut encoded = serde_json::to_value(&typed).unwrap();
encoded["id"] = json!("server-1");
assert!(server_request.is_valid(&encoded), "{method}: {encoded}");
let result = instances.value(result_schema, mode, 0);
let answer = v2::ServerRequestResponse::decode(*method, result.clone()).unwrap();
assert_eq!(answer.method(), *method);
assert_eq!(serde_json::to_value(&answer).unwrap(), result);
}
*counts.entry("server requests").or_insert(0) += 1;
}
for method in ServerNotificationMethod::ALL {
let (_, params_schema) = instances.lookup(method.params_type().unwrap()).unwrap();
for mode in [Mode::Full, Mode::Minimal] {
let params = instances.value(params_schema, mode, 0);
let typed: v2::ServerNotification =
serde_json::from_value(json!({"method": method.method(), "params": params}))
.unwrap();
assert_eq!(typed.method(), *method);
let encoded = serde_json::to_value(&typed).unwrap();
assert!(encoded.get("jsonrpc").is_none() && encoded.get("id").is_none());
assert!(
server_notification.is_valid(&encoded),
"{method}: {encoded}"
);
}
*counts.entry("server notifications").or_insert(0) += 1;
}
let initialized = serde_json::to_value(v2::ClientNotification::Initialized).unwrap();
assert_eq!(initialized, json!({"method": "initialized"}));
assert!(client_notification.is_valid(&initialized));
eprintln!("typed envelopes checked: {counts:?}");
assert_eq!(counts["client requests"], 44);
assert_eq!(counts["server requests"], 5);
assert_eq!(counts["server notifications"], 33);
}
#[test]
fn tier_c_and_undecided_methods_are_method_not_found_and_never_typed() {
let manifest = manifest();
let mut refused = 0;
for (kind, _, _) in KINDS {
for row in manifest["methods"][kind].as_array().unwrap() {
if matches!(row["tier"].as_str(), Some("A" | "B")) {
continue;
}
let method = row["method"].as_str().unwrap();
assert!(!disposition(kind, method).unwrap().selected());
let envelope = json!({"method": method, "params": {}});
match kind {
"clientRequests" => {
assert_eq!(
methods::unselected_request_error(method),
Some(error_code::METHOD_NOT_FOUND)
);
assert!(ClientRequestMethod::from_method(method).is_none());
assert!(serde_json::from_value::<v2::ClientRequestParams>(envelope).is_err());
}
"serverRequests" => {
assert!(ServerRequestMethod::from_method(method).is_none());
assert!(serde_json::from_value::<v2::ServerRequestParams>(envelope).is_err());
}
"serverNotifications" => {
assert!(ServerNotificationMethod::from_method(method).is_none());
assert!(serde_json::from_value::<v2::ServerNotification>(envelope).is_err());
}
_ => {}
}
refused += 1;
}
}
assert_eq!(error_code::METHOD_NOT_FOUND, -32601);
assert_eq!(methods::unselected_request_error("thread/start"), None);
assert_eq!(
methods::unselected_request_error("not/in/the/export"),
Some(error_code::METHOD_NOT_FOUND)
);
eprintln!("unselected methods refused: {refused}");
assert!(refused > 100);
}
#[test]
fn codewhale_extensions_stay_optional_and_valid_upstream() {
let instances = Instances::new();
let thread = instances.validator("#/definitions/v2/Thread");
let (_, schema) = instances.lookup("Thread").unwrap();
let mut value = instances.value(schema, Mode::Minimal, 0);
let plain: v2::Thread = serde_json::from_value(value.clone()).unwrap();
assert!(plain.executor.is_none() && plain.environment.is_none());
assert_eq!(serde_json::to_value(&plain).unwrap(), value);
value["executor"] = json!({"kind": "acp", "agentId": "claude", "agentName": "Claude"});
value["environment"] = json!({"id": "env-1", "providerKind": "remote"});
assert!(thread.is_valid(&value));
let placed: v2::Thread = serde_json::from_value(value.clone()).unwrap();
assert_eq!(placed.executor.as_ref().unwrap().agent_id, "claude");
assert_eq!(serde_json::to_value(&placed).unwrap(), value);
let refusal = instances.validator("#/definitions/codewhale/CapabilityRefusalData");
let data = serde_json::to_value(v2::CapabilityRefusalData {
codewhale_refusal: v2::CapabilityRefusal {
code: v2::CapabilityRefusalCode::EnvironmentNotSupported,
method: Some("turn/start".into()),
thread_id: Some("thread-1".into()),
},
})
.unwrap();
assert!(refusal.is_valid(&data));
assert_eq!(
data["codewhaleRefusal"]["code"],
"environment_not_supported"
);
assert_eq!(error_code::CAPABILITY_UNSUPPORTED, -32020);
assert!(
serde_json::from_value::<v2::CapabilityRefusal>(json!({"code": "no_acp", "token": "x"}))
.is_err()
);
}
@@ -1,88 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ClientInfo": {
"properties": {
"name": {
"type": "string"
},
"title": {
"type": [
"string",
"null"
]
},
"version": {
"type": "string"
}
},
"required": [
"name",
"version"
],
"type": "object"
},
"InitializeCapabilities": {
"description": "Client-declared capabilities negotiated during initialize.",
"properties": {
"experimentalApi": {
"default": false,
"description": "Opt into receiving experimental API methods and fields.",
"type": "boolean"
},
"explicitGatewayOauth": {
"description": "Use explicit gateway OAuth login instead of automatic browser authorization. Applies to this app-server's gateway runtime; later connections cannot undo it.",
"type": "boolean"
},
"extensions": {
"additionalProperties": true,
"description": "MCP extension settings declared by the app-server client.",
"type": [
"object",
"null"
]
},
"mcpServerOpenaiFormElicitation": {
"description": "Legacy opt-in for the `openai/form` MCP extension.\n\nNew clients should declare `openai/form` in [`Self::extensions`].",
"type": "boolean"
},
"optOutNotificationMethods": {
"description": "Exact notification method names that should be suppressed for this connection (for example `thread/started`).",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"requestAttestation": {
"default": false,
"description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.",
"type": "boolean"
}
},
"type": "object"
}
},
"properties": {
"capabilities": {
"anyOf": [
{
"$ref": "#/definitions/InitializeCapabilities"
},
{
"type": "null"
}
]
},
"clientInfo": {
"$ref": "#/definitions/ClientInfo"
}
},
"required": [
"clientInfo"
],
"title": "InitializeParams",
"type": "object"
}
@@ -1,38 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
}
},
"properties": {
"codexHome": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Absolute path to the server's $CODEX_HOME directory."
},
"platformFamily": {
"description": "Platform family for the running app-server target, for example `\"unix\"` or `\"windows\"`.",
"type": "string"
},
"platformOs": {
"description": "Operating system for the running app-server target, for example `\"macos\"`, `\"linux\"`, or `\"windows\"`.",
"type": "string"
},
"userAgent": {
"type": "string"
}
},
"required": [
"codexHome",
"platformFamily",
"platformOs",
"userAgent"
],
"title": "InitializeResponse",
"type": "object"
}
@@ -1,48 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"JSONRPCErrorError": {
"properties": {
"code": {
"format": "int64",
"type": "integer"
},
"data": true,
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
},
"RequestId": {
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
]
}
},
"description": "A response to a request that indicates an error occurred.",
"properties": {
"error": {
"$ref": "#/definitions/JSONRPCErrorError"
},
"id": {
"$ref": "#/definitions/RequestId"
}
},
"required": [
"error",
"id"
],
"title": "JSONRPCError",
"type": "object"
}
@@ -1,137 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"anyOf": [
{
"$ref": "#/definitions/JSONRPCRequest"
},
{
"$ref": "#/definitions/JSONRPCNotification"
},
{
"$ref": "#/definitions/JSONRPCResponse"
},
{
"$ref": "#/definitions/JSONRPCError"
}
],
"definitions": {
"JSONRPCError": {
"description": "A response to a request that indicates an error occurred.",
"properties": {
"error": {
"$ref": "#/definitions/JSONRPCErrorError"
},
"id": {
"$ref": "#/definitions/RequestId"
}
},
"required": [
"error",
"id"
],
"type": "object"
},
"JSONRPCErrorError": {
"properties": {
"code": {
"format": "int64",
"type": "integer"
},
"data": true,
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
},
"JSONRPCNotification": {
"description": "A notification which does not expect a response.",
"properties": {
"method": {
"type": "string"
},
"params": true
},
"required": [
"method"
],
"type": "object"
},
"JSONRPCRequest": {
"description": "A request that expects a response.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"type": "string"
},
"params": true,
"trace": {
"anyOf": [
{
"$ref": "#/definitions/W3cTraceContext"
},
{
"type": "null"
}
],
"description": "Optional W3C Trace Context for distributed tracing."
}
},
"required": [
"id",
"method"
],
"type": "object"
},
"JSONRPCResponse": {
"description": "A successful (non-error) response to a request.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"result": true
},
"required": [
"id",
"result"
],
"type": "object"
},
"RequestId": {
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
]
},
"W3cTraceContext": {
"properties": {
"traceparent": {
"type": [
"string",
"null"
]
},
"tracestate": {
"type": [
"string",
"null"
]
}
},
"type": "object"
}
},
"description": "Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent.",
"title": "JSONRPCMessage"
}
@@ -1,15 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "A notification which does not expect a response.",
"properties": {
"method": {
"type": "string"
},
"params": true
},
"required": [
"method"
],
"title": "JSONRPCNotification",
"type": "object"
}
@@ -1,60 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"RequestId": {
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
]
},
"W3cTraceContext": {
"properties": {
"traceparent": {
"type": [
"string",
"null"
]
},
"tracestate": {
"type": [
"string",
"null"
]
}
},
"type": "object"
}
},
"description": "A request that expects a response.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"type": "string"
},
"params": true,
"trace": {
"anyOf": [
{
"$ref": "#/definitions/W3cTraceContext"
},
{
"type": "null"
}
],
"description": "Optional W3C Trace Context for distributed tracing."
}
},
"required": [
"id",
"method"
],
"title": "JSONRPCRequest",
"type": "object"
}
@@ -1,29 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"RequestId": {
"anyOf": [
{
"type": "string"
},
{
"format": "int64",
"type": "integer"
}
]
}
},
"description": "A successful (non-error) response to a request.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"result": true
},
"required": [
"id",
"result"
],
"title": "JSONRPCResponse",
"type": "object"
}
@@ -1,5 +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.
export type ClientNotification = { "method": "initialized" };
@@ -1,109 +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 { FuzzyFileSearchParams } from "./FuzzyFileSearchParams";
import type { GetAuthStatusParams } from "./GetAuthStatusParams";
import type { GetConversationSummaryParams } from "./GetConversationSummaryParams";
import type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams";
import type { InitializeParams } from "./InitializeParams";
import type { RequestId } from "./RequestId";
import type { AppsInstalledParams } from "./v2/AppsInstalledParams";
import type { AppsListParams } from "./v2/AppsListParams";
import type { AppsReadParams } from "./v2/AppsReadParams";
import type { CancelLoginAccountParams } from "./v2/CancelLoginAccountParams";
import type { CommandExecParams } from "./v2/CommandExecParams";
import type { CommandExecResizeParams } from "./v2/CommandExecResizeParams";
import type { CommandExecTerminateParams } from "./v2/CommandExecTerminateParams";
import type { CommandExecWriteParams } from "./v2/CommandExecWriteParams";
import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams";
import type { ConfigReadParams } from "./v2/ConfigReadParams";
import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams";
import type { ConsumeAccountRateLimitResetCreditParams } from "./v2/ConsumeAccountRateLimitResetCreditParams";
import type { ExperimentalFeatureEnablementSetParams } from "./v2/ExperimentalFeatureEnablementSetParams";
import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureListParams";
import type { ExternalAgentConfigDetectParams } from "./v2/ExternalAgentConfigDetectParams";
import type { ExternalAgentConfigImportHistoryRecordParams } from "./v2/ExternalAgentConfigImportHistoryRecordParams";
import type { ExternalAgentConfigImportParams } from "./v2/ExternalAgentConfigImportParams";
import type { FeedbackSaveParams } from "./v2/FeedbackSaveParams";
import type { FeedbackUploadParams } from "./v2/FeedbackUploadParams";
import type { FsCopyParams } from "./v2/FsCopyParams";
import type { FsCreateDirectoryParams } from "./v2/FsCreateDirectoryParams";
import type { FsGetMetadataParams } from "./v2/FsGetMetadataParams";
import type { FsReadDirectoryParams } from "./v2/FsReadDirectoryParams";
import type { FsReadFileParams } from "./v2/FsReadFileParams";
import type { FsRemoveParams } from "./v2/FsRemoveParams";
import type { FsUnwatchParams } from "./v2/FsUnwatchParams";
import type { FsWatchParams } from "./v2/FsWatchParams";
import type { FsWriteFileParams } from "./v2/FsWriteFileParams";
import type { GetAccountParams } from "./v2/GetAccountParams";
import type { GetAccountRateLimitsParams } from "./v2/GetAccountRateLimitsParams";
import type { GetAccountTokenUsageParams } from "./v2/GetAccountTokenUsageParams";
import type { HooksListParams } from "./v2/HooksListParams";
import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams";
import type { LoginAccountParams } from "./v2/LoginAccountParams";
import type { MarketplaceAddParams } from "./v2/MarketplaceAddParams";
import type { MarketplaceRemoveParams } from "./v2/MarketplaceRemoveParams";
import type { MarketplaceUpgradeParams } from "./v2/MarketplaceUpgradeParams";
import type { McpResourceReadParams } from "./v2/McpResourceReadParams";
import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams";
import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams";
import type { ModelListParams } from "./v2/ModelListParams";
import type { ModelProviderCapabilitiesReadParams } from "./v2/ModelProviderCapabilitiesReadParams";
import type { PermissionProfileListParams } from "./v2/PermissionProfileListParams";
import type { PluginInstallParams } from "./v2/PluginInstallParams";
import type { PluginInstalledParams } from "./v2/PluginInstalledParams";
import type { PluginListParams } from "./v2/PluginListParams";
import type { PluginReadParams } from "./v2/PluginReadParams";
import type { PluginReconcileParams } from "./v2/PluginReconcileParams";
import type { PluginShareCheckoutParams } from "./v2/PluginShareCheckoutParams";
import type { PluginShareDeleteParams } from "./v2/PluginShareDeleteParams";
import type { PluginShareListParams } from "./v2/PluginShareListParams";
import type { PluginShareSaveParams } from "./v2/PluginShareSaveParams";
import type { PluginShareUpdateTargetsParams } from "./v2/PluginShareUpdateTargetsParams";
import type { PluginSkillReadParams } from "./v2/PluginSkillReadParams";
import type { PluginUninstallParams } from "./v2/PluginUninstallParams";
import type { ReviewStartParams } from "./v2/ReviewStartParams";
import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams";
import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams";
import type { SkillsExtraRootsSetParams } from "./v2/SkillsExtraRootsSetParams";
import type { SkillsListParams } from "./v2/SkillsListParams";
import type { ThreadApproveGuardianDeniedActionParams } from "./v2/ThreadApproveGuardianDeniedActionParams";
import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams";
import type { ThreadAttachmentAddParams } from "./v2/ThreadAttachmentAddParams";
import type { ThreadAttachmentListParams } from "./v2/ThreadAttachmentListParams";
import type { ThreadAttachmentOwnerListParams } from "./v2/ThreadAttachmentOwnerListParams";
import type { ThreadAttachmentRemoveParams } from "./v2/ThreadAttachmentRemoveParams";
import type { ThreadCompactStartParams } from "./v2/ThreadCompactStartParams";
import type { ThreadDeleteParams } from "./v2/ThreadDeleteParams";
import type { ThreadForkParams } from "./v2/ThreadForkParams";
import type { ThreadGoalClearParams } from "./v2/ThreadGoalClearParams";
import type { ThreadGoalGetParams } from "./v2/ThreadGoalGetParams";
import type { ThreadGoalSetParams } from "./v2/ThreadGoalSetParams";
import type { ThreadInjectItemsParams } from "./v2/ThreadInjectItemsParams";
import type { ThreadItemsListParams } from "./v2/ThreadItemsListParams";
import type { ThreadListParams } from "./v2/ThreadListParams";
import type { ThreadLoadedListParams } from "./v2/ThreadLoadedListParams";
import type { ThreadMetadataUpdateParams } from "./v2/ThreadMetadataUpdateParams";
import type { ThreadReadParams } from "./v2/ThreadReadParams";
import type { ThreadResumeParams } from "./v2/ThreadResumeParams";
import type { ThreadRevertParams } from "./v2/ThreadRevertParams";
import type { ThreadSectionCreateParams } from "./v2/ThreadSectionCreateParams";
import type { ThreadSectionDeleteParams } from "./v2/ThreadSectionDeleteParams";
import type { ThreadSectionListParams } from "./v2/ThreadSectionListParams";
import type { ThreadSectionMoveParams } from "./v2/ThreadSectionMoveParams";
import type { ThreadSectionUpdateParams } from "./v2/ThreadSectionUpdateParams";
import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams";
import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams";
import type { ThreadStartParams } from "./v2/ThreadStartParams";
import type { ThreadTurnsListParams } from "./v2/ThreadTurnsListParams";
import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams";
import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams";
import type { TurnInterruptParams } from "./v2/TurnInterruptParams";
import type { TurnStartParams } from "./v2/TurnStartParams";
import type { TurnSteerParams } from "./v2/TurnSteerParams";
import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupStartParams";
/**
* Request from the client to the server.
*/
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/attachment/add", id: RequestId, params: ThreadAttachmentAddParams, } | { "method": "thread/attachment/list", id: RequestId, params: ThreadAttachmentListParams, } | { "method": "thread/attachmentOwner/list", id: RequestId, params: ThreadAttachmentOwnerListParams, } | { "method": "thread/attachment/remove", id: RequestId, params: ThreadAttachmentRemoveParams, } | { "method": "thread/section/move", id: RequestId, params: ThreadSectionMoveParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/revert", id: RequestId, params: ThreadRevertParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "threadSection/list", id: RequestId, params: ThreadSectionListParams, } | { "method": "threadSection/create", id: RequestId, params: ThreadSectionCreateParams, } | { "method": "threadSection/update", id: RequestId, params: ThreadSectionUpdateParams, } | { "method": "threadSection/delete", id: RequestId, params: ThreadSectionDeleteParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/items/list", id: RequestId, params: ThreadItemsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/reconcile", id: RequestId, params: PluginReconcileParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/read", id: RequestId, params: AppsReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "app/installed", id: RequestId, params: AppsInstalledParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigLine truncated
@@ -1,93 +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 { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification";
import type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification";
import type { AccountLoginCompletedNotification } from "./v2/AccountLoginCompletedNotification";
import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimitsUpdatedNotification";
import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification";
import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification";
import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification";
import type { AuthRecoveryNotification } from "./v2/AuthRecoveryNotification";
import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification";
import type { CommandExecutionOutputDeltaNotification } from "./v2/CommandExecutionOutputDeltaNotification";
import type { ConfigWarningNotification } from "./v2/ConfigWarningNotification";
import type { ContextCompactedNotification } from "./v2/ContextCompactedNotification";
import type { DeprecationNoticeNotification } from "./v2/DeprecationNoticeNotification";
import type { EnvironmentConnectionNotification } from "./v2/EnvironmentConnectionNotification";
import type { ErrorNotification } from "./v2/ErrorNotification";
import type { ExternalAgentConfigImportCompletedNotification } from "./v2/ExternalAgentConfigImportCompletedNotification";
import type { ExternalAgentConfigImportProgressNotification } from "./v2/ExternalAgentConfigImportProgressNotification";
import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDeltaNotification";
import type { FileChangePatchUpdatedNotification } from "./v2/FileChangePatchUpdatedNotification";
import type { FsChangedNotification } from "./v2/FsChangedNotification";
import type { GatewayOAuthChangedNotification } from "./v2/GatewayOAuthChangedNotification";
import type { GuardianWarningNotification } from "./v2/GuardianWarningNotification";
import type { HookCompletedNotification } from "./v2/HookCompletedNotification";
import type { HookStartedNotification } from "./v2/HookStartedNotification";
import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification";
import type { ItemGuardianApprovalReviewCompletedNotification } from "./v2/ItemGuardianApprovalReviewCompletedNotification";
import type { ItemGuardianApprovalReviewStartedNotification } from "./v2/ItemGuardianApprovalReviewStartedNotification";
import type { ItemStartedNotification } from "./v2/ItemStartedNotification";
import type { McpServerEventStreamNotification } from "./v2/McpServerEventStreamNotification";
import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification";
import type { McpServerStatusUpdatedNotification } from "./v2/McpServerStatusUpdatedNotification";
import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification";
import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification";
import type { ModelSafetyBufferingUpdatedNotification } from "./v2/ModelSafetyBufferingUpdatedNotification";
import type { ModelVerificationNotification } from "./v2/ModelVerificationNotification";
import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification";
import type { ProcessExitedNotification } from "./v2/ProcessExitedNotification";
import type { ProcessOutputDeltaNotification } from "./v2/ProcessOutputDeltaNotification";
import type { ProjectChangedNotification } from "./v2/ProjectChangedNotification";
import type { RawResponseCompletedNotification } from "./v2/RawResponseCompletedNotification";
import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification";
import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification";
import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification";
import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification";
import type { RemoteControlStatusChangedNotification } from "./v2/RemoteControlStatusChangedNotification";
import type { ServerRequestResolvedNotification } from "./v2/ServerRequestResolvedNotification";
import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification";
import type { StrictReviewRequiredNotification } from "./v2/StrictReviewRequiredNotification";
import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification";
import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification";
import type { ThreadAttachmentUpdatedNotification } from "./v2/ThreadAttachmentUpdatedNotification";
import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification";
import type { ThreadDeletedNotification } from "./v2/ThreadDeletedNotification";
import type { ThreadGoalClearedNotification } from "./v2/ThreadGoalClearedNotification";
import type { ThreadGoalUpdatedNotification } from "./v2/ThreadGoalUpdatedNotification";
import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification";
import type { ThreadPredictionUpdatedNotification } from "./v2/ThreadPredictionUpdatedNotification";
import type { ThreadProjectUpdatedNotification } from "./v2/ThreadProjectUpdatedNotification";
import type { ThreadQueueChangedNotification } from "./v2/ThreadQueueChangedNotification";
import type { ThreadReadStateChangedNotification } from "./v2/ThreadReadStateChangedNotification";
import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosedNotification";
import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification";
import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification";
import type { ThreadRealtimeItemCompletedNotification } from "./v2/ThreadRealtimeItemCompletedNotification";
import type { ThreadRealtimeItemStartedNotification } from "./v2/ThreadRealtimeItemStartedNotification";
import type { ThreadRealtimeItemTranscriptDeltaNotification } from "./v2/ThreadRealtimeItemTranscriptDeltaNotification";
import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification";
import type { ThreadRealtimeSdpNotification } from "./v2/ThreadRealtimeSdpNotification";
import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification";
import type { ThreadRealtimeTranscriptDeltaNotification } from "./v2/ThreadRealtimeTranscriptDeltaNotification";
import type { ThreadRealtimeTranscriptDoneNotification } from "./v2/ThreadRealtimeTranscriptDoneNotification";
import type { ThreadRevertedNotification } from "./v2/ThreadRevertedNotification";
import type { ThreadSettingsUpdatedNotification } from "./v2/ThreadSettingsUpdatedNotification";
import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification";
import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification";
import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification";
import type { ThreadUnarchivedNotification } from "./v2/ThreadUnarchivedNotification";
import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification";
import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification";
import type { TurnModerationMetadataNotification } from "./v2/TurnModerationMetadataNotification";
import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification";
import type { TurnStartedNotification } from "./v2/TurnStartedNotification";
import type { WarningNotification } from "./v2/WarningNotification";
import type { WindowsSandboxSetupCompletedNotification } from "./v2/WindowsSandboxSetupCompletedNotification";
import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldWritableWarningNotification";
/**
* Notification sent from the server to the client.
*/
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/readState/changed", "params": ThreadReadStateChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/attachment/updated", "params": ThreadAttachmentUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/prediction/updated", "params": ThreadPredictionUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/gatewayOAuth/changed", "params": GatewayOAuthChangedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method":Line truncated
@@ -1,19 +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 { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
import type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams";
import type { RequestId } from "./RequestId";
import type { AttestationGenerateParams } from "./v2/AttestationGenerateParams";
import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefreshParams";
import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams";
import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams";
import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams";
import type { McpServerElicitationRequestParams } from "./v2/McpServerElicitationRequestParams";
import type { PermissionsRequestApprovalParams } from "./v2/PermissionsRequestApprovalParams";
import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams";
/**
* Request initiated from the server and sent to the client.
*/
export type ServerRequest ={ "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "attestation/generate", id: RequestId, params: AttestationGenerateParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };
File diff suppressed because it is too large. Load diff
-127
View File
@@ -1,127 +0,0 @@
#![cfg(feature = "schema")]
use codewhale_app_server_protocol::runtime::{
DynamicToolCallResult, DynamicToolSpec, RuntimeEventEnvelope, RuntimeImageInput,
};
use std::collections::BTreeMap;
#[test]
fn runtime_schema_matches_real_nullable_and_flattened_wire_fields() {
let schema = codewhale_app_server_protocol::export::runtime_schema();
let definitions = schema["$defs"].as_object().unwrap();
let event = &definitions["RuntimeEventEnvelope"];
let turn = &event["properties"]["turn_id"];
let nullable = turn["type"]
.as_array()
.is_some_and(|types| types.iter().any(|ty| ty == "null"))
|| turn["anyOf"]
.as_array()
.is_some_and(|types| types.iter().any(|ty| ty["type"] == "null"));
assert!(nullable);
let required = event["required"].as_array().unwrap();
for field in ["schema_version", "turn_id", "item_id", "payload"] {
assert!(required.iter().any(|name| name == field));
}
assert!(!required.iter().any(|name| name == "created_at"));
assert_eq!(event["properties"]["created_at"]["type"], "string");
for name in ["RuntimeCapabilities", "RuntimeExperimentalCapabilities"] {
let record = &definitions[name];
let required = record["required"].as_array().unwrap();
for field in record["properties"].as_object().unwrap().keys() {
assert!(
required
.iter()
.any(|name| name.as_str() == Some(field.as_str()))
);
}
}
let spec = &definitions["DynamicToolSpec"];
assert!(
!spec["required"]
.as_array()
.unwrap()
.iter()
.any(|name| name == "defer_loading")
);
let published_spec = &definitions["DynamicToolSpecOutput"];
assert!(
published_spec["required"]
.as_array()
.unwrap()
.iter()
.any(|name| name == "defer_loading")
);
assert_eq!(published_spec["properties"]["namespace"]["type"], "string");
assert!(
!definitions["DynamicToolCallResult"]["required"]
.as_array()
.unwrap()
.iter()
.any(|name| name == "content")
);
let input: DynamicToolSpec = serde_json::from_value(
serde_json::json!({"name":"tool","description":"fixture","input_schema":{}}),
)
.unwrap();
assert!(!input.defer_loading);
let result: DynamicToolCallResult =
serde_json::from_value(serde_json::json!({"success":false})).unwrap();
assert!(result.content.is_empty());
assert!(
serde_json::from_value::<RuntimeImageInput>(
serde_json::json!({"mime":"image/png","dataBase64":"","extra":true})
)
.is_err()
);
assert!(event["properties"].get("extra").is_none());
assert_eq!(
definitions["RuntimeImageInput"]["additionalProperties"],
false
);
let value = serde_json::to_value(RuntimeEventEnvelope {
schema_version: 1,
seq: 4,
event: "future.name".into(),
kind: "future.name".into(),
thread_id: "thread".into(),
turn_id: None,
item_id: None,
timestamp: "now".into(),
created_at: None,
payload: serde_json::json!({"receipt": null}),
extra: BTreeMap::new(),
})
.unwrap();
assert!(value["turn_id"].is_null());
assert!(value.get("created_at").is_none());
let typescript = codewhale_app_server_protocol::export::runtime_typescript(&schema);
assert!(typescript.contains("turn_id: (string | null)"));
assert!(!typescript.contains("turn_id?:"));
assert!(typescript.contains("created_at?: string"));
assert!(typescript.contains("export type RuntimeImageInput ="));
assert!(typescript.contains("Json[] | { [key: string]: Json | undefined }"));
assert!(typescript.contains("type: 'input_text'"));
assert!(typescript.contains("dataBase64: string"));
assert!(typescript.contains("[key: string]: Json | undefined"));
}
#[test]
fn committed_runtime_schema_and_sdk_exports_match_one_rust_contract() {
let package = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let repository = package.parent().and_then(std::path::Path::parent).unwrap();
let schema = codewhale_app_server_protocol::export::runtime_schema();
let json = serde_json::to_string_pretty(&schema).unwrap() + "\n";
let typescript = codewhale_app_server_protocol::export::runtime_typescript(&schema);
assert_eq!(
std::fs::read_to_string(package.join("schema/json/runtime.schema.json")).unwrap(),
json
);
assert_eq!(
std::fs::read_to_string(package.join("schema/typescript/runtime.d.ts")).unwrap(),
typescript
);
assert_eq!(
std::fs::read_to_string(repository.join("npm/runtime-sdk/generated/runtime.d.ts")).unwrap(),
typescript
);
}
-146
View File
@@ -1,146 +0,0 @@
use codewhale_app_server_protocol::rpc::{
RequestId, RpcCall, RpcErrorDetails, RpcErrorResponse, RpcMessage, RpcResponse,
};
use serde_json::{Value, json};
#[test]
fn correlated_rpc_envelopes_match_pinned_stable_schema() {
let request_schema: Value =
serde_json::from_str(include_str!("pinned/JSONRPCRequest.json")).unwrap();
let response_schema: Value =
serde_json::from_str(include_str!("pinned/JSONRPCResponse.json")).unwrap();
let error_schema: Value =
serde_json::from_str(include_str!("pinned/JSONRPCError.json")).unwrap();
let message_schema: Value =
serde_json::from_str(include_str!("pinned/JSONRPCMessage.json")).unwrap();
let notification_schema: Value =
serde_json::from_str(include_str!("pinned/JSONRPCNotification.json")).unwrap();
let message_validator = jsonschema::validator_for(&message_schema).unwrap();
let notification_validator = jsonschema::validator_for(&notification_schema).unwrap();
let request_validator = jsonschema::validator_for(&request_schema).unwrap();
let response_validator = jsonschema::validator_for(&response_schema).unwrap();
let error_validator = jsonschema::validator_for(&error_schema).unwrap();
for id in [
RequestId::String(String::new()),
RequestId::String("request/string".into()),
RequestId::Integer(i64::MIN),
RequestId::Integer(i64::MAX),
] {
let call: RpcCall = serde_json::from_value(json!({"id":id,"method":"initialize","params":{"clientInfo":{"name":"fixture","version":"1"}}})).unwrap();
let request = serde_json::to_value(call).unwrap();
assert!(request.get("jsonrpc").is_none());
assert!(request_validator.is_valid(&request));
assert!(message_validator.is_valid(&request));
assert!(matches!(
serde_json::from_value::<RpcMessage>(request).unwrap(),
RpcMessage::Request(_)
));
let response = serde_json::to_value(RpcResponse {
id: id.clone(),
result: json!({}),
})
.unwrap();
assert!(response_validator.is_valid(&response));
assert!(message_validator.is_valid(&response));
assert!(matches!(
serde_json::from_value::<RpcMessage>(response).unwrap(),
RpcMessage::Response(_)
));
let error = serde_json::to_value(RpcErrorResponse {
id,
error: RpcErrorDetails {
code: -32600,
data: None,
message: "Not initialized".into(),
},
})
.unwrap();
assert!(error_validator.is_valid(&error));
assert!(error["error"].get("data").is_none());
assert!(message_validator.is_valid(&error));
assert!(matches!(
serde_json::from_value::<RpcMessage>(error).unwrap(),
RpcMessage::Error(_)
));
}
let notification: RpcMessage = serde_json::from_value(json!({"method":"initialized"})).unwrap();
let value = serde_json::to_value(notification).unwrap();
assert!(notification_validator.is_valid(&value));
assert!(message_validator.is_valid(&value));
assert!(value.get("id").is_none());
}
#[test]
fn rpc_call_preserves_notification_absence_and_refuses_non_ids() {
let notification: RpcCall =
serde_json::from_value(json!({"jsonrpc":"2.0","method":"initialized"})).unwrap();
let value = serde_json::to_value(notification).unwrap();
assert!(value.get("id").is_none());
assert!(value.get("jsonrpc").is_none());
for id in [
json!(null),
json!(true),
json!(1.0),
json!(u64::MAX),
json!({}),
json!([]),
] {
assert!(serde_json::from_value::<RequestId>(id.clone()).is_err());
let call: RpcCall = serde_json::from_value(json!({"id":id,"method":"shutdown"})).unwrap();
assert!(call.id.is_none());
}
for version in [json!(null), json!(true), json!("1.0")] {
let call: RpcCall =
serde_json::from_value(json!({"jsonrpc":version,"id":1,"method":"initialize"}))
.unwrap();
assert_eq!(call.id, Some(RequestId::Integer(1)));
assert!(serde_json::to_value(call).unwrap().get("jsonrpc").is_none());
}
assert!(serde_json::from_value::<RpcCall>(json!({"id":1,"method":7})).is_err());
assert!(serde_json::from_value::<RpcMessage>(json!({"id":null,"result":{}})).is_err());
assert!(
serde_json::from_value::<RpcMessage>(
json!({"id":null,"error":{"code":-32600,"message":"invalid"}})
)
.is_err()
);
assert!(serde_json::from_value::<RpcCall>(json!({"id":1,"result":{}})).is_err());
for params in [json!({}), json!(null), json!(["value"])] {
let message: RpcMessage =
serde_json::from_value(json!({"id":1,"method":"fixture","params":params})).unwrap();
let encoded = serde_json::to_value(message).unwrap();
if params.is_null() {
assert!(encoded.get("params").is_none());
} else {
assert_eq!(encoded["params"], params);
}
}
}
#[test]
fn initialization_contract_matches_pinned_stable_schema() {
use codewhale_app_server_protocol::lifecycle::{InitializeParams, InitializeResponse};
let params_schema: Value =
serde_json::from_str(include_str!("pinned/InitializeParams.json")).unwrap();
let result_schema: Value =
serde_json::from_str(include_str!("pinned/InitializeResponse.json")).unwrap();
let params: InitializeParams = serde_json::from_value(json!({"clientInfo":{"name":"fixture","version":"1"},"capabilities":{"experimentalApi":false,"optOutNotificationMethods":null}})).unwrap();
assert!(
jsonschema::validator_for(&params_schema)
.unwrap()
.is_valid(&serde_json::to_value(params).unwrap())
);
let result = InitializeResponse {
user_agent: "Codewhale/fixture".into(),
codex_home: std::path::PathBuf::from("/fixture/codewhale"),
platform_family: "unix".into(),
platform_os: "macos".into(),
};
let value = serde_json::to_value(result).unwrap();
assert!(
jsonschema::validator_for(&result_schema)
.unwrap()
.is_valid(&value)
);
assert_eq!(value["codexHome"], "/fixture/codewhale");
}
+11 -91
View File
@@ -8,42 +8,24 @@ repository.workspace = true
description = "App-server transport for Codewhale runtime integrations"
# `codewhale app-server` is owned by codewhale-cli; this crate is library-only.
autobins = false
exclude = [
"tests/**",
"src/**/tests.rs",
"src/**/tests/**",
"src/**/*_tests.rs",
"src/**/*test_support.rs",
"src/runtime_web/app.test.mjs",
"src/runtime_web/orca-evidence.html",
]
[lints]
workspace = true
[features]
default = []
test-support = ["codewhale-engine/test-support"]
[dependencies]
agent-client-protocol.workspace = true
codewhale-account-cli = { path = "../account-cli", version = "0.10.3" }
codewhale-app-server-protocol = { path = "../app-server-protocol", version = "0.10.3" }
anyhow.workspace = true
axum.workspace = true
chrono.workspace = true
codewhale-agent = { path = "../agent", version = "0.10.3" }
codewhale-config = { path = "../config", version = "0.10.3" }
codewhale-core = { path = "../core", version = "0.10.3" }
codewhale-hooks = { path = "../hooks", version = "0.10.3" }
codewhale-palette = { path = "../palette", version = "0.10.3", default-features = false }
codewhale-paths = { path = "../paths", version = "0.10.3" }
codewhale-protocol = { path = "../protocol", version = "0.10.3" }
codewhale-release = { path = "../release", version = "0.10.3" }
codewhale-runtime = { path = "../runtime", version = "0.10.3" }
codewhale-state = { path = "../state", version = "0.10.3" }
codewhale-agent = { path = "../agent", version = "0.10.2" }
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-core = { path = "../core", version = "0.10.2" }
codewhale-hooks = { path = "../hooks", version = "0.10.2" }
codewhale-paths = { path = "../paths", version = "0.10.2" }
codewhale-protocol = { path = "../protocol", version = "0.10.2" }
codewhale-release = { path = "../release", version = "0.10.2" }
codewhale-state = { path = "../state", version = "0.10.2" }
serde.workspace = true
serde_json = { workspace = true, features = ["preserve_order", "raw_value"] }
serde_json.workspace = true
rustls.workspace = true
thiserror.workspace = true
tokio.workspace = true
@@ -51,76 +33,14 @@ tower-http.workspace = true
tracing.workspace = true
reqwest = { workspace = true, features = ["json"] }
uuid.workspace = true
# The store-derived control endpoint hashes the runtime store root
# (`daemon_socket::store_selected_socket_path`).
sha2.workspace = true
async-stream = "0.3.6"
base64 = "0.23.1"
clap.workspace = true
codewhale-command-contract = { path = "../command-contract", version = "0.10.3" }
codewhale-dependencies = { path = "../dependencies", version = "0.10.3" }
codewhale-engine = { path = "../engine", version = "0.10.3" }
codewhale-error-taxonomy = { path = "../error-taxonomy", version = "0.10.3" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.10.3" }
codewhale-lane = { path = "../lane", version = "0.10.3" }
codewhale-localization = { path = "../localization", version = "0.10.3" }
codewhale-lsp = { path = "../lsp", version = "0.10.3" }
codewhale-mcp = { path = "../mcp", version = "0.10.3" }
codewhale-memory = { path = "../memory", version = "0.10.3" }
codewhale-models = { path = "../models", version = "0.10.3" }
codewhale-net = { path = "../net", version = "0.10.3" }
codewhale-notify = { path = "../notify", version = "0.10.3" }
codewhale-plugins = { path = "../plugins", version = "0.10.3" }
codewhale-sandbox = { path = "../sandbox", version = "0.10.3" }
codewhale-secrets = { path = "../secrets", version = "0.10.3" }
codewhale-skill-install = { path = "../skill-install", version = "0.10.3" }
codewhale-skills = { path = "../skills", version = "0.10.3" }
codewhale-snapshot = { path = "../snapshot", version = "0.10.3" }
codewhale-telemetry = { path = "../telemetry", version = "0.10.3" }
codewhale-tools = { path = "../tools", version = "0.10.3" }
codewhale-tui-kit = { path = "../tui-kit", version = "0.10.3" }
codewhale-util = { path = "../util", version = "0.10.3" }
colored = "3.0.0"
futures-util = "0.3.31"
tokio-tungstenite = "0.30"
ignore = "0.4"
parking_lot = "0.12"
qrcode = { version = "0.14", default-features = false }
regex = "1.11"
rmcp = { version = "3.9.0", default-features = false, features = ["auth", "client", "server", "transport-io"] }
semver.workspace = true
tempfile.workspace = true
tokio-util = { version = "0.7.16", features = ["io"] }
toml.workspace = true
url = "2.5"
urlencoding = "2.1"
webbrowser = "1.0"
[dev-dependencies]
agent-client-protocol-schema.workspace = true
rusqlite.workspace = true
tempfile.workspace = true
tower = "0.5"
async-trait.workspace = true
codewhale-agent-contract = { path = "../agent-contract", version = "0.10.3", features = [
"test-support",
] }
codewhale-config = { path = "../config", version = "0.10.3", features = ["schema", "test-support"] }
codewhale-engine = { path = "../engine", version = "0.10.3", features = ["test-support"] }
codewhale-notify = { path = "../notify", version = "0.10.3", features = ["test-thread-scoped-state"] }
codewhale-runtime = { path = "../runtime", version = "0.10.3", features = [
"test-support",
"test-thread-scoped-state",
] }
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
rquickjs.workspace = true
rustls.workspace = true
wiremock = "0.6"
reqwest = { workspace = true, features = ["stream"] }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading", "Win32_System_Pipes"] }
[build-dependencies]
codewhale-build-support = { path = "../build-support", version = "0.10.3" }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
-10
View File
@@ -1,10 +0,0 @@
use std::path::PathBuf;
fn main() {
let manifest_dir = PathBuf::from(
std::env::var_os("CARGO_MANIFEST_DIR").expect("Cargo sets CARGO_MANIFEST_DIR"),
);
println!("cargo:rerun-if-env-changed=CARGO_MANIFEST_DIR");
codewhale_build_support::declare_rerun_conditions(&manifest_dir);
codewhale_build_support::emit_build_version(&manifest_dir, env!("CARGO_PKG_VERSION"));
}
-648
View File
@@ -1,648 +0,0 @@
#[cfg(test)]
use crate::stdio::{emit_stdio_event, interrupt_turn_request};
#[cfg(test)]
use crate::turns::{RuntimeTurnInput, extract_runtime_thread_id, turn_terminal_status};
use crate::{AppState, DispatchError, RuntimeBridge, SharedRuntimeBridge, thread_control};
#[cfg(test)]
use crate::{
InFlightTurn, MAX_SSE_FRAME_BYTES, RuntimeThreadHint, TurnExecutionOwner, TurnRegistry,
TurnTerminalStatus, TurnTranscript,
};
use anyhow::Context as _;
use anyhow::{Result, anyhow, bail};
#[cfg(test)]
use codewhale_protocol::{EventFrame, ResponseChannel};
use serde_json::{Value, json};
#[cfg(test)]
use std::collections::HashMap;
#[cfg(test)]
use std::path::Path;
#[cfg(test)]
use std::path::PathBuf;
#[cfg(test)]
use std::process::{Command, Stdio};
#[cfg(all(test, unix))]
use std::sync::Arc;
use std::time::Duration;
#[cfg(test)]
use tokio::io::AsyncWrite;
#[cfg(all(test, unix))]
use tokio::sync::Mutex;
/// Historical cold-child cache comparator. It has no production producer.
#[cfg(all(test, unix))]
pub(crate) async fn acquire_historical_runtime_bridge<F, Fut>(
state: &AppState,
start: F,
) -> std::result::Result<SharedRuntimeBridge, DispatchError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<RuntimeBridge>>,
{
if let Some(bridge) = state.runtime_bridge.lock().await.as_ref() {
return Ok(bridge.clone());
}
if state.captured_owner.is_some() {
return Err(DispatchError::runtime_unavailable(
"the captured Runtime owner is unavailable; refusing another owner",
));
}
let bridge =
Arc::new(Mutex::new(start().await.map_err(|err| {
DispatchError::runtime_unavailable(err.to_string())
})?));
let mut slot = state.runtime_bridge.lock().await;
// Prefer a bridge cached by a concurrent caller while we were spawning;
// dropping our unused one kills the extra child via `Drop`.
Ok(slot.get_or_insert_with(|| bridge.clone()).clone())
}
/// Use the exact bridge installed by the held canonical owner. No process,
/// store or transcript can be created when this captured connection is absent.
pub(crate) async fn acquire_runtime_bridge(
state: &AppState,
) -> std::result::Result<SharedRuntimeBridge, DispatchError> {
state
.runtime_bridge
.lock()
.await
.as_ref()
.cloned()
.ok_or_else(|| {
DispatchError::runtime_unavailable(
"the captured Runtime owner is unavailable; refusing another owner",
)
})
}
#[cfg(test)]
pub(crate) async fn acquire_live_runtime_bridge(
state: &AppState,
) -> std::result::Result<tokio::sync::OwnedMutexGuard<RuntimeBridge>, DispatchError> {
Ok(acquire_runtime_bridge(state).await?.lock_owned().await)
}
/// Historical respawn comparison; all actual frontends use the held owner.
#[cfg(all(test, unix))]
pub(crate) async fn acquire_live_runtime_bridge_with<F, Fut>(
state: &AppState,
start: F,
) -> std::result::Result<tokio::sync::OwnedMutexGuard<RuntimeBridge>, DispatchError>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<RuntimeBridge>>,
{
for _ in 0..2 {
let shared = acquire_historical_runtime_bridge(state, &start).await?;
let mut bridge = shared.clone().lock_owned().await;
if !bridge.child_exited() {
return Ok(bridge);
}
drop(bridge);
let mut slot = state.runtime_bridge.lock().await;
// Evict only the dead bridge: a concurrent caller may already have
// replaced it with a live one.
if slot
.as_ref()
.is_some_and(|cached| Arc::ptr_eq(cached, &shared))
{
*slot = None;
}
}
Err(DispatchError::runtime_unavailable(
"runtime API bridge exited immediately after starting",
))
}
/// Historical config/cache comparator. Captured owners retain their bridge.
#[cfg(test)]
pub(crate) async fn invalidate_runtime_bridge(state: &AppState) {
if state.captured_owner.is_some() {
return;
}
let mut bridge = state.runtime_bridge.lock().await;
*bridge = None;
}
impl RuntimeBridge {
/// The child binds an ephemeral loopback port itself (`--port 0`) and
/// reports it on stdout; the parent never reserves a port for it to race.
#[cfg(test)]
pub(crate) fn runtime_command(config_path: Option<&Path>, auth_token: &str) -> Result<Command> {
let current_exe = std::env::current_exe().ok();
let mut command = if let Some(path) = current_exe {
Command::new(path)
} else {
Command::new("codewhale")
};
// Pass the runtime auth token out-of-band via env (not argv) so local
// `ps` cannot read credential material from the child command line.
// The TUI/runtime server already accepts CODEWHALE_RUNTIME_TOKEN /
// DEEPSEEK_RUNTIME_TOKEN when --auth-token is absent.
command
.arg("app-server")
.arg("--http")
.arg("--host")
.arg("127.0.0.1")
.arg("--port")
.arg("0")
.env("CODEWHALE_RUNTIME_TOKEN", auth_token)
.env("DEEPSEEK_RUNTIME_TOKEN", auth_token)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
if let Some(config_path) = config_path {
command.arg("--config").arg(config_path);
}
Ok(command)
}
pub(crate) fn authed(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
match self.auth_token.as_deref() {
Some(token) => builder.bearer_auth(token),
None => builder,
}
}
pub(crate) async fn request_json(&self, builder: reqwest::RequestBuilder) -> Result<Value> {
thread_control::read_json_response(builder.send().await?).await
}
/// Resolve `stdio_thread_id` to a runtime thread, minting one only when
/// `thread_map` has no entry. The map lives on [`AppState`] and outlives
/// this bridge, so a thread created under a previous child keeps its id
/// here as long as the store it was persisted to is shared (#6246).
#[cfg(test)]
pub(crate) async fn ensure_runtime_thread(
&mut self,
thread_map: &mut HashMap<String, String>,
stdio_thread_id: &str,
hint: Option<RuntimeThreadHint>,
) -> Result<String> {
if let Some(runtime_thread_id) = thread_map.get(stdio_thread_id) {
return Ok(runtime_thread_id.clone());
}
let hint = hint.unwrap_or_default();
let runtime_thread_id = self
.create_runtime_thread(hint.model, hint.workspace)
.await?;
thread_map.insert(stdio_thread_id.to_string(), runtime_thread_id.clone());
Ok(runtime_thread_id)
}
#[cfg(test)]
async fn create_runtime_thread(
&mut self,
model: Option<String>,
workspace: Option<PathBuf>,
) -> Result<String> {
let record = self
.request_json(
self.authed(self.client.post(format!("{}/v1/threads", self.base_url)))
.json(&json!({
"model": model,
"workspace": workspace,
"mode": "agent",
"archived": false,
})),
)
.await?;
let thread_id = extract_runtime_thread_id(&record)?.to_string();
self.last_seq_by_thread
.entry(thread_id.clone())
.or_insert(0);
Ok(thread_id)
}
/// Run one turn to completion, streaming its events to `writer`.
///
/// `registration` is `Some` on the stdio path: it publishes the live turn
/// so an `thread/interrupt` arriving mid-stream can reach the runtime
/// without waiting on the bridge mutex this call holds.
#[cfg(test)]
pub(crate) async fn message_thread<W: AsyncWrite + Unpin>(
&mut self,
thread_id: &str,
input: RuntimeTurnInput<'_>,
writer: &mut W,
registration: Option<(TurnRegistry, String)>,
mut transcript: Option<&mut TurnTranscript>,
) -> Result<Value> {
let RuntimeTurnInput {
input,
images,
max_output_tokens,
expected_workspace,
cancellation: _,
} = input;
let mut request = json!({ "prompt": input, "allowed_tools": [] });
if let Some(workspace) = expected_workspace {
request["expected_workspace"] = json!(workspace);
}
if !images.is_empty() {
let info = self
.request_json(
self.authed(
self.client
.get(format!("{}/v1/runtime/info", self.base_url)),
),
)
.await?;
if info
.pointer("/capabilities/turn_image_inputs")
.and_then(Value::as_bool)
!= Some(true)
{
bail!(
"Runtime image input is unavailable; update the Runtime before sending attachments"
);
}
request["images"] = json!(images);
}
if let Some(limit) = max_output_tokens {
request["maxOutputTokens"] = json!(limit);
}
let turn = self
.request_json(
self.authed(
self.client
.post(format!("{}/v1/threads/{thread_id}/turns", self.base_url)),
)
.json(&request),
)
.await?;
let turn_id = turn
.pointer("/turn/id")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("runtime API turn response missing turn.id"))?
.to_string();
let response_id = format!("{thread_id}:{turn_id}");
if let Some(transcript) = transcript.as_deref_mut() {
transcript.model = turn
.pointer("/thread/model")
.and_then(Value::as_str)
.map(str::to_string);
transcript.events.push(EventFrame::ResponseStart {
response_id: response_id.clone(),
});
}
if let Err(error) = emit_stdio_event(
writer,
json!({
"type": "response_start",
"response_id": response_id,
}),
)
.await
{
let orphan = InFlightTurn {
owner: TurnExecutionOwner::HistoricalHttp {
base_url: self.base_url.clone(),
auth_token: self.auth_token.clone(),
},
runtime_thread_id: thread_id.to_string(),
turn_id: turn_id.clone(),
connection: None,
};
if let Err(interrupt) = interrupt_turn_request(&orphan).await {
tracing::warn!(
thread_id,
turn_id = %turn_id,
"failed to interrupt a turn whose response_start could not be written: {}",
interrupt.wire.message
);
}
return Err(error);
}
// Publish the turn only for the streaming window, and take it back
// before any `?` below: a turn that has already finished must never
// look cancellable.
if let Some((registry, key)) = registration.as_ref() {
registry.lock().await.insert(
key.clone(),
InFlightTurn {
owner: TurnExecutionOwner::HistoricalHttp {
base_url: self.base_url.clone(),
auth_token: self.auth_token.clone(),
},
runtime_thread_id: thread_id.to_string(),
turn_id: turn_id.clone(),
connection: None,
},
);
}
let since_seq = self.last_seq_by_thread.get(thread_id).copied().unwrap_or(0);
let stream_result = self
.stream_turn_events(
thread_id,
&turn_id,
&response_id,
writer,
since_seq,
transcript.as_deref_mut(),
)
.await;
if let Some((registry, key)) = registration.as_ref() {
registry.lock().await.remove(key);
}
if stream_result.is_ok() {
emit_stdio_event(
writer,
json!({
"type": "response_end",
"response_id": response_id,
}),
)
.await?;
if let Some(transcript) = transcript {
transcript.events.push(EventFrame::ResponseEnd {
response_id: response_id.clone(),
});
}
} else {
// The stream broke before `turn.completed` (transport error,
// oversized or invalid frame, a writer that went away). Ending
// the response here reported success ahead of the error, and the
// runtime turn kept running with nothing left able to interrupt
// it (the registry entry is gone), so the thread refused its next
// message until the orphan finished. Stop it, best effort, and
// let the error below be the only outcome the client sees.
let orphan = InFlightTurn {
owner: TurnExecutionOwner::HistoricalHttp {
base_url: self.base_url.clone(),
auth_token: self.auth_token.clone(),
},
runtime_thread_id: thread_id.to_string(),
turn_id: turn_id.clone(),
connection: None,
};
if let Err(error) = interrupt_turn_request(&orphan).await {
tracing::warn!(
thread_id,
turn_id = %turn_id,
"failed to interrupt a turn whose event stream broke: {}",
error.wire.message
);
}
}
let (last_seq, status, error) = stream_result?;
self.last_seq_by_thread
.insert(thread_id.to_string(), last_seq);
match status {
TurnTerminalStatus::Completed => Ok(json!({
"thread_id": thread_id,
"status": "accepted",
"thread": Value::Null,
"threads": [],
"model": Value::Null,
"model_provider": Value::Null,
"cwd": Value::Null,
"approval_policy": Value::Null,
"sandbox": Value::Null,
"events": [],
"data": { "turn_id": turn_id },
})),
TurnTerminalStatus::Failed => Err(anyhow!(
"{}",
error.unwrap_or_else(|| "turn failed".to_string())
)),
TurnTerminalStatus::Interrupted => Err(anyhow!(
"{}",
error.unwrap_or_else(|| "turn interrupted".to_string())
)),
TurnTerminalStatus::Canceled => Err(anyhow!(
"{}",
error.unwrap_or_else(|| "turn canceled".to_string())
)),
}
}
#[cfg(test)]
async fn stream_turn_events<W: AsyncWrite + Unpin>(
&self,
thread_id: &str,
turn_id: &str,
response_id: &str,
writer: &mut W,
since_seq: u64,
mut transcript: Option<&mut TurnTranscript>,
) -> Result<(u64, TurnTerminalStatus, Option<String>)> {
let mut response = self
.authed(self.client.get(format!(
"{}/v1/threads/{thread_id}/events?since_seq={since_seq}",
self.base_url
)))
.send()
.await?
.error_for_status()?;
let mut buffer = Vec::new();
let mut last_seq = since_seq;
while let Some(chunk) = response.chunk().await? {
buffer.extend_from_slice(&chunk);
if buffer.len() > MAX_SSE_FRAME_BYTES {
bail!(
"runtime SSE frame exceeded {MAX_SSE_FRAME_BYTES} bytes without a frame delimiter"
);
}
while let Some(frame_bytes) = take_sse_frame(&mut buffer) {
let Some((event_name, frame_data)) = parse_sse_frame(&frame_bytes) else {
continue;
};
let envelope: Value = serde_json::from_str(&frame_data)
.with_context(|| format!("invalid SSE json for {event_name}: {frame_data}"))?;
if let Some(seq) = envelope.get("seq").and_then(Value::as_u64) {
last_seq = last_seq.max(seq);
}
if envelope.get("turn_id").and_then(Value::as_str) != Some(turn_id) {
continue;
}
let payload = envelope.get("payload").cloned().unwrap_or(Value::Null);
match event_name.as_str() {
"approval.required" | "user_input.required" => {
bail!(
"interactive_execution_unsupported: Runtime requested interaction during a tool-free compatibility turn; use codewhale app-server --http and /v1/*"
);
}
"item.delta" => {
let kind = payload
.get("kind")
.and_then(Value::as_str)
.unwrap_or_default();
if kind == "agent_message"
&& let Some(delta) = payload.get("delta").and_then(Value::as_str)
&& !delta.is_empty()
{
emit_stdio_event(
writer,
json!({
"type": "response_delta",
"response_id": response_id,
"delta": delta,
}),
)
.await?;
if let Some(transcript) = transcript.as_deref_mut() {
transcript.text.push_str(delta);
transcript.events.push(EventFrame::ResponseDelta {
response_id: response_id.to_string(),
delta: delta.to_string(),
channel: ResponseChannel::Text,
});
}
}
}
"turn.completed" => {
let status = turn_terminal_status(&payload)?;
let error = payload
.pointer("/turn/error")
.and_then(Value::as_str)
.map(str::to_string);
return Ok((last_seq, status, error));
}
_ => {}
}
}
}
bail!("runtime event stream ended before turn.completed")
}
#[cfg(test)]
pub(crate) fn from_base_url_for_test(base_url: String) -> Self {
install_rustls_crypto_provider();
Self {
base_url,
client: codewhale_release::platform_http_client_builder()
.timeout(Duration::from_secs(5))
.build()
.expect("build reqwest test client"),
auth_token: None,
child: None,
last_seq_by_thread: HashMap::new(),
}
}
}
#[cfg(test)]
impl RuntimeBridge {
/// Whether the historical fixture child has exited. A bridge without a child (tests,
/// or an externally managed runtime) never reports exited. A `try_wait`
/// error counts as exited: with `WNOHANG` it only fails when the pid is no
/// longer this process's child (already reaped elsewhere), and such a
/// child can neither be tracked nor killed on drop.
#[cfg(unix)]
fn child_exited(&mut self) -> bool {
self.child
.as_mut()
.is_some_and(|child| !matches!(child.try_wait(), Ok(None)))
}
/// Kills the managed runtime child and reaps it on a detached thread so
/// neither an explicit shutdown nor Drop blocks a Tokio runtime thread.
fn shutdown_child(&mut self) {
if let Some(mut child) = self.child.take() {
let _ = child.kill();
std::thread::spawn(move || {
let _ = child.wait();
});
}
}
}
#[cfg(test)]
impl Drop for RuntimeBridge {
fn drop(&mut self) {
self.shutdown_child();
}
}
/// The line the Runtime prints once it holds its listener (the Runtime's
/// `RUNTIME_LISTENING_PREFIX`; this crate does not depend on that one).
#[cfg(test)]
pub(crate) const RUNTIME_LISTENING_PREFIX: &str = "Runtime API listening on http://";
/// The endpoint line is short and comes first; anything larger is not it.
#[cfg(test)]
pub(crate) const RUNTIME_READY_MAX_BYTES: usize = 1024;
/// The endpoint a Runtime child reports for itself: exactly a nonzero port on
/// `127.0.0.1`, the host the parent asked it to bind.
#[cfg(test)]
pub(crate) fn parse_runtime_endpoint(line: &str) -> Result<std::net::SocketAddr> {
let address = line
.trim_end()
.strip_prefix(RUNTIME_LISTENING_PREFIX)
.context("runtime API bridge did not report its endpoint")?;
let endpoint: std::net::SocketAddr = address
.parse()
.context("runtime API bridge reported an invalid endpoint")?;
if endpoint.ip() != std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST) || endpoint.port() == 0
{
bail!("runtime API bridge reported an endpoint that is not a loopback port");
}
Ok(endpoint)
}
/// Read the first stdout line, bounded.
#[cfg(test)]
pub(crate) fn read_runtime_ready_line(stdout: &mut impl std::io::Read) -> Result<String> {
let mut line = Vec::new();
let mut byte = [0u8; 1];
loop {
if stdout.read(&mut byte)? == 0 {
bail!("runtime API bridge closed stdout before reporting its endpoint");
}
if byte[0] == b'\n' {
break;
}
if line.len() >= RUNTIME_READY_MAX_BYTES {
bail!("runtime API bridge sent an oversized readiness line");
}
line.push(byte[0]);
}
String::from_utf8(line).context("runtime API bridge readiness line is not UTF-8")
}
pub(crate) fn install_rustls_crypto_provider() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
#[cfg(test)]
pub(crate) fn take_sse_frame(buffer: &mut Vec<u8>) -> Option<Vec<u8>> {
if let Some(pos) = buffer.windows(4).position(|window| window == b"\r\n\r\n") {
return Some(buffer.drain(..pos + 4).collect());
}
buffer
.windows(2)
.position(|window| window == b"\n\n")
.map(|pos| buffer.drain(..pos + 2).collect())
}
#[cfg(test)]
pub(crate) fn parse_sse_frame(frame_bytes: &[u8]) -> Option<(String, String)> {
let text = String::from_utf8(frame_bytes.to_vec()).ok()?;
let mut event_name = None;
let mut data_lines = Vec::new();
for raw_line in text.lines() {
let line = raw_line.trim_end_matches('\r');
if let Some(value) = line.strip_prefix("event:") {
event_name = Some(value.trim().to_string());
} else if let Some(value) = line.strip_prefix("data:") {
data_lines.push(value.trim_start().to_string());
}
}
match (event_name, data_lines.is_empty()) {
(Some(event), false) => Some((event, data_lines.join("\n"))),
_ => None,
}
}
File diff suppressed because it is too large. Load diff
-276
View File
@@ -1,276 +0,0 @@
#[cfg(test)]
use crate::bridge::invalidate_runtime_bridge;
use crate::http::ADVERTISED_ROUTES;
use crate::owner_calls::runtime_app_models;
use crate::{AppState, AppTransport};
use anyhow::Context as _;
#[cfg(not(test))]
use anyhow::bail;
use anyhow::{Result, anyhow};
use codewhale_config::ConfigStore;
use codewhale_protocol::{AppRequest, AppResponse};
use serde_json::json;
use std::time::Duration;
pub(crate) async fn process_app_request(
state: &AppState,
req: AppRequest,
_transport: AppTransport,
) -> AppResponse {
match req {
AppRequest::Capabilities => AppResponse {
ok: true,
data: json!({
"routes": ADVERTISED_ROUTES,
"interactive_execution": false,
"goal_execution": false,
"tool_free_turns": true,
"full_api_entrypoint": "codewhale app-server --http",
"config": ["get", "set", "unset", "list", "reload"],
"events": ["response_start", "response_delta", "response_end"],
"transport": "stdio+http",
"config_path": state.config_path.as_ref().map(|p| p.display().to_string()),
}),
events: Vec::new(),
},
AppRequest::ConfigGet { key } => {
let cfg = state.config.read().await;
let value = cfg.get_display_value(&key);
AppResponse {
ok: true,
data: json!({ "key": key, "value": value }),
events: Vec::new(),
}
}
AppRequest::ConfigSet { key, value } => {
// Only propagate a mutation that actually happened. `set_value`
// leaves the config untouched on an unknown key or invalid value,
// so this is a no-op from the caller's point of view — but
// `propagate_config` invalidates the cached stdio bridge
// regardless, and dropping the last reference kills the running
// child runtime along with its thread map. A single typo'd key
// would orphan every in-flight thread on that bridge.
let result = {
let (key, value) = (key.clone(), value.clone());
persist_config_mutation(state, move |cfg| cfg.set_value(&key, &value)).await
};
let ok = result.is_ok();
let message = result.err().map(|e| e.to_string());
let value = if ok {
value
} else {
codewhale_config::persistence::redact_secrets(&value)
};
AppResponse {
ok,
data: json!({ "key": key, "value": value, "error": message }),
events: Vec::new(),
}
}
AppRequest::ConfigUnset { key } => {
// See ConfigSet: a failed unset changed nothing and must not tear
// down the runtime bridge.
let result = {
let key = key.clone();
persist_config_mutation(state, move |cfg| cfg.unset_value(&key)).await
};
let ok = result.is_ok();
let message = result.err().map(|e| e.to_string());
AppResponse {
ok,
data: json!({ "key": key, "error": message }),
events: Vec::new(),
}
}
AppRequest::ConfigList => {
let cfg = state.config.read().await;
AppResponse {
ok: true,
data: json!({ "values": cfg.list_values() }),
events: Vec::new(),
}
}
AppRequest::ConfigReload => {
// Re-read both `config.toml` and the sibling `permissions.toml`
// from disk (the headless equivalent of the TUI
// `reload_runtime_config` codepath) and push the fresh
// snapshots into `state.config` and the live `Runtime`.
//
// `ConfigStore::load` resolves the same default config path
// that `build_state` used at startup when `config_path` is
// `None`, so a `None` here reloads from the same on-disk file
// the server booted from.
// Disk is already the source of truth here, so nothing to
// persist. External `permissions.toml` edits reach the Engine
// because the update invalidates the runtime bridge; the next
// turn's child loads both files fresh.
if let Err(error) = update_config_store(state, |_| Ok(())).await {
return AppResponse {
ok: false,
data: json!({ "error": error.to_string() }),
events: Vec::new(),
};
}
AppResponse {
ok: true,
data: json!({ "reloaded": true }),
events: Vec::new(),
}
}
AppRequest::Models => {
let result = tokio::time::timeout(Duration::from_secs(30), runtime_app_models(state))
.await
.context("Runtime model catalog request timed out")
.and_then(|result| result);
match result {
Ok(models) => AppResponse {
ok: true,
data: json!({ "models": models }),
events: Vec::new(),
},
Err(error) => AppResponse {
ok: false,
data: json!({ "error": error.to_string() }),
events: Vec::new(),
},
}
}
AppRequest::ThreadLoadedList => {
let response = crate::thread_control::loaded(state).await;
match response {
Ok(thread_resp) => AppResponse {
ok: true,
data: json!({ "threads": thread_resp.threads }),
events: thread_resp.events,
},
Err(err) => AppResponse {
ok: false,
data: json!({ "error": err.to_string() }),
events: Vec::new(),
},
}
}
AppRequest::SubmitUserInput { request_id, .. } => {
// This transport cannot deliver a clarification answer, and
// saying otherwise was the bug: the previous implementation
// reported `resolved: true` and filed the answers in a map with
// no reader anywhere in this crate.
//
// It cannot be made to work here. `handle_line_during_turn`
// executes exactly one method while a turn is streaming —
// `thread/interrupt`. Everything else, `app/request` included,
// queues until the turn ends, so an answer sent over this
// transport would wait on the very turn that is waiting for it.
// The runtime API owns the pending request and can resume the
// turn, so that is where the reply belongs.
AppResponse {
ok: false,
data: json!({
"error": "user_input_reply_unsupported",
"request_id": request_id,
"message": concat!(
"the app-server control transport cannot deliver clarification answers: ",
"only `thread/interrupt` runs while a turn is streaming, so an answer sent ",
"here would queue behind the turn waiting for it. Reply on the runtime API ",
"instead: POST /v1/user-input/{thread_id}/{request_id}."
),
}),
events: Vec::new(),
}
}
}
}
/// Install the saved config in the bookkeeping Runtime, then apply it through
/// the already captured canonical owner. Unbound production frontends refuse
/// application while retaining the saved config for explicit recovery.
/// A saved config whose owner reload fails is reported as retained but unapplied.
pub(crate) async fn propagate_config(state: &AppState) -> Result<()> {
{
let mut runtime = state.runtime.write().await;
let snapshot = state.config.read().await.clone();
runtime.update_config(snapshot);
}
if state.captured_owner.is_some() {
let owner = crate::owner_calls::captured(state)
.context("config saved but captured Runtime owner is unavailable")?;
tokio::time::timeout(
Duration::from_secs(10),
crate::owner_calls::execute(&owner, crate::RuntimeOwnerRequest::ReloadConfig),
)
.await
.context("config saved; captured owner reload outcome is uncertain, not replayed")?
.context("config saved but captured owner rejected reload")?;
} else {
#[cfg(test)]
invalidate_runtime_bridge(state).await;
#[cfg(not(test))]
bail!("config saved but no captured Runtime owner can apply it");
}
Ok(())
}
/// Prefix of a config error that is the server's fault (the file could not
/// be read, parsed, or written), reported over HTTP `/app` as a 500 rather
/// than the 400 a rejected key or value gets.
pub(crate) const CONFIG_LOAD_ERROR: &str = "failed to load config";
/// See [`CONFIG_LOAD_ERROR`].
pub(crate) const CONFIG_SAVE_ERROR: &str = "failed to save config";
/// Apply `mutate` to the config on disk, then propagate the saved result.
///
/// The mutation runs against a freshly loaded store, not the in-memory
/// snapshot, so edits another process (TUI, `codewhale login`) saved since
/// startup survive. With no explicit `--config` the store resolves the same
/// default path the runtime child reads, so the change reaches turns and
/// survives a restart. Any load, mutation, or save failure is returned and
/// nothing is propagated, so the caller never reports `ok` for a change
/// that was not kept.
///
/// The work runs on its own task: once the file is written, a caller that
/// goes away (an HTTP client disconnect) must not leave disk ahead of
/// `state.config`, the live runtime, and the cached bridge.
pub(crate) async fn persist_config_mutation(
state: &AppState,
mutate: impl FnOnce(&mut codewhale_config::ConfigToml) -> Result<()> + Send + 'static,
) -> Result<()> {
update_config_store(state, move |store| {
mutate(&mut store.config)?;
store
.save()
.map_err(|err| anyhow!("{CONFIG_SAVE_ERROR}: {err}"))
})
.await
}
/// Serialize reloads and writes before reading disk, and finish propagation
/// even if the requesting connection disappears. A reload mutates nothing.
pub(crate) async fn update_config_store(
state: &AppState,
update: impl FnOnce(&mut ConfigStore) -> Result<()> + Send + 'static,
) -> Result<()> {
let state = state.clone();
tokio::spawn(async move {
// Own the write guard across load→mutate→save→install so two
// concurrent mutations cannot overwrite one another. All disk work
// runs off-runtime; the owned operation still finishes propagation
// when the requesting connection goes away.
let mut config = state.config.clone().write_owned().await;
let config_path = state.config_path.clone();
tokio::task::spawn_blocking(move || -> Result<()> {
let mut store = ConfigStore::load(config_path)
.map_err(|err| anyhow!("{CONFIG_LOAD_ERROR}: {err}"))?;
update(&mut store)?;
*config = store.config;
Ok(())
})
.await
.map_err(|err| anyhow!("config store task failed: {err}"))??;
propagate_config(&state).await?;
Ok(())
})
.await
.map_err(|err| anyhow!("config update task failed: {err}"))?
}
-88
View File
@@ -1,88 +0,0 @@
use crate::{DispatchError, params_or_object, parse_params};
use codewhale_app_server_protocol::lifecycle::{InitializeParams, InitializeResponse};
use serde_json::Value;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct ConnectionId(u64);
impl ConnectionId {
fn mint() -> Self {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
Self(NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed))
}
}
impl std::fmt::Display for ConnectionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
pub(crate) struct ConnectionSession {
id: ConnectionId,
initialization: Option<InitializeParams>,
}
impl Default for ConnectionSession {
fn default() -> Self {
Self {
id: ConnectionId::mint(),
initialization: None,
}
}
}
impl ConnectionSession {
pub(crate) fn id(&self) -> ConnectionId {
self.id
}
pub(crate) fn initialized(&self) -> bool {
self.initialization.is_some()
}
pub(crate) fn initialize(&mut self, params: Value) -> Result<Value, DispatchError> {
if self.initialized() {
return Err(DispatchError::invalid_request("Already initialized"));
}
let params: InitializeParams = parse_params(params_or_object(params))?;
axum::http::HeaderValue::from_str(&params.client_info.name)
.map_err(|_| DispatchError::invalid_request("Invalid clientInfo.name"))?;
if let Some(capabilities) = params.capabilities.as_ref()
&& (capabilities.explicit_gateway_oauth
|| capabilities.request_attestation
|| capabilities.mcp_server_openai_form_elicitation
|| capabilities
.opt_out_notification_methods
.as_ref()
.is_some_and(|methods| !methods.is_empty())
|| capabilities
.extensions
.as_ref()
.is_some_and(|extensions| !extensions.is_empty()))
{
return Err(DispatchError::invalid_request(
"Requested initialization capability is not implemented",
));
}
let home = codewhale_paths::codewhale_home()
.map_err(|error| DispatchError::internal(error.to_string()))?
.filter(|path| path.is_absolute())
.ok_or_else(|| DispatchError::internal("absolute Codewhale home unavailable"))?;
let response = InitializeResponse {
user_agent: format!(
"Codewhale/{} ({}; {})",
env!("CARGO_PKG_VERSION"),
params.client_info.name,
params.client_info.version
),
codex_home: home,
platform_family: std::env::consts::FAMILY.to_string(),
platform_os: std::env::consts::OS.to_string(),
};
let value = serde_json::to_value(response)
.map_err(|error| DispatchError::internal(error.to_string()))?;
self.initialization = Some(params);
Ok(value)
}
}
+42 -191
View File
@@ -9,26 +9,15 @@ use std::path::PathBuf;
mod platform {
use super::*;
use crate::daemon_socket::AuthorizedPeer;
#[cfg(windows)]
use crate::daemon_socket::default_socket_path;
use crate::stdio::{
BoundedLines, ParsedStdioLine, is_frame_refusal, parse_stdio_line, serialize_control_frame,
write_control_line, write_stdio_line,
};
use crate::daemon_socket::{default_socket_path, owner_work};
use crate::{BoundedLines, ParsedStdioLine, parse_stdio_line, write_stdio_line};
use anyhow::{Context as _, bail};
use codewhale_app_server_protocol::lifecycle::InitializeResponse;
use codewhale_app_server_protocol::rpc::{
RequestId, RpcCall, RpcErrorResponse, RpcNotification, RpcResponse,
};
use codewhale_config::private_directory::PrivateDirectory;
#[cfg(unix)]
use codewhale_paths::daemon_socket::default_socket_path;
use codewhale_protocol::RuntimeOwnerReceipt;
use codewhale_runtime::runtime_control::owner_work;
use serde_json::{Value, json};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::BufReader;
use tokio::io::{AsyncWriteExt as _, BufReader};
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(unix)]
@@ -36,47 +25,13 @@ mod platform {
#[cfg(windows)]
type Connection = tokio::net::windows::named_pipe::NamedPipeClient;
type OwnerTransport = crate::websocket::LocalWebSocket<
tokio::io::ReadHalf<Connection>,
tokio::io::WriteHalf<Connection>,
>;
fn same_socket_selection(left: &std::path::Path, right: &std::path::Path) -> bool {
if left == right {
return true;
}
#[cfg(unix)]
{
if left.file_name().is_none() || left.file_name() != right.file_name() {
return false;
}
match (
left.parent()
.map(codewhale_paths::resolve_root_owned_ancestors),
right
.parent()
.map(codewhale_paths::resolve_root_owned_ancestors),
) {
(Some(left), Some(right)) => left == right,
_ => false,
}
}
#[cfg(windows)]
{
false
}
}
pub struct OwnerClient {
read: BoundedLines<BufReader<tokio::io::ReadHalf<OwnerTransport>>>,
write: tokio::io::WriteHalf<OwnerTransport>,
read: BoundedLines<BufReader<tokio::io::ReadHalf<Connection>>>,
write: tokio::io::WriteHalf<Connection>,
peer: Arc<AuthorizedPeer>,
receipt: RuntimeOwnerReceipt,
routing: Option<crate::RuntimeOwnerRouting>,
frontend: crate::daemon_socket::AttachFrontend,
output_failed: bool,
native_initialized: bool,
pending_initialize: Option<RequestId>,
}
/// Host startup is the only caller allowed to acquire the Runtime lease.
@@ -220,7 +175,7 @@ mod platform {
old.version == owner.version
&& old.data_dir == owner.data_dir
&& old.execution_scope == owner.execution_scope
&& same_socket_selection(&old.socket_path, endpoint)
&& old.socket_path == endpoint
&& old.config_path == owner.config_path
&& old.principal == owner.principal,
"captured stale owner belongs to another selected store/config/principal"
@@ -275,15 +230,7 @@ mod platform {
#[cfg(windows)]
let name = "daemon.owner.json".to_string();
owner_work(move || {
// This runs before the engine has anywhere to log, so a refusal
// here is the first thing a user sees and the last thing they can
// diagnose. A mount that reports its own ownership (HarmonyOS
// hmdfs) makes the strict proof impossible for every object on it,
// so discovering an owner — not authenticating one — takes the
// measured exception. The receipt is still read through the
// retained handle, must be a single-link file, and the socket
// identity is still compared.
let parent = match PrivateDirectory::inspect_trusting_mount(&directory) {
let parent = match PrivateDirectory::inspect(&directory) {
Ok(parent) => Arc::new(parent),
Err(error)
if error
@@ -304,7 +251,7 @@ mod platform {
anyhow::ensure!(
receipt.version == 1
&& receipt.pid > 0
&& same_socket_selection(&receipt.socket_path, &socket_path)
&& receipt.socket_path == socket_path
&& !receipt.lease_generation.is_empty(),
"selected owner receipt is invalid"
);
@@ -599,20 +546,13 @@ mod platform {
.is_none_or(|expected| expected == &receipt),
"selected owner changed before frontend admission"
);
let (read, write) = tokio::io::split(stream);
let transport = crate::websocket::connect(read, write).await?;
let (read, mut write) = tokio::io::split(transport);
let (read, mut write) = tokio::io::split(stream);
let mut replies = BoundedLines::new(BufReader::new(read));
let attach_id = uuid::Uuid::new_v4().to_string();
let attach = json!(RpcCall {
id: Some(RequestId::String(attach_id.clone())),
method: "daemon/attach".into(),
params: json!({
"client":{"name":"codewhale-stdio","version":env!("CARGO_PKG_VERSION"),"pid":std::process::id()},
"mode":"attach","frontend":frontend,"listener":listener,"scope":scope,"acp_model":acp_model,"expect_daemon_version":env!("CARGO_PKG_VERSION"),"expect_owner":receipt
}),
trace: None
});
let attach = json!({"jsonrpc":"2.0","id":attach_id,"method":"daemon/attach","params":{
"client":{"name":"codewhale-stdio","version":env!("CARGO_PKG_VERSION"),"pid":std::process::id()},
"mode":"attach","frontend":frontend,"listener":listener,"scope":scope,"acp_model":acp_model,"expect_daemon_version":env!("CARGO_PKG_VERSION"),"expect_owner":receipt
}});
tokio::time::timeout(
Duration::from_secs(5),
write_stdio_line(&mut write, &attach),
@@ -650,9 +590,6 @@ mod platform {
receipt,
routing,
frontend,
output_failed: false,
native_initialized: false,
pending_initialize: None,
})))
}
@@ -665,159 +602,60 @@ mod platform {
&self.receipt
}
pub async fn send(&mut self, id: Value, method: &str, params: Value) -> Result<()> {
let frame = json!({"jsonrpc":"2.0","id":id,"method":method,"params":params});
let bytes = serde_json::to_vec(&frame)?;
anyhow::ensure!(
!self.output_failed,
"owner transport write previously failed; outcomes uncertain, not replayed"
bytes.len() <= crate::MAX_RUNTIME_IMAGE_BODY_BYTES,
"owner request exceeds transport bound"
);
if self.frontend == crate::daemon_socket::AttachFrontend::Acp {
return self
.send_frame(json!({"jsonrpc":"2.0","id":id,"method":method,"params":params}))
.await;
}
let id: RequestId = serde_json::from_value(id)?;
anyhow::ensure!(
self.pending_initialize.is_none(),
"receive the pending initialize acknowledgement before sending another request"
);
if self.frontend == crate::daemon_socket::AttachFrontend::Control
&& !self.native_initialized
&& method != "initialize"
{
let initialize_id = RequestId::String(format!(
"codewhale-native-initialize-{}",
uuid::Uuid::new_v4()
));
self.send_call(RpcCall { id: Some(initialize_id.clone()), method: "initialize".into(), params: json!({"clientInfo":{"name":"codewhale-native-control","version":env!("CARGO_PKG_VERSION")}}), trace: None }).await?;
self.pending_initialize = Some(initialize_id.clone());
let acknowledged: Result<()> = async {
let response = tokio::time::timeout(Duration::from_secs(5), self.recv()).await.context("owner initialize response deadline expired; connection outcome uncertain")??.context("owner input closed before initialize acknowledgement")?;
anyhow::ensure!(response.get("id") == Some(&json!(initialize_id)) && response.get("error").is_none() && response.get("result").is_some(), "owner initialize acknowledgement mismatch or refusal");
Ok(())
}.await;
if let Err(error) = acknowledged {
self.output_failed = true;
return Err(error);
}
self.native_initialized = true;
}
let pending = (self.frontend == crate::daemon_socket::AttachFrontend::Control
&& method == "initialize"
&& !self.native_initialized)
.then(|| id.clone());
self.send_call(RpcCall {
id: Some(id),
method: method.to_string(),
params,
trace: None,
})
.await?;
self.pending_initialize = pending;
Ok(())
}
async fn send_call(&mut self, request: RpcCall) -> Result<()> {
self.send_frame(json!(request)).await
}
async fn send_frame(&mut self, frame: Value) -> Result<()> {
anyhow::ensure!(
!self.output_failed,
"owner transport write previously failed; outcomes uncertain, not replayed"
);
let bytes = serialize_control_frame(&frame)?;
let check = self.peer.clone();
owner_work(move || check.check()).await?;
self.output_failed = true;
tokio::time::timeout(
Duration::from_secs(5),
write_control_line(&mut self.write, &bytes),
write_stdio_line(&mut self.write, &frame),
)
.await
.context("owner write deadline expired; outcome uncertain, not replayed")??;
self.output_failed = false;
Ok(())
}
pub async fn recv(&mut self) -> Result<Option<Value>> {
anyhow::ensure!(
!self.output_failed,
"owner transport write previously failed; outcomes uncertain, not replayed"
);
let Some(line) = self.read.next_line().await? else {
return Ok(None);
};
let check = self.peer.clone();
owner_work(move || check.check()).await?;
let value: Value = serde_json::from_str(&line)?;
if let Some(id) = self.pending_initialize.as_ref()
&& value.get("id") == Some(&json!(id))
{
let response = serde_json::from_value::<RpcResponse>(value.clone());
let accepted = response
.ok()
.filter(|_| value.get("error").is_none())
.and_then(|response| {
serde_json::from_value::<InitializeResponse>(response.result).ok()
})
.is_some_and(|response| response.codex_home.is_absolute());
let rejected = serde_json::from_value::<RpcErrorResponse>(value.clone()).is_ok();
if !accepted && !rejected {
self.output_failed = true;
anyhow::bail!("initialize acknowledgement does not match its typed response");
}
self.native_initialized = accepted;
self.pending_initialize = None;
if accepted {
self.send_call(RpcCall {
id: None,
method: "initialized".into(),
params: Value::Null,
trace: None,
})
.await?;
}
}
Ok(Some(value))
serde_json::from_str(&line).map(Some).map_err(Into::into)
}
pub async fn forward<I, O>(mut self, input: I, output: O) -> Result<()>
where
I: tokio::io::AsyncRead + Unpin,
O: tokio::io::AsyncWrite + Unpin,
{
anyhow::ensure!(
!self.output_failed,
"owner transport write previously failed; outcomes uncertain, not replayed"
);
let mut input = BoundedLines::new(BufReader::new(input));
let mut output = tokio::io::BufWriter::new(output);
let mut input_open = true;
loop {
tokio::select! {
line = input.next_line(), if input_open => match match line {
Ok(line) => line,
Err(error) if self.frontend == crate::daemon_socket::AttachFrontend::Acp && is_frame_refusal(&error) => {
write_stdio_line(&mut output, &json!({"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":error.to_string()}})).await?;
continue;
}
Err(error) => return Err(error.into()),
} {
line = input.next_line(), if input_open => match line? {
None => {
input_open=false;
tokio::time::timeout(Duration::from_secs(5), async {
write_stdio_line(&mut self.write,&json!(RpcNotification { method:"daemon/input_closed".into(), params:Some(json!({})) })).await?;
Ok::<(), anyhow::Error>(())
}).await.context("owner input-close write outcome uncertain, not replayed")??;
tokio::time::timeout(Duration::from_secs(5),write_stdio_line(&mut self.write,&json!({"jsonrpc":"2.0","method":"daemon/input_closed","params":{}}))).await.context("owner input-close write outcome uncertain, not replayed")??;
self.write.shutdown().await?;
}
Some(line) if self.frontend==crate::daemon_socket::AttachFrontend::Acp => {
let check=self.peer.clone();
owner_work(move || check.check()).await?;
let value:Value=serde_json::from_str(&line)?;
anyhow::ensure!(value.is_object() && value["jsonrpc"]=="2.0","ACP frame must be a JSON-RPC object");
// Preserve response IDs/permission replies verbatim.
// Actual ACP authority/parser stays in AcpServer.
tokio::time::timeout(Duration::from_secs(5),write_control_line(&mut self.write,line.as_bytes())).await.context("ACP write outcome uncertain; not replayed")??;
tokio::time::timeout(Duration::from_secs(5),async {self.write.write_all(line.as_bytes()).await?;self.write.write_all(b"\n").await?;self.write.flush().await}).await.context("ACP write outcome uncertain; not replayed")??;
}
Some(line) => match parse_stdio_line(&line) {
ParsedStdioLine::Blank => {},
ParsedStdioLine::Ignored => {},
ParsedStdioLine::Response(response) => self.send_frame(response).await?,
ParsedStdioLine::Rejected(response) => write_stdio_line(&mut output, &response).await?,
ParsedStdioLine::Request(request) => {
self.send_call(request).await?;
self.send(request.id.unwrap_or(Value::Null),&request.method,request.params).await?;
}
}
},
@@ -827,7 +665,9 @@ mod platform {
None => bail!("selected owner connection closed; pending operation outcomes may be uncertain"),
Some(line) => {
let _: Value = serde_json::from_str(&line).context("invalid owner response")?;
write_control_line(&mut output, line.as_bytes()).await?;
output.write_all(line.as_bytes()).await?;
output.write_all(b"\n").await?;
output.flush().await?;
}
}
}
@@ -835,6 +675,12 @@ mod platform {
}
}
pub async fn forward_stdio(config_path: Option<PathBuf>) -> Result<()> {
connect(config_path, None)
.await?
.forward(tokio::io::stdin(), tokio::io::stdout())
.await
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
@@ -861,6 +707,11 @@ mod platform {
pub use platform::{
HostOwnerProbe, OwnerClient, PublicationChangedBeforeAttach, UnavailablePublication, connect,
connect_acp_if_published, connect_if_published, connect_listener_if_published,
connect_scoped_control_if_published, connect_selected_acp_if_published,
connect_scoped_control_if_published, connect_selected_acp_if_published, forward_stdio,
probe_owner_for_host_startup, publication_is_absent,
};
#[cfg(not(any(unix, windows)))]
pub async fn forward_stdio(_config_path: Option<PathBuf>) -> Result<()> {
anyhow::bail!("authenticated live-owner control is not implemented on this platform")
}
Loaded 100 of 2901 files, more files were not shown because too many files have changed in this diff. Show more