mirror of
https://github.com/openai/codex.git
synced 2026-05-08 13:26:34 +00:00
Compare commits
25 Commits
jif/hello-
...
owen/sqlit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ddf828d4c | ||
|
|
a3de5bde6e | ||
|
|
79154e6952 | ||
|
|
893038f77c | ||
|
|
31b233c7c6 | ||
|
|
0d0835dd53 | ||
|
|
54ef99a365 | ||
|
|
80a8563e48 | ||
|
|
8abcc5357d | ||
|
|
27ec488ad5 | ||
|
|
8367ef4522 | ||
|
|
163eac9306 | ||
|
|
4242bba2eb | ||
|
|
0274398901 | ||
|
|
56823ec46b | ||
|
|
0dc1885a5c | ||
|
|
566f2cb612 | ||
|
|
eb0462f2af | ||
|
|
129401df43 | ||
|
|
857e731478 | ||
|
|
114bac1409 | ||
|
|
3444b0d60a | ||
|
|
9b6c6f7a01 | ||
|
|
e64a8979b0 | ||
|
|
acac786d91 |
2
.github/actions/prepare-bazel-ci/action.yml
vendored
2
.github/actions/prepare-bazel-ci/action.yml
vendored
@@ -50,7 +50,7 @@ runs:
|
||||
- name: Restore bazel repository cache
|
||||
id: cache_bazel_repository_restore
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.cache_bazel_repository_key.outputs.repository-cache-key }}
|
||||
|
||||
4
.github/actions/windows-code-sign/action.yml
vendored
4
.github/actions/windows-code-sign/action.yml
vendored
@@ -30,7 +30,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Azure login for Trusted Signing (OIDC)
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
client-id: ${{ inputs.client-id }}
|
||||
tenant-id: ${{ inputs.tenant-id }}
|
||||
@@ -54,7 +54,7 @@ runs:
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sign Windows binaries with Azure Trusted Signing
|
||||
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0
|
||||
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11
|
||||
with:
|
||||
endpoint: ${{ inputs.endpoint }}
|
||||
trusted-signing-account-name: ${{ inputs.account-name }}
|
||||
|
||||
12
.github/dependabot.yaml
vendored
12
.github/dependabot.yaml
vendored
@@ -6,25 +6,37 @@ updates:
|
||||
directory: .github/actions/codex
|
||||
schedule:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: cargo
|
||||
directories:
|
||||
- codex-rs
|
||||
- codex-rs/*
|
||||
schedule:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: devcontainers
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: docker
|
||||
directory: codex-cli
|
||||
schedule:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: rust-toolchain
|
||||
directory: codex-rs
|
||||
schedule:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
24
.github/workflows/bazel.yml
vendored
24
.github/workflows/bazel.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
name: Bazel test on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check rusty_v8 MODULE.bazel checksums
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: bazel-execution-logs-test-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main)
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Bazel CI
|
||||
id: prepare_bazel
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: bazel-execution-logs-test-windows-native-x86_64-pc-windows-gnullvm
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Bazel CI
|
||||
id: prepare_bazel
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: bazel-execution-logs-clippy-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
@@ -297,7 +297,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Bazel CI
|
||||
id: prepare_bazel
|
||||
@@ -390,7 +390,7 @@ jobs:
|
||||
- name: Upload Bazel execution logs
|
||||
if: always() && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: bazel-execution-logs-verify-release-build-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/bazel-execution-logs
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
|
||||
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
|
||||
|
||||
2
.github/workflows/blob-size-policy.yml
vendored
2
.github/workflows/blob-size-policy.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
name: Blob size policy
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
2
.github/workflows/cargo-deny.yml
vendored
2
.github/workflows/cargo-deny.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
working-directory: ./codex-rs
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Verify codex-rs Cargo manifests inherit workspace settings
|
||||
run: python3 .github/scripts/verify_cargo_workspace_manifests.py
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload staged npm package artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: codex-npm-staging
|
||||
path: ${{ steps.stage_npm_package.outputs.pack_output }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close inactive PRs from contributors
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
4
.github/workflows/codespell.yml
vendored
4
.github/workflows/codespell.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Annotate locations with typos
|
||||
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1
|
||||
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1.1.0
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
||||
with:
|
||||
|
||||
6
.github/workflows/issue-deduplicator.yml
vendored
6
.github/workflows/issue-deduplicator.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
reason: ${{ steps.normalize-all.outputs.reason }}
|
||||
has_matches: ${{ steps.normalize-all.outputs.has_matches }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Codex inputs
|
||||
env:
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
reason: ${{ steps.normalize-open.outputs.reason }}
|
||||
has_matches: ${{ steps.normalize-open.outputs.has_matches }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Prepare Codex inputs
|
||||
env:
|
||||
@@ -342,7 +342,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Comment on issue
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
CODEX_OUTPUT: ${{ needs.select-final.outputs.codex_output }}
|
||||
with:
|
||||
|
||||
2
.github/workflows/issue-labeler.yml
vendored
2
.github/workflows/issue-labeler.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
outputs:
|
||||
codex_output: ${{ steps.codex.outputs.final-message }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- id: codex
|
||||
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02 # v1.7
|
||||
|
||||
54
.github/workflows/rust-ci-full.yml
vendored
54
.github/workflows/rust-ci-full.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
components: rustfmt
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: cargo-shear
|
||||
version: 1.5.1
|
||||
version: 1.11.2
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
|
||||
@@ -47,14 +47,14 @@ jobs:
|
||||
CARGO_DYLINT_VERSION: 5.0.0
|
||||
DYLINT_LINK_VERSION: 5.0.0
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
toolchain: nightly-2025-09-18
|
||||
components: llvm-tools-preview, rustc-dev, rust-src
|
||||
- name: Cache cargo-dylint tooling
|
||||
id: cargo_dylint_cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-dylint
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
group: codex-runners
|
||||
labels: codex-windows-x64
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/setup-bazel-ci
|
||||
with:
|
||||
target: ${{ runner.os }}
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
# avoid caching the large target dir on the gnu-dev job.
|
||||
- name: Restore cargo home cache
|
||||
id: cache_cargo_home_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
# Install and restore sccache cache
|
||||
- name: Install sccache
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: sccache
|
||||
version: 0.7.5
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
- name: Restore sccache cache (fallback)
|
||||
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
|
||||
id: cache_sccache_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
@@ -348,7 +348,7 @@ jobs:
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Restore APT cache (musl)
|
||||
id: cache_apt_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
/var/cache/apt
|
||||
@@ -356,7 +356,7 @@ jobs:
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Install Zig
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
|
||||
with:
|
||||
version: 0.14.0
|
||||
|
||||
@@ -430,7 +430,7 @@ jobs:
|
||||
|
||||
- name: Install cargo-chef
|
||||
if: ${{ matrix.profile == 'release' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: cargo-chef
|
||||
version: 0.1.71
|
||||
@@ -445,11 +445,11 @@ jobs:
|
||||
cargo chef cook --recipe-path "$RECIPE" --target ${{ matrix.target }} --release
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo clippy --target ${{ matrix.target }} --tests --profile ${{ matrix.profile }} --timings -- -D warnings
|
||||
run: cargo clippy --target ${{ matrix.target }} --tests --profile ${{ matrix.profile }} --timings --locked -- -D warnings
|
||||
|
||||
- name: Upload Cargo timings (clippy)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -460,7 +460,7 @@ jobs:
|
||||
- name: Save cargo home cache
|
||||
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -476,7 +476,7 @@ jobs:
|
||||
- name: Save sccache cache (fallback)
|
||||
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
@@ -501,7 +501,7 @@ jobs:
|
||||
- name: Save APT cache (musl)
|
||||
if: always() && !cancelled() && (matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl') && steps.cache_apt_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
/var/cache/apt
|
||||
@@ -559,7 +559,7 @@ jobs:
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Install Linux build dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -590,7 +590,7 @@ jobs:
|
||||
|
||||
- name: Restore cargo home cache
|
||||
id: cache_cargo_home_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -603,7 +603,7 @@ jobs:
|
||||
|
||||
- name: Install sccache
|
||||
if: ${{ env.USE_SCCACHE == 'true' }}
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: sccache
|
||||
version: 0.7.5
|
||||
@@ -630,7 +630,7 @@ jobs:
|
||||
- name: Restore sccache cache (fallback)
|
||||
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
|
||||
id: cache_sccache_restore
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
@@ -638,7 +638,7 @@ jobs:
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
|
||||
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
|
||||
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: nextest
|
||||
version: 0.9.103
|
||||
@@ -674,7 +674,7 @@ jobs:
|
||||
|
||||
- name: Upload Cargo timings (nextest)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-ci-nextest-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -683,7 +683,7 @@ jobs:
|
||||
- name: Save cargo home cache
|
||||
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
@@ -695,7 +695,7 @@ jobs:
|
||||
- name: Save sccache cache (fallback)
|
||||
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.sccache/
|
||||
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
|
||||
|
||||
16
.github/workflows/rust-ci.yml
vendored
16
.github/workflows/rust-ci.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
codex: ${{ steps.detect.outputs.codex }}
|
||||
workflows: ${{ steps.detect.outputs.workflows }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Detect changed paths (no external action)
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
components: rustfmt
|
||||
@@ -77,12 +77,12 @@ jobs:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
|
||||
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
|
||||
with:
|
||||
tool: cargo-shear
|
||||
version: 1.5.1
|
||||
version: 1.11.2
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
CARGO_DYLINT_VERSION: 5.0.0
|
||||
DYLINT_LINK_VERSION: 5.0.0
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- name: Install nightly argument-comment-lint toolchain
|
||||
shell: bash
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
rustup default nightly-2025-09-18
|
||||
- name: Cache cargo-dylint tooling
|
||||
id: cargo_dylint_cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-dylint
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
|
||||
echo "No argument-comment-lint relevant changes."
|
||||
echo "run=false" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
if: ${{ steps.argument_comment_lint_gate.outputs.run == 'true' }}
|
||||
- name: Run argument comment lint on codex-rs via Bazel
|
||||
if: ${{ steps.argument_comment_lint_gate.outputs.run == 'true' }}
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
labels: codex-windows-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
with:
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
- name: Cargo build
|
||||
working-directory: tools/argument-comment-lint
|
||||
shell: bash
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
run: cargo build --release --target ${{ matrix.target }} --locked
|
||||
|
||||
- name: Stage artifact
|
||||
shell: bash
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
(cd "${RUNNER_TEMP}" && tar -czf "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint)
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: argument-comment-lint-${{ matrix.target }}
|
||||
path: dist/argument-comment-lint/${{ matrix.target }}/*
|
||||
|
||||
4
.github/workflows/rust-release-prepare.yml
vendored
4
.github/workflows/rust-release-prepare.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
if: github.repository == 'openai/codex'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
curl --http1.1 --fail --show-error --location "${headers[@]}" "${url}" | jq '.' > codex-rs/models-manager/models.json
|
||||
|
||||
- name: Open pull request (if changed)
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
commit-message: "Update models.json"
|
||||
title: "Update models.json"
|
||||
|
||||
18
.github/workflows/rust-release-windows.yml
vendored
18
.github/workflows/rust-release-windows.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Print runner specs (Windows)
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -109,10 +109,10 @@ jobs:
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
build_args+=(--bin "$binary")
|
||||
done
|
||||
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
|
||||
cargo build --target ${{ matrix.target }} --release --timings --locked "${build_args[@]}"
|
||||
|
||||
- name: Upload Cargo timings
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-release-windows-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Upload Windows binaries
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: |
|
||||
@@ -165,22 +165,22 @@ jobs:
|
||||
labels: codex-windows-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download prebuilt Windows primary binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-primary
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows helper binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-helpers
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows app-server binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-app-server
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
"${GITHUB_WORKSPACE}/.github/workflows/zstd" -T0 -19 "$dest/$base"
|
||||
done
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: |
|
||||
|
||||
8
.github/workflows/rust-release-zsh.yml
vendored
8
.github/workflows/rust-release-zsh.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
git \
|
||||
libncursesw5-dev
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build, smoke-test, and stage zsh artifact
|
||||
shell: bash
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
|
||||
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: codex-zsh-${{ matrix.target }}
|
||||
path: dist/zsh/${{ matrix.target }}/*
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
brew install autoconf
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build, smoke-test, and stage zsh artifact
|
||||
shell: bash
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
|
||||
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: codex-zsh-${{ matrix.target }}
|
||||
path: dist/zsh/${{ matrix.target }}/*
|
||||
|
||||
24
.github/workflows/rust-release.yml
vendored
24
.github/workflows/rust-release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
tag-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
- name: Validate tag matches Cargo.toml version
|
||||
shell: bash
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
build_dmg: "false"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Print runner specs (Linux)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
|
||||
name: Install Zig
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
|
||||
with:
|
||||
version: 0.14.0
|
||||
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
target="${{ matrix.target }}"
|
||||
cargo build --target "$target" --release --timings --bin bwrap
|
||||
cargo build --target "$target" --release --timings --locked --bin bwrap
|
||||
|
||||
bwrap_path="target/${target}/release/bwrap"
|
||||
if [[ ! -f "$bwrap_path" ]]; then
|
||||
@@ -281,10 +281,10 @@ jobs:
|
||||
build_args+=(--bin "$binary")
|
||||
done
|
||||
echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}"
|
||||
cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}"
|
||||
cargo build --target ${{ matrix.target }} --release --timings --locked "${build_args[@]}"
|
||||
|
||||
- name: Upload Cargo timings
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
@@ -430,7 +430,7 @@ jobs:
|
||||
zstd -T0 -19 --rm "$dest/$base"
|
||||
done
|
||||
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
# Upload the per-binary .zst files, .tar.gz equivalents, and any
|
||||
@@ -476,7 +476,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Generate release notes from tag commit message
|
||||
id: release_notes
|
||||
@@ -498,7 +498,7 @@ jobs:
|
||||
|
||||
echo "path=${notes_path}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: dist
|
||||
|
||||
@@ -553,7 +553,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js for npm packaging
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -579,7 +579,7 @@ jobs:
|
||||
cp scripts/install/install.ps1 dist/install.ps1
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
name: ${{ steps.release_name.outputs.name }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
@@ -638,7 +638,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
# Node 24 bundles npm >= 11.5.1, which trusted publishing requires.
|
||||
node-version: 24
|
||||
|
||||
14
.github/workflows/rusty-v8-release.yml
vendored
14
.github/workflows/rusty-v8-release.yml
vendored
@@ -17,10 +17,10 @@ jobs:
|
||||
v8_version: ${{ steps.v8_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
target: aarch64-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: ./.github/actions/setup-bazel-ci
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
@@ -161,12 +161,12 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
tag_name: ${{ needs.metadata.outputs.release_tag }}
|
||||
name: ${{ needs.metadata.outputs.release_tag }}
|
||||
|
||||
6
.github/workflows/sdk.yml
vendored
6
.github/workflows/sdk.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install Linux bwrap build dependencies
|
||||
shell: bash
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
|
||||
10
.github/workflows/v8-canary.yml
vendored
10
.github/workflows/v8-canary.yml
vendored
@@ -40,10 +40,10 @@ jobs:
|
||||
v8_version: ${{ steps.v8_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
target: aarch64-unknown-linux-musl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: ./.github/actions/setup-bazel-ci
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
|
||||
path: dist/${{ matrix.target }}/*
|
||||
|
||||
@@ -130,7 +130,7 @@ When UI or text output changes intentionally, update the snapshots as follows:
|
||||
|
||||
If you don’t have the tool:
|
||||
|
||||
- `cargo install cargo-insta`
|
||||
- `cargo install --locked cargo-insta`
|
||||
|
||||
### Test assertions
|
||||
|
||||
|
||||
66
codex-rs/Cargo.lock
generated
66
codex-rs/Cargo.lock
generated
@@ -1866,7 +1866,6 @@ dependencies = [
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-core-plugins",
|
||||
"codex-device-key",
|
||||
"codex-exec-server",
|
||||
"codex-external-agent-migration",
|
||||
"codex-external-agent-sessions",
|
||||
@@ -2444,14 +2443,12 @@ dependencies = [
|
||||
"codex-core-skills",
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
"codex-extension-api",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-git-utils",
|
||||
"codex-hooks",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-memories",
|
||||
"codex-memories-read",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
@@ -2639,22 +2636,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-device-key"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"p256",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-exec"
|
||||
version = "0.0.0"
|
||||
@@ -2782,19 +2763,6 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-extension-api"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-git-attribution",
|
||||
"codex-guardian",
|
||||
"codex-memories",
|
||||
"codex-multi-agent-v2",
|
||||
"codex-tools",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-external-agent-migration"
|
||||
version = "0.0.0"
|
||||
@@ -2873,13 +2841,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-git-attribution"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-extension-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-git-utils"
|
||||
version = "0.0.0"
|
||||
@@ -2904,13 +2865,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-guardian"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-extension-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-hooks"
|
||||
version = "0.0.0"
|
||||
@@ -3094,19 +3048,6 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-memories"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-extension-api",
|
||||
"codex-memories-read",
|
||||
"codex-utils-absolute-path",
|
||||
"rmcp",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-memories-mcp"
|
||||
version = "0.0.0"
|
||||
@@ -3248,13 +3189,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-multi-agent-v2"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-extension-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-network-proxy"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -30,7 +30,6 @@ members = [
|
||||
"collaboration-mode-templates",
|
||||
"connectors",
|
||||
"config",
|
||||
"device-key",
|
||||
"shell-command",
|
||||
"shell-escalation",
|
||||
"skills",
|
||||
@@ -45,11 +44,6 @@ members = [
|
||||
"exec-server",
|
||||
"execpolicy",
|
||||
"execpolicy-legacy",
|
||||
"ext/extension-api",
|
||||
"ext/guardian",
|
||||
"ext/git-attribution",
|
||||
"ext/memories",
|
||||
"ext/multi-agent-v2",
|
||||
"external-agent-migration",
|
||||
"external-agent-sessions",
|
||||
"keyring-store",
|
||||
@@ -159,20 +153,14 @@ codex-core = { path = "core" }
|
||||
codex-core-api = { path = "core-api" }
|
||||
codex-core-plugins = { path = "core-plugins" }
|
||||
codex-core-skills = { path = "core-skills" }
|
||||
codex-device-key = { path = "device-key" }
|
||||
codex-exec = { path = "exec" }
|
||||
codex-file-system = { path = "file-system" }
|
||||
codex-exec-server = { path = "exec-server" }
|
||||
codex-execpolicy = { path = "execpolicy" }
|
||||
codex-extension-api = { path = "ext/extension-api" }
|
||||
codex-external-agent-migration = { path = "external-agent-migration" }
|
||||
codex-external-agent-sessions = { path = "external-agent-sessions" }
|
||||
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
|
||||
codex-features = { path = "features" }
|
||||
codex-guardian = { path = "ext/guardian" }
|
||||
codex-git-attribution = { path = "ext/git-attribution" }
|
||||
codex-memories = { path = "ext/memories" }
|
||||
codex-multi-agent-v2 = { path = "ext/multi-agent-v2" }
|
||||
codex-feedback = { path = "feedback" }
|
||||
codex-install-context = { path = "install-context" }
|
||||
codex-file-search = { path = "file-search" }
|
||||
@@ -329,7 +317,6 @@ os_info = "3.12.0"
|
||||
owo-colors = "4.3.0"
|
||||
path-absolutize = "3.1.1"
|
||||
pathdiff = "0.2"
|
||||
p256 = "0.13.2"
|
||||
portable-pty = "0.9.0"
|
||||
predicates = "3"
|
||||
pretty_assertions = "1.4.1"
|
||||
@@ -488,13 +475,13 @@ ignored = [
|
||||
[profile.dev]
|
||||
# Keep line tables/backtraces while avoiding expensive full variable debug info
|
||||
# across local dev builds.
|
||||
debug = 1
|
||||
debug = "limited"
|
||||
|
||||
[profile.dev-small]
|
||||
inherits = "dev"
|
||||
opt-level = 0
|
||||
debug = 0
|
||||
strip = true
|
||||
debug = "none"
|
||||
strip = "symbols"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
@@ -506,8 +493,15 @@ strip = "symbols"
|
||||
# See https://github.com/openai/codex/issues/1411 for details.
|
||||
codegen-units = 1
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = "full"
|
||||
lto = false
|
||||
strip = false
|
||||
|
||||
[profile.ci-test]
|
||||
debug = 1 # Reduce debug symbol size
|
||||
# Reduce binary size to reduce disk pressure.
|
||||
debug = "limited"
|
||||
inherits = "test"
|
||||
opt-level = 0
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ version.workspace = true
|
||||
[lib]
|
||||
name = "codex_agent_graph_store"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1012,7 +1012,7 @@ fn command_execution_event_serializes_expected_shape() {
|
||||
runtime_os_version: "15.3.1".to_string(),
|
||||
runtime_arch: "aarch64".to_string(),
|
||||
},
|
||||
thread_source: Some("user"),
|
||||
thread_source: Some(ThreadSource::User),
|
||||
subagent_source: None,
|
||||
parent_thread_id: None,
|
||||
tool_name: "shell".to_string(),
|
||||
|
||||
@@ -70,6 +70,8 @@ pub(crate) enum TrackEventRequest {
|
||||
CollabAgentToolCall(CodexCollabAgentToolCallEventRequest),
|
||||
WebSearch(CodexWebSearchEventRequest),
|
||||
ImageGeneration(CodexImageGenerationEventRequest),
|
||||
#[allow(dead_code)]
|
||||
ReviewEvent(CodexReviewEventRequest),
|
||||
PluginUsed(CodexPluginUsedEventRequest),
|
||||
PluginInstalled(CodexPluginEventRequest),
|
||||
PluginUninstalled(CodexPluginEventRequest),
|
||||
@@ -442,7 +444,7 @@ pub(crate) struct CodexToolItemEventBase {
|
||||
pub(crate) item_id: String,
|
||||
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
||||
pub(crate) runtime: CodexRuntimeMetadata,
|
||||
pub(crate) thread_source: Option<&'static str>,
|
||||
pub(crate) thread_source: Option<ThreadSource>,
|
||||
pub(crate) subagent_source: Option<String>,
|
||||
pub(crate) parent_thread_id: Option<String>,
|
||||
pub(crate) tool_name: String,
|
||||
@@ -462,6 +464,83 @@ pub(crate) struct CodexToolItemEventBase {
|
||||
pub(crate) requested_network_access: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum ReviewSubjectKind {
|
||||
CommandExecution,
|
||||
FileChange,
|
||||
McpToolCall,
|
||||
Permissions,
|
||||
NetworkAccess,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum Reviewer {
|
||||
Guardian,
|
||||
User,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum ReviewTrigger {
|
||||
Initial,
|
||||
SandboxDenial,
|
||||
NetworkPolicyDenial,
|
||||
ExecveIntercept,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum ReviewStatus {
|
||||
Approved,
|
||||
Denied,
|
||||
Aborted,
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum ReviewResolution {
|
||||
None,
|
||||
SessionApproval,
|
||||
ExecPolicyAmendment,
|
||||
NetworkPolicyAmendment,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexReviewEventParams {
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) item_id: Option<String>,
|
||||
pub(crate) review_id: String,
|
||||
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
||||
pub(crate) runtime: CodexRuntimeMetadata,
|
||||
pub(crate) thread_source: Option<ThreadSource>,
|
||||
pub(crate) subagent_source: Option<String>,
|
||||
pub(crate) parent_thread_id: Option<String>,
|
||||
pub(crate) tool_kind: ReviewSubjectKind,
|
||||
pub(crate) tool_name: String,
|
||||
pub(crate) reviewer: Reviewer,
|
||||
pub(crate) trigger: ReviewTrigger,
|
||||
pub(crate) status: ReviewStatus,
|
||||
pub(crate) resolution: ReviewResolution,
|
||||
pub(crate) started_at_ms: u64,
|
||||
pub(crate) completed_at_ms: u64,
|
||||
pub(crate) duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexReviewEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexReviewEventParams,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum WebSearchActionKind {
|
||||
|
||||
@@ -1447,7 +1447,7 @@ fn tool_item_base(
|
||||
item_id,
|
||||
app_server_client: context.connection_state.app_server_client.clone(),
|
||||
runtime: context.connection_state.runtime.clone(),
|
||||
thread_source: thread_metadata.thread_source.map(ThreadSource::as_str),
|
||||
thread_source: thread_metadata.thread_source,
|
||||
subagent_source: thread_metadata.subagent_source.clone(),
|
||||
parent_thread_id: thread_metadata.parent_thread_id.clone(),
|
||||
tool_name,
|
||||
|
||||
@@ -7,6 +7,8 @@ license.workspace = true
|
||||
[lib]
|
||||
name = "codex_ansi_escape"
|
||||
path = "src/lib.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
[lib]
|
||||
name = "codex_app_server_client"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
[lib]
|
||||
name = "codex_app_server_protocol"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -533,200 +533,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"DeviceKeyCreateParams": {
|
||||
"description": "Create a controller-local device key with a random key id.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DeviceKeyProtectionPolicy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Defaults to `hardware_only` when omitted."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"clientId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeyProtectionPolicy": {
|
||||
"description": "Protection policy for creating or loading a controller-local device key.",
|
||||
"enum": [
|
||||
"hardware_only",
|
||||
"allow_os_protected_nonextractable"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DeviceKeyPublicParams": {
|
||||
"description": "Fetch a controller-local device key public key by id.",
|
||||
"properties": {
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"keyId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeySignParams": {
|
||||
"description": "Sign an accepted structured payload with a controller-local device key.",
|
||||
"properties": {
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/definitions/DeviceKeySignPayload"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"keyId",
|
||||
"payload"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeySignPayload": {
|
||||
"description": "Structured payloads accepted by `device/key/sign`.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Payload bound to one remote-control controller websocket `/client` connection challenge.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"audience": {
|
||||
"$ref": "#/definitions/RemoteControlClientConnectionAudience"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"description": "Must contain exactly `remote_control_controller_websocket`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Backend-issued websocket session id that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetOrigin": {
|
||||
"description": "Origin of the backend endpoint that issued the challenge and will verify this proof.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetPath": {
|
||||
"description": "Websocket route path that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenExpiresAt": {
|
||||
"description": "Remote-control token expiration as Unix seconds.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenSha256Base64url": {
|
||||
"description": "SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remoteControlClientConnection"
|
||||
],
|
||||
"title": "RemoteControlClientConnectionDeviceKeySignPayloadType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"audience",
|
||||
"clientId",
|
||||
"nonce",
|
||||
"scopes",
|
||||
"sessionId",
|
||||
"targetOrigin",
|
||||
"targetPath",
|
||||
"tokenExpiresAt",
|
||||
"tokenSha256Base64url",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteControlClientConnectionDeviceKeySignPayload",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Payload bound to a remote-control client `/client/enroll` ownership challenge.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"audience": {
|
||||
"$ref": "#/definitions/RemoteControlClientEnrollmentAudience"
|
||||
},
|
||||
"challengeExpiresAt": {
|
||||
"description": "Enrollment challenge expiration as Unix seconds.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"challengeId": {
|
||||
"description": "Backend-issued enrollment challenge id that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceIdentitySha256Base64url": {
|
||||
"description": "SHA-256 of the requested device identity operation, encoded as unpadded base64url.",
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string"
|
||||
},
|
||||
"targetOrigin": {
|
||||
"description": "Origin of the backend endpoint that issued the challenge and will verify this proof.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetPath": {
|
||||
"description": "HTTP route path that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remoteControlClientEnrollment"
|
||||
],
|
||||
"title": "RemoteControlClientEnrollmentDeviceKeySignPayloadType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"audience",
|
||||
"challengeExpiresAt",
|
||||
"challengeId",
|
||||
"clientId",
|
||||
"deviceIdentitySha256Base64url",
|
||||
"nonce",
|
||||
"targetOrigin",
|
||||
"targetPath",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteControlClientEnrollmentDeviceKeySignPayload",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"DynamicToolSpec": {
|
||||
"properties": {
|
||||
"deferLoading": {
|
||||
@@ -2504,20 +2310,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"RemoteControlClientConnectionAudience": {
|
||||
"description": "Audience for a remote-control client connection device-key proof.",
|
||||
"enum": [
|
||||
"remote_control_client_websocket"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RemoteControlClientEnrollmentAudience": {
|
||||
"description": "Audience for a remote-control client enrollment device-key proof.",
|
||||
"enum": [
|
||||
"remote_control_client_enrollment"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -4304,6 +4096,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStartParams": {
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
@@ -5308,78 +5125,6 @@
|
||||
"title": "App/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"device/key/create"
|
||||
],
|
||||
"title": "Device/key/createRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/DeviceKeyCreateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Device/key/createRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"device/key/public"
|
||||
],
|
||||
"title": "Device/key/publicRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/DeviceKeyPublicParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Device/key/publicRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"device/key/sign"
|
||||
],
|
||||
"title": "Device/key/signRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/DeviceKeySignParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Device/key/signRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -6461,4 +6206,4 @@
|
||||
}
|
||||
],
|
||||
"title": "ClientRequest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -906,78 +906,6 @@
|
||||
"title": "App/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"device/key/create"
|
||||
],
|
||||
"title": "Device/key/createRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/DeviceKeyCreateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Device/key/createRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"device/key/public"
|
||||
],
|
||||
"title": "Device/key/publicRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/DeviceKeyPublicParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Device/key/publicRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"device/key/sign"
|
||||
],
|
||||
"title": "Device/key/signRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/DeviceKeySignParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Device/key/signRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -7947,300 +7875,6 @@
|
||||
"title": "DeprecationNoticeNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeyAlgorithm": {
|
||||
"description": "Device-key algorithm reported at enrollment and signing boundaries.",
|
||||
"enum": [
|
||||
"ecdsa_p256_sha256"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DeviceKeyCreateParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Create a controller-local device key with a random key id.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/DeviceKeyProtectionPolicy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Defaults to `hardware_only` when omitted."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"clientId"
|
||||
],
|
||||
"title": "DeviceKeyCreateParams",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeyCreateResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Device-key metadata and public key returned by create/public APIs.",
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"$ref": "#/definitions/v2/DeviceKeyAlgorithm"
|
||||
},
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionClass": {
|
||||
"$ref": "#/definitions/v2/DeviceKeyProtectionClass"
|
||||
},
|
||||
"publicKeySpkiDerBase64": {
|
||||
"description": "SubjectPublicKeyInfo DER encoded as base64.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"algorithm",
|
||||
"keyId",
|
||||
"protectionClass",
|
||||
"publicKeySpkiDerBase64"
|
||||
],
|
||||
"title": "DeviceKeyCreateResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeyProtectionClass": {
|
||||
"description": "Platform protection class for a controller-local device key.",
|
||||
"enum": [
|
||||
"hardware_secure_enclave",
|
||||
"hardware_tpm",
|
||||
"os_protected_nonextractable"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DeviceKeyProtectionPolicy": {
|
||||
"description": "Protection policy for creating or loading a controller-local device key.",
|
||||
"enum": [
|
||||
"hardware_only",
|
||||
"allow_os_protected_nonextractable"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DeviceKeyPublicParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Fetch a controller-local device key public key by id.",
|
||||
"properties": {
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"keyId"
|
||||
],
|
||||
"title": "DeviceKeyPublicParams",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeyPublicResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Device-key public metadata returned by `device/key/public`.",
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"$ref": "#/definitions/v2/DeviceKeyAlgorithm"
|
||||
},
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionClass": {
|
||||
"$ref": "#/definitions/v2/DeviceKeyProtectionClass"
|
||||
},
|
||||
"publicKeySpkiDerBase64": {
|
||||
"description": "SubjectPublicKeyInfo DER encoded as base64.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"algorithm",
|
||||
"keyId",
|
||||
"protectionClass",
|
||||
"publicKeySpkiDerBase64"
|
||||
],
|
||||
"title": "DeviceKeyPublicResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeySignParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Sign an accepted structured payload with a controller-local device key.",
|
||||
"properties": {
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/definitions/v2/DeviceKeySignPayload"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"keyId",
|
||||
"payload"
|
||||
],
|
||||
"title": "DeviceKeySignParams",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeySignPayload": {
|
||||
"description": "Structured payloads accepted by `device/key/sign`.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Payload bound to one remote-control controller websocket `/client` connection challenge.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"audience": {
|
||||
"$ref": "#/definitions/v2/RemoteControlClientConnectionAudience"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"description": "Must contain exactly `remote_control_controller_websocket`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Backend-issued websocket session id that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetOrigin": {
|
||||
"description": "Origin of the backend endpoint that issued the challenge and will verify this proof.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetPath": {
|
||||
"description": "Websocket route path that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenExpiresAt": {
|
||||
"description": "Remote-control token expiration as Unix seconds.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenSha256Base64url": {
|
||||
"description": "SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remoteControlClientConnection"
|
||||
],
|
||||
"title": "RemoteControlClientConnectionDeviceKeySignPayloadType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"audience",
|
||||
"clientId",
|
||||
"nonce",
|
||||
"scopes",
|
||||
"sessionId",
|
||||
"targetOrigin",
|
||||
"targetPath",
|
||||
"tokenExpiresAt",
|
||||
"tokenSha256Base64url",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteControlClientConnectionDeviceKeySignPayload",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Payload bound to a remote-control client `/client/enroll` ownership challenge.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"audience": {
|
||||
"$ref": "#/definitions/v2/RemoteControlClientEnrollmentAudience"
|
||||
},
|
||||
"challengeExpiresAt": {
|
||||
"description": "Enrollment challenge expiration as Unix seconds.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"challengeId": {
|
||||
"description": "Backend-issued enrollment challenge id that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceIdentitySha256Base64url": {
|
||||
"description": "SHA-256 of the requested device identity operation, encoded as unpadded base64url.",
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string"
|
||||
},
|
||||
"targetOrigin": {
|
||||
"description": "Origin of the backend endpoint that issued the challenge and will verify this proof.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetPath": {
|
||||
"description": "HTTP route path that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remoteControlClientEnrollment"
|
||||
],
|
||||
"title": "RemoteControlClientEnrollmentDeviceKeySignPayloadType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"audience",
|
||||
"challengeExpiresAt",
|
||||
"challengeId",
|
||||
"clientId",
|
||||
"deviceIdentitySha256Base64url",
|
||||
"nonce",
|
||||
"targetOrigin",
|
||||
"targetPath",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteControlClientEnrollmentDeviceKeySignPayload",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"DeviceKeySignResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "ASN.1 DER signature returned by `device/key/sign`.",
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"$ref": "#/definitions/v2/DeviceKeyAlgorithm"
|
||||
},
|
||||
"signatureDerBase64": {
|
||||
"description": "ECDSA signature DER encoded as base64.",
|
||||
"type": "string"
|
||||
},
|
||||
"signedPayloadBase64": {
|
||||
"description": "Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte string directly and must not reserialize `payload`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"algorithm",
|
||||
"signatureDerBase64",
|
||||
"signedPayloadBase64"
|
||||
],
|
||||
"title": "DeviceKeySignResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"DynamicToolCallOutputContentItem": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -12553,6 +12187,21 @@
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginSharePrincipal"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -13595,20 +13244,6 @@
|
||||
"title": "ReasoningTextDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"RemoteControlClientConnectionAudience": {
|
||||
"description": "Audience for a remote-control client connection device-key proof.",
|
||||
"enum": [
|
||||
"remote_control_client_websocket"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RemoteControlClientEnrollmentAudience": {
|
||||
"description": "Audience for a remote-control client enrollment device-key proof.",
|
||||
"enum": [
|
||||
"remote_control_client_enrollment"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RemoteControlConnectionStatus": {
|
||||
"enum": [
|
||||
"disabled",
|
||||
@@ -18754,4 +18389,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocol",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1665,78 +1665,6 @@
|
||||
"title": "App/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"device/key/create"
|
||||
],
|
||||
"title": "Device/key/createRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/DeviceKeyCreateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Device/key/createRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"device/key/public"
|
||||
],
|
||||
"title": "Device/key/publicRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/DeviceKeyPublicParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Device/key/publicRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"device/key/sign"
|
||||
],
|
||||
"title": "Device/key/signRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/DeviceKeySignParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Device/key/signRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -4403,300 +4331,6 @@
|
||||
"title": "DeprecationNoticeNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeyAlgorithm": {
|
||||
"description": "Device-key algorithm reported at enrollment and signing boundaries.",
|
||||
"enum": [
|
||||
"ecdsa_p256_sha256"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DeviceKeyCreateParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Create a controller-local device key with a random key id.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DeviceKeyProtectionPolicy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Defaults to `hardware_only` when omitted."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"clientId"
|
||||
],
|
||||
"title": "DeviceKeyCreateParams",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeyCreateResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Device-key metadata and public key returned by create/public APIs.",
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"$ref": "#/definitions/DeviceKeyAlgorithm"
|
||||
},
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionClass": {
|
||||
"$ref": "#/definitions/DeviceKeyProtectionClass"
|
||||
},
|
||||
"publicKeySpkiDerBase64": {
|
||||
"description": "SubjectPublicKeyInfo DER encoded as base64.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"algorithm",
|
||||
"keyId",
|
||||
"protectionClass",
|
||||
"publicKeySpkiDerBase64"
|
||||
],
|
||||
"title": "DeviceKeyCreateResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeyProtectionClass": {
|
||||
"description": "Platform protection class for a controller-local device key.",
|
||||
"enum": [
|
||||
"hardware_secure_enclave",
|
||||
"hardware_tpm",
|
||||
"os_protected_nonextractable"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DeviceKeyProtectionPolicy": {
|
||||
"description": "Protection policy for creating or loading a controller-local device key.",
|
||||
"enum": [
|
||||
"hardware_only",
|
||||
"allow_os_protected_nonextractable"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DeviceKeyPublicParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Fetch a controller-local device key public key by id.",
|
||||
"properties": {
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"keyId"
|
||||
],
|
||||
"title": "DeviceKeyPublicParams",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeyPublicResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Device-key public metadata returned by `device/key/public`.",
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"$ref": "#/definitions/DeviceKeyAlgorithm"
|
||||
},
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionClass": {
|
||||
"$ref": "#/definitions/DeviceKeyProtectionClass"
|
||||
},
|
||||
"publicKeySpkiDerBase64": {
|
||||
"description": "SubjectPublicKeyInfo DER encoded as base64.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"algorithm",
|
||||
"keyId",
|
||||
"protectionClass",
|
||||
"publicKeySpkiDerBase64"
|
||||
],
|
||||
"title": "DeviceKeyPublicResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeySignParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Sign an accepted structured payload with a controller-local device key.",
|
||||
"properties": {
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/definitions/DeviceKeySignPayload"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"keyId",
|
||||
"payload"
|
||||
],
|
||||
"title": "DeviceKeySignParams",
|
||||
"type": "object"
|
||||
},
|
||||
"DeviceKeySignPayload": {
|
||||
"description": "Structured payloads accepted by `device/key/sign`.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Payload bound to one remote-control controller websocket `/client` connection challenge.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"audience": {
|
||||
"$ref": "#/definitions/RemoteControlClientConnectionAudience"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"description": "Must contain exactly `remote_control_controller_websocket`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Backend-issued websocket session id that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetOrigin": {
|
||||
"description": "Origin of the backend endpoint that issued the challenge and will verify this proof.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetPath": {
|
||||
"description": "Websocket route path that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenExpiresAt": {
|
||||
"description": "Remote-control token expiration as Unix seconds.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenSha256Base64url": {
|
||||
"description": "SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remoteControlClientConnection"
|
||||
],
|
||||
"title": "RemoteControlClientConnectionDeviceKeySignPayloadType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"audience",
|
||||
"clientId",
|
||||
"nonce",
|
||||
"scopes",
|
||||
"sessionId",
|
||||
"targetOrigin",
|
||||
"targetPath",
|
||||
"tokenExpiresAt",
|
||||
"tokenSha256Base64url",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteControlClientConnectionDeviceKeySignPayload",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Payload bound to a remote-control client `/client/enroll` ownership challenge.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"audience": {
|
||||
"$ref": "#/definitions/RemoteControlClientEnrollmentAudience"
|
||||
},
|
||||
"challengeExpiresAt": {
|
||||
"description": "Enrollment challenge expiration as Unix seconds.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"challengeId": {
|
||||
"description": "Backend-issued enrollment challenge id that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceIdentitySha256Base64url": {
|
||||
"description": "SHA-256 of the requested device identity operation, encoded as unpadded base64url.",
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string"
|
||||
},
|
||||
"targetOrigin": {
|
||||
"description": "Origin of the backend endpoint that issued the challenge and will verify this proof.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetPath": {
|
||||
"description": "HTTP route path that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remoteControlClientEnrollment"
|
||||
],
|
||||
"title": "RemoteControlClientEnrollmentDeviceKeySignPayloadType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"audience",
|
||||
"challengeExpiresAt",
|
||||
"challengeId",
|
||||
"clientId",
|
||||
"deviceIdentitySha256Base64url",
|
||||
"nonce",
|
||||
"targetOrigin",
|
||||
"targetPath",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteControlClientEnrollmentDeviceKeySignPayload",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"DeviceKeySignResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "ASN.1 DER signature returned by `device/key/sign`.",
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"$ref": "#/definitions/DeviceKeyAlgorithm"
|
||||
},
|
||||
"signatureDerBase64": {
|
||||
"description": "ECDSA signature DER encoded as base64.",
|
||||
"type": "string"
|
||||
},
|
||||
"signedPayloadBase64": {
|
||||
"description": "Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte string directly and must not reserialize `payload`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"algorithm",
|
||||
"signatureDerBase64",
|
||||
"signedPayloadBase64"
|
||||
],
|
||||
"title": "DeviceKeySignResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"DynamicToolCallOutputContentItem": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -9164,6 +8798,21 @@
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -10206,20 +9855,6 @@
|
||||
"title": "ReasoningTextDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"RemoteControlClientConnectionAudience": {
|
||||
"description": "Audience for a remote-control client connection device-key proof.",
|
||||
"enum": [
|
||||
"remote_control_client_websocket"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RemoteControlClientEnrollmentAudience": {
|
||||
"description": "Audience for a remote-control client enrollment device-key proof.",
|
||||
"enum": [
|
||||
"remote_control_client_enrollment"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RemoteControlConnectionStatus": {
|
||||
"enum": [
|
||||
"disabled",
|
||||
@@ -16639,4 +16274,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocolV2",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"DeviceKeyProtectionPolicy": {
|
||||
"description": "Protection policy for creating or loading a controller-local device key.",
|
||||
"enum": [
|
||||
"hardware_only",
|
||||
"allow_os_protected_nonextractable"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Create a controller-local device key with a random key id.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DeviceKeyProtectionPolicy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Defaults to `hardware_only` when omitted."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"clientId"
|
||||
],
|
||||
"title": "DeviceKeyCreateParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"DeviceKeyAlgorithm": {
|
||||
"description": "Device-key algorithm reported at enrollment and signing boundaries.",
|
||||
"enum": [
|
||||
"ecdsa_p256_sha256"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DeviceKeyProtectionClass": {
|
||||
"description": "Platform protection class for a controller-local device key.",
|
||||
"enum": [
|
||||
"hardware_secure_enclave",
|
||||
"hardware_tpm",
|
||||
"os_protected_nonextractable"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Device-key metadata and public key returned by create/public APIs.",
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"$ref": "#/definitions/DeviceKeyAlgorithm"
|
||||
},
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionClass": {
|
||||
"$ref": "#/definitions/DeviceKeyProtectionClass"
|
||||
},
|
||||
"publicKeySpkiDerBase64": {
|
||||
"description": "SubjectPublicKeyInfo DER encoded as base64.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"algorithm",
|
||||
"keyId",
|
||||
"protectionClass",
|
||||
"publicKeySpkiDerBase64"
|
||||
],
|
||||
"title": "DeviceKeyCreateResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Fetch a controller-local device key public key by id.",
|
||||
"properties": {
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"keyId"
|
||||
],
|
||||
"title": "DeviceKeyPublicParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"DeviceKeyAlgorithm": {
|
||||
"description": "Device-key algorithm reported at enrollment and signing boundaries.",
|
||||
"enum": [
|
||||
"ecdsa_p256_sha256"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DeviceKeyProtectionClass": {
|
||||
"description": "Platform protection class for a controller-local device key.",
|
||||
"enum": [
|
||||
"hardware_secure_enclave",
|
||||
"hardware_tpm",
|
||||
"os_protected_nonextractable"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Device-key public metadata returned by `device/key/public`.",
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"$ref": "#/definitions/DeviceKeyAlgorithm"
|
||||
},
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"protectionClass": {
|
||||
"$ref": "#/definitions/DeviceKeyProtectionClass"
|
||||
},
|
||||
"publicKeySpkiDerBase64": {
|
||||
"description": "SubjectPublicKeyInfo DER encoded as base64.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"algorithm",
|
||||
"keyId",
|
||||
"protectionClass",
|
||||
"publicKeySpkiDerBase64"
|
||||
],
|
||||
"title": "DeviceKeyPublicResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"DeviceKeySignPayload": {
|
||||
"description": "Structured payloads accepted by `device/key/sign`.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Payload bound to one remote-control controller websocket `/client` connection challenge.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"audience": {
|
||||
"$ref": "#/definitions/RemoteControlClientConnectionAudience"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"description": "Must contain exactly `remote_control_controller_websocket`.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Backend-issued websocket session id that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetOrigin": {
|
||||
"description": "Origin of the backend endpoint that issued the challenge and will verify this proof.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetPath": {
|
||||
"description": "Websocket route path that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenExpiresAt": {
|
||||
"description": "Remote-control token expiration as Unix seconds.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenSha256Base64url": {
|
||||
"description": "SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remoteControlClientConnection"
|
||||
],
|
||||
"title": "RemoteControlClientConnectionDeviceKeySignPayloadType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"audience",
|
||||
"clientId",
|
||||
"nonce",
|
||||
"scopes",
|
||||
"sessionId",
|
||||
"targetOrigin",
|
||||
"targetPath",
|
||||
"tokenExpiresAt",
|
||||
"tokenSha256Base64url",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteControlClientConnectionDeviceKeySignPayload",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Payload bound to a remote-control client `/client/enroll` ownership challenge.",
|
||||
"properties": {
|
||||
"accountUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"audience": {
|
||||
"$ref": "#/definitions/RemoteControlClientEnrollmentAudience"
|
||||
},
|
||||
"challengeExpiresAt": {
|
||||
"description": "Enrollment challenge expiration as Unix seconds.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"challengeId": {
|
||||
"description": "Backend-issued enrollment challenge id that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceIdentitySha256Base64url": {
|
||||
"description": "SHA-256 of the requested device identity operation, encoded as unpadded base64url.",
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string"
|
||||
},
|
||||
"targetOrigin": {
|
||||
"description": "Origin of the backend endpoint that issued the challenge and will verify this proof.",
|
||||
"type": "string"
|
||||
},
|
||||
"targetPath": {
|
||||
"description": "HTTP route path that this proof authorizes.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remoteControlClientEnrollment"
|
||||
],
|
||||
"title": "RemoteControlClientEnrollmentDeviceKeySignPayloadType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accountUserId",
|
||||
"audience",
|
||||
"challengeExpiresAt",
|
||||
"challengeId",
|
||||
"clientId",
|
||||
"deviceIdentitySha256Base64url",
|
||||
"nonce",
|
||||
"targetOrigin",
|
||||
"targetPath",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteControlClientEnrollmentDeviceKeySignPayload",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RemoteControlClientConnectionAudience": {
|
||||
"description": "Audience for a remote-control client connection device-key proof.",
|
||||
"enum": [
|
||||
"remote_control_client_websocket"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RemoteControlClientEnrollmentAudience": {
|
||||
"description": "Audience for a remote-control client enrollment device-key proof.",
|
||||
"enum": [
|
||||
"remote_control_client_enrollment"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Sign an accepted structured payload with a controller-local device key.",
|
||||
"properties": {
|
||||
"keyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/definitions/DeviceKeySignPayload"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"keyId",
|
||||
"payload"
|
||||
],
|
||||
"title": "DeviceKeySignParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"DeviceKeyAlgorithm": {
|
||||
"description": "Device-key algorithm reported at enrollment and signing boundaries.",
|
||||
"enum": [
|
||||
"ecdsa_p256_sha256"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "ASN.1 DER signature returned by `device/key/sign`.",
|
||||
"properties": {
|
||||
"algorithm": {
|
||||
"$ref": "#/definitions/DeviceKeyAlgorithm"
|
||||
},
|
||||
"signatureDerBase64": {
|
||||
"description": "ECDSA signature DER encoded as base64.",
|
||||
"type": "string"
|
||||
},
|
||||
"signedPayloadBase64": {
|
||||
"description": "Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte string directly and must not reserialize `payload`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"algorithm",
|
||||
"signatureDerBase64",
|
||||
"signedPayloadBase64"
|
||||
],
|
||||
"title": "DeviceKeySignResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -248,6 +248,21 @@
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -255,6 +270,33 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipal": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
@@ -302,6 +302,21 @@
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -309,6 +324,33 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipal": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
@@ -183,6 +183,21 @@
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -215,6 +230,33 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipal": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +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.
|
||||
|
||||
/**
|
||||
* Device-key algorithm reported at enrollment and signing boundaries.
|
||||
*/
|
||||
export type DeviceKeyAlgorithm = "ecdsa_p256_sha256";
|
||||
@@ -1,13 +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 { DeviceKeyProtectionPolicy } from "./DeviceKeyProtectionPolicy";
|
||||
|
||||
/**
|
||||
* Create a controller-local device key with a random key id.
|
||||
*/
|
||||
export type DeviceKeyCreateParams = {
|
||||
/**
|
||||
* Defaults to `hardware_only` when omitted.
|
||||
*/
|
||||
protectionPolicy?: DeviceKeyProtectionPolicy | null, accountUserId: string, clientId: string, };
|
||||
@@ -1,14 +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 { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm";
|
||||
import type { DeviceKeyProtectionClass } from "./DeviceKeyProtectionClass";
|
||||
|
||||
/**
|
||||
* Device-key metadata and public key returned by create/public APIs.
|
||||
*/
|
||||
export type DeviceKeyCreateResponse = { keyId: string,
|
||||
/**
|
||||
* SubjectPublicKeyInfo DER encoded as base64.
|
||||
*/
|
||||
publicKeySpkiDerBase64: string, algorithm: DeviceKeyAlgorithm, protectionClass: DeviceKeyProtectionClass, };
|
||||
@@ -1,8 +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.
|
||||
|
||||
/**
|
||||
* Platform protection class for a controller-local device key.
|
||||
*/
|
||||
export type DeviceKeyProtectionClass = "hardware_secure_enclave" | "hardware_tpm" | "os_protected_nonextractable";
|
||||
@@ -1,8 +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.
|
||||
|
||||
/**
|
||||
* Protection policy for creating or loading a controller-local device key.
|
||||
*/
|
||||
export type DeviceKeyProtectionPolicy = "hardware_only" | "allow_os_protected_nonextractable";
|
||||
@@ -1,8 +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.
|
||||
|
||||
/**
|
||||
* Fetch a controller-local device key public key by id.
|
||||
*/
|
||||
export type DeviceKeyPublicParams = { keyId: string, };
|
||||
@@ -1,14 +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 { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm";
|
||||
import type { DeviceKeyProtectionClass } from "./DeviceKeyProtectionClass";
|
||||
|
||||
/**
|
||||
* Device-key public metadata returned by `device/key/public`.
|
||||
*/
|
||||
export type DeviceKeyPublicResponse = { keyId: string,
|
||||
/**
|
||||
* SubjectPublicKeyInfo DER encoded as base64.
|
||||
*/
|
||||
publicKeySpkiDerBase64: string, algorithm: DeviceKeyAlgorithm, protectionClass: DeviceKeyProtectionClass, };
|
||||
@@ -1,9 +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 { DeviceKeySignPayload } from "./DeviceKeySignPayload";
|
||||
|
||||
/**
|
||||
* Sign an accepted structured payload with a controller-local device key.
|
||||
*/
|
||||
export type DeviceKeySignParams = { keyId: string, payload: DeviceKeySignPayload, };
|
||||
@@ -1,54 +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 { RemoteControlClientConnectionAudience } from "./RemoteControlClientConnectionAudience";
|
||||
import type { RemoteControlClientEnrollmentAudience } from "./RemoteControlClientEnrollmentAudience";
|
||||
|
||||
/**
|
||||
* Structured payloads accepted by `device/key/sign`.
|
||||
*/
|
||||
export type DeviceKeySignPayload = { "type": "remoteControlClientConnection", nonce: string, audience: RemoteControlClientConnectionAudience,
|
||||
/**
|
||||
* Backend-issued websocket session id that this proof authorizes.
|
||||
*/
|
||||
sessionId: string,
|
||||
/**
|
||||
* Origin of the backend endpoint that issued the challenge and will verify this proof.
|
||||
*/
|
||||
targetOrigin: string,
|
||||
/**
|
||||
* Websocket route path that this proof authorizes.
|
||||
*/
|
||||
targetPath: string, accountUserId: string, clientId: string,
|
||||
/**
|
||||
* Remote-control token expiration as Unix seconds.
|
||||
*/
|
||||
tokenExpiresAt: number,
|
||||
/**
|
||||
* SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.
|
||||
*/
|
||||
tokenSha256Base64url: string,
|
||||
/**
|
||||
* Must contain exactly `remote_control_controller_websocket`.
|
||||
*/
|
||||
scopes: Array<string>, } | { "type": "remoteControlClientEnrollment", nonce: string, audience: RemoteControlClientEnrollmentAudience,
|
||||
/**
|
||||
* Backend-issued enrollment challenge id that this proof authorizes.
|
||||
*/
|
||||
challengeId: string,
|
||||
/**
|
||||
* Origin of the backend endpoint that issued the challenge and will verify this proof.
|
||||
*/
|
||||
targetOrigin: string,
|
||||
/**
|
||||
* HTTP route path that this proof authorizes.
|
||||
*/
|
||||
targetPath: string, accountUserId: string, clientId: string,
|
||||
/**
|
||||
* SHA-256 of the requested device identity operation, encoded as unpadded base64url.
|
||||
*/
|
||||
deviceIdentitySha256Base64url: string,
|
||||
/**
|
||||
* Enrollment challenge expiration as Unix seconds.
|
||||
*/
|
||||
challengeExpiresAt: number, };
|
||||
@@ -1,18 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm";
|
||||
|
||||
/**
|
||||
* ASN.1 DER signature returned by `device/key/sign`.
|
||||
*/
|
||||
export type DeviceKeySignResponse = {
|
||||
/**
|
||||
* ECDSA signature DER encoded as base64.
|
||||
*/
|
||||
signatureDerBase64: string,
|
||||
/**
|
||||
* Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte
|
||||
* string directly and must not reserialize `payload`.
|
||||
*/
|
||||
signedPayloadBase64: string, algorithm: DeviceKeyAlgorithm, };
|
||||
@@ -1,5 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginSharePrincipal } from "./PluginSharePrincipal";
|
||||
|
||||
export type PluginShareContext = { remotePluginId: string, creatorAccountUserId: string | null, creatorName: string | null, };
|
||||
export type PluginShareContext = { remotePluginId: string, shareUrl: string | null, creatorAccountUserId: string | null, creatorName: string | null, shareTargets: Array<PluginSharePrincipal> | null, };
|
||||
|
||||
@@ -1,8 +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.
|
||||
|
||||
/**
|
||||
* Audience for a remote-control client connection device-key proof.
|
||||
*/
|
||||
export type RemoteControlClientConnectionAudience = "remote_control_client_websocket";
|
||||
@@ -1,8 +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.
|
||||
|
||||
/**
|
||||
* Audience for a remote-control client enrollment device-key proof.
|
||||
*/
|
||||
export type RemoteControlClientEnrollmentAudience = "remote_control_client_enrollment";
|
||||
@@ -79,16 +79,6 @@ export type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup";
|
||||
export type { ContextCompactedNotification } from "./ContextCompactedNotification";
|
||||
export type { CreditsSnapshot } from "./CreditsSnapshot";
|
||||
export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification";
|
||||
export type { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm";
|
||||
export type { DeviceKeyCreateParams } from "./DeviceKeyCreateParams";
|
||||
export type { DeviceKeyCreateResponse } from "./DeviceKeyCreateResponse";
|
||||
export type { DeviceKeyProtectionClass } from "./DeviceKeyProtectionClass";
|
||||
export type { DeviceKeyProtectionPolicy } from "./DeviceKeyProtectionPolicy";
|
||||
export type { DeviceKeyPublicParams } from "./DeviceKeyPublicParams";
|
||||
export type { DeviceKeyPublicResponse } from "./DeviceKeyPublicResponse";
|
||||
export type { DeviceKeySignParams } from "./DeviceKeySignParams";
|
||||
export type { DeviceKeySignPayload } from "./DeviceKeySignPayload";
|
||||
export type { DeviceKeySignResponse } from "./DeviceKeySignResponse";
|
||||
export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
|
||||
export type { DynamicToolCallParams } from "./DynamicToolCallParams";
|
||||
export type { DynamicToolCallResponse } from "./DynamicToolCallResponse";
|
||||
@@ -319,8 +309,6 @@ export type { ReasoningEffortOption } from "./ReasoningEffortOption";
|
||||
export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification";
|
||||
export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification";
|
||||
export type { ReasoningTextDeltaNotification } from "./ReasoningTextDeltaNotification";
|
||||
export type { RemoteControlClientConnectionAudience } from "./RemoteControlClientConnectionAudience";
|
||||
export type { RemoteControlClientEnrollmentAudience } from "./RemoteControlClientEnrollmentAudience";
|
||||
export type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus";
|
||||
export type { RemoteControlStatusChangedNotification } from "./RemoteControlStatusChangedNotification";
|
||||
export type { RequestPermissionProfile } from "./RequestPermissionProfile";
|
||||
|
||||
@@ -581,6 +581,13 @@ client_request_definitions! {
|
||||
serialization: None,
|
||||
response: v2::ThreadTurnsListResponse,
|
||||
},
|
||||
#[experimental("thread/turns/items/list")]
|
||||
ThreadTurnsItemsList => "thread/turns/items/list" {
|
||||
params: v2::ThreadTurnsItemsListParams,
|
||||
// Explicitly concurrent: this primarily reads append-only rollout storage.
|
||||
serialization: None,
|
||||
response: v2::ThreadTurnsItemsListResponse,
|
||||
},
|
||||
/// Append raw Responses API items to the thread history without starting a user turn.
|
||||
ThreadInjectItems => "thread/inject_items" {
|
||||
params: v2::ThreadInjectItemsParams,
|
||||
@@ -652,21 +659,6 @@ client_request_definitions! {
|
||||
serialization: None,
|
||||
response: v2::AppsListResponse,
|
||||
},
|
||||
DeviceKeyCreate => "device/key/create" {
|
||||
params: v2::DeviceKeyCreateParams,
|
||||
serialization: global("device-key"),
|
||||
response: v2::DeviceKeyCreateResponse,
|
||||
},
|
||||
DeviceKeyPublic => "device/key/public" {
|
||||
params: v2::DeviceKeyPublicParams,
|
||||
serialization: global("device-key"),
|
||||
response: v2::DeviceKeyPublicResponse,
|
||||
},
|
||||
DeviceKeySign => "device/key/sign" {
|
||||
params: v2::DeviceKeySignParams,
|
||||
serialization: global("device-key"),
|
||||
response: v2::DeviceKeySignResponse,
|
||||
},
|
||||
// File system requests are intentionally concurrent. Desktop already treats local
|
||||
// file system operations as concurrent, and app-server remote fs mirrors that model.
|
||||
FsReadFile => "fs/readFile" {
|
||||
@@ -1789,19 +1781,6 @@ mod tests {
|
||||
Some(ClientRequestSerializationScope::Global("config"))
|
||||
);
|
||||
|
||||
let device_key_create = ClientRequest::DeviceKeyCreate {
|
||||
request_id: request_id(),
|
||||
params: v2::DeviceKeyCreateParams {
|
||||
protection_policy: None,
|
||||
account_user_id: "user".to_string(),
|
||||
client_id: "client".to_string(),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
device_key_create.serialization_scope(),
|
||||
Some(ClientRequestSerializationScope::Global("device-key"))
|
||||
);
|
||||
|
||||
let add_credits_nudge = ClientRequest::SendAddCreditsNudgeEmail {
|
||||
request_id: request_id(),
|
||||
params: v2::SendAddCreditsNudgeEmailParams {
|
||||
@@ -1871,10 +1850,23 @@ mod tests {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
sort_direction: None,
|
||||
items_view: None,
|
||||
},
|
||||
};
|
||||
assert_eq!(thread_turns_list.serialization_scope(), None);
|
||||
|
||||
let thread_turns_items_list = ClientRequest::ThreadTurnsItemsList {
|
||||
request_id: request_id(),
|
||||
params: v2::ThreadTurnsItemsListParams {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
cursor: None,
|
||||
limit: None,
|
||||
sort_direction: None,
|
||||
},
|
||||
};
|
||||
assert_eq!(thread_turns_items_list.serialization_scope(), None);
|
||||
|
||||
let mcp_resource_read = ClientRequest::McpResourceRead {
|
||||
request_id: request_id(),
|
||||
params: v2::McpResourceReadParams {
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// Device-key algorithm reported at enrollment and signing boundaries.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case", export_to = "v2/")]
|
||||
pub enum DeviceKeyAlgorithm {
|
||||
EcdsaP256Sha256,
|
||||
}
|
||||
|
||||
/// Platform protection class for a controller-local device key.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case", export_to = "v2/")]
|
||||
pub enum DeviceKeyProtectionClass {
|
||||
HardwareSecureEnclave,
|
||||
HardwareTpm,
|
||||
OsProtectedNonextractable,
|
||||
}
|
||||
|
||||
/// Protection policy for creating or loading a controller-local device key.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case", export_to = "v2/")]
|
||||
pub enum DeviceKeyProtectionPolicy {
|
||||
HardwareOnly,
|
||||
AllowOsProtectedNonextractable,
|
||||
}
|
||||
|
||||
/// Create a controller-local device key with a random key id.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DeviceKeyCreateParams {
|
||||
/// Defaults to `hardware_only` when omitted.
|
||||
#[ts(optional = nullable)]
|
||||
pub protection_policy: Option<DeviceKeyProtectionPolicy>,
|
||||
pub account_user_id: String,
|
||||
pub client_id: String,
|
||||
}
|
||||
|
||||
/// Device-key metadata and public key returned by create/public APIs.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DeviceKeyCreateResponse {
|
||||
pub key_id: String,
|
||||
/// SubjectPublicKeyInfo DER encoded as base64.
|
||||
pub public_key_spki_der_base64: String,
|
||||
pub algorithm: DeviceKeyAlgorithm,
|
||||
pub protection_class: DeviceKeyProtectionClass,
|
||||
}
|
||||
|
||||
/// Fetch a controller-local device key public key by id.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DeviceKeyPublicParams {
|
||||
pub key_id: String,
|
||||
}
|
||||
|
||||
/// Device-key public metadata returned by `device/key/public`.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DeviceKeyPublicResponse {
|
||||
pub key_id: String,
|
||||
/// SubjectPublicKeyInfo DER encoded as base64.
|
||||
pub public_key_spki_der_base64: String,
|
||||
pub algorithm: DeviceKeyAlgorithm,
|
||||
pub protection_class: DeviceKeyProtectionClass,
|
||||
}
|
||||
|
||||
/// Current remote-control connection status and environment id exposed to clients.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlStatusChangedNotification {
|
||||
pub status: RemoteControlConnectionStatus,
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub enum RemoteControlConnectionStatus {
|
||||
Disabled,
|
||||
Connecting,
|
||||
Connected,
|
||||
Errored,
|
||||
}
|
||||
|
||||
/// Audience for a remote-control client connection device-key proof.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case", export_to = "v2/")]
|
||||
pub enum RemoteControlClientConnectionAudience {
|
||||
RemoteControlClientWebsocket,
|
||||
}
|
||||
|
||||
/// Audience for a remote-control client enrollment device-key proof.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case", export_to = "v2/")]
|
||||
pub enum RemoteControlClientEnrollmentAudience {
|
||||
RemoteControlClientEnrollment,
|
||||
}
|
||||
|
||||
/// Structured payloads accepted by `device/key/sign`.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type", export_to = "v2/")]
|
||||
pub enum DeviceKeySignPayload {
|
||||
/// Payload bound to one remote-control controller websocket `/client` connection challenge.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
RemoteControlClientConnection {
|
||||
nonce: String,
|
||||
audience: RemoteControlClientConnectionAudience,
|
||||
/// Backend-issued websocket session id that this proof authorizes.
|
||||
session_id: String,
|
||||
/// Origin of the backend endpoint that issued the challenge and will verify this proof.
|
||||
target_origin: String,
|
||||
/// Websocket route path that this proof authorizes.
|
||||
target_path: String,
|
||||
account_user_id: String,
|
||||
client_id: String,
|
||||
/// Remote-control token expiration as Unix seconds.
|
||||
#[ts(type = "number")]
|
||||
token_expires_at: i64,
|
||||
/// SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.
|
||||
token_sha256_base64url: String,
|
||||
/// Must contain exactly `remote_control_controller_websocket`.
|
||||
scopes: Vec<String>,
|
||||
},
|
||||
/// Payload bound to a remote-control client `/client/enroll` ownership challenge.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
RemoteControlClientEnrollment {
|
||||
nonce: String,
|
||||
audience: RemoteControlClientEnrollmentAudience,
|
||||
/// Backend-issued enrollment challenge id that this proof authorizes.
|
||||
challenge_id: String,
|
||||
/// Origin of the backend endpoint that issued the challenge and will verify this proof.
|
||||
target_origin: String,
|
||||
/// HTTP route path that this proof authorizes.
|
||||
target_path: String,
|
||||
account_user_id: String,
|
||||
client_id: String,
|
||||
/// SHA-256 of the requested device identity operation, encoded as unpadded base64url.
|
||||
device_identity_sha256_base64url: String,
|
||||
/// Enrollment challenge expiration as Unix seconds.
|
||||
#[ts(type = "number")]
|
||||
challenge_expires_at: i64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Sign an accepted structured payload with a controller-local device key.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DeviceKeySignParams {
|
||||
pub key_id: String,
|
||||
pub payload: DeviceKeySignPayload,
|
||||
}
|
||||
|
||||
/// ASN.1 DER signature returned by `device/key/sign`.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct DeviceKeySignResponse {
|
||||
/// ECDSA signature DER encoded as base64.
|
||||
pub signature_der_base64: String,
|
||||
/// Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte
|
||||
/// string directly and must not reserialize `payload`.
|
||||
pub signed_payload_base64: String,
|
||||
pub algorithm: DeviceKeyAlgorithm,
|
||||
}
|
||||
@@ -5,7 +5,6 @@ mod apps;
|
||||
mod collaboration_mode;
|
||||
mod command_exec;
|
||||
mod config;
|
||||
mod device_key;
|
||||
mod experimental_feature;
|
||||
mod feedback;
|
||||
mod fs;
|
||||
@@ -18,6 +17,7 @@ mod permissions;
|
||||
mod plugin;
|
||||
mod process;
|
||||
mod realtime;
|
||||
mod remote_control;
|
||||
mod review;
|
||||
mod thread;
|
||||
mod thread_data;
|
||||
@@ -29,7 +29,6 @@ pub use apps::*;
|
||||
pub use collaboration_mode::*;
|
||||
pub use command_exec::*;
|
||||
pub use config::*;
|
||||
pub use device_key::*;
|
||||
pub use experimental_feature::*;
|
||||
pub use feedback::*;
|
||||
pub use fs::*;
|
||||
@@ -42,6 +41,7 @@ pub use permissions::*;
|
||||
pub use plugin::*;
|
||||
pub use process::*;
|
||||
pub use realtime::*;
|
||||
pub use remote_control::*;
|
||||
pub use review::*;
|
||||
pub use shared::*;
|
||||
pub use thread::*;
|
||||
|
||||
@@ -539,8 +539,10 @@ pub struct PluginSummary {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareContext {
|
||||
pub remote_plugin_id: String,
|
||||
pub share_url: Option<String>,
|
||||
pub creator_account_user_id: Option<String>,
|
||||
pub creator_name: Option<String>,
|
||||
pub share_targets: Option<Vec<PluginSharePrincipal>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// Current remote-control connection status and environment id exposed to clients.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlStatusChangedNotification {
|
||||
pub status: RemoteControlConnectionStatus,
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub enum RemoteControlConnectionStatus {
|
||||
Disabled,
|
||||
Connecting,
|
||||
Connected,
|
||||
Errored,
|
||||
}
|
||||
@@ -95,6 +95,59 @@ fn turn_defaults_legacy_missing_items_view_to_full() {
|
||||
assert_eq!(turn.items_view, TurnItemsView::Full);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_turns_list_params_accepts_items_view() {
|
||||
let params = serde_json::from_value::<ThreadTurnsListParams>(json!({
|
||||
"threadId": "thr_123",
|
||||
"cursor": null,
|
||||
"limit": 25,
|
||||
"sortDirection": "desc",
|
||||
"itemsView": "notLoaded",
|
||||
}))
|
||||
.expect("thread turns list params should deserialize");
|
||||
|
||||
assert_eq!(params.thread_id, "thr_123");
|
||||
assert_eq!(params.items_view, Some(TurnItemsView::NotLoaded));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_turns_items_list_round_trips() {
|
||||
let params = ThreadTurnsItemsListParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
turn_id: "turn_456".to_string(),
|
||||
cursor: Some("cursor_1".to_string()),
|
||||
limit: Some(50),
|
||||
sort_direction: Some(SortDirection::Asc),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(¶ms).expect("serialize params"),
|
||||
json!({
|
||||
"threadId": "thr_123",
|
||||
"turnId": "turn_456",
|
||||
"cursor": "cursor_1",
|
||||
"limit": 50,
|
||||
"sortDirection": "asc",
|
||||
})
|
||||
);
|
||||
let response = ThreadTurnsItemsListResponse {
|
||||
data: vec![ThreadItem::ContextCompaction {
|
||||
id: "item_1".to_string(),
|
||||
}],
|
||||
next_cursor: None,
|
||||
backwards_cursor: Some("cursor_0".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(&response).expect("serialize response"),
|
||||
json!({
|
||||
"data": [{"type": "contextCompaction", "id": "item_1"}],
|
||||
"nextCursor": null,
|
||||
"backwardsCursor": "cursor_0",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_list_params_accepts_single_cwd() {
|
||||
let params = serde_json::from_value::<ThreadListParams>(json!({
|
||||
@@ -664,181 +717,6 @@ fn fs_read_file_params_round_trip() {
|
||||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn device_key_create_params_round_trip_uses_protection_policy() {
|
||||
let params = DeviceKeyCreateParams {
|
||||
protection_policy: None,
|
||||
account_user_id: "account-user-1".to_string(),
|
||||
client_id: "cli_123".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¶ms).expect("serialize device/key/create params");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"accountUserId": "account-user-1",
|
||||
"clientId": "cli_123",
|
||||
"protectionPolicy": null,
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<DeviceKeyCreateParams>(value)
|
||||
.expect("deserialize device/key/create params");
|
||||
assert_eq!(decoded, params);
|
||||
|
||||
let params = DeviceKeyCreateParams {
|
||||
protection_policy: Some(DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable),
|
||||
account_user_id: "account-user-1".to_string(),
|
||||
client_id: "cli_123".to_string(),
|
||||
};
|
||||
let value = serde_json::to_value(¶ms)
|
||||
.expect("serialize device/key/create params with protection policy");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"accountUserId": "account-user-1",
|
||||
"clientId": "cli_123",
|
||||
"protectionPolicy": "allow_os_protected_nonextractable",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn device_key_create_response_round_trips_protection_class() {
|
||||
let response = DeviceKeyCreateResponse {
|
||||
key_id: "dk_123".to_string(),
|
||||
public_key_spki_der_base64: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE".to_string(),
|
||||
algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256,
|
||||
protection_class: DeviceKeyProtectionClass::OsProtectedNonextractable,
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&response).expect("serialize device/key/create response");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"keyId": "dk_123",
|
||||
"publicKeySpkiDerBase64": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE",
|
||||
"algorithm": "ecdsa_p256_sha256",
|
||||
"protectionClass": "os_protected_nonextractable",
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<DeviceKeyCreateResponse>(value)
|
||||
.expect("deserialize device/key/create response");
|
||||
assert_eq!(decoded, response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn device_key_sign_params_round_trip_uses_accepted_payload_enum() {
|
||||
let params = DeviceKeySignParams {
|
||||
key_id: "dk_123".to_string(),
|
||||
payload: DeviceKeySignPayload::RemoteControlClientConnection {
|
||||
nonce: "nonce-1".to_string(),
|
||||
audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket,
|
||||
session_id: "wssess_123".to_string(),
|
||||
target_origin: "https://chatgpt.com".to_string(),
|
||||
target_path: "/api/codex/remote/control/client".to_string(),
|
||||
account_user_id: "account-user-1".to_string(),
|
||||
client_id: "cli_123".to_string(),
|
||||
token_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU".to_string(),
|
||||
token_expires_at: 1_700_000_000,
|
||||
scopes: vec!["remote_control_controller_websocket".to_string()],
|
||||
},
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¶ms).expect("serialize device/key/sign params");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"keyId": "dk_123",
|
||||
"payload": {
|
||||
"type": "remoteControlClientConnection",
|
||||
"nonce": "nonce-1",
|
||||
"audience": "remote_control_client_websocket",
|
||||
"sessionId": "wssess_123",
|
||||
"targetOrigin": "https://chatgpt.com",
|
||||
"targetPath": "/api/codex/remote/control/client",
|
||||
"accountUserId": "account-user-1",
|
||||
"clientId": "cli_123",
|
||||
"tokenSha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU",
|
||||
"tokenExpiresAt": 1_700_000_000,
|
||||
"scopes": ["remote_control_controller_websocket"],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<DeviceKeySignParams>(value)
|
||||
.expect("deserialize device/key/sign params");
|
||||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn device_key_sign_params_round_trip_uses_enrollment_payload() {
|
||||
let params = DeviceKeySignParams {
|
||||
key_id: "dk_123".to_string(),
|
||||
payload: DeviceKeySignPayload::RemoteControlClientEnrollment {
|
||||
nonce: "nonce-1".to_string(),
|
||||
audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment,
|
||||
challenge_id: "rch_123".to_string(),
|
||||
target_origin: "https://chatgpt.com".to_string(),
|
||||
target_path: "/wham/remote/control/client/enroll".to_string(),
|
||||
account_user_id: "account-user-1".to_string(),
|
||||
client_id: "cli_123".to_string(),
|
||||
device_identity_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"
|
||||
.to_string(),
|
||||
challenge_expires_at: 1_700_000_000,
|
||||
},
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¶ms)
|
||||
.expect("serialize device/key/sign params with enrollment payload");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"keyId": "dk_123",
|
||||
"payload": {
|
||||
"type": "remoteControlClientEnrollment",
|
||||
"nonce": "nonce-1",
|
||||
"audience": "remote_control_client_enrollment",
|
||||
"challengeId": "rch_123",
|
||||
"targetOrigin": "https://chatgpt.com",
|
||||
"targetPath": "/wham/remote/control/client/enroll",
|
||||
"accountUserId": "account-user-1",
|
||||
"clientId": "cli_123",
|
||||
"deviceIdentitySha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU",
|
||||
"challengeExpiresAt": 1_700_000_000,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<DeviceKeySignParams>(value)
|
||||
.expect("deserialize device/key/sign params with enrollment payload");
|
||||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn device_key_sign_response_returns_signed_payload_bytes() {
|
||||
let response = DeviceKeySignResponse {
|
||||
signature_der_base64: "MEUCIQD".to_string(),
|
||||
signed_payload_base64: "eyJkb21haW4iOiJjb2RleA".to_string(),
|
||||
algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256,
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&response).expect("serialize device/key/sign response");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"signatureDerBase64": "MEUCIQD",
|
||||
"signedPayloadBase64": "eyJkb21haW4iOiJjb2RleA",
|
||||
"algorithm": "ecdsa_p256_sha256",
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<DeviceKeySignResponse>(value)
|
||||
.expect("deserialize device/key/sign response");
|
||||
assert_eq!(decoded, response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fs_create_directory_params_round_trip_with_default_recursive() {
|
||||
let params = FsCreateDirectoryParams {
|
||||
|
||||
@@ -6,9 +6,11 @@ use super::PermissionProfileSelectionParams;
|
||||
use super::SandboxMode;
|
||||
use super::SandboxPolicy;
|
||||
use super::Thread;
|
||||
use super::ThreadItem;
|
||||
use super::ThreadSource;
|
||||
use super::Turn;
|
||||
use super::TurnEnvironmentParams;
|
||||
use super::TurnItemsView;
|
||||
use super::shared::v2_enum_from_core;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::config_types::Personality;
|
||||
@@ -1005,6 +1007,9 @@ pub struct ThreadTurnsListParams {
|
||||
/// Optional turn pagination direction; defaults to descending.
|
||||
#[ts(optional = nullable)]
|
||||
pub sort_direction: Option<SortDirection>,
|
||||
/// How much item detail to include for each returned turn; defaults to summary.
|
||||
#[ts(optional = nullable)]
|
||||
pub items_view: Option<TurnItemsView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -1022,6 +1027,36 @@ pub struct ThreadTurnsListResponse {
|
||||
pub backwards_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadTurnsItemsListParams {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
/// Opaque cursor to pass to the next call to continue after the last item.
|
||||
#[ts(optional = nullable)]
|
||||
pub cursor: Option<String>,
|
||||
/// Optional item page size.
|
||||
#[ts(optional = nullable)]
|
||||
pub limit: Option<u32>,
|
||||
/// Optional item pagination direction; defaults to ascending.
|
||||
#[ts(optional = nullable)]
|
||||
pub sort_direction: Option<SortDirection>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadTurnsItemsListResponse {
|
||||
pub data: Vec<ThreadItem>,
|
||||
/// Opaque cursor to pass to the next call to continue after the last item.
|
||||
/// if None, there are no more items to return.
|
||||
pub next_cursor: Option<String>,
|
||||
/// Opaque cursor to pass as `cursor` when reversing `sortDirection`.
|
||||
/// This is only populated when the page contains at least one item.
|
||||
pub backwards_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -23,3 +23,7 @@ tracing-subscriber = { workspace = true }
|
||||
tungstenite = { workspace = true }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
[lib]
|
||||
name = "codex_app_server_transport"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -171,14 +171,6 @@ pub enum ConnectionOrigin {
|
||||
RemoteControl,
|
||||
}
|
||||
|
||||
impl ConnectionOrigin {
|
||||
pub fn allows_device_key_requests(self) -> bool {
|
||||
// Device-key endpoints are only for local connections that own the app-server instance.
|
||||
// Do not include remote transports such as SSH or remote-control websocket connections.
|
||||
matches!(self, Self::Stdio | Self::InProcess)
|
||||
}
|
||||
}
|
||||
|
||||
static CONNECTION_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn next_connection_id() -> ConnectionId {
|
||||
|
||||
@@ -15,6 +15,7 @@ path = "src/bin/notify_capture.rs"
|
||||
[lib]
|
||||
name = "codex_app_server"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -35,7 +36,6 @@ codex-cloud-requirements = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-core-plugins = { workspace = true }
|
||||
codex-device-key = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-external-agent-migration = { workspace = true }
|
||||
codex-external-agent-sessions = { workspace = true }
|
||||
@@ -72,7 +72,6 @@ clap = { workspace = true, features = ["derive"] }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
time = { workspace = true }
|
||||
@@ -88,20 +87,19 @@ tokio = { workspace = true, features = [
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
app_test_support = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
axum = { workspace = true, default-features = false, features = [
|
||||
"http1",
|
||||
"json",
|
||||
"tokio",
|
||||
] }
|
||||
core_test_support = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
core_test_support = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
opentelemetry = { workspace = true }
|
||||
@@ -114,8 +112,10 @@ rmcp = { workspace = true, default-features = false, features = [
|
||||
"transport-streamable-http-server",
|
||||
] }
|
||||
serial_test = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tracing-opentelemetry = { workspace = true }
|
||||
url = { workspace = true }
|
||||
wiremock = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
|
||||
@@ -149,7 +149,8 @@ Example with notification opt-out:
|
||||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
||||
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `nextCursor`, and `backwardsCursor`.
|
||||
- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
|
||||
- `thread/turns/items/list` — experimental; reserved for paging full items for one turn. The API shape is present, but app-server currently returns an unsupported-method JSON-RPC error.
|
||||
- `thread/metadata/update` — patch stored thread metadata in sqlite; currently supports updating persisted `gitInfo` fields and returns the refreshed `thread`.
|
||||
- `thread/memoryMode/set` — experimental; set a thread’s persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success.
|
||||
- `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success.
|
||||
@@ -212,9 +213,6 @@ Example with notification opt-out:
|
||||
- `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle.
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
- `app/list` — list available apps.
|
||||
- `device/key/create` — create or load a controller-local device signing key for an account/client binding. This local-key API is available only over local transports such as stdio and in-process; remote transports reject it. Hardware-backed providers are the target protection class; an OS-protected non-extractable fallback is allowed only with `protectionPolicy: "allow_os_protected_nonextractable"` and returns the reported `protectionClass`.
|
||||
- `device/key/public` — return a device key's SPKI DER public key as base64 plus its `algorithm` and `protectionClass`.
|
||||
- `device/key/sign` — sign one of the accepted structured payload variants with a controller-local device key. The only accepted payload today is `remoteControlClientConnection`, which binds a server-issued `/client` websocket challenge to the enrolled controller device without signing the bearer token itself; this is intentionally not an arbitrary-byte signing API.
|
||||
- `remoteControl/status/changed` — notification emitted when the remote-control status or client-visible environment id changes. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `environmentId` is a string when the app-server has a current enrollment and `null` when that enrollment is cleared, invalidated, or remote control is disabled. Newly initialized app-server clients always receive the current status snapshot.
|
||||
- `skills/config/write` — write user-level skill config by name or absolute path.
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
|
||||
@@ -427,13 +425,14 @@ Use `thread/read` to fetch a stored thread by id without resuming it. Pass `incl
|
||||
|
||||
Use `thread/turns/list` with `capabilities.experimentalApi = true` to page a stored thread’s turn history without resuming it. By default, results are sorted descending so clients can start at the present and fetch older turns with `nextCursor`. The response also includes `backwardsCursor`; pass it as `cursor` on a later request with `sortDirection: "asc"` to fetch turns newer than the first item from the earlier page.
|
||||
|
||||
Every returned `Turn` includes `itemsView`, which tells clients whether the `items` array was omitted intentionally (`notLoaded`), contains only summary items (`summary`), or contains every item available from persisted app-server history (`full`). Current `thread/turns/list` responses return `full` turns.
|
||||
Every returned `Turn` includes `itemsView`, which tells clients whether the `items` array was omitted intentionally (`notLoaded`), contains only summary items (`summary`), or contains every item available from persisted app-server history (`full`). Pass `itemsView` to choose the returned detail level; omitted `itemsView` defaults to `"summary"`.
|
||||
|
||||
```json
|
||||
{ "method": "thread/turns/list", "id": 24, "params": {
|
||||
"threadId": "thr_123",
|
||||
"limit": 50,
|
||||
"sortDirection": "desc"
|
||||
"sortDirection": "desc",
|
||||
"itemsView": "summary"
|
||||
} }
|
||||
{ "id": 24, "result": {
|
||||
"data": [ ... ],
|
||||
@@ -442,6 +441,19 @@ Every returned `Turn` includes `itemsView`, which tells clients whether the `ite
|
||||
} }
|
||||
```
|
||||
|
||||
`thread/turns/items/list` is the planned hydration API for fetching full items for one turn:
|
||||
|
||||
```json
|
||||
{ "method": "thread/turns/items/list", "id": 25, "params": {
|
||||
"threadId": "thr_123",
|
||||
"turnId": "turn_456",
|
||||
"limit": 100,
|
||||
"sortDirection": "asc"
|
||||
} }
|
||||
```
|
||||
|
||||
This method currently returns JSON-RPC `-32601` with message `thread/turns/items/list is not supported yet`.
|
||||
|
||||
### Example: Update stored thread metadata
|
||||
|
||||
Use `thread/metadata/update` to patch sqlite-backed metadata for a thread without resuming it. Today this supports persisted `gitInfo`; omitted fields are left unchanged, while explicit `null` clears a stored value.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
|
||||
pub(crate) const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
pub(crate) const METHOD_NOT_FOUND_ERROR_CODE: i64 = -32601;
|
||||
pub const INVALID_PARAMS_ERROR_CODE: i64 = -32602;
|
||||
pub(crate) const INTERNAL_ERROR_CODE: i64 = -32603;
|
||||
pub(crate) const OVERLOADED_ERROR_CODE: i64 = -32001;
|
||||
@@ -10,6 +11,10 @@ pub(crate) fn invalid_request(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
error(INVALID_REQUEST_ERROR_CODE, message)
|
||||
}
|
||||
|
||||
pub(crate) fn method_not_found(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
error(METHOD_NOT_FOUND_ERROR_CODE, message)
|
||||
}
|
||||
|
||||
pub(crate) fn invalid_params(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
error(INVALID_PARAMS_ERROR_CODE, message)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ use crate::outgoing_message::OutgoingMessage;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::outgoing_message::QueuedOutgoingMessage;
|
||||
use crate::transport::CHANNEL_CAPACITY;
|
||||
use crate::transport::ConnectionOrigin;
|
||||
use crate::transport::OutboundConnectionState;
|
||||
use crate::transport::route_outgoing_envelope;
|
||||
use codex_analytics::AppServerRpcTransport;
|
||||
@@ -435,7 +434,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
|
||||
plugin_startup_tasks: crate::PluginStartupTasks::Start,
|
||||
}));
|
||||
let mut thread_created_rx = processor.thread_created_receiver();
|
||||
let session = Arc::new(ConnectionSessionState::new(ConnectionOrigin::InProcess));
|
||||
let session = Arc::new(ConnectionSessionState::new());
|
||||
let mut listen_for_threads = true;
|
||||
|
||||
loop {
|
||||
@@ -722,14 +721,8 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ConfigRequirementsReadResponse;
|
||||
use codex_app_server_protocol::DeviceKeyPublicParams;
|
||||
use codex_app_server_protocol::DeviceKeySignParams;
|
||||
use codex_app_server_protocol::DeviceKeySignPayload;
|
||||
use codex_app_server_protocol::RemoteControlClientConnectionAudience;
|
||||
use codex_app_server_protocol::RemoteControlClientEnrollmentAudience;
|
||||
use codex_app_server_protocol::SessionSource as ApiSessionSource;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
@@ -821,87 +814,6 @@ mod tests {
|
||||
.expect("in-process runtime should shutdown cleanly");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn in_process_allows_device_key_requests_to_reach_device_key_processor() {
|
||||
let client = start_test_client(SessionSource::Cli).await;
|
||||
const MALFORMED_KEY_ID_MESSAGE: &str = concat!(
|
||||
"invalid device key payload: keyId must be dk_hse_, dk_tpm_, or dk_osn_ ",
|
||||
"followed by unpadded base64url-encoded 32 bytes"
|
||||
);
|
||||
let requests = [
|
||||
(
|
||||
ClientRequest::DeviceKeyPublic {
|
||||
request_id: RequestId::Integer(11),
|
||||
params: DeviceKeyPublicParams {
|
||||
key_id: String::new(),
|
||||
},
|
||||
},
|
||||
MALFORMED_KEY_ID_MESSAGE,
|
||||
),
|
||||
(
|
||||
ClientRequest::DeviceKeySign {
|
||||
request_id: RequestId::Integer(12),
|
||||
params: DeviceKeySignParams {
|
||||
key_id: String::new(),
|
||||
payload: DeviceKeySignPayload::RemoteControlClientConnection {
|
||||
nonce: "nonce-123".to_string(),
|
||||
audience:
|
||||
RemoteControlClientConnectionAudience::RemoteControlClientWebsocket,
|
||||
session_id: "wssess_123".to_string(),
|
||||
target_origin: "https://chatgpt.com".to_string(),
|
||||
target_path: "/api/codex/remote/control/client".to_string(),
|
||||
account_user_id: "acct_123".to_string(),
|
||||
client_id: "cli_123".to_string(),
|
||||
token_expires_at: 4_102_444_800,
|
||||
token_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"
|
||||
.to_string(),
|
||||
scopes: vec!["remote_control_controller_websocket".to_string()],
|
||||
},
|
||||
},
|
||||
},
|
||||
MALFORMED_KEY_ID_MESSAGE,
|
||||
),
|
||||
(
|
||||
ClientRequest::DeviceKeySign {
|
||||
request_id: RequestId::Integer(13),
|
||||
params: DeviceKeySignParams {
|
||||
key_id: String::new(),
|
||||
payload: DeviceKeySignPayload::RemoteControlClientEnrollment {
|
||||
nonce: "nonce-123".to_string(),
|
||||
audience:
|
||||
RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment,
|
||||
challenge_id: "rch_123".to_string(),
|
||||
target_origin: "https://chatgpt.com".to_string(),
|
||||
target_path: "/wham/remote/control/client/enroll".to_string(),
|
||||
account_user_id: "acct_123".to_string(),
|
||||
client_id: "cli_123".to_string(),
|
||||
device_identity_sha256_base64url:
|
||||
"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU".to_string(),
|
||||
challenge_expires_at: 4_102_444_800,
|
||||
},
|
||||
},
|
||||
},
|
||||
MALFORMED_KEY_ID_MESSAGE,
|
||||
),
|
||||
];
|
||||
|
||||
for (request, expected_message) in requests {
|
||||
let error = client
|
||||
.request(request)
|
||||
.await
|
||||
.expect("request transport should work")
|
||||
.expect_err("request should be rejected");
|
||||
|
||||
assert_eq!(error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(error.message, expected_message);
|
||||
}
|
||||
|
||||
client
|
||||
.shutdown()
|
||||
.await
|
||||
.expect("in-process runtime should shutdown cleanly");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn in_process_start_uses_requested_session_source_for_thread_start() {
|
||||
for (requested_source, expected_source) in [
|
||||
|
||||
@@ -17,7 +17,6 @@ use crate::request_processors::AppsRequestProcessor;
|
||||
use crate::request_processors::CatalogRequestProcessor;
|
||||
use crate::request_processors::CommandExecRequestProcessor;
|
||||
use crate::request_processors::ConfigRequestProcessor;
|
||||
use crate::request_processors::DeviceKeyRequestProcessor;
|
||||
use crate::request_processors::ExternalAgentConfigRequestProcessor;
|
||||
use crate::request_processors::FeedbackRequestProcessor;
|
||||
use crate::request_processors::FsRequestProcessor;
|
||||
@@ -37,7 +36,6 @@ use crate::request_serialization::RequestSerializationQueueKey;
|
||||
use crate::request_serialization::RequestSerializationQueues;
|
||||
use crate::thread_state::ThreadStateManager;
|
||||
use crate::transport::AppServerTransport;
|
||||
use crate::transport::ConnectionOrigin;
|
||||
use crate::transport::RemoteControlHandle;
|
||||
use async_trait::async_trait;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
@@ -160,7 +158,6 @@ pub(crate) struct MessageProcessor {
|
||||
command_exec_processor: CommandExecRequestProcessor,
|
||||
process_exec_processor: ProcessExecRequestProcessor,
|
||||
config_processor: ConfigRequestProcessor,
|
||||
device_key_processor: DeviceKeyRequestProcessor,
|
||||
external_agent_config_processor: ExternalAgentConfigRequestProcessor,
|
||||
feedback_processor: FeedbackRequestProcessor,
|
||||
fs_processor: FsRequestProcessor,
|
||||
@@ -179,7 +176,6 @@ pub(crate) struct MessageProcessor {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ConnectionSessionState {
|
||||
origin: ConnectionOrigin,
|
||||
pub(crate) rpc_gate: Arc<ConnectionRpcGate>,
|
||||
initialized: OnceLock<InitializedConnectionSessionState>,
|
||||
}
|
||||
@@ -194,14 +190,13 @@ pub(crate) struct InitializedConnectionSessionState {
|
||||
|
||||
impl Default for ConnectionSessionState {
|
||||
fn default() -> Self {
|
||||
Self::new(ConnectionOrigin::WebSocket)
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionSessionState {
|
||||
pub(crate) fn new(origin: ConnectionOrigin) -> Self {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
origin,
|
||||
rpc_gate: Arc::new(ConnectionRpcGate::new()),
|
||||
initialized: OnceLock::new(),
|
||||
}
|
||||
@@ -211,10 +206,6 @@ impl ConnectionSessionState {
|
||||
self.initialized.get().is_some()
|
||||
}
|
||||
|
||||
fn allows_device_key_requests(&self) -> bool {
|
||||
self.origin.allows_device_key_requests()
|
||||
}
|
||||
|
||||
pub(crate) fn experimental_api_enabled(&self) -> bool {
|
||||
self.initialized
|
||||
.get()
|
||||
@@ -397,7 +388,7 @@ impl MessageProcessor {
|
||||
thread_watch_manager.clone(),
|
||||
Arc::clone(&thread_list_state_permit),
|
||||
thread_goal_processor.clone(),
|
||||
state_db.clone(),
|
||||
state_db,
|
||||
);
|
||||
let turn_processor = TurnRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
@@ -441,7 +432,6 @@ impl MessageProcessor {
|
||||
arg0_paths,
|
||||
config.codex_home.to_path_buf(),
|
||||
);
|
||||
let device_key_processor = DeviceKeyRequestProcessor::new(outgoing.clone(), state_db);
|
||||
let fs_processor = FsRequestProcessor::new(
|
||||
thread_manager
|
||||
.environment_manager()
|
||||
@@ -463,7 +453,6 @@ impl MessageProcessor {
|
||||
command_exec_processor,
|
||||
process_exec_processor,
|
||||
config_processor,
|
||||
device_key_processor,
|
||||
external_agent_config_processor,
|
||||
feedback_processor,
|
||||
fs_processor,
|
||||
@@ -770,7 +759,6 @@ impl MessageProcessor {
|
||||
let serialization_scope = codex_request.serialization_scope();
|
||||
let app_server_client_name = session.app_server_client_name().map(str::to_string);
|
||||
let client_version = session.client_version().map(str::to_string);
|
||||
let device_key_requests_allowed = session.allows_device_key_requests();
|
||||
let error_request_id = connection_request_id.clone();
|
||||
let rpc_gate = Arc::clone(&session.rpc_gate);
|
||||
let processor = Arc::clone(self);
|
||||
@@ -786,7 +774,6 @@ impl MessageProcessor {
|
||||
request_context,
|
||||
app_server_client_name,
|
||||
client_version,
|
||||
device_key_requests_allowed,
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
@@ -816,7 +803,6 @@ impl MessageProcessor {
|
||||
request_context: RequestContext,
|
||||
app_server_client_name: Option<String>,
|
||||
client_version: Option<String>,
|
||||
device_key_requests_allowed: bool,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let connection_id = connection_request_id.connection_id;
|
||||
let request_id = ConnectionRequestId {
|
||||
@@ -864,30 +850,6 @@ impl MessageProcessor {
|
||||
.config_requirements_read()
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::DeviceKeyCreate { params, .. } => {
|
||||
self.device_key_processor.create(
|
||||
request_id.clone(),
|
||||
params,
|
||||
device_key_requests_allowed,
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
ClientRequest::DeviceKeyPublic { params, .. } => {
|
||||
self.device_key_processor.public(
|
||||
request_id.clone(),
|
||||
params,
|
||||
device_key_requests_allowed,
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
ClientRequest::DeviceKeySign { params, .. } => {
|
||||
self.device_key_processor.sign(
|
||||
request_id.clone(),
|
||||
params,
|
||||
device_key_requests_allowed,
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
ClientRequest::FsReadFile { params, .. } => self
|
||||
.fs_processor
|
||||
.read_file(params)
|
||||
@@ -1046,6 +1008,9 @@ impl MessageProcessor {
|
||||
ClientRequest::ThreadTurnsList { params, .. } => {
|
||||
self.thread_processor.thread_turns_list(params).await
|
||||
}
|
||||
ClientRequest::ThreadTurnsItemsList { params, .. } => {
|
||||
self.thread_processor.thread_turns_items_list(params).await
|
||||
}
|
||||
ClientRequest::ThreadShellCommand { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_shell_command(&request_id, params)
|
||||
|
||||
@@ -6,21 +6,16 @@ use crate::config_manager::ConfigManager;
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::transport::AppServerTransport;
|
||||
use crate::transport::ConnectionOrigin;
|
||||
use anyhow::Result;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use app_test_support::write_mock_responses_config_toml;
|
||||
use codex_analytics::AppServerRpcTransport;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::DeviceKeySignParams;
|
||||
use codex_app_server_protocol::DeviceKeySignPayload;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::InitializeResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_app_server_protocol::RemoteControlClientConnectionAudience;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
@@ -121,10 +116,6 @@ struct TracingHarness {
|
||||
|
||||
impl TracingHarness {
|
||||
async fn new() -> Result<Self> {
|
||||
Self::new_with_origin(ConnectionOrigin::WebSocket).await
|
||||
}
|
||||
|
||||
async fn new_with_origin(origin: ConnectionOrigin) -> Result<Self> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = Arc::new(build_test_config(codex_home.path(), &server.uri()).await?);
|
||||
@@ -137,7 +128,7 @@ impl TracingHarness {
|
||||
_codex_home: codex_home,
|
||||
processor,
|
||||
outgoing_rx,
|
||||
session: Arc::new(ConnectionSessionState::new(origin)),
|
||||
session: Arc::new(ConnectionSessionState::new()),
|
||||
tracing,
|
||||
};
|
||||
|
||||
@@ -196,29 +187,6 @@ impl TracingHarness {
|
||||
read_response(&mut self.outgoing_rx, request_id).await
|
||||
}
|
||||
|
||||
async fn request_error(
|
||||
&mut self,
|
||||
request: ClientRequest,
|
||||
trace: Option<W3cTraceContext>,
|
||||
) -> JSONRPCErrorError {
|
||||
let request_id = match request.id() {
|
||||
RequestId::Integer(request_id) => *request_id,
|
||||
request_id => panic!("expected integer request id in test harness, got {request_id:?}"),
|
||||
};
|
||||
let mut request = request_from_client_request(request);
|
||||
request.trace = trace;
|
||||
|
||||
self.processor
|
||||
.process_request(
|
||||
TEST_CONNECTION_ID,
|
||||
request,
|
||||
&AppServerTransport::Stdio,
|
||||
Arc::clone(&self.session),
|
||||
)
|
||||
.await;
|
||||
read_error(&mut self.outgoing_rx, request_id).await
|
||||
}
|
||||
|
||||
async fn start_thread(
|
||||
&mut self,
|
||||
request_id: i64,
|
||||
@@ -485,36 +453,6 @@ async fn read_response<T: serde::de::DeserializeOwned>(
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_error(
|
||||
outgoing_rx: &mut mpsc::Receiver<crate::outgoing_message::OutgoingEnvelope>,
|
||||
request_id: i64,
|
||||
) -> JSONRPCErrorError {
|
||||
loop {
|
||||
let envelope = tokio::time::timeout(std::time::Duration::from_secs(5), outgoing_rx.recv())
|
||||
.await
|
||||
.expect("timed out waiting for error")
|
||||
.expect("outgoing channel closed");
|
||||
let crate::outgoing_message::OutgoingEnvelope::ToConnection {
|
||||
connection_id,
|
||||
message,
|
||||
..
|
||||
} = envelope
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if connection_id != TEST_CONNECTION_ID {
|
||||
continue;
|
||||
}
|
||||
let crate::outgoing_message::OutgoingMessage::Error(error) = message else {
|
||||
continue;
|
||||
};
|
||||
if error.id != RequestId::Integer(request_id) {
|
||||
continue;
|
||||
}
|
||||
return error.error;
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_thread_started_notification(
|
||||
outgoing_rx: &mut mpsc::Receiver<crate::outgoing_message::OutgoingEnvelope>,
|
||||
) {
|
||||
@@ -693,47 +631,6 @@ fn thread_start_jsonrpc_span_exports_server_span_and_parents_children() -> Resul
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[serial(app_server_tracing)]
|
||||
async fn remote_control_origin_rejects_device_key_requests() -> Result<()> {
|
||||
let mut harness = TracingHarness::new_with_origin(ConnectionOrigin::RemoteControl).await?;
|
||||
|
||||
let error = harness
|
||||
.request_error(
|
||||
ClientRequest::DeviceKeySign {
|
||||
request_id: RequestId::Integer(20_004),
|
||||
params: DeviceKeySignParams {
|
||||
key_id: "dk_123".to_string(),
|
||||
payload: DeviceKeySignPayload::RemoteControlClientConnection {
|
||||
nonce: "nonce-123".to_string(),
|
||||
audience:
|
||||
RemoteControlClientConnectionAudience::RemoteControlClientWebsocket,
|
||||
session_id: "wssess_123".to_string(),
|
||||
target_origin: "https://chatgpt.com".to_string(),
|
||||
target_path: "/api/codex/remote/control/client".to_string(),
|
||||
account_user_id: "acct_123".to_string(),
|
||||
client_id: "cli_123".to_string(),
|
||||
token_expires_at: 4_102_444_800,
|
||||
token_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"
|
||||
.to_string(),
|
||||
scopes: vec!["remote_control_controller_websocket".to_string()],
|
||||
},
|
||||
},
|
||||
},
|
||||
/*trace*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(error.code, crate::error_code::INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(
|
||||
error.message,
|
||||
"device/key/sign is not available over remote transports"
|
||||
);
|
||||
|
||||
harness.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[serial(app_server_tracing)]
|
||||
async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
|
||||
|
||||
@@ -13,10 +13,8 @@ use crate::outgoing_message::RequestContext;
|
||||
use crate::outgoing_message::ThreadScopedOutgoingMessageSender;
|
||||
use crate::thread_status::ThreadWatchManager;
|
||||
use crate::thread_status::resolve_thread_status;
|
||||
use chrono::DateTime;
|
||||
use chrono::Duration as ChronoDuration;
|
||||
use chrono::SecondsFormat;
|
||||
use chrono::Utc;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_analytics::AnalyticsJsonRpcError;
|
||||
use codex_analytics::InputError;
|
||||
@@ -217,6 +215,7 @@ use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::ThreadTurnsItemsListParams;
|
||||
use codex_app_server_protocol::ThreadTurnsListParams;
|
||||
use codex_app_server_protocol::ThreadTurnsListResponse;
|
||||
use codex_app_server_protocol::ThreadUnarchiveParams;
|
||||
@@ -274,7 +273,6 @@ use codex_core::exec::ExecCapturePolicy;
|
||||
use codex_core::exec::ExecExpiration;
|
||||
use codex_core::exec::ExecParams;
|
||||
use codex_core::exec_env::create_env;
|
||||
use codex_core::find_thread_name_by_id;
|
||||
use codex_core::find_thread_path_by_id_str;
|
||||
use codex_core::path_utils;
|
||||
#[cfg(test)]
|
||||
@@ -435,7 +433,6 @@ mod apps_processor;
|
||||
mod catalog_processor;
|
||||
mod command_exec_processor;
|
||||
mod config_processor;
|
||||
mod device_key_processor;
|
||||
mod external_agent_config_processor;
|
||||
mod feedback_processor;
|
||||
mod fs_processor;
|
||||
@@ -456,7 +453,6 @@ pub(crate) use apps_processor::AppsRequestProcessor;
|
||||
pub(crate) use catalog_processor::CatalogRequestProcessor;
|
||||
pub(crate) use command_exec_processor::CommandExecRequestProcessor;
|
||||
pub(crate) use config_processor::ConfigRequestProcessor;
|
||||
pub(crate) use device_key_processor::DeviceKeyRequestProcessor;
|
||||
pub(crate) use external_agent_config_processor::ExternalAgentConfigRequestProcessor;
|
||||
pub(crate) use feedback_processor::FeedbackRequestProcessor;
|
||||
pub(crate) use fs_processor::FsRequestProcessor;
|
||||
@@ -498,6 +494,7 @@ pub(crate) use self::thread_lifecycle::populate_thread_turns_from_history;
|
||||
pub(crate) use self::thread_processor::thread_from_stored_thread;
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::thread_summary::read_summary_from_rollout;
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::thread_summary::summary_to_thread;
|
||||
|
||||
pub(crate) fn build_api_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
|
||||
|
||||
@@ -46,7 +46,6 @@ use codex_login::AuthManager;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::protocol::Op;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -378,14 +377,22 @@ impl ConfigRequestProcessor {
|
||||
}
|
||||
|
||||
async fn reload_user_config(&self) {
|
||||
let next_config = match self.load_latest_config(/*fallback_cwd*/ None).await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"failed to rebuild user config for runtime refresh: {}",
|
||||
err.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let thread_ids = self.thread_manager.list_thread_ids().await;
|
||||
for thread_id in thread_ids {
|
||||
let Ok(thread) = self.thread_manager.get_thread(thread_id).await else {
|
||||
continue;
|
||||
};
|
||||
if let Err(err) = thread.submit(Op::ReloadUserConfig).await {
|
||||
tracing::warn!("failed to request user config reload: {err}");
|
||||
}
|
||||
thread.refresh_runtime_config(next_config.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::outgoing_message::ConnectionRequestId;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use codex_app_server_protocol::ClientResponsePayload;
|
||||
use codex_app_server_protocol::DeviceKeyAlgorithm;
|
||||
use codex_app_server_protocol::DeviceKeyCreateParams;
|
||||
use codex_app_server_protocol::DeviceKeyCreateResponse;
|
||||
use codex_app_server_protocol::DeviceKeyProtectionClass;
|
||||
use codex_app_server_protocol::DeviceKeyPublicParams;
|
||||
use codex_app_server_protocol::DeviceKeyPublicResponse;
|
||||
use codex_app_server_protocol::DeviceKeySignParams;
|
||||
use codex_app_server_protocol::DeviceKeySignPayload;
|
||||
use codex_app_server_protocol::DeviceKeySignResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_device_key::DeviceKeyBinding;
|
||||
use codex_device_key::DeviceKeyBindingStore;
|
||||
use codex_device_key::DeviceKeyCreateRequest;
|
||||
use codex_device_key::DeviceKeyError;
|
||||
use codex_device_key::DeviceKeyGetPublicRequest;
|
||||
use codex_device_key::DeviceKeyInfo;
|
||||
use codex_device_key::DeviceKeyProtectionPolicy;
|
||||
use codex_device_key::DeviceKeySignRequest;
|
||||
use codex_device_key::DeviceKeyStore;
|
||||
use codex_device_key::RemoteControlClientConnectionAudience;
|
||||
use codex_device_key::RemoteControlClientConnectionSignPayload;
|
||||
use codex_device_key::RemoteControlClientEnrollmentAudience;
|
||||
use codex_device_key::RemoteControlClientEnrollmentSignPayload;
|
||||
use codex_state::DeviceKeyBindingRecord;
|
||||
use codex_state::StateRuntime;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DeviceKeyRequestProcessor {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
store: DeviceKeyStore,
|
||||
}
|
||||
|
||||
impl DeviceKeyRequestProcessor {
|
||||
pub(crate) fn new(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
outgoing,
|
||||
store: DeviceKeyStore::new(Arc::new(StateDeviceKeyBindingStore::new(state_db))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: DeviceKeyCreateParams,
|
||||
device_key_requests_allowed: bool,
|
||||
) {
|
||||
self.spawn_request(
|
||||
request_id,
|
||||
"device/key/create",
|
||||
device_key_requests_allowed,
|
||||
move |store| async move { create_device_key(store, params).await },
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn public(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: DeviceKeyPublicParams,
|
||||
device_key_requests_allowed: bool,
|
||||
) {
|
||||
self.spawn_request(
|
||||
request_id,
|
||||
"device/key/public",
|
||||
device_key_requests_allowed,
|
||||
move |store| async move { public_device_key(store, params).await },
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn sign(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: DeviceKeySignParams,
|
||||
device_key_requests_allowed: bool,
|
||||
) {
|
||||
self.spawn_request(
|
||||
request_id,
|
||||
"device/key/sign",
|
||||
device_key_requests_allowed,
|
||||
move |store| async move { sign_device_key(store, params).await },
|
||||
);
|
||||
}
|
||||
|
||||
fn spawn_request<R, F, Fut>(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
method: &'static str,
|
||||
device_key_requests_allowed: bool,
|
||||
run_request: F,
|
||||
) where
|
||||
R: Into<ClientResponsePayload> + Send + 'static,
|
||||
F: FnOnce(DeviceKeyStore) -> Fut + Send + 'static,
|
||||
Fut: Future<Output = Result<R, JSONRPCErrorError>> + Send + 'static,
|
||||
{
|
||||
let store = self.store.clone();
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
tokio::spawn(async move {
|
||||
let result = if !device_key_requests_allowed {
|
||||
Err(invalid_request(format!(
|
||||
"{method} is not available over remote transports"
|
||||
)))
|
||||
} else {
|
||||
run_request(store).await
|
||||
};
|
||||
outgoing.send_result(request_id, result).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_device_key(
|
||||
store: DeviceKeyStore,
|
||||
params: DeviceKeyCreateParams,
|
||||
) -> Result<DeviceKeyCreateResponse, JSONRPCErrorError> {
|
||||
let info = store
|
||||
.create(DeviceKeyCreateRequest {
|
||||
protection_policy: protection_policy_from_params(params.protection_policy),
|
||||
binding: DeviceKeyBinding {
|
||||
account_user_id: params.account_user_id,
|
||||
client_id: params.client_id,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.map_err(map_device_key_error)?;
|
||||
Ok(create_response_from_info(info))
|
||||
}
|
||||
|
||||
async fn public_device_key(
|
||||
store: DeviceKeyStore,
|
||||
params: DeviceKeyPublicParams,
|
||||
) -> Result<DeviceKeyPublicResponse, JSONRPCErrorError> {
|
||||
let info = store
|
||||
.get_public(DeviceKeyGetPublicRequest {
|
||||
key_id: params.key_id,
|
||||
})
|
||||
.await
|
||||
.map_err(map_device_key_error)?;
|
||||
Ok(public_response_from_info(info))
|
||||
}
|
||||
|
||||
async fn sign_device_key(
|
||||
store: DeviceKeyStore,
|
||||
params: DeviceKeySignParams,
|
||||
) -> Result<DeviceKeySignResponse, JSONRPCErrorError> {
|
||||
let signature = store
|
||||
.sign(DeviceKeySignRequest {
|
||||
key_id: params.key_id,
|
||||
payload: payload_from_params(params.payload),
|
||||
})
|
||||
.await
|
||||
.map_err(map_device_key_error)?;
|
||||
Ok(DeviceKeySignResponse {
|
||||
signature_der_base64: STANDARD.encode(signature.signature_der),
|
||||
signed_payload_base64: STANDARD.encode(signature.signed_payload),
|
||||
algorithm: algorithm_from_store(signature.algorithm),
|
||||
})
|
||||
}
|
||||
|
||||
struct StateDeviceKeyBindingStore {
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
}
|
||||
|
||||
impl StateDeviceKeyBindingStore {
|
||||
fn new(state_db: Option<Arc<StateRuntime>>) -> Self {
|
||||
Self { state_db }
|
||||
}
|
||||
|
||||
async fn state_db(&self) -> Result<Arc<StateRuntime>, DeviceKeyError> {
|
||||
self.state_db
|
||||
.clone()
|
||||
.ok_or_else(|| DeviceKeyError::Platform("sqlite state db unavailable".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for StateDeviceKeyBindingStore {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("StateDeviceKeyBindingStore")
|
||||
.field("has_state_db", &self.state_db.is_some())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DeviceKeyBindingStore for StateDeviceKeyBindingStore {
|
||||
async fn get_binding(&self, key_id: &str) -> Result<Option<DeviceKeyBinding>, DeviceKeyError> {
|
||||
let state_db = self.state_db().await?;
|
||||
state_db
|
||||
.get_device_key_binding(key_id)
|
||||
.await
|
||||
.map(|record| {
|
||||
record.map(|record| DeviceKeyBinding {
|
||||
account_user_id: record.account_user_id,
|
||||
client_id: record.client_id,
|
||||
})
|
||||
})
|
||||
.map_err(|err| DeviceKeyError::Platform(err.to_string()))
|
||||
}
|
||||
|
||||
async fn put_binding(
|
||||
&self,
|
||||
key_id: &str,
|
||||
binding: &DeviceKeyBinding,
|
||||
) -> Result<(), DeviceKeyError> {
|
||||
let state_db = self.state_db().await?;
|
||||
state_db
|
||||
.upsert_device_key_binding(&DeviceKeyBindingRecord {
|
||||
key_id: key_id.to_string(),
|
||||
account_user_id: binding.account_user_id.clone(),
|
||||
client_id: binding.client_id.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| DeviceKeyError::Platform(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn create_response_from_info(info: DeviceKeyInfo) -> DeviceKeyCreateResponse {
|
||||
DeviceKeyCreateResponse {
|
||||
key_id: info.key_id,
|
||||
public_key_spki_der_base64: STANDARD.encode(info.public_key_spki_der),
|
||||
algorithm: algorithm_from_store(info.algorithm),
|
||||
protection_class: protection_class_from_store(info.protection_class),
|
||||
}
|
||||
}
|
||||
|
||||
fn public_response_from_info(info: DeviceKeyInfo) -> DeviceKeyPublicResponse {
|
||||
DeviceKeyPublicResponse {
|
||||
key_id: info.key_id,
|
||||
public_key_spki_der_base64: STANDARD.encode(info.public_key_spki_der),
|
||||
algorithm: algorithm_from_store(info.algorithm),
|
||||
protection_class: protection_class_from_store(info.protection_class),
|
||||
}
|
||||
}
|
||||
|
||||
fn protection_policy_from_params(
|
||||
protection_policy: Option<codex_app_server_protocol::DeviceKeyProtectionPolicy>,
|
||||
) -> DeviceKeyProtectionPolicy {
|
||||
match protection_policy
|
||||
.unwrap_or(codex_app_server_protocol::DeviceKeyProtectionPolicy::HardwareOnly)
|
||||
{
|
||||
codex_app_server_protocol::DeviceKeyProtectionPolicy::HardwareOnly => {
|
||||
DeviceKeyProtectionPolicy::HardwareOnly
|
||||
}
|
||||
codex_app_server_protocol::DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable => {
|
||||
DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn payload_from_params(payload: DeviceKeySignPayload) -> codex_device_key::DeviceKeySignPayload {
|
||||
match payload {
|
||||
DeviceKeySignPayload::RemoteControlClientConnection {
|
||||
nonce,
|
||||
audience,
|
||||
session_id,
|
||||
target_origin,
|
||||
target_path,
|
||||
account_user_id,
|
||||
client_id,
|
||||
token_sha256_base64url,
|
||||
token_expires_at,
|
||||
scopes,
|
||||
} => codex_device_key::DeviceKeySignPayload::RemoteControlClientConnection(
|
||||
RemoteControlClientConnectionSignPayload {
|
||||
nonce,
|
||||
audience: remote_control_client_connection_audience_from_protocol(audience),
|
||||
session_id,
|
||||
target_origin,
|
||||
target_path,
|
||||
account_user_id,
|
||||
client_id,
|
||||
token_sha256_base64url,
|
||||
token_expires_at,
|
||||
scopes,
|
||||
},
|
||||
),
|
||||
DeviceKeySignPayload::RemoteControlClientEnrollment {
|
||||
nonce,
|
||||
audience,
|
||||
challenge_id,
|
||||
target_origin,
|
||||
target_path,
|
||||
account_user_id,
|
||||
client_id,
|
||||
device_identity_sha256_base64url,
|
||||
challenge_expires_at,
|
||||
} => codex_device_key::DeviceKeySignPayload::RemoteControlClientEnrollment(
|
||||
RemoteControlClientEnrollmentSignPayload {
|
||||
nonce,
|
||||
audience: remote_control_client_enrollment_audience_from_protocol(audience),
|
||||
challenge_id,
|
||||
target_origin,
|
||||
target_path,
|
||||
account_user_id,
|
||||
client_id,
|
||||
device_identity_sha256_base64url,
|
||||
challenge_expires_at,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_control_client_connection_audience_from_protocol(
|
||||
audience: codex_app_server_protocol::RemoteControlClientConnectionAudience,
|
||||
) -> RemoteControlClientConnectionAudience {
|
||||
match audience {
|
||||
codex_app_server_protocol::RemoteControlClientConnectionAudience::RemoteControlClientWebsocket => {
|
||||
RemoteControlClientConnectionAudience::RemoteControlClientWebsocket
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_control_client_enrollment_audience_from_protocol(
|
||||
audience: codex_app_server_protocol::RemoteControlClientEnrollmentAudience,
|
||||
) -> RemoteControlClientEnrollmentAudience {
|
||||
match audience {
|
||||
codex_app_server_protocol::RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment => {
|
||||
RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn algorithm_from_store(algorithm: codex_device_key::DeviceKeyAlgorithm) -> DeviceKeyAlgorithm {
|
||||
match algorithm {
|
||||
codex_device_key::DeviceKeyAlgorithm::EcdsaP256Sha256 => {
|
||||
DeviceKeyAlgorithm::EcdsaP256Sha256
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn protection_class_from_store(
|
||||
protection_class: codex_device_key::DeviceKeyProtectionClass,
|
||||
) -> DeviceKeyProtectionClass {
|
||||
match protection_class {
|
||||
codex_device_key::DeviceKeyProtectionClass::HardwareSecureEnclave => {
|
||||
DeviceKeyProtectionClass::HardwareSecureEnclave
|
||||
}
|
||||
codex_device_key::DeviceKeyProtectionClass::HardwareTpm => {
|
||||
DeviceKeyProtectionClass::HardwareTpm
|
||||
}
|
||||
codex_device_key::DeviceKeyProtectionClass::OsProtectedNonextractable => {
|
||||
DeviceKeyProtectionClass::OsProtectedNonextractable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_device_key_error(error: DeviceKeyError) -> JSONRPCErrorError {
|
||||
match &error {
|
||||
DeviceKeyError::DegradedProtectionNotAllowed { .. }
|
||||
| DeviceKeyError::HardwareBackedKeysUnavailable
|
||||
| DeviceKeyError::KeyNotFound
|
||||
| DeviceKeyError::InvalidPayload(_) => invalid_request(error.to_string()),
|
||||
DeviceKeyError::Platform(_) | DeviceKeyError::Crypto(_) => {
|
||||
internal_error(error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,13 @@ impl FeedbackRequestProcessor {
|
||||
{
|
||||
tracing::info!(target: "feedback_tags", chatgpt_user_id);
|
||||
}
|
||||
if let Some(account_id) = self
|
||||
.auth_manager
|
||||
.auth_cached()
|
||||
.and_then(|auth| auth.get_account_id())
|
||||
{
|
||||
tracing::info!(target: "feedback_tags", account_id);
|
||||
}
|
||||
let snapshot = self.feedback.snapshot(conversation_id);
|
||||
let thread_id = snapshot.thread_id.clone();
|
||||
let (feedback_thread_ids, sqlite_feedback_logs, state_db_ctx) = if include_logs {
|
||||
|
||||
@@ -108,8 +108,10 @@ fn share_context_for_source(
|
||||
.cloned()
|
||||
.map(|remote_plugin_id| PluginShareContext {
|
||||
remote_plugin_id,
|
||||
share_url: None,
|
||||
creator_account_user_id: None,
|
||||
creator_name: None,
|
||||
share_targets: None,
|
||||
}),
|
||||
MarketplacePluginSource::Git { .. } => None,
|
||||
}
|
||||
@@ -1473,8 +1475,15 @@ fn remote_plugin_share_context_to_info(
|
||||
) -> PluginShareContext {
|
||||
PluginShareContext {
|
||||
remote_plugin_id: context.remote_plugin_id,
|
||||
share_url: context.share_url,
|
||||
creator_account_user_id: context.creator_account_user_id,
|
||||
creator_name: context.creator_name,
|
||||
share_targets: context.share_targets.map(|targets| {
|
||||
targets
|
||||
.into_iter()
|
||||
.map(plugin_share_principal_from_remote)
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::error_code::method_not_found;
|
||||
|
||||
const THREAD_LIST_DEFAULT_LIMIT: usize = 25;
|
||||
const THREAD_LIST_MAX_LIMIT: usize = 100;
|
||||
@@ -591,6 +592,15 @@ impl ThreadRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_turns_items_list(
|
||||
&self,
|
||||
_params: ThreadTurnsItemsListParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
Err(method_not_found(
|
||||
"thread/turns/items/list is not supported yet",
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_shell_command(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
@@ -1605,11 +1615,8 @@ impl ThreadRequestProcessor {
|
||||
.unarchive_thread(StoreArchiveThreadParams { thread_id })
|
||||
.await
|
||||
.map_err(|err| thread_store_archive_error("unarchive", err))?;
|
||||
let summary = summary_from_stored_thread(stored_thread, fallback_provider.as_str())
|
||||
.ok_or_else(|| {
|
||||
internal_error(format!("failed to read unarchived thread {thread_id}"))
|
||||
})?;
|
||||
let mut thread = summary_to_thread(summary, &self.config.cwd);
|
||||
let (mut thread, _) =
|
||||
thread_from_stored_thread(stored_thread, fallback_provider.as_str(), &self.config.cwd);
|
||||
|
||||
thread.status = resolve_thread_status(
|
||||
self.thread_watch_manager
|
||||
@@ -2075,7 +2082,9 @@ impl ThreadRequestProcessor {
|
||||
cursor,
|
||||
limit,
|
||||
sort_direction,
|
||||
items_view,
|
||||
} = params;
|
||||
let items_view = items_view.unwrap_or(TurnItemsView::Summary);
|
||||
|
||||
let thread_uuid = ThreadId::from_string(&thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
||||
@@ -2104,7 +2113,7 @@ impl ThreadRequestProcessor {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let turns = reconstruct_thread_turns_for_turns_list(
|
||||
let mut turns = reconstruct_thread_turns_for_turns_list(
|
||||
&items,
|
||||
self.thread_watch_manager
|
||||
.loaded_status_for_thread(&thread_uuid.to_string())
|
||||
@@ -2112,6 +2121,41 @@ impl ThreadRequestProcessor {
|
||||
has_live_running_thread,
|
||||
active_turn,
|
||||
);
|
||||
for turn in &mut turns {
|
||||
match items_view {
|
||||
TurnItemsView::NotLoaded => {
|
||||
turn.items.clear();
|
||||
turn.items_view = TurnItemsView::NotLoaded;
|
||||
}
|
||||
TurnItemsView::Summary => {
|
||||
let first_user_message = turn
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| matches!(item, ThreadItem::UserMessage { .. }))
|
||||
.cloned();
|
||||
let final_agent_message = turn
|
||||
.items
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|item| matches!(item, ThreadItem::AgentMessage { .. }))
|
||||
.cloned();
|
||||
turn.items = match (first_user_message, final_agent_message) {
|
||||
(Some(user_message), Some(agent_message))
|
||||
if user_message.id() != agent_message.id() =>
|
||||
{
|
||||
vec![user_message, agent_message]
|
||||
}
|
||||
(Some(user_message), _) => vec![user_message],
|
||||
(None, Some(agent_message)) => vec![agent_message],
|
||||
(None, None) => Vec::new(),
|
||||
};
|
||||
turn.items_view = TurnItemsView::Summary;
|
||||
}
|
||||
TurnItemsView::Full => {
|
||||
turn.items_view = TurnItemsView::Full;
|
||||
}
|
||||
}
|
||||
}
|
||||
let page = paginate_thread_turns(
|
||||
turns,
|
||||
cursor.as_deref(),
|
||||
@@ -2914,10 +2958,19 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
|
||||
async fn attach_thread_name(&self, thread_id: ThreadId, thread: &mut Thread) {
|
||||
if let Some(title) =
|
||||
title_from_state_db(&self.config, self.state_db.as_ref(), thread_id).await
|
||||
if let Ok(stored_thread) = self
|
||||
.thread_store
|
||||
.read_thread(StoreReadThreadParams {
|
||||
thread_id,
|
||||
include_archived: true,
|
||||
include_history: false,
|
||||
})
|
||||
.await
|
||||
&& let Some(title) = stored_thread.name.as_deref().map(str::trim)
|
||||
&& !title.is_empty()
|
||||
&& stored_thread.preview.trim() != title
|
||||
{
|
||||
set_thread_name_from_title(thread, title);
|
||||
set_thread_name_from_title(thread, title.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3206,12 +3259,7 @@ impl ThreadRequestProcessor {
|
||||
};
|
||||
|
||||
let stored_thread = read_result?;
|
||||
let summary =
|
||||
summary_from_stored_thread(stored_thread, fallback_provider).ok_or_else(|| {
|
||||
internal_error(
|
||||
"failed to load conversation summary: thread is missing rollout path",
|
||||
)
|
||||
})?;
|
||||
let summary = summary_from_stored_thread(stored_thread, fallback_provider);
|
||||
Ok(GetConversationSummaryResponse { summary })
|
||||
}
|
||||
|
||||
@@ -3495,19 +3543,30 @@ fn normalize_thread_turns_status(
|
||||
|
||||
enum ThreadReadViewError {
|
||||
InvalidRequest(String),
|
||||
Unsupported(&'static str),
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
fn thread_read_view_error(err: ThreadReadViewError) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadReadViewError::InvalidRequest(message) => invalid_request(message),
|
||||
ThreadReadViewError::Unsupported(operation) => {
|
||||
unsupported_thread_store_operation(operation)
|
||||
}
|
||||
ThreadReadViewError::Internal(message) => internal_error(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_thread_store_operation(operation: &'static str) -> JSONRPCErrorError {
|
||||
method_not_found(format!("{operation} is not supported yet"))
|
||||
}
|
||||
|
||||
fn thread_store_list_error(err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
ThreadStoreError::Unsupported { operation } => {
|
||||
unsupported_thread_store_operation(operation)
|
||||
}
|
||||
err => internal_error(format!("failed to list threads: {err}")),
|
||||
}
|
||||
}
|
||||
@@ -3515,6 +3574,9 @@ fn thread_store_list_error(err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
fn thread_store_resume_read_error(err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
ThreadStoreError::Unsupported { operation } => {
|
||||
unsupported_thread_store_operation(operation)
|
||||
}
|
||||
ThreadStoreError::ThreadNotFound { thread_id } => {
|
||||
invalid_request(format!("no rollout found for thread id {thread_id}"))
|
||||
}
|
||||
@@ -3537,6 +3599,7 @@ fn thread_turns_list_history_load_error(
|
||||
ThreadStoreError::InvalidRequest { message } => {
|
||||
ThreadReadViewError::InvalidRequest(message)
|
||||
}
|
||||
ThreadStoreError::Unsupported { operation } => ThreadReadViewError::Unsupported(operation),
|
||||
err => ThreadReadViewError::Internal(format!(
|
||||
"failed to load thread history for thread {thread_id}: {err}"
|
||||
)),
|
||||
@@ -3563,6 +3626,7 @@ fn thread_read_history_load_error(
|
||||
ThreadStoreError::InvalidRequest { message } => {
|
||||
ThreadReadViewError::InvalidRequest(message)
|
||||
}
|
||||
ThreadStoreError::Unsupported { operation } => ThreadReadViewError::Unsupported(operation),
|
||||
err => ThreadReadViewError::Internal(format!(
|
||||
"failed to load thread history for thread {thread_id}: {err}"
|
||||
)),
|
||||
@@ -3578,6 +3642,9 @@ fn conversation_summary_thread_id_read_error(
|
||||
ThreadStoreError::InvalidRequest { message } if message == no_rollout_message => {
|
||||
conversation_summary_not_found_error(conversation_id)
|
||||
}
|
||||
ThreadStoreError::Unsupported { operation } => {
|
||||
unsupported_thread_store_operation(operation)
|
||||
}
|
||||
ThreadStoreError::ThreadNotFound { thread_id } if thread_id == conversation_id => {
|
||||
conversation_summary_not_found_error(conversation_id)
|
||||
}
|
||||
@@ -3600,6 +3667,9 @@ fn conversation_summary_rollout_path_read_error(
|
||||
) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
ThreadStoreError::Unsupported { operation } => {
|
||||
unsupported_thread_store_operation(operation)
|
||||
}
|
||||
err => internal_error(format!(
|
||||
"failed to load conversation summary from {}: {}",
|
||||
path.display(),
|
||||
@@ -3614,6 +3684,9 @@ fn thread_store_write_error(operation: &str, err: ThreadStoreError) -> JSONRPCEr
|
||||
invalid_request(format!("thread not found: {thread_id}"))
|
||||
}
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
ThreadStoreError::Unsupported { operation } => {
|
||||
unsupported_thread_store_operation(operation)
|
||||
}
|
||||
err => internal_error(format!("failed to {operation}: {err}")),
|
||||
}
|
||||
}
|
||||
@@ -3621,41 +3694,13 @@ fn thread_store_write_error(operation: &str, err: ThreadStoreError) -> JSONRPCEr
|
||||
fn thread_store_archive_error(operation: &str, err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
ThreadStoreError::Unsupported {
|
||||
operation: unsupported_operation,
|
||||
} => unsupported_thread_store_operation(unsupported_operation),
|
||||
err => internal_error(format!("failed to {operation} thread: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn title_from_state_db(
|
||||
config: &Config,
|
||||
state_db_ctx: Option<&StateDbHandle>,
|
||||
thread_id: ThreadId,
|
||||
) -> Option<String> {
|
||||
if let Some(state_db_ctx) = state_db_ctx
|
||||
&& let Some(metadata) = state_db_ctx.get_thread(thread_id).await.ok().flatten()
|
||||
&& let Some(title) = distinct_title(&metadata)
|
||||
{
|
||||
return Some(title);
|
||||
}
|
||||
find_thread_name_by_id(&config.codex_home, &thread_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn non_empty_title(metadata: &ThreadMetadata) -> Option<String> {
|
||||
let title = metadata.title.trim();
|
||||
(!title.is_empty()).then(|| title.to_string())
|
||||
}
|
||||
|
||||
fn distinct_title(metadata: &ThreadMetadata) -> Option<String> {
|
||||
let title = non_empty_title(metadata)?;
|
||||
if metadata.first_user_message.as_deref().map(str::trim) == Some(title.as_str()) {
|
||||
None
|
||||
} else {
|
||||
Some(title)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_thread_name_from_title(thread: &mut Thread, title: String) {
|
||||
if title.trim().is_empty() || thread.preview.trim() == title.trim() {
|
||||
return;
|
||||
@@ -3719,8 +3764,8 @@ pub(crate) fn thread_from_stored_thread(
|
||||
fn summary_from_stored_thread(
|
||||
thread: StoredThread,
|
||||
fallback_provider: &str,
|
||||
) -> Option<ConversationSummary> {
|
||||
let path = thread.rollout_path?;
|
||||
) -> ConversationSummary {
|
||||
let path = thread.rollout_path.unwrap_or_default();
|
||||
let source = with_thread_spawn_agent_metadata(
|
||||
thread.source,
|
||||
thread.agent_nickname.clone(),
|
||||
@@ -3731,7 +3776,7 @@ fn summary_from_stored_thread(
|
||||
branch: git.branch,
|
||||
origin_url: git.repository_url,
|
||||
});
|
||||
Some(ConversationSummary {
|
||||
ConversationSummary {
|
||||
conversation_id: thread.thread_id,
|
||||
path,
|
||||
preview: thread.first_user_message.unwrap_or(thread.preview),
|
||||
@@ -3756,7 +3801,7 @@ fn summary_from_stored_thread(
|
||||
cli_version: thread.cli_version,
|
||||
source,
|
||||
git_info,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -409,8 +409,7 @@ mod thread_processor_behavior_tests {
|
||||
history: None,
|
||||
};
|
||||
|
||||
let summary =
|
||||
summary_from_stored_thread(stored_thread, "fallback").expect("summary should exist");
|
||||
let summary = summary_from_stored_thread(stored_thread, "fallback");
|
||||
|
||||
assert_eq!(
|
||||
summary.timestamp.as_deref(),
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
use chrono::DateTime;
|
||||
#[cfg(test)]
|
||||
use chrono::Utc;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn read_summary_from_rollout(
|
||||
path: &Path,
|
||||
@@ -203,6 +208,7 @@ pub(super) fn thread_response_sandbox_policy(
|
||||
sandbox_policy.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>> {
|
||||
timestamp.and_then(|ts| {
|
||||
chrono::DateTime::parse_from_rfc3339(ts)
|
||||
@@ -229,6 +235,7 @@ pub(super) fn thread_started_notification(mut thread: Thread) -> ThreadStartedNo
|
||||
ThreadStartedNotification { thread }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn summary_to_thread(
|
||||
summary: ConversationSummary,
|
||||
fallback_cwd: &AbsolutePathBuf,
|
||||
@@ -257,6 +264,7 @@ pub(crate) fn summary_to_thread(
|
||||
AbsolutePathBuf::relative_to_current_dir(path_utils::normalize_for_native_workdir(cwd))
|
||||
.unwrap_or_else(|err| {
|
||||
warn!(
|
||||
conversation_id = %conversation_id,
|
||||
path = %path.display(),
|
||||
"failed to normalize thread cwd while summarizing thread: {err}"
|
||||
);
|
||||
@@ -274,7 +282,7 @@ pub(crate) fn summary_to_thread(
|
||||
created_at: created_at.map(|dt| dt.timestamp()).unwrap_or(0),
|
||||
updated_at: updated_at.map(|dt| dt.timestamp()).unwrap_or(0),
|
||||
status: ThreadStatus::NotLoaded,
|
||||
path: Some(path),
|
||||
path: (!path.as_os_str().is_empty()).then_some(path),
|
||||
cwd,
|
||||
cli_version,
|
||||
agent_nickname: source.get_nickname(),
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(crate) struct ConnectionState {
|
||||
|
||||
impl ConnectionState {
|
||||
pub(crate) fn new(
|
||||
origin: ConnectionOrigin,
|
||||
_origin: ConnectionOrigin,
|
||||
outbound_initialized: Arc<AtomicBool>,
|
||||
outbound_experimental_api_enabled: Arc<AtomicBool>,
|
||||
outbound_opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
|
||||
@@ -45,7 +45,7 @@ impl ConnectionState {
|
||||
outbound_initialized,
|
||||
outbound_experimental_api_enabled,
|
||||
outbound_opted_out_notification_methods,
|
||||
session: Arc::new(ConnectionSessionState::new(origin)),
|
||||
session: Arc::new(ConnectionSessionState::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ license.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -89,6 +89,7 @@ use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadSetNameParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadTurnsItemsListParams;
|
||||
use codex_app_server_protocol::ThreadTurnsListParams;
|
||||
use codex_app_server_protocol::ThreadUnarchiveParams;
|
||||
use codex_app_server_protocol::ThreadUnsubscribeParams;
|
||||
@@ -522,6 +523,15 @@ impl McpProcess {
|
||||
self.send_request("thread/turns/list", params).await
|
||||
}
|
||||
|
||||
/// Send a `thread/turns/items/list` JSON-RPC request.
|
||||
pub async fn send_thread_turns_items_list_request(
|
||||
&mut self,
|
||||
params: ThreadTurnsItemsListParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("thread/turns/items/list", params).await
|
||||
}
|
||||
|
||||
/// Send a `model/list` JSON-RPC request.
|
||||
pub async fn send_list_models_request(
|
||||
&mut self,
|
||||
|
||||
@@ -3,20 +3,41 @@ use app_test_support::McpProcess;
|
||||
use app_test_support::create_fake_rollout;
|
||||
use app_test_support::rollout_path;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server::in_process;
|
||||
use codex_app_server::in_process::InProcessStartArgs;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ConversationSummary;
|
||||
use codex_app_server_protocol::GetConversationSummaryParams;
|
||||
use codex_app_server_protocol::GetConversationSummaryResponse;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_thread_store::CreateThreadParams;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::ThreadEventPersistenceMode;
|
||||
use codex_thread_store::ThreadPersistenceMetadata;
|
||||
use codex_thread_store::ThreadStore;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const FILENAME_TS: &str = "2025-01-02T12-00-00";
|
||||
@@ -47,7 +68,9 @@ fn normalized_canonical_path(path: impl AsRef<Path>) -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
fn normalized_summary_path(mut summary: ConversationSummary) -> Result<ConversationSummary> {
|
||||
summary.path = normalized_canonical_path(&summary.path)?;
|
||||
if !summary.path.as_os_str().is_empty() {
|
||||
summary.path = normalized_canonical_path(summary.path)?;
|
||||
}
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
@@ -122,6 +145,87 @@ async fn get_conversation_summary_by_rollout_path_rejects_remote_thread_store()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let store_id = Uuid::new_v4().to_string();
|
||||
create_config_toml_with_in_memory_thread_store(codex_home.path(), &store_id)?;
|
||||
let store = InMemoryThreadStore::for_id(store_id.clone());
|
||||
let _in_memory_store = InMemoryThreadStoreId { store_id };
|
||||
let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000125")?;
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
source: SessionSource::Cli,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
memory_mode: ThreadMemoryMode::Disabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let loader_overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.loader_overrides(loader_overrides.clone())
|
||||
.build()
|
||||
.await?;
|
||||
let client = in_process::start(InProcessStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(config),
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides,
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
enable_codex_api_key_env: false,
|
||||
initialize: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-app-server-tests".to_string(),
|
||||
title: None,
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
channel_capacity: in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let result = client
|
||||
.request(ClientRequest::GetConversationSummary {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: GetConversationSummaryParams::ThreadId {
|
||||
conversation_id: thread_id,
|
||||
},
|
||||
})
|
||||
.await?
|
||||
.expect("getConversationSummary should succeed");
|
||||
let GetConversationSummaryResponse { summary } = serde_json::from_value(result)?;
|
||||
|
||||
assert_eq!(summary.conversation_id, thread_id);
|
||||
assert_eq!(summary.path, PathBuf::new());
|
||||
assert_eq!(summary.cwd, PathBuf::new());
|
||||
assert_eq!(summary.model_provider, "test");
|
||||
|
||||
client.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_home() -> Result<()>
|
||||
{
|
||||
@@ -157,3 +261,39 @@ async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_h
|
||||
assert_eq!(normalized_summary_path(received.summary)?, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct InMemoryThreadStoreId {
|
||||
store_id: String,
|
||||
}
|
||||
|
||||
impl Drop for InMemoryThreadStoreId {
|
||||
fn drop(&mut self) {
|
||||
InMemoryThreadStore::remove_id(&self.store_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config_toml_with_in_memory_thread_store(
|
||||
codex_home: &Path,
|
||||
store_id: &str,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
experimental_thread_store = {{ type = "in_memory", id = "{store_id}" }}
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "http://127.0.0.1:1/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
use super::connection_handling_websocket::connect_websocket;
|
||||
use super::connection_handling_websocket::create_config_toml;
|
||||
use super::connection_handling_websocket::read_error_for_id;
|
||||
use super::connection_handling_websocket::read_response_for_id;
|
||||
use super::connection_handling_websocket::send_initialize_request;
|
||||
use super::connection_handling_websocket::send_request;
|
||||
use super::connection_handling_websocket::spawn_websocket_server;
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[cfg(any(target_os = "macos", windows))]
|
||||
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
async fn initialized_mcp(codex_home: &TempDir) -> Result<McpProcess> {
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
Ok(mcp)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn device_key_create_rejects_empty_account_user_id() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = initialized_mcp(&codex_home).await?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"device/key/create",
|
||||
Some(json!({
|
||||
"accountUserId": "",
|
||||
"clientId": "cli_123",
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"invalid device key payload: accountUserId must not be empty"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn device_key_methods_are_rejected_over_websocket() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
let mut ws = connect_websocket(bind_addr).await?;
|
||||
send_initialize_request(&mut ws, /*id*/ 1, "device_key_ws_test").await?;
|
||||
let initialize_response = read_response_for_id(&mut ws, /*id*/ 1).await?;
|
||||
assert_eq!(initialize_response.id, RequestId::Integer(1));
|
||||
|
||||
let cases = [
|
||||
(
|
||||
"device/key/create",
|
||||
json!({
|
||||
"accountUserId": "acct_123",
|
||||
"clientId": "cli_123",
|
||||
}),
|
||||
),
|
||||
(
|
||||
"device/key/public",
|
||||
json!({
|
||||
"keyId": "device-key-123",
|
||||
}),
|
||||
),
|
||||
(
|
||||
"device/key/sign",
|
||||
json!({
|
||||
"keyId": "device-key-123",
|
||||
"payload": {
|
||||
"type": "remoteControlClientConnection",
|
||||
"nonce": "nonce-123",
|
||||
"audience": "remote_control_client_websocket",
|
||||
"sessionId": "wssess_123",
|
||||
"targetOrigin": "https://chatgpt.com",
|
||||
"targetPath": "/api/codex/remote/control/client",
|
||||
"accountUserId": "acct_123",
|
||||
"clientId": "cli_123",
|
||||
"tokenExpiresAt": 4_102_444_800i64,
|
||||
"tokenSha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU",
|
||||
"scopes": ["remote_control_controller_websocket"],
|
||||
},
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (index, (method, params)) in cases.into_iter().enumerate() {
|
||||
let id = 2 + index as i64;
|
||||
send_request(&mut ws, method, id, Some(params)).await?;
|
||||
let error = read_error_for_id(&mut ws, id).await?;
|
||||
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
format!("{method} is not available over remote transports")
|
||||
);
|
||||
}
|
||||
|
||||
process.kill().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -10,7 +10,6 @@ mod config_rpc;
|
||||
mod connection_handling_websocket;
|
||||
#[cfg(unix)]
|
||||
mod connection_handling_websocket_unix;
|
||||
mod device_key;
|
||||
mod dynamic_tools;
|
||||
mod experimental_api;
|
||||
mod experimental_feature_list;
|
||||
|
||||
@@ -13,6 +13,8 @@ use codex_app_server_protocol::PluginListMarketplaceKind;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||
use codex_app_server_protocol::PluginSharePrincipal;
|
||||
use codex_app_server_protocol::PluginSharePrincipalType;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
use codex_app_server_protocol::PluginSummary;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
@@ -692,8 +694,10 @@ async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
.as_ref()
|
||||
.expect("expected share context");
|
||||
assert_eq!(share_context.remote_plugin_id, "plugins_123");
|
||||
assert_eq!(share_context.share_url, None);
|
||||
assert_eq!(share_context.creator_account_user_id, None);
|
||||
assert_eq!(share_context.creator_name, None);
|
||||
assert_eq!(share_context.share_targets, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1735,6 +1739,18 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
Some("user-gavin__account-123")
|
||||
);
|
||||
assert_eq!(share_context.creator_name.as_deref(), Some("Gavin"));
|
||||
assert_eq!(
|
||||
share_context.share_url.as_deref(),
|
||||
Some("https://chatgpt.example/plugins/share/share-key-1")
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.share_targets,
|
||||
Some(vec![PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-ada__account-123".to_string(),
|
||||
name: "Ada".to_string(),
|
||||
}])
|
||||
);
|
||||
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -2260,10 +2276,25 @@ fn workspace_remote_plugin_page_body(
|
||||
"name": "{plugin_name}",
|
||||
"scope": "WORKSPACE",
|
||||
"creator_account_user_id": "user-gavin__account-123",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"status": "ENABLED",
|
||||
"creator_name": "Gavin",
|
||||
"share_principals": [
|
||||
{{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-gavin__account-123",
|
||||
"role": "owner",
|
||||
"name": "Gavin"
|
||||
}},
|
||||
{{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-ada__account-123",
|
||||
"role": "reader",
|
||||
"name": "Ada"
|
||||
}}
|
||||
],
|
||||
"release": {{
|
||||
"display_name": "{display_name}",
|
||||
"description": "Track work",
|
||||
|
||||
@@ -23,6 +23,8 @@ use codex_app_server_protocol::PluginAuthPolicy;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::PluginSharePrincipal;
|
||||
use codex_app_server_protocol::PluginSharePrincipalType;
|
||||
use codex_app_server_protocol::PluginSkillReadParams;
|
||||
use codex_app_server_protocol::PluginSkillReadResponse;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
@@ -237,6 +239,21 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
|
||||
"scope": "WORKSPACE",
|
||||
"creator_account_user_id": "user-gavin__account-123",
|
||||
"creator_name": "Gavin",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"share_principals": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-gavin__account-123",
|
||||
"role": "owner",
|
||||
"name": "Gavin"
|
||||
},
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-ada__account-123",
|
||||
"role": "reader",
|
||||
"name": "Ada"
|
||||
}
|
||||
],
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
@@ -307,6 +324,18 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
|
||||
Some("user-gavin__account-123")
|
||||
);
|
||||
assert_eq!(share_context.creator_name.as_deref(), Some("Gavin"));
|
||||
assert_eq!(
|
||||
share_context.share_url.as_deref(),
|
||||
Some("https://chatgpt.example/plugins/share/share-key-1")
|
||||
);
|
||||
assert_eq!(
|
||||
share_context.share_targets,
|
||||
Some(vec![PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-ada__account-123".to_string(),
|
||||
name: "Ada".to_string(),
|
||||
}])
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -766,8 +795,10 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
.as_ref()
|
||||
.expect("expected share context");
|
||||
assert_eq!(share_context.remote_plugin_id, "plugins_123");
|
||||
assert_eq!(share_context.share_url, None);
|
||||
assert_eq!(share_context.creator_account_user_id, None);
|
||||
assert_eq!(share_context.creator_name, None);
|
||||
assert_eq!(share_context.share_targets, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -662,8 +662,10 @@ fn expected_plugin_interface() -> PluginInterface {
|
||||
fn expected_share_context(plugin_id: &str) -> PluginShareContext {
|
||||
PluginShareContext {
|
||||
remote_plugin_id: plugin_id.to_string(),
|
||||
share_url: Some("https://chatgpt.example/plugins/share/share-key-1".to_string()),
|
||||
creator_account_user_id: None,
|
||||
creator_name: None,
|
||||
share_targets: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ use codex_app_server_protocol::ThreadSetNameResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::ThreadTurnsItemsListParams;
|
||||
use codex_app_server_protocol::ThreadTurnsListParams;
|
||||
use codex_app_server_protocol::ThreadTurnsListResponse;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
@@ -46,6 +47,7 @@ use codex_core::config::ConfigBuilder;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::protocol::AgentMessageEvent;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionSource as ProtocolSessionSource;
|
||||
@@ -223,6 +225,7 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> {
|
||||
cursor: None,
|
||||
limit: Some(2),
|
||||
sort_direction: Some(SortDirection::Desc),
|
||||
items_view: None,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
@@ -238,7 +241,7 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> {
|
||||
assert_eq!(turn_user_texts(&data), vec!["third", "second"]);
|
||||
assert!(
|
||||
data.iter()
|
||||
.all(|turn| turn.items_view == TurnItemsView::Full)
|
||||
.all(|turn| turn.items_view == TurnItemsView::Summary)
|
||||
);
|
||||
let next_cursor = next_cursor.expect("expected nextCursor for older turns");
|
||||
let backwards_cursor = backwards_cursor.expect("expected backwardsCursor for newest turn");
|
||||
@@ -249,6 +252,7 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> {
|
||||
cursor: Some(next_cursor),
|
||||
limit: Some(10),
|
||||
sort_direction: Some(SortDirection::Desc),
|
||||
items_view: None,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
@@ -267,6 +271,7 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> {
|
||||
cursor: Some(backwards_cursor),
|
||||
limit: Some(10),
|
||||
sort_direction: Some(SortDirection::Asc),
|
||||
items_view: None,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
@@ -280,6 +285,74 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_turns_list_supports_requested_items_view() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let filename_ts = "2025-01-05T12-00-00";
|
||||
let conversation_id = create_fake_rollout_with_text_elements(
|
||||
codex_home.path(),
|
||||
filename_ts,
|
||||
"2025-01-05T12:00:00Z",
|
||||
"first",
|
||||
vec![],
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
let rollout_path = rollout_path(codex_home.path(), filename_ts, &conversation_id);
|
||||
append_agent_message(rollout_path.as_path(), "2025-01-05T12:01:00Z", "draft")?;
|
||||
append_agent_message(rollout_path.as_path(), "2025-01-05T12:02:00Z", "final")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let full = read_single_turn_items_view(
|
||||
&mut mcp,
|
||||
conversation_id.as_str(),
|
||||
Some(TurnItemsView::Full),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(full.items_view, TurnItemsView::Full);
|
||||
assert_eq!(
|
||||
turn_agent_texts(std::slice::from_ref(&full)),
|
||||
vec!["draft", "final"]
|
||||
);
|
||||
|
||||
let summary = read_single_turn_items_view(
|
||||
&mut mcp,
|
||||
conversation_id.as_str(),
|
||||
Some(TurnItemsView::Summary),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(summary.items_view, TurnItemsView::Summary);
|
||||
assert_eq!(
|
||||
turn_user_texts(std::slice::from_ref(&summary)),
|
||||
vec!["first"]
|
||||
);
|
||||
assert_eq!(
|
||||
turn_agent_texts(std::slice::from_ref(&summary)),
|
||||
vec!["final"]
|
||||
);
|
||||
|
||||
let not_loaded = read_single_turn_items_view(
|
||||
&mut mcp,
|
||||
conversation_id.as_str(),
|
||||
Some(TurnItemsView::NotLoaded),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(not_loaded.items_view, TurnItemsView::NotLoaded);
|
||||
assert!(not_loaded.items.is_empty());
|
||||
assert_eq!(not_loaded.id, full.id);
|
||||
assert_eq!(not_loaded.status, full.status);
|
||||
assert_eq!(not_loaded.started_at, full.started_at);
|
||||
assert_eq!(not_loaded.completed_at, full.completed_at);
|
||||
assert_eq!(not_loaded.duration_ms, full.duration_ms);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_turns_list_reads_store_history_without_rollout_path() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -334,6 +407,7 @@ async fn thread_turns_list_reads_store_history_without_rollout_path() -> Result<
|
||||
cursor: None,
|
||||
limit: Some(10),
|
||||
sort_direction: Some(SortDirection::Asc),
|
||||
items_view: None,
|
||||
},
|
||||
})
|
||||
.await?
|
||||
@@ -583,6 +657,7 @@ async fn thread_turns_list_rejects_cursor_when_anchor_turn_is_rolled_back() -> R
|
||||
cursor: None,
|
||||
limit: Some(2),
|
||||
sort_direction: Some(SortDirection::Desc),
|
||||
items_view: None,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
@@ -607,6 +682,7 @@ async fn thread_turns_list_rejects_cursor_when_anchor_turn_is_rolled_back() -> R
|
||||
cursor: Some(backwards_cursor),
|
||||
limit: Some(10),
|
||||
sort_direction: Some(SortDirection::Asc),
|
||||
items_view: None,
|
||||
})
|
||||
.await?;
|
||||
let read_err: JSONRPCError = timeout(
|
||||
@@ -963,6 +1039,7 @@ async fn thread_turns_list_rejects_unmaterialized_loaded_thread() -> Result<()>
|
||||
cursor: None,
|
||||
limit: None,
|
||||
sort_direction: None,
|
||||
items_view: None,
|
||||
})
|
||||
.await?;
|
||||
let read_err: JSONRPCError = timeout(
|
||||
@@ -983,6 +1060,39 @@ async fn thread_turns_list_rejects_unmaterialized_loaded_thread() -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_turns_items_list_returns_unsupported() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let read_id = mcp
|
||||
.send_thread_turns_items_list_request(ThreadTurnsItemsListParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
turn_id: "turn_456".to_string(),
|
||||
cursor: None,
|
||||
limit: None,
|
||||
sort_direction: None,
|
||||
})
|
||||
.await?;
|
||||
let read_err: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(read_err.error.code, -32601);
|
||||
assert_eq!(
|
||||
read_err.error.message,
|
||||
"thread/turns/items/list is not supported yet"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_read_reports_system_error_idle_flag_after_failed_turn() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -1068,6 +1178,24 @@ fn append_user_message(path: &Path, timestamp: &str, text: &str) -> std::io::Res
|
||||
)
|
||||
}
|
||||
|
||||
fn append_agent_message(path: &Path, timestamp: &str, text: &str) -> anyhow::Result<()> {
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(path)?;
|
||||
writeln!(
|
||||
file,
|
||||
"{}",
|
||||
json!({
|
||||
"timestamp": timestamp,
|
||||
"type": "event_msg",
|
||||
"payload": serde_json::to_value(EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: text.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}))?,
|
||||
})
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_thread_rollback(path: &Path, timestamp: &str, num_turns: u32) -> std::io::Result<()> {
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(path)?;
|
||||
writeln!(
|
||||
@@ -1084,6 +1212,31 @@ fn append_thread_rollback(path: &Path, timestamp: &str, num_turns: u32) -> std::
|
||||
)
|
||||
}
|
||||
|
||||
async fn read_single_turn_items_view(
|
||||
mcp: &mut McpProcess,
|
||||
thread_id: &str,
|
||||
items_view: Option<TurnItemsView>,
|
||||
) -> anyhow::Result<codex_app_server_protocol::Turn> {
|
||||
let read_id = mcp
|
||||
.send_thread_turns_list_request(ThreadTurnsListParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
cursor: None,
|
||||
limit: Some(10),
|
||||
sort_direction: Some(SortDirection::Asc),
|
||||
items_view,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadTurnsListResponse { mut data, .. } =
|
||||
to_response::<ThreadTurnsListResponse>(read_resp)?;
|
||||
assert_eq!(data.len(), 1);
|
||||
Ok(data.remove(0))
|
||||
}
|
||||
|
||||
fn turn_user_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str> {
|
||||
turns
|
||||
.iter()
|
||||
@@ -1100,6 +1253,17 @@ fn turn_user_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn turn_agent_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str> {
|
||||
turns
|
||||
.iter()
|
||||
.flat_map(|turn| &turn.items)
|
||||
.filter_map(|item| match item {
|
||||
ThreadItem::AgentMessage { text, .. } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct InMemoryThreadStoreId {
|
||||
store_id: String,
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ async fn thread_shell_command_history_responses_exclude_persisted_command_execut
|
||||
cursor: None,
|
||||
limit: None,
|
||||
sort_direction: Some(SortDirection::Asc),
|
||||
items_view: None,
|
||||
})
|
||||
.await?;
|
||||
let turns_list_resp: JSONRPCResponse = timeout(
|
||||
|
||||
@@ -2,6 +2,12 @@ use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server::in_process;
|
||||
use codex_app_server::in_process::InProcessStartArgs;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
@@ -15,17 +21,36 @@ use codex_app_server_protocol::ThreadUnarchivedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::find_archived_thread_path_by_id_str;
|
||||
use codex_core::find_thread_path_by_id_str;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_thread_store::CreateThreadParams;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::ThreadEventPersistenceMode;
|
||||
use codex_thread_store::ThreadMetadataPatch;
|
||||
use codex_thread_store::ThreadPersistenceMetadata;
|
||||
use codex_thread_store::ThreadStore;
|
||||
use codex_thread_store::UpdateThreadMetadataParams;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use std::fs::FileTimes;
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
@@ -172,11 +197,139 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let store_id = Uuid::new_v4().to_string();
|
||||
create_config_toml_with_in_memory_thread_store(codex_home.path(), &store_id)?;
|
||||
let store = InMemoryThreadStore::for_id(store_id.clone());
|
||||
let _in_memory_store = InMemoryThreadStoreId { store_id };
|
||||
let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000126")?;
|
||||
let parent_thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000127")?;
|
||||
store
|
||||
.create_thread(CreateThreadParams {
|
||||
thread_id,
|
||||
forked_from_id: Some(parent_thread_id),
|
||||
source: SessionSource::Cli,
|
||||
thread_source: None,
|
||||
base_instructions: BaseInstructions::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
memory_mode: ThreadMemoryMode::Disabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::default(),
|
||||
})
|
||||
.await?;
|
||||
store
|
||||
.update_thread_metadata(UpdateThreadMetadataParams {
|
||||
thread_id,
|
||||
patch: ThreadMetadataPatch {
|
||||
name: Some("named pathless thread".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
include_archived: true,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let loader_overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.loader_overrides(loader_overrides.clone())
|
||||
.build()
|
||||
.await?;
|
||||
let client = in_process::start(InProcessStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(config),
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides,
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
enable_codex_api_key_env: false,
|
||||
initialize: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-app-server-tests".to_string(),
|
||||
title: None,
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
channel_capacity: in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let result = client
|
||||
.request(ClientRequest::ThreadUnarchive {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: ThreadUnarchiveParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
},
|
||||
})
|
||||
.await?
|
||||
.expect("thread/unarchive should succeed");
|
||||
let ThreadUnarchiveResponse { thread } = serde_json::from_value(result)?;
|
||||
|
||||
assert_eq!(thread.id, thread_id.to_string());
|
||||
assert_eq!(thread.path, None);
|
||||
assert_eq!(thread.forked_from_id, Some(parent_thread_id.to_string()));
|
||||
assert_eq!(thread.name, Some("named pathless thread".to_string()));
|
||||
|
||||
client.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(config_toml, config_contents(server_uri))
|
||||
}
|
||||
|
||||
struct InMemoryThreadStoreId {
|
||||
store_id: String,
|
||||
}
|
||||
|
||||
impl Drop for InMemoryThreadStoreId {
|
||||
fn drop(&mut self) {
|
||||
InMemoryThreadStore::remove_id(&self.store_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config_toml_with_in_memory_thread_store(
|
||||
codex_home: &Path,
|
||||
store_id: &str,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
experimental_thread_store = {{ type = "in_memory", id = "{store_id}" }}
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "http://127.0.0.1:1/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn config_contents(server_uri: &str) -> String {
|
||||
format!(
|
||||
r#"model = "mock-model"
|
||||
|
||||
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
[lib]
|
||||
name = "codex_apply_patch"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[[bin]]
|
||||
name = "apply_patch"
|
||||
|
||||
@@ -192,13 +192,33 @@ impl AppliedPatchDelta {
|
||||
Self { changes, exact }
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self::new(Vec::new(), /*exact*/ true)
|
||||
}
|
||||
|
||||
pub fn changes(&self) -> &[AppliedPatchChange] {
|
||||
&self.changes
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.changes.is_empty()
|
||||
}
|
||||
|
||||
pub fn is_exact(&self) -> bool {
|
||||
self.exact
|
||||
}
|
||||
|
||||
/// Appends a later committed prefix while preserving the aggregate exactness.
|
||||
pub fn append(&mut self, other: Self) {
|
||||
self.changes.extend(other.changes);
|
||||
self.exact &= other.exact;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppliedPatchDelta {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// A committed file change, preserved in the order it was applied.
|
||||
@@ -225,6 +245,34 @@ pub enum AppliedPatchFileChange {
|
||||
},
|
||||
}
|
||||
|
||||
/// A failed patch application together with the textual mutations that were
|
||||
/// definitely committed before the failure was observed.
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{error}")]
|
||||
pub struct ApplyPatchFailure {
|
||||
#[source]
|
||||
error: ApplyPatchError,
|
||||
delta: AppliedPatchDelta,
|
||||
}
|
||||
|
||||
impl ApplyPatchFailure {
|
||||
fn new(error: ApplyPatchError, delta: AppliedPatchDelta) -> Self {
|
||||
Self { error, delta }
|
||||
}
|
||||
|
||||
fn without_delta(error: ApplyPatchError) -> Self {
|
||||
Self::new(error, AppliedPatchDelta::empty())
|
||||
}
|
||||
|
||||
pub fn delta(&self) -> &AppliedPatchDelta {
|
||||
&self.delta
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (ApplyPatchError, AppliedPatchDelta) {
|
||||
(self.error, self.delta)
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the patch and prints the result to stdout/stderr.
|
||||
pub async fn apply_patch(
|
||||
patch: &str,
|
||||
@@ -233,13 +281,15 @@ pub async fn apply_patch(
|
||||
stderr: &mut impl std::io::Write,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> Result<AppliedPatchDelta, ApplyPatchError> {
|
||||
) -> Result<AppliedPatchDelta, ApplyPatchFailure> {
|
||||
let hunks = match parse_patch(patch) {
|
||||
Ok(source) => source.hunks,
|
||||
Err(e) => {
|
||||
match &e {
|
||||
InvalidPatchError(message) => {
|
||||
writeln!(stderr, "Invalid patch: {message}").map_err(ApplyPatchError::from)?;
|
||||
writeln!(stderr, "Invalid patch: {message}")
|
||||
.map_err(ApplyPatchError::from)
|
||||
.map_err(ApplyPatchFailure::without_delta)?;
|
||||
}
|
||||
InvalidHunkError {
|
||||
message,
|
||||
@@ -249,10 +299,13 @@ pub async fn apply_patch(
|
||||
stderr,
|
||||
"Invalid patch hunk on line {line_number}: {message}"
|
||||
)
|
||||
.map_err(ApplyPatchError::from)?;
|
||||
.map_err(ApplyPatchError::from)
|
||||
.map_err(ApplyPatchFailure::without_delta)?;
|
||||
}
|
||||
}
|
||||
return Err(ApplyPatchError::ParseError(e));
|
||||
return Err(ApplyPatchFailure::without_delta(
|
||||
ApplyPatchError::ParseError(e),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -267,24 +320,29 @@ pub async fn apply_hunks(
|
||||
stderr: &mut impl std::io::Write,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> Result<AppliedPatchDelta, ApplyPatchError> {
|
||||
// Delegate to a helper that applies each hunk to the filesystem.
|
||||
match apply_hunks_to_files(hunks, cwd, fs, sandbox).await {
|
||||
Ok(applied) => {
|
||||
print_summary(&applied.affected_paths, stdout).map_err(ApplyPatchError::from)?;
|
||||
Ok(applied.delta)
|
||||
) -> Result<AppliedPatchDelta, ApplyPatchFailure> {
|
||||
let mut delta = AppliedPatchDelta::empty();
|
||||
match apply_hunks_to_files(hunks, cwd, fs, sandbox, &mut delta).await {
|
||||
Ok(affected_paths) => {
|
||||
print_summary(&affected_paths, stdout).map_err(|error| {
|
||||
ApplyPatchFailure::new(ApplyPatchError::from(error), delta.clone())
|
||||
})?;
|
||||
Ok(delta)
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
writeln!(stderr, "{msg}").map_err(ApplyPatchError::from)?;
|
||||
if let Some(io) = err.downcast_ref::<std::io::Error>() {
|
||||
Err(ApplyPatchError::from(io))
|
||||
Err(error) => {
|
||||
let msg = error.to_string();
|
||||
writeln!(stderr, "{msg}").map_err(|error| {
|
||||
ApplyPatchFailure::new(ApplyPatchError::from(error), delta.clone())
|
||||
})?;
|
||||
let error = if let Some(io) = error.downcast_ref::<std::io::Error>() {
|
||||
ApplyPatchError::from(io)
|
||||
} else {
|
||||
Err(ApplyPatchError::IoError(IoError {
|
||||
ApplyPatchError::IoError(IoError {
|
||||
context: msg,
|
||||
source: std::io::Error::other(err),
|
||||
}))
|
||||
}
|
||||
source: std::io::Error::other(error),
|
||||
})
|
||||
};
|
||||
Err(ApplyPatchFailure::new(error, delta))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,11 +357,6 @@ pub struct AffectedPaths {
|
||||
pub deleted: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
struct AppliedHunks {
|
||||
affected_paths: AffectedPaths,
|
||||
delta: AppliedPatchDelta,
|
||||
}
|
||||
|
||||
/// Apply the hunks to the filesystem, returning which files were added, modified, or deleted.
|
||||
/// Returns an error if the patch could not be applied.
|
||||
async fn apply_hunks_to_files(
|
||||
@@ -311,7 +364,8 @@ async fn apply_hunks_to_files(
|
||||
cwd: &AbsolutePathBuf,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> anyhow::Result<AppliedHunks> {
|
||||
delta: &mut AppliedPatchDelta,
|
||||
) -> anyhow::Result<AffectedPaths> {
|
||||
if hunks.is_empty() {
|
||||
anyhow::bail!("No files were modified.");
|
||||
}
|
||||
@@ -319,24 +373,39 @@ async fn apply_hunks_to_files(
|
||||
let mut added: Vec<PathBuf> = Vec::new();
|
||||
let mut modified: Vec<PathBuf> = Vec::new();
|
||||
let mut deleted: Vec<PathBuf> = Vec::new();
|
||||
let mut delta_changes = Vec::new();
|
||||
let mut delta_exact = true;
|
||||
// A failed write can still have modified the target before surfacing an
|
||||
// error (for example by truncating before ENOSPC), so the accumulated
|
||||
// delta is no longer exact when a write fails.
|
||||
macro_rules! try_write {
|
||||
($result:expr) => {
|
||||
match $result {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
delta.exact = false;
|
||||
return Err(anyhow::Error::from(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for hunk in hunks {
|
||||
let affected_path = hunk.path().to_path_buf();
|
||||
let path_abs = hunk.resolve_path(cwd);
|
||||
match hunk {
|
||||
Hunk::AddFile { contents, .. } => {
|
||||
let overwritten_content =
|
||||
read_optional_file_text_for_delta(&path_abs, fs, sandbox, &mut delta_exact)
|
||||
read_optional_file_text_for_delta(&path_abs, fs, sandbox, &mut delta.exact)
|
||||
.await;
|
||||
write_file_with_missing_parent_retry(
|
||||
fs,
|
||||
&path_abs,
|
||||
contents.clone().into_bytes(),
|
||||
sandbox,
|
||||
)
|
||||
.await?;
|
||||
delta_changes.push(AppliedPatchChange {
|
||||
try_write!(
|
||||
write_file_with_missing_parent_retry(
|
||||
fs,
|
||||
&path_abs,
|
||||
contents.clone().into_bytes(),
|
||||
sandbox,
|
||||
)
|
||||
.await
|
||||
);
|
||||
delta.changes.push(AppliedPatchChange {
|
||||
path: path_abs.into_path_buf(),
|
||||
change: AppliedPatchFileChange::Add {
|
||||
content: contents.clone(),
|
||||
@@ -346,20 +415,16 @@ async fn apply_hunks_to_files(
|
||||
added.push(affected_path);
|
||||
}
|
||||
Hunk::DeleteFile { .. } => {
|
||||
note_existing_path_delta_support(&path_abs, fs, sandbox, &mut delta_exact).await;
|
||||
note_existing_path_delta_support(&path_abs, fs, sandbox, &mut delta.exact).await;
|
||||
let deleted_content = fs.read_file_text(&path_abs, sandbox).await.ok();
|
||||
if deleted_content.is_none() {
|
||||
delta_exact = false;
|
||||
delta.exact = false;
|
||||
}
|
||||
let result: io::Result<()> = async {
|
||||
let metadata = fs.get_metadata(&path_abs, sandbox).await?;
|
||||
if metadata.is_directory {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"path is a directory",
|
||||
));
|
||||
}
|
||||
fs.remove(
|
||||
ensure_not_directory(&path_abs, fs, sandbox)
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete file {}", path_abs.display()))?;
|
||||
if let Err(error) = fs
|
||||
.remove(
|
||||
&path_abs,
|
||||
RemoveOptions {
|
||||
recursive: false,
|
||||
@@ -368,11 +433,19 @@ async fn apply_hunks_to_files(
|
||||
sandbox,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete file {}", path_abs.display()))
|
||||
{
|
||||
delta.exact &= remove_failure_was_side_effect_free(
|
||||
&path_abs,
|
||||
deleted_content.as_deref(),
|
||||
fs,
|
||||
sandbox,
|
||||
)
|
||||
.await;
|
||||
return Err(error);
|
||||
}
|
||||
.await;
|
||||
result.with_context(|| format!("Failed to delete file {}", path_abs.display()))?;
|
||||
if let Some(content) = deleted_content {
|
||||
delta_changes.push(AppliedPatchChange {
|
||||
delta.changes.push(AppliedPatchChange {
|
||||
path: path_abs.into_path_buf(),
|
||||
change: AppliedPatchFileChange::Delete { content },
|
||||
});
|
||||
@@ -382,7 +455,7 @@ async fn apply_hunks_to_files(
|
||||
Hunk::UpdateFile {
|
||||
move_path, chunks, ..
|
||||
} => {
|
||||
note_existing_path_delta_support(&path_abs, fs, sandbox, &mut delta_exact).await;
|
||||
note_existing_path_delta_support(&path_abs, fs, sandbox, &mut delta.exact).await;
|
||||
let AppliedPatch {
|
||||
original_contents,
|
||||
new_contents,
|
||||
@@ -390,24 +463,32 @@ async fn apply_hunks_to_files(
|
||||
if let Some(dest) = move_path {
|
||||
let dest_abs = AbsolutePathBuf::resolve_path_against_base(dest, cwd);
|
||||
let overwritten_move_content =
|
||||
read_optional_file_text_for_delta(&dest_abs, fs, sandbox, &mut delta_exact)
|
||||
read_optional_file_text_for_delta(&dest_abs, fs, sandbox, &mut delta.exact)
|
||||
.await;
|
||||
write_file_with_missing_parent_retry(
|
||||
fs,
|
||||
&dest_abs,
|
||||
new_contents.clone().into_bytes(),
|
||||
sandbox,
|
||||
)
|
||||
.await?;
|
||||
let result: io::Result<()> = async {
|
||||
let metadata = fs.get_metadata(&path_abs, sandbox).await?;
|
||||
if metadata.is_directory {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"path is a directory",
|
||||
));
|
||||
}
|
||||
fs.remove(
|
||||
try_write!(
|
||||
write_file_with_missing_parent_retry(
|
||||
fs,
|
||||
&dest_abs,
|
||||
new_contents.clone().into_bytes(),
|
||||
sandbox,
|
||||
)
|
||||
.await
|
||||
);
|
||||
let dest_write_change_index = delta.changes.len();
|
||||
delta.changes.push(AppliedPatchChange {
|
||||
path: dest_abs.to_path_buf(),
|
||||
change: AppliedPatchFileChange::Add {
|
||||
content: new_contents.clone(),
|
||||
overwritten_content: overwritten_move_content.clone(),
|
||||
},
|
||||
});
|
||||
ensure_not_directory(&path_abs, fs, sandbox)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to remove original {}", path_abs.display())
|
||||
})?;
|
||||
if let Err(error) = fs
|
||||
.remove(
|
||||
&path_abs,
|
||||
RemoveOptions {
|
||||
recursive: false,
|
||||
@@ -416,12 +497,20 @@ async fn apply_hunks_to_files(
|
||||
sandbox,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to remove original {}", path_abs.display())
|
||||
})
|
||||
{
|
||||
delta.exact &= remove_failure_was_side_effect_free(
|
||||
&path_abs,
|
||||
Some(&original_contents),
|
||||
fs,
|
||||
sandbox,
|
||||
)
|
||||
.await;
|
||||
return Err(error);
|
||||
}
|
||||
.await;
|
||||
result.with_context(|| {
|
||||
format!("Failed to remove original {}", path_abs.display())
|
||||
})?;
|
||||
delta_changes.push(AppliedPatchChange {
|
||||
delta.changes[dest_write_change_index] = AppliedPatchChange {
|
||||
path: path_abs.into_path_buf(),
|
||||
change: AppliedPatchFileChange::Update {
|
||||
move_path: Some(dest_abs.into_path_buf()),
|
||||
@@ -429,13 +518,18 @@ async fn apply_hunks_to_files(
|
||||
overwritten_move_content,
|
||||
new_content: new_contents,
|
||||
},
|
||||
});
|
||||
};
|
||||
modified.push(affected_path);
|
||||
} else {
|
||||
fs.write_file(&path_abs, new_contents.clone().into_bytes(), sandbox)
|
||||
.await
|
||||
.with_context(|| format!("Failed to write file {}", path_abs.display()))?;
|
||||
delta_changes.push(AppliedPatchChange {
|
||||
try_write!(
|
||||
fs.write_file(&path_abs, new_contents.clone().into_bytes(), sandbox)
|
||||
.await
|
||||
.with_context(|| format!(
|
||||
"Failed to write file {}",
|
||||
path_abs.display()
|
||||
))
|
||||
);
|
||||
delta.changes.push(AppliedPatchChange {
|
||||
path: path_abs.into_path_buf(),
|
||||
change: AppliedPatchFileChange::Update {
|
||||
move_path: None,
|
||||
@@ -449,16 +543,43 @@ async fn apply_hunks_to_files(
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(AppliedHunks {
|
||||
affected_paths: AffectedPaths {
|
||||
added,
|
||||
modified,
|
||||
deleted,
|
||||
},
|
||||
delta: AppliedPatchDelta::new(delta_changes, delta_exact),
|
||||
Ok(AffectedPaths {
|
||||
added,
|
||||
modified,
|
||||
deleted,
|
||||
})
|
||||
}
|
||||
|
||||
async fn ensure_not_directory(
|
||||
path: &AbsolutePathBuf,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> io::Result<()> {
|
||||
let metadata = fs.get_metadata(path, sandbox).await?;
|
||||
if metadata.is_directory {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"path is a directory",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_failure_was_side_effect_free(
|
||||
path: &AbsolutePathBuf,
|
||||
expected_content: Option<&str>,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> bool {
|
||||
match expected_content {
|
||||
Some(expected_content) => fs
|
||||
.read_file_text(path, sandbox)
|
||||
.await
|
||||
.is_ok_and(|content| content == expected_content),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_optional_file_text_for_delta(
|
||||
path: &AbsolutePathBuf,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
@@ -972,6 +1093,61 @@ mod tests {
|
||||
assert_eq!(contents, "line2\n");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_failed_move_returns_committed_destination_delta() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let source_dir = dir.path().join("locked");
|
||||
let dest_dir = dir.path().join("out");
|
||||
fs::create_dir(&source_dir).unwrap();
|
||||
fs::create_dir(&dest_dir).unwrap();
|
||||
let src = source_dir.join("src.txt");
|
||||
let dest = dest_dir.join("dst.txt");
|
||||
fs::write(&src, "line\n").unwrap();
|
||||
fs::set_permissions(&source_dir, fs::Permissions::from_mode(0o555)).unwrap();
|
||||
|
||||
let patch = wrap_patch(
|
||||
"*** Update File: locked/src.txt\n*** Move to: out/dst.txt\n@@\n-line\n+line2",
|
||||
);
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let failure = apply_patch(
|
||||
&patch,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("source removal should fail after destination write");
|
||||
|
||||
fs::set_permissions(&source_dir, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
|
||||
assert!(
|
||||
String::from_utf8(stderr)
|
||||
.unwrap()
|
||||
.contains(&format!("Failed to remove original {}", src.display()))
|
||||
);
|
||||
assert_eq!(
|
||||
failure.delta(),
|
||||
&AppliedPatchDelta::new(
|
||||
vec![AppliedPatchChange {
|
||||
path: dest.clone(),
|
||||
change: AppliedPatchFileChange::Add {
|
||||
content: "line2\n".to_string(),
|
||||
overwritten_content: None,
|
||||
},
|
||||
}],
|
||||
/*exact*/ true,
|
||||
)
|
||||
);
|
||||
assert_eq!(fs::read_to_string(src).unwrap(), "line\n");
|
||||
assert_eq!(fs::read_to_string(dest).unwrap(), "line2\n");
|
||||
}
|
||||
|
||||
/// Verify that a single `Update File` hunk with multiple change chunks can update different
|
||||
/// parts of a file and that the file is listed only once in the summary.
|
||||
#[tokio::test]
|
||||
@@ -1427,19 +1603,17 @@ g
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_apply_patch_fails_on_write_error() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("readonly.txt");
|
||||
fs::write(&path, "before\n").unwrap();
|
||||
let mut perms = fs::metadata(&path).unwrap().permissions();
|
||||
perms.set_readonly(true);
|
||||
fs::set_permissions(&path, perms).unwrap();
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let patch = wrap_patch(&format!(
|
||||
"*** Update File: {}\n@@\n-before\n+after\n*** End Patch",
|
||||
path.display()
|
||||
));
|
||||
let dir = tempdir().unwrap();
|
||||
let locked_dir = dir.path().join("locked");
|
||||
fs::create_dir(&locked_dir).unwrap();
|
||||
fs::set_permissions(&locked_dir, fs::Permissions::from_mode(0o555)).unwrap();
|
||||
|
||||
let patch = wrap_patch("*** Add File: locked/new.txt\n+after");
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
@@ -1452,7 +1626,11 @@ g
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
let failure = result.expect_err("write should fail");
|
||||
|
||||
fs::set_permissions(&locked_dir, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
|
||||
assert!(!failure.delta().is_exact());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
[lib]
|
||||
name = "codex_arg0"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -14,3 +14,6 @@ tokio-util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
@@ -7,6 +7,7 @@ publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -7,6 +7,7 @@ version.workspace = true
|
||||
[lib]
|
||||
name = "codex_builtin_mcps"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user