From 80638591430df8c0157e438511fa9257e184211a Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 20:07:07 +0800 Subject: [PATCH 01/14] feat: install Core on Linux, macOS and Windows through one workflow (#476) * feat: share Core installation across Linux macOS and Windows * fix: recover interrupted staging and qualify native platforms * fix: use native Web base images and one Compose startup * test: exercise the PowerShell launcher with native oac * test: share launcher fixtures across PowerShell script scopes * docs: align installation guidance with shared platform support * docs: correct secret mounts and installation locking * docs: clarify native management command invocation --- .github/workflows/check.yml | 19 +- .github/workflows/native.yml | 22 ++ .github/workflows/release.yml | 7 +- Makefile | 2 +- README.md | 8 +- README.zh-CN.md | 8 +- apps/web/DESIGN.md | 2 +- apps/web/PRODUCT.md | 2 +- apps/web/e2e/access.spec.ts | 4 +- .../src/features/first-run/ConsoleAccess.tsx | 11 +- apps/web/src/i18n/locales/en/core-errors.ts | 2 +- .../web/src/i18n/locales/zh-CN/core-errors.ts | 2 +- apps/web/src/lib/console-auth-strings.ts | 4 +- deploy/README.md | 8 +- deploy/compose/compose.yaml | 54 +-- deploy/compose/test_compose.py | 10 +- deploy/install.dev.sh | 4 +- deploy/install.ps1 | 31 ++ deploy/install.sh | 245 +----------- deploy/test_install.ps1 | 34 ++ deploy/test_install.py | 335 +++------------- docs/configuration.md | 45 +-- docs/getting-started/install-options.md | 14 +- docs/getting-started/install.md | 28 +- docs/getting-started/operations.md | 30 +- docs/maintainers.md | 17 +- docs/zh/configuration.md | 47 +-- docs/zh/getting-started/install-options.md | 18 +- docs/zh/getting-started/install.md | 30 +- docs/zh/getting-started/operations.md | 32 +- docs/zh/maintainers.md | 21 +- scripts/build-core-distribution.sh | 31 +- scripts/build-core-image-context.sh | 4 +- scripts/build-e2b-provider.sh | 12 +- scripts/ci_plan.py | 2 + scripts/ci_plan_test.py | 6 +- scripts/compose-smoke.py | 22 +- scripts/core-distribution-manifest.py | 35 +- scripts/core-distribution-manifest.test.py | 9 +- scripts/publish-core-release.py | 146 ++++--- scripts/publish-core-release.test.py | 214 +++++------ services/core/cmd/oac/init.go | 204 +++++----- services/core/cmd/oac/init_test.go | 31 +- services/core/cmd/oac/install.go | 358 ++++++++++++++++++ services/core/cmd/oac/install_test.go | 245 ++++++++++++ services/core/cmd/oac/install_unix_test.go | 46 +++ services/core/cmd/oac/main.go | 97 ++--- services/core/cmd/oac/oac_test.go | 28 +- .../core/tools/e2b-provider/Build.Dockerfile | 2 +- services/core/tools/e2b-provider/build.py | 11 +- services/web/Dockerfile | 2 +- 51 files changed, 1506 insertions(+), 1095 deletions(-) create mode 100644 deploy/install.ps1 create mode 100644 deploy/test_install.ps1 create mode 100644 services/core/cmd/oac/install.go create mode 100644 services/core/cmd/oac/install_test.go create mode 100644 services/core/cmd/oac/install_unix_test.go diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 151a0c410..0a307c7ad 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -184,8 +184,18 @@ jobs: compose: needs: plan if: needs.plan.result == 'success' && contains(fromJSON(needs.plan.outputs.jobs || '[]'), 'compose') - runs-on: ${{ vars.OAC_USE_GITHUB_RUNNERS == 'true' && 'ubuntu-22.04' || 'blacksmith-2vcpu-ubuntu-2204' }} - timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + architecture: amd64 + - runner: ubuntu-24.04-arm + architecture: arm64 + runs-on: ${{ matrix.runner }} + env: + GOARCH: ${{ matrix.architecture }} + timeout-minutes: 30 steps: - uses: actions/checkout@v7 with: @@ -193,6 +203,11 @@ jobs: - uses: actions/setup-go@v7 with: go-version-file: go.mod + - name: Test the native Core installation lifecycle + run: go test ./services/core/cmd/oac -count=1 -timeout=3m + - uses: ./.github/actions/e2b-provider + - name: Build and verify the architecture-matched E2B helper + run: bash scripts/build-e2b-provider.sh - name: Allocate an isolated Compose project run: python3 -c 'import uuid; print("COMPOSE_SMOKE_PROJECT=oac-smoke-" + uuid.uuid4().hex)' >> "$GITHUB_ENV" - name: Build the images, start the installation and verify it diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index 7be750d35..c901adc6f 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -63,6 +63,14 @@ jobs: run: | go build -ldflags "-X github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/cli.Version=$(git rev-parse HEAD)" -o "$RUNNER_TEMP/oac-daemon${{ runner.os == 'Windows' && '.exe' || '' }}" ./apps/daemon/cmd/oac-daemon node --test scripts/build-native-installer.test.mjs + - name: Build and test the shared Core installer + run: | + go build -o "$RUNNER_TEMP/oac${{ runner.os == 'Windows' && '.exe' || '' }}" ./services/core/cmd/oac + go test ./services/core/cmd/oac -count=1 -timeout=3m + - name: Exercise the Windows Core launcher + if: runner.os == 'Windows' + shell: pwsh + run: ./deploy/test_install.ps1 -Binary "$env:RUNNER_TEMP/oac.exe" - name: Verify native download bootstrap and recovery run: go test ./services/core/internal/nativeinstaller -count=1 -timeout=3m - name: Native filesystem, authentication and process lifecycle @@ -163,3 +171,17 @@ jobs: path: ${{ runner.temp }}/native-ci-diagnostics/ retention-days: 7 if-no-files-found: ignore + + core-intel-mac: + name: Core installer (macOS Intel) + runs-on: macos-15-intel + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref || github.sha }} + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + - run: go test ./services/core/cmd/oac -count=1 -timeout=3m + - run: go build -o "$RUNNER_TEMP/oac" ./services/core/cmd/oac diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d798c1976..c8ff4ab28 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,6 +114,9 @@ jobs: path: ${{ runner.temp }}/native-artifacts - name: Assemble the native installation catalog run: node scripts/build-native-catalog.mjs "$RUNNER_TEMP/native-artifacts" "$RUNNER_TEMP/native-installers" + - uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 - uses: ./.github/actions/e2b-provider - name: Build matched artifacts env: @@ -133,8 +136,8 @@ jobs: for asset in "$HOME/.oac/build/core-distribution/"*; do if [[ -f "$asset" ]]; then ln "$asset" "$HOME/.oac/build/release-upload/"; fi done - cp deploy/install.sh "$HOME/.oac/build/release-upload/install.sh" - (cd "$HOME/.oac/build/release-upload" && sha256sum install.sh > install.sh.sha256) + cp deploy/install.sh deploy/install.ps1 "$HOME/.oac/build/release-upload/" + (cd "$HOME/.oac/build/release-upload" && sha256sum install.sh > install.sh.sha256 && sha256sum install.ps1 > install.ps1.sha256) - name: Sign in to GHCR if: github.event_name == 'push' || inputs.draft_release env: diff --git a/Makefile b/Makefile index 24b6a13ab..1958607db 100644 --- a/Makefile +++ b/Makefile @@ -177,7 +177,7 @@ check-microsandbox-provider: .PHONY: check-distribution build-core-distribution check-distribution: node --test scripts/build-native-catalog.test.mjs - go test ./services/web -count=1 + go test ./services/web ./services/core/cmd/oac -count=1 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s deploy/node -p 'test_*.py' PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s deploy/compose -p 'test_*.py' PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s scripts/acceptance -p 'test_*.py' diff --git a/README.md b/README.md index 5713ac292..d8056f965 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,18 @@ Core keeps durable execution state. The Runtime runs the chosen harness inside t ## Install -On a Linux amd64 host with Docker and Python 3.9+: +On Linux or macOS with [Docker configured](https://openagentcore.dev/docs/getting-started/install#prerequisites): ```sh curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash ``` +Windows PowerShell: + +```powershell +irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.ps1 | iex +``` + Then: 1. **Sign in to Web**, the admin console, with the Core key the installer created, and **configure the domain and HTTPS**. diff --git a/README.zh-CN.md b/README.zh-CN.md index 5414cea3c..fb55a62a5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -42,12 +42,18 @@ OpenAgentCore 在你自己的基础设施上运行 AI Agent,对外提供 [Open ## 安装 -在已准备 Docker 和 Python 3.9+ 的 Linux amd64 主机上: +在已按[前置条件](https://openagentcore.dev/zh/docs/getting-started/install#prerequisites)准备 Docker 的 Linux 或 macOS 上: ```sh curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash ``` +Windows PowerShell: + +```powershell +irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.ps1 | iex +``` + 然后: 1. 用安装器生成的 Core key **登录 Web**(管理控制台),并**配置域名和 HTTPS**。 diff --git a/apps/web/DESIGN.md b/apps/web/DESIGN.md index 64bb19dd3..86c08ae95 100644 --- a/apps/web/DESIGN.md +++ b/apps/web/DESIGN.md @@ -402,7 +402,7 @@ A failed action whose outcome needs a decision (a sandbox change with no answer, A local-only installation has the same amber notice on Overview, Nodes and System: other machines cannot connect, followed by Core's configuration path and apply command as copyable values. If Core has no configuration snapshot, state that those instructions are unavailable; never fill in a path or command. Add node is disabled with its reason beside the action, and Getting started leaves its first step to do with the address fix visible. A pending or failed installation read cannot complete that step; a failed read shows Unknown and Retry. ### Onboarding -Signing in and the console tour share one frame: a dark stage on the left (always dark, whatever the theme) and the task panel on the right, which follows the theme. The stage is the product's one authored moment: a flickering indigo dot grid under slow light rays (Magic UI's flickering grid and light rays), Core as the OpenAgentCore mark on a tile with a travelling border beam, and two orbits of Agents, Sessions, Skills, Vaults, files, templates and machines around it; the OpenAgentCore mark is itself nodes on a ring. Brand copy sits bottom-left in solid ink; it is a paragraph, not a heading, because the panel's title names the task. Signing in asks for one thing, the deployment's Core key, in a single password field; the default key location and a copyable read command stay visible beneath it, with a reminder to substitute a custom installation directory. The key’s authority stays in a help tip. A refused key, too many attempts or an unavailable console is an error beside the field. Signing in opens the console on the Overview. The optional tour has three chapters — Monitor, Resources, Platform — whose stage shows a real dark screenshot of those pages, tilted towards the panel; it takes the place of the console until its last button, Skip or Escape, and then returns the focus to the control that opened it. Entering the console or the tour, and leaving the tour, happen inside a View Transition: the old page dissolves forward and the new one is revealed in a circle growing from the pressed button. With reduced motion the orbits hold their places, the grid is a still frame and no transition runs. +Signing in and the console tour share one frame: a dark stage on the left (always dark, whatever the theme) and the task panel on the right, which follows the theme. The stage is the product's one authored moment: a flickering indigo dot grid under slow light rays (Magic UI's flickering grid and light rays), Core as the OpenAgentCore mark on a tile with a travelling border beam, and two orbits of Agents, Sessions, Skills, Vaults, files, templates and machines around it; the OpenAgentCore mark is itself nodes on a ring. Brand copy sits bottom-left in solid ink; it is a paragraph, not a heading, because the panel's title names the task. Signing in asks for one thing, the deployment's Core key, in a single password field; a copyable Docker Compose command to read the key stays visible beneath it, with a reminder to substitute a custom installation directory. The key’s authority stays in a help tip. A refused key, too many attempts or an unavailable console is an error beside the field. Signing in opens the console on the Overview. The optional tour has three chapters — Monitor, Resources, Platform — whose stage shows a real dark screenshot of those pages, tilted towards the panel; it takes the place of the console until its last button, Skip or Escape, and then returns the focus to the control that opened it. Entering the console or the tour, and leaving the tour, happen inside a View Transition: the old page dissolves forward and the new one is revealed in a circle growing from the pressed button. With reduced motion the orbits hold their places, the grid is a still frame and no transition runs. ### Getting started The first card on the Overview while any step is to do: a card header ("Getting started", "n of 4 done", a help tip, then a ghost Take the tour button and an icon button that hides it) over four rows split by Faint Rules. Each row has a 22px numbered ring (a check on the tile wash when done), a 13px/600 title over one 12.5px Graphite line, a status dot (Done in green, To do in Pencil, Checking pending, Unknown for a failed read) and one outline action while the step is to do: Set up sandboxes, Add node, Open Nodes or Open sandbox backend; Open System; Create project (which continues to the new project's first key) or Issue key; See how to call (the newest active project, preferring one with an active key), or Projects and keys without an active project. Add node, Create project and Issue key open their page with the dialog already open; Open System brings the Default model provider section to the top of the page body and focuses the default harness's Set or Replace; See how to call opens the project and, once its keys, usage and address are read, brings its How to call heading to the top of the page body, focused. Only the page body scrolls; the page header stays. Every step done turns it into one line, "You're set", with Take the tour and Dismiss; it stays, through the tour, until dismissed, and the checklist does not come back on its own. The choice is kept per installation in the browser, also while the deployment cannot be read; Show Getting started, a quiet row above the sidebar's account controls, opens it again at any time. diff --git a/apps/web/PRODUCT.md b/apps/web/PRODUCT.md index 2aa72c28c..090ca06fd 100644 --- a/apps/web/PRODUCT.md +++ b/apps/web/PRODUCT.md @@ -22,7 +22,7 @@ The console runs beside the administrator's own Core, with execution, files and ## Operating Context -- Paired console (`services/web`): the administrator signs in with the deployment's Core key, the administration credential the installer writes to `secrets/core.key` under the installation directory (by default `~/.oac/core/secrets/core.key`; keeping and rotating it is described in [Core key](../../docs/getting-started/operations.md#core-key)). There are no console accounts or usernames. Sign-in shows the default file location and a copyable `cat ~/.oac/core/secrets/core.key` command for the Core host, with a reminder to substitute a custom installation directory. The browser sends the key only to sign in and keeps only the session cookie; the console server holds the Core key and forwards the Web API (`/core/v1/**`, including sandbox administration under `/core/v1/sandbox/**`). The console never calls `/v1`. +- Paired console (`services/web`): the administrator signs in with the deployment's [Core key](../../docs/getting-started/operations.md#core-key). Sign-in shows a copyable Docker Compose command to read the key on the Core host, with a reminder to substitute a custom installation directory. The browser sends the key only to sign in and keeps only the session cookie; the console server holds the Core key and forwards the Web API (`/core/v1/**`, including sandbox administration under `/core/v1/sandbox/**`). The console never calls `/v1`. - The Core key is not an Agents API identity and cannot call `/v1`. An administrator who wants to call the Agents API issues a project API key like any other caller. - `/console/config` reports the node installer (`node_installer`, `node_installer_sha256`), offered only with a 64-hex digest. Native self-hosted installation does not depend on this endpoint. It also lists the providers it has node files for (`node_artifacts`); without the deployment's provider, Add node says so and issues no command. Signing in grants administration, sandbox administration included. - Chinese and English UI; light and dark themes; reduced motion honored. diff --git a/apps/web/e2e/access.spec.ts b/apps/web/e2e/access.spec.ts index 41149ef97..f8d48a28d 100644 --- a/apps/web/e2e/access.spec.ts +++ b/apps/web/e2e/access.spec.ts @@ -18,11 +18,11 @@ test("signs in with the Core key, keeps it out of the browser, and signs out and await page.goto("/"); await expect(page.getByRole("heading", { name: "Sign in to OpenAgentCore" })).toBeVisible(); - await expect(page.getByText("cat ~/.oac/core/secrets/core.key", { exact: true })).toBeVisible(); + await expect(page.getByText('docker compose -f "$HOME/.oac/core/compose.yaml" exec -T web oac-web core-key', { exact: true })).toBeVisible(); await expect(page.getByText("For a custom installation directory, replace the path in this command.")).toBeVisible(); await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); await page.getByRole("button", { name: "Copy key read command" }).click(); - expect(await page.evaluate(() => navigator.clipboard.readText())).toBe("cat ~/.oac/core/secrets/core.key"); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe('docker compose -f "$HOME/.oac/core/compose.yaml" exec -T web oac-web core-key'); await signIn(page, "not-the-core-key"); await expect(page.getByRole("alert")).toHaveText("This Core key is not correct. Check it and try again."); await signIn(page, FIXTURE_CORE_KEY); diff --git a/apps/web/src/features/first-run/ConsoleAccess.tsx b/apps/web/src/features/first-run/ConsoleAccess.tsx index d9ef1f408..5e561f330 100644 --- a/apps/web/src/features/first-run/ConsoleAccess.tsx +++ b/apps/web/src/features/first-run/ConsoleAccess.tsx @@ -12,13 +12,6 @@ import { withTransition } from "../onboarding/view-transition"; import { changeConsoleAuth, ConsoleAuthError, readConsoleAuth, type ConsoleAuth } from "./auth"; import "./console-access.css"; -/** - * Where the installer writes the Core key: its file inside the installation - * directory, and that file under the default installation directory. The - * visible sign-in instructions name both; the actual custom path is not public. - */ -const CORE_KEY_LOCATION = { file: "secrets/core.key", defaultPath: "~/.oac/core/secrets/core.key" } as const; - const ConsoleAccountContext = createContext<{ logout: () => Promise } | null>(null); export const useConsoleAccount = () => useContext(ConsoleAccountContext); @@ -146,8 +139,8 @@ function CoreKeyForm({ onAuthenticated }: { readOnly={busy} aria-invalid={error ? true : undefined} aria-describedby={`${id}-location${error ? ` ${id}-error` : ""}`} />
-

{t("The installer saved the key in {{file}} inside the installation directory. On the Core host, read the default location with:", CORE_KEY_LOCATION)}

- +

{t("On the Core host, run this command to read the Core key:")}

+

{t("For a custom installation directory, replace the path in this command.")}

{error ? : null} diff --git a/apps/web/src/i18n/locales/en/core-errors.ts b/apps/web/src/i18n/locales/en/core-errors.ts index ba11e4086..8b86a85a3 100644 --- a/apps/web/src/i18n/locales/en/core-errors.ts +++ b/apps/web/src/i18n/locales/en/core-errors.ts @@ -1,5 +1,5 @@ export const coreErrors = { - "invalid_admin_key": "The console's Core key was rejected. Update secrets/core.key on the Core host and run oac apply.", + "invalid_admin_key": "The console's Core key was rejected. Rotate it on the Core host, then sign in again.", "console_sign_in_required": "Sign in to the console again.", "console_origin_rejected": "Open the console at its configured address.", "console_request_invalid": "The console request was rejected. Reload the page.", diff --git a/apps/web/src/i18n/locales/zh-CN/core-errors.ts b/apps/web/src/i18n/locales/zh-CN/core-errors.ts index 4334ca3c1..310ade305 100644 --- a/apps/web/src/i18n/locales/zh-CN/core-errors.ts +++ b/apps/web/src/i18n/locales/zh-CN/core-errors.ts @@ -1,5 +1,5 @@ export const coreErrors = { - "invalid_admin_key": "控制台的 Core Key 被拒绝。请更新 Core 主机上的 secrets/core.key,再运行 oac apply。", + "invalid_admin_key": "控制台的 Core Key 被拒绝。请在 Core 主机上轮换密钥,然后重新登录。", "console_sign_in_required": "请重新登录控制台。", "console_origin_rejected": "请通过配置的地址打开控制台。", "console_request_invalid": "控制台请求被拒绝,请重新加载页面。", diff --git a/apps/web/src/lib/console-auth-strings.ts b/apps/web/src/lib/console-auth-strings.ts index c2c8afdf1..c6490daef 100644 --- a/apps/web/src/lib/console-auth-strings.ts +++ b/apps/web/src/lib/console-auth-strings.ts @@ -4,8 +4,8 @@ export const consoleAuthChinese = { "Core key": "Core Key", "The Core key is an administration credential: it cannot call the /v1 Agents API, and the console never keeps it in your browser.": "Core Key 是管理凭据:不能调用 /v1 Agents API,控制台也不会把它保存在你的浏览器里。", - "The installer saved the key in {{file}} inside the installation directory. On the Core host, read the default location with:": - "安装器将 key 保存在安装目录下的 {{file}} 中。在 Core 主机上运行以下命令可读取默认位置:", + "On the Core host, run this command to read the Core key:": + "在 Core 主机上运行以下命令,读取 Core Key:", "Copy key read command": "复制 key 读取命令", "For a custom installation directory, replace the path in this command.": "如果使用了自定义安装目录,请替换命令中的路径。", "Sign in": "登录", diff --git a/deploy/README.md b/deploy/README.md index 716971334..20a61d2e4 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -2,18 +2,18 @@ | Path | Contents | | --- | --- | -| `install.sh` | Host installer published with each release | +| `install.sh`, `install.ps1` | Native command launchers published with each release | | `compose/` | Compose template and its tests | | `distribution/` | Image Dockerfiles | | `node/` | [Node installer](node/README.md), packaged as `node-install.pyz` | ## Installation -`install.sh` downloads its release's `compose.yaml`, checks it against `compose-sha256sums.txt`, writes `.env`, and starts Compose. Core applies database migrations when it starts. The host needs Linux amd64 and Docker Compose 2.26 or newer. [Configuration](../docs/configuration.md) owns the installation layout and settings. +`install.sh` and `install.ps1` select a native `oac` release binary, verify its checksum and invoke `oac install`. That Go command owns the same installation lifecycle on Linux, macOS and Windows: validate Docker, verify release configuration, prepare `.env` and the host command, pull images, publish the installation directory, initialize the data volume and start Compose. [Installation](../docs/getting-started/install.md#prerequisites) owns platform prerequisites; [Configuration](../docs/configuration.md) owns settings and storage. -The Core installer validates downloads and Compose in the private sibling `.staging` directory, and pulls all images before publishing the installation directory. A retry clears leftover staging only when its atomically created ownership link names this installation, including after the previous process was killed. An unrecognized nonempty staging directory is preserved. The staging log moves with the configuration and is removed on normal exit. Once published, failures retain configuration, containers and data. A retry verifies and starts that saved installation without replacing its Compose file, cached images or existing containers. Missing images are downloaded explicitly; Compose never pulls images implicitly. It refuses unrelated nonempty directories. `.install.lock` serializes directory preparation; `.oac.lock` serializes service changes with mutating `oac` commands. Both lock files remain after exit. Download failures have bounded retries and timeouts; service startup failures print Compose status and recent logs. +Preparation happens in `.staging`. On entry, the installer removes an empty stage or one with the empty ownership directory `.oac-installer-`; it preserves unrecognized directories. Ordinary failures remove owned staging immediately. The ownership marker is created atomically before downloading files. A killed process leaves staging for the next attempt. Publication happens only after downloads, checks and pulls succeed. Later failures preserve saved configuration, containers and the Docker data volume. Retries reuse those settings and cached images, pull missing images and start services with `--pull never --no-recreate`. `.lock` is a persistent lock directory shared by installation and mutating operator commands; the existing `runtimefs` platform adapter owns locking. -`oac` is a Go command (`services/core/cmd/oac`) in the Core image and the ingress image. The host copy implements `apply`, `core-key` and `rotate-core-key`; `core-key --show` runs `oac-web core-key` in the Web container. Start, stop, logs and removal are `docker compose`. `apply` runs `oac-core check-config` before recreating services. The ingress image runs data initialization as `oac init`, verifies and copies its bundled node metadata without network access, and contains no Python. No service receives a Docker socket. Initialization writes structured logs to stderr for directory preparation, lock acquisition, existing-installation verification, bundled metadata verification and publication, credential generation or reuse, and receipt persistence. Each step records its start and completion; failures identify the current step, and completion records elapsed milliseconds. Credential values and digests are never logged. View these logs with `docker compose logs --timestamps init`. +The host `oac` command also implements `apply`, `core-key` and `rotate-core-key`. `apply` checks Core configuration before recreating services. Reading the key executes `oac-web core-key` in Web. Rotation runs in the initialization container, where Linux file ownership is identical on every host, then restarts Core and Web. Start, stop, logs and removal use `docker compose`. The initialization image verifies and copies bundled node metadata without network access. Its receipt authenticates immutable metadata and credentials; the operator-rotatable Core key and its digest remain mutable. No service receives a Docker socket. Initialization emits structured step logs without credential values; view them with `docker compose logs --timestamps init`. Web serves the console and forwards `/v1` and `/api/v1` to Core, so it is the only published service. HTTPS is terminated by the operator's reverse proxy or hosting platform, which routes to `web:8080`; `OAC_PUBLIC_URL` records that origin. diff --git a/deploy/compose/compose.yaml b/deploy/compose/compose.yaml index 21ecceea9..6fe45a4db 100644 --- a/deploy/compose/compose.yaml +++ b/deploy/compose/compose.yaml @@ -1,24 +1,23 @@ # Release template. The publisher pins the initialization image to this release. # Core and Web default to latest; OAC_IMAGE_* selects another reference. -# Data is bind-mounted from ${OAC_DATA_DIR:-./data}. +# Docker owns data permissions on every host platform. x-ingress-image: &ingress-image ${OAC_IMAGE_INGRESS:-__OAC_INIT_IMAGE__} services: init: image: *ingress-image - platform: linux/amd64 restart: "no" security_opt: [no-new-privileges:true] command: [/usr/local/bin/oac, init] environment: OAC_REVISION: __OAC_REVISION__ volumes: - - type: bind - source: ${OAC_DATA_DIR:-./data} + - type: volume + source: data target: /data + volume: {nocopy: true} database: image: postgres:16-alpine - platform: linux/amd64 restart: unless-stopped depends_on: init: {condition: service_completed_successfully} @@ -27,12 +26,14 @@ services: POSTGRES_DB: agents_api POSTGRES_PASSWORD_FILE: /run/database/password volumes: - - type: bind - source: ${OAC_DATA_DIR:-./data}/database + - type: volume + source: data target: /var/lib/postgresql/data - - type: bind - source: ${OAC_DATA_DIR:-./data}/secrets/database + volume: {nocopy: true, subpath: database} + - type: volume + source: data target: /run/database + volume: {nocopy: true, subpath: secrets/database} read_only: true healthcheck: test: [CMD-SHELL, "pg_isready -h 127.0.0.1 -U agents_api -d agents_api"] @@ -42,7 +43,6 @@ services: core: image: ${OAC_IMAGE_CORE:-ghcr.io/minimax-ai/openagentcore/core:latest} - platform: linux/amd64 user: "65532:65532" restart: unless-stopped read_only: true @@ -70,23 +70,28 @@ services: OAC_LOG_ADD_SOURCE: ${OAC_LOG_ADD_SOURCE:-} OAC_HISTORY_SETTINGS_FILE: ${OAC_HISTORY_SETTINGS_FILE:-} volumes: - - type: bind - source: ${OAC_DATA_DIR:-./data}/secrets/core + - type: volume + source: data target: /run/oac + volume: {nocopy: true, subpath: secrets/core} read_only: true - - type: bind - source: ${OAC_DATA_DIR:-./data}/secrets/database + - type: volume + source: data target: /run/database + volume: {nocopy: true, subpath: secrets/database} read_only: true - - type: bind - source: ${OAC_DATA_DIR:-./data}/state + - type: volume + source: data target: /state + volume: {nocopy: true, subpath: state} web: ports: - - "${OAC_HOST:-127.0.0.1}:${OAC_WEB_PORT:-8080}:8080" + - target: 8080 + published: "${OAC_WEB_PORT:-8080}" + host_ip: "${OAC_HOST:-127.0.0.1}" + protocol: tcp image: ${OAC_IMAGE_WEB:-ghcr.io/minimax-ai/openagentcore/web:latest} - platform: linux/amd64 user: "65532:65532" restart: unless-stopped read_only: true @@ -102,16 +107,21 @@ services: OAC_LOG_FORMAT: ${OAC_LOG_FORMAT:-} OAC_LOG_ADD_SOURCE: ${OAC_LOG_ADD_SOURCE:-} volumes: - - type: bind - source: ${OAC_DATA_DIR:-./data}/secrets/web + - type: volume + source: data target: /run/oac + volume: {nocopy: true, subpath: secrets/web} read_only: true - - type: bind - source: ${OAC_DATA_DIR:-./data}/node-payload + - type: volume + source: data target: /node-payload + volume: {nocopy: true, subpath: node-payload} read_only: true healthcheck: test: [CMD, /usr/local/bin/oac-web, healthcheck] interval: 5s timeout: 10s retries: 30 + +volumes: + data: diff --git a/deploy/compose/test_compose.py b/deploy/compose/test_compose.py index 11f666e15..ce428f89a 100644 --- a/deploy/compose/test_compose.py +++ b/deploy/compose/test_compose.py @@ -61,7 +61,9 @@ def test_compose_uses_private_services_and_ordered_initialization(self): self.assertTrue(service['image'].endswith(':latest') or service['image'] == 'postgres:16-alpine' or service['image'].endswith('@sha256:' + 'e' * 64)) for volume in service.get('volumes', []): self.assertNotIn('docker.sock', json.dumps(volume)) - self.assertEqual(volume['type'], 'bind') + self.assertEqual(volume['type'], 'volume') + self.assertEqual(volume['source'], 'data') + self.assertNotIn('platform', service) self.assertEqual({v['target'] for v in services['web']['volumes']}, {'/run/oac', '/node-payload'}) self.assertIsNone(services['core']['command']) self.assertNotIn('OAC_WEB_INSTALLATION_SOCKET', services['web']['environment']) @@ -95,6 +97,12 @@ def ports(config): ['docker', 'compose', '--env-file', os.devnull, '-f', str(self.compose_file), 'config', '--format', 'json'], env=env)) self.assertEqual(ports(configured), {'web': [('0.0.0.0', '9080')]}) + env['OAC_HOST'] = '::1' + configured = json.loads(subprocess.check_output( + ['docker', 'compose', '--env-file', os.devnull, '-f', str(self.compose_file), + 'config', '--format', 'json'], env=env)) + self.assertEqual(ports(configured), {'web': [('::1', '9080')]}) + def test_platform_network_injection_keeps_the_file_valid(self): # Dokploy isolated deployments attach a project network to every service. diff --git a/deploy/install.dev.sh b/deploy/install.dev.sh index eada59aa3..7a3fb5135 100755 --- a/deploy/install.dev.sh +++ b/deploy/install.dev.sh @@ -47,8 +47,7 @@ if [[ "$install_dir" != /* ]]; then exit 1 fi -mkdir -p "$install_dir/data" -chmod 700 "$install_dir/data" +mkdir -p "$install_dir" build="$install_dir/image-build" rm -rf "$build" mkdir -p "$build" @@ -115,7 +114,6 @@ PY umask 077 cat >"$install_dir/.env" <:; otherwise only from this host. -HTTPS is terminated by your reverse proxy or hosting platform. -EOF -} - +case "$(uname -s)" in Linux) os=linux;; Darwin) os=darwin;; *) echo 'Use install.ps1 on Windows.' >&2; exit 1;; esac +case "$(uname -m)" in x86_64) arch=amd64;; arm64|aarch64) arch=arm64;; *) echo 'Unsupported CPU architecture.' >&2; exit 1;; esac +version=latest +args=("$@") while [[ $# -gt 0 ]]; do - case "$1" in - --version) version="${2:?}"; shift 2 ;; - --install-dir) install_dir="${2:?}"; shift 2 ;; - --public-url) public_url="${2:?}"; shift 2 ;; - --host) host_address="${2:?}"; shift 2 ;; - --web-port) web_port="${2:?}"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;; - esac + case "$1" in --version) version="${2:?Missing release tag}"; shift 2;; *) shift;; esac done - -if [[ "$(uname -s)" != Linux || "$(uname -m)" != x86_64 ]]; then - echo "Core installs on Linux amd64." >&2 - exit 1 -fi -fail() { printf '%s\n' "$*" >&2; exit 1; } -for tool in docker curl sha256sum flock readlink od sed awk grep; do - command -v "$tool" >/dev/null || fail "Required command missing: $tool. Install it and rerun this command. Docker needs Compose 2.26 or newer." -done -compose_version="$(docker compose version --short 2>/dev/null | sed 's/^v//' || true)" -major="${compose_version%%.*}" -minor="${compose_version#*.}" -minor="${minor%%.*}" -if [[ ! "$major" =~ ^[0-9]+$ || ! "$minor" =~ ^[0-9]+$ ]] || (( major < 2 || (major == 2 && minor < 26) )); then - fail "Docker Compose 2.26 or newer is required (found ${compose_version:-none})." -fi -docker info >/dev/null 2>&1 || fail "Cannot reach Docker. Start Docker and check this account's access, then rerun." -[[ "$install_dir" == /* && "$install_dir" != / ]] || fail "--install-dir must be an absolute directory other than /." -# These values are written as literal dotenv strings, never shell commands. -for value in "$install_dir" "$public_url" "$host_address" "$version"; do - [[ "$value" != *$'\n'* && "$value" != *$'\r'* && "$value" != *"'"* && "$value" != *\\* ]] || fail "Installation options cannot contain newlines, quotes or backslashes." -done -[[ "$web_port" =~ ^[0-9]{1,5}$ ]] && (( 10#$web_port >= 1 && 10#$web_port <= 65535 )) || fail "--web-port must be between 1 and 65535." -[[ ! -L "$install_dir" ]] || fail "Installation directory must not be a symbolic link." -[[ ! -e "$install_dir" || -d "$install_dir" ]] || fail "Installation path must be a directory." -case "$(basename "$install_dir")" in .|..) fail "--install-dir must name the installation directory, not . or ...";; esac +base="https://github.com/${OAC_REPOSITORY:-MiniMax-AI/OpenAgentCore}/releases/latest/download" +if [[ "$version" != latest ]]; then base="https://github.com/${OAC_REPOSITORY:-MiniMax-AI/OpenAgentCore}/releases/download/$version"; fi umask 077 -parent="$(dirname "$install_dir")" -mkdir -p "$parent" -parent="$(cd "$parent" && pwd -P)" -install_dir="$parent/$(basename "$install_dir")" -lock="$install_dir.install.lock" -[[ ! -L "$lock" && ( ! -e "$lock" || ( -f "$lock" && -O "$lock" ) ) ]] || fail "Invalid installation lock: $lock" -exec 9>>"$lock" -flock -n 9 || fail "Another installation is using this directory. Wait for it to finish and rerun." -stage="$install_dir.staging" -[[ ! -L "$stage" && ( ! -e "$stage" || ( -d "$stage" && -O "$stage" ) ) ]] || fail "Invalid installation staging directory: $stage" -if [[ -d "$stage" && -n "$(ls -A "$stage")" ]]; then - [[ -L "$stage/.oac-installer" && "$(readlink "$stage/.oac-installer")" == "$install_dir" ]] || fail "Unrecognized staging directory; preserve it and choose another --install-dir: $stage" -fi -rm -rf "$stage" -mkdir "$stage" -ln -s "$install_dir" "$stage/.oac-installer" -log="$stage/install.log" -: >"$log" -cleanup() { - local status=$? - trap - EXIT - if [[ "$status" != 0 && "$published" == 1 ]]; then - printf '\nInstallation and data retained at %s.\n' "$install_dir" >&2 - (cd "$install_dir" && docker compose ps --all && docker compose logs --no-color --tail 50) >&2 || true - printf 'Fix the reported problem, then rerun install.sh --install-dir %q.\n' "$install_dir" >&2 - fi - [[ -z "$stage" ]] || rm -rf "$stage" - rm -f "$log" - exit "$status" -} -trap cleanup EXIT -trap 'exit 130' INT -trap 'exit 143' TERM HUP - -# step DESCRIPTION COMMAND... prints the command's output only when it fails. -step() { - printf '%s... ' "$1" - shift - if "$@" >"$log" 2>&1; then echo done; else echo failed; cat "$log" >&2; return 1; fi -} - -resume=0 -if [[ -e "$install_dir" && -n "$(ls -A "$install_dir")" ]]; then - for file in compose.yaml compose-sha256sums.txt .env; do - [[ -f "$install_dir/$file" && ! -L "$install_dir/$file" ]] || fail "Directory is not a complete Core installation: $install_dir. Preserve it and choose another directory." - done - resume=1 - published=1 -fi - -port_busy() { - local port="$1" - if command -v ss >/dev/null; then - if ss -ltn | awk '{print $4}' | grep -Eq "(^|:|\\])${port}$"; then - return 0 - fi - return 1 - fi - (echo >/dev/tcp/127.0.0.1/"$port") >/dev/null 2>&1 -} -if [[ "$resume" == 0 ]] && port_busy "$web_port"; then fail "Port $web_port is already in use. Choose another with --web-port."; fi -asset_base="https://github.com/${repository}/releases/latest/download" -if [[ "$version" != latest ]]; then - asset_base="https://github.com/${repository}/releases/download/${version}" -fi - -# The source address of this host's default route, when it is a private one. -private_address() { - command -v ip >/dev/null || return 0 - ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p' | - grep -E '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)' || true -} -local_only=0 -if [[ -z "$public_url" && "$host_address" == 0.0.0.0 ]]; then - address="$(private_address)" - if [[ -n "$address" ]]; then public_url="http://$address:$web_port"; fi -fi -if [[ -z "$public_url" ]]; then public_url="http://localhost:$web_port"; local_only=1; fi - -if [[ "$resume" == 0 ]]; then - # Prepare configuration privately until downloads and image pulls succeed. - # Before publication only this invocation's private staging directory is removed. - download() { - curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \ - --connect-timeout 15 --max-time 120 --retry 2 --retry-connrefused --retry-delay 1 \ - --max-filesize 1048576 "$asset_base/$1" --output "$stage/$1" - } - step "Downloading release checksums" download compose-sha256sums.txt - step "Downloading Compose configuration" download compose.yaml - { - echo "COMPOSE_PROJECT_NAME=oac-$(od -An -N5 -tx1 /dev/urandom | tr -d ' \n')" - printf "OAC_INSTALL_DIR='%s'\nOAC_HOST='%s'\nOAC_WEB_PORT='%s'\nOAC_PUBLIC_URL='%s'\n" "$install_dir" "$host_address" "$web_port" "$public_url" - } >"$stage/.env" -fi - -if [[ "$resume" == 0 ]]; then cd "$stage"; else cd "$install_dir"; fi -[[ ! -L .oac.lock && ( ! -e .oac.lock || ( -f .oac.lock && -O .oac.lock ) ) ]] || fail "Invalid installation lock: $install_dir/.oac.lock" -exec 8>>.oac.lock -flock -n 8 || fail "Another oac command is using this installation. Wait for it to finish and rerun." -[[ ! -L install.log && ( ! -e install.log || ( -f install.log && -O install.log ) ) ]] || fail "Invalid installation log: $install_dir/install.log" -log="$PWD/install.log" -: >"$log" -step "Verifying saved configuration" sha256sum --check --quiet compose-sha256sums.txt -step "Checking Compose configuration" docker compose config --quiet -if [[ "$resume" == 1 ]]; then - printf 'Using saved settings from .env; installation flags only apply to new directories. Existing data is preserved.\n' -fi -public_url="$(docker compose config --environment | sed -n 's/^OAC_PUBLIC_URL=//p')" -local_only=0 -[[ "$public_url" != http://localhost:* && "$public_url" != http://127.0.0.1:* ]] || local_only=1 -pull_images() { - local images image - images="$(docker compose config --images)" || return - while IFS= read -r image; do - [[ -n "$image" ]] || continue - if [[ "$resume" == 0 ]] || ! docker image inspect "$image" >/dev/null 2>&1; then - docker pull --platform linux/amd64 "$image" || return - fi - done <<<"$images" -} -step "Checking and downloading images" pull_images -if [[ "$resume" == 0 ]]; then - # An existing empty directory may be replaced, never a directory with user data. - if [[ -e "$install_dir" ]]; then rmdir "$install_dir"; fi - mv "$stage" "$install_dir" - stage="" - cd "$install_dir" - log="$install_dir/install.log" - published=1 -fi -rm -f .oac-installer -copy_cli() ( - [[ ! -L ./oac.download ]] || fail "Temporary oac command must not be a symbolic link." - trap 'rm -f ./oac.download' EXIT - docker compose create --pull never --no-recreate core && - docker compose cp core:/usr/local/bin/oac ./oac.download && - mv ./oac.download ./oac -) -if [[ ! -x ./oac ]]; then step "Installing the oac command" copy_cli; fi -step "Starting services" docker compose up -d --wait --wait-timeout 180 --pull never --no-recreate -if ! key="$(./oac core-key --show)"; then - fail "Services started, but the Core key could not be read. Inspect Web's logs and retry; data is preserved." -fi - -sudo="" -if [[ "$EUID" == 0 && -n "${SUDO_USER:-}" ]]; then sudo="sudo "; fi -cat </dev/null; then actual="$(sha256sum "$asset")"; else actual="$(shasum -a 256 "$asset")"; fi +[[ "$actual" == "$(cat "$asset.sha256")" ]] || { echo 'Installer checksum mismatch.' >&2; exit 1; } +chmod 700 "$asset" +"./$asset" install "${args[@]}" diff --git a/deploy/test_install.ps1 b/deploy/test_install.ps1 new file mode 100644 index 000000000..bed472823 --- /dev/null +++ b/deploy/test_install.ps1 @@ -0,0 +1,34 @@ +# Exercise the actual launcher with a native oac binary and local release assets. +param([Parameter(Mandatory=$true)][string]$Binary) +$ErrorActionPreference = 'Stop' +$testAssets = @{ Binary = $Binary; Corrupt = $false } +function Invoke-WebRequest { + param([switch]$UseBasicParsing, [string]$Uri, [string]$OutFile) + if ($Uri.EndsWith('.sha256')) { + $digest = (Get-FileHash -Algorithm SHA256 $testAssets.Binary).Hash.ToLowerInvariant() + if ($testAssets.Corrupt) { $digest = '0' * 64 } + [IO.File]::WriteAllText($OutFile, "$digest oac-windows-amd64.exe`n") + } else { + Copy-Item $testAssets.Binary $OutFile + } +} +& "$PSScriptRoot/install.ps1" --help + +$testAssets.Corrupt = $true +$caught = $false +try { & "$PSScriptRoot/install.ps1" --help } +catch { + if ($_.Exception.Message -notlike '*checksum mismatch*') { throw } + $caught = $true +} +if (-not $caught) { throw 'A corrupt binary was executed.' } + +$testAssets.Corrupt = $false +$caught = $false +try { & "$PSScriptRoot/install.ps1" --unknown-option } +catch { + if ($_.Exception.Message -notlike '*Installation failed*') { throw } + $caught = $true +} +if (-not $caught) { throw 'A failed native command was reported as successful.' } +exit 0 diff --git a/deploy/test_install.py b/deploy/test_install.py index f4bb80cdd..5b3a3ec05 100644 --- a/deploy/test_install.py +++ b/deploy/test_install.py @@ -1,294 +1,55 @@ -"""install.sh writes .env and starts Compose without host Python.""" +"""The POSIX launcher selects and verifies a binary; lifecycle tests live in oac.""" +import hashlib import os -import fcntl -import sys from pathlib import Path -import stat import subprocess import tempfile -import textwrap import unittest - -ROOT = Path(__file__).resolve().parents[1] -INSTALL = ROOT / "deploy/install.sh" - - -class InstallScriptTests(unittest.TestCase): - def install(self, root, *args, compose_up=0, docker_info=0, key_status=0, download_status=0, kill_download=False, kill_start=False, pull_status=0, kill_pull=False, file_limit=False, missing_image="", route="1.1.1.1 via 10.0.0.1 dev eth0 src 10.0.0.5 uid 0"): - bin_dir = root / "bin" - bin_dir.mkdir(exist_ok=True) - log = root / "docker.log" - self.write_executable(bin_dir / "docker", textwrap.dedent(f"""\ - #!/bin/sh - [ {1 if file_limit else 0} -eq 1 ] || printf '%s\\n' "$*" >> {log} - if [ "$1" = info ]; then exit {docker_info}; fi - if [ "$1" = pull ]; then {'kill -KILL "$PPID"' if kill_pull else ':'}; exit {pull_status}; fi - if [ "$1" = image ] && [ "$2" = inspect ] && [ "$3" = "{missing_image}" ]; then exit 1; fi - if [ "$1" = compose ] && [ "$2" = config ] && [ "$3" = --images ]; then printf 'fixture-core:latest\\nfixture-web:latest\\n'; fi - if [ "$1" = compose ] && [ "$2" = logs ]; then echo service-diagnostic >&2; fi - if [ "$1" = compose ] && [ "$2" = config ] && [ "$3" = --environment ]; then sed "s/'//g" .env; fi - if [ "$1" = compose ] && [ "$2" = version ]; then printf 'v2.29.1\\n'; exit 0; fi - if [ "$1" = compose ] && [ "$2" = cp ]; then printf '#!/bin/sh\\necho oac_core_fixture\\nexit {key_status}\\n' > ./oac.download; chmod +x ./oac.download; exit 0; fi - if [ "$1" = compose ] && [ "$2" = up ]; then {'kill -KILL "$PPID"' if kill_start else ':'}; exit {compose_up}; fi - exit 0 - """)) - self.write_executable(bin_dir / "curl", textwrap.dedent(f"""\ - #!/bin/sh - exit_status={download_status} - [ "$exit_status" = 0 ] || exit "$exit_status" - output="" - while [ $# -gt 0 ]; do - if [ "$1" = --output ]; then output="$2"; shift 2; continue; fi - shift - done - printf 'fixture\\n' > "$output" - {'kill -KILL "$PPID"' if kill_download else ':'} - """)) - self.write_executable(bin_dir / "sha256sum", "#!/bin/sh\nexit 0\n") - self.write_executable(bin_dir / "uname", '#!/bin/sh\ncase "$1" in -s) echo Linux;; -m) echo x86_64;; esac\n') - self.write_executable(bin_dir / "flock", f'#!{sys.executable}\nimport fcntl, sys\ntry: fcntl.flock(int(sys.argv[-1]), fcntl.LOCK_EX | fcntl.LOCK_NB)\nexcept BlockingIOError: sys.exit(1)\n') - self.write_executable(bin_dir / "ss", "#!/bin/sh\nexit 0\n") - self.write_executable(bin_dir / "ip", f"#!/bin/sh\nprintf '%s\\n' '{route}'\n") - env = dict(os.environ, PATH=str(bin_dir) + os.pathsep + os.environ["PATH"], HOME=str(root)) - command = ["bash", str(INSTALL), "--install-dir", str(root / "oac"), *args] - if file_limit: - command = ["bash", "-c", 'ulimit -f 0; exec "$@"', "--", *command] - completed = subprocess.run(command, - env=env, capture_output=True, text=True) - return completed, log.read_text() if log.exists() else "" - - def test_a_failed_start_preserves_configuration_and_reports_service_logs(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - completed, recorded = self.install(root, "--host", "127.0.0.1", "--web-port", "59991", - "--public-url", "https://core.example", compose_up=1) - self.assertNotEqual(completed.returncode, 0, completed.stderr) - self.assertTrue((root / "oac/.env").exists()) - self.assertNotIn("compose down", recorded) - self.assertIn("service-diagnostic", completed.stderr) - self.assertIn("data retained", completed.stderr) - self.assertIn("pull --platform linux/amd64 fixture-core:latest", recorded) - self.assertIn("compose up -d --wait", recorded) - self.assertIn("compose logs", recorded, "a failed start must show the services' logs") - - def test_the_private_address_is_the_default_public_url(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - completed, _ = self.install(root) - self.assertEqual(completed.returncode, 0, completed.stderr) - self.assertIn("OAC_PUBLIC_URL='http://10.0.0.5:8080'\n", (root / "oac/.env").read_text()) - self.assertIn("Console http://10.0.0.5:8080", completed.stdout) - self.assertIn("Core key oac_core_fixture", completed.stdout) - self.assertNotIn("Only this host", completed.stdout) - - def test_without_a_private_address_only_this_host_reaches_web(self): - for args, route in ((["--web-port", "59992"], "1.1.1.1 dev eth0 src 203.0.113.5 uid 0"), - (["--host", "127.0.0.1", "--web-port", "59992"], "1.1.1.1 dev eth0 src 10.0.0.5 uid 0")): - with self.subTest(args=args), tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - completed, _ = self.install(root, *args, route=route) - self.assertEqual(completed.returncode, 0, completed.stderr) - self.assertIn("OAC_PUBLIC_URL='http://localhost:59992'\n", (root / "oac/.env").read_text()) - self.assertIn("Only this host can open the console", completed.stdout) - - def test_env_holds_only_the_installation_choices(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - completed, _ = self.install(root, "--public-url", "https://core.example") - self.assertEqual(completed.returncode, 0, completed.stderr) - env = dict((lambda pair: (pair[0], pair[1].strip("'")))(line.split("=", 1)) for line in (root / "oac/.env").read_text().splitlines()) - self.assertEqual(env["OAC_PUBLIC_URL"], "https://core.example") - self.assertEqual(sorted(env), ["COMPOSE_PROJECT_NAME", "OAC_HOST", "OAC_INSTALL_DIR", "OAC_PUBLIC_URL", "OAC_WEB_PORT"]) - - def test_key_failure_never_removes_a_started_installation(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - completed, recorded = self.install(root, key_status=1) - self.assertNotEqual(completed.returncode, 0) - self.assertTrue((root / "oac/.env").exists()) - self.assertNotIn("compose down", recorded) - self.assertIn("Core key could not be read", completed.stderr) - - def test_retry_reuses_saved_settings_and_keeps_data(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - failed, _ = self.install(root, "--public-url", "https://core.example", compose_up=1) - self.assertNotEqual(failed.returncode, 0) - configuration = (root / "oac/.env").read_bytes() - data = root / "oac/data" - data.mkdir() - (data / "keep").write_text("user data") - (root / "docker.log").unlink() - completed, recorded = self.install(root, download_status=22) - self.assertEqual(completed.returncode, 0, completed.stderr) - self.assertEqual((root / "oac/.env").read_bytes(), configuration) - self.assertEqual((data / "keep").read_text(), "user data") - self.assertIn("Console https://core.example", completed.stdout) - self.assertNotIn("pull --platform", recorded) - self.assertNotIn("Only this host", completed.stdout) - - def test_retry_downloads_only_missing_images_and_prevents_implicit_updates(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - failed, _ = self.install(root, compose_up=1) - self.assertNotEqual(failed.returncode, 0) - (root / "docker.log").unlink() - (root / "oac/oac").unlink() - completed, recorded = self.install(root, missing_image="fixture-web:latest") - self.assertEqual(completed.returncode, 0, completed.stderr) - self.assertNotIn("pull --platform linux/amd64 fixture-core:latest", recorded) - self.assertIn("pull --platform linux/amd64 fixture-web:latest", recorded) - for line in recorded.splitlines(): - if line.startswith(("compose create", "compose up")): - self.assertIn("--pull never", line) - self.assertIn("--no-recreate", line) - - def test_retry_after_file_size_limit_recovers_staging(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - failed, _ = self.install(root, file_limit=True) - self.assertNotEqual(failed.returncode, 0) - self.assertIn("Downloading release checksums", failed.stdout) - completed, _ = self.install(root) - self.assertEqual(completed.returncode, 0, completed.stderr) - self.assertFalse((root / "oac.staging").exists()) - - def test_failed_initial_pull_never_adopts_old_cached_images_on_retry(self): - for kill in (False, True): - with self.subTest(kill=kill), tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - failed, _ = self.install(root, pull_status=1, kill_pull=kill) - self.assertNotEqual(failed.returncode, 0) - self.assertFalse((root / "oac").exists()) - (root / "docker.log").unlink() - # Every image inspect succeeds: another installation cached old tags. - completed, recorded = self.install(root) - self.assertEqual(completed.returncode, 0, completed.stderr) - for image in ("fixture-core:latest", "fixture-web:latest"): - self.assertIn("pull --platform linux/amd64 " + image, recorded) - self.assertFalse((root / "oac.staging").exists()) - - def test_invalid_port_and_unavailable_docker_fail_before_installation(self): - for args, status in [(["--web-port", "0"], 0), (["--web-port", "70000"], 0), ([], 1)]: - with self.subTest(args=args, status=status), tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - completed, recorded = self.install(root, *args, docker_info=status) - self.assertNotEqual(completed.returncode, 0) - self.assertFalse((root / "oac").exists()) - self.assertNotIn("pull --platform", recorded) - - def test_failed_download_leaves_no_installation_or_staging(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - completed, recorded = self.install(root, download_status=22) - self.assertNotEqual(completed.returncode, 0) - self.assertFalse((root / "oac").exists()) - self.assertFalse((root / "oac.staging").exists()) - self.assertNotIn("compose down", recorded) - - def test_retry_after_sigkill_cleans_unfinished_download(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - killed, _ = self.install(root, kill_download=True) - self.assertEqual(killed.returncode, -9, killed.stderr) - self.assertTrue((root / "oac.staging").exists()) - self.assertFalse((root / "oac").exists()) - completed, _ = self.install(root) - self.assertEqual(completed.returncode, 0, completed.stderr) - self.assertFalse((root / "oac.staging").exists()) - self.assertTrue((root / "oac/oac").exists()) - - def test_retry_after_start_sigkill_cleans_published_log(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - killed, _ = self.install(root, kill_start=True) - self.assertEqual(killed.returncode, -9, killed.stderr) - self.assertTrue((root / "oac/install.log").exists()) - completed, _ = self.install(root) - self.assertEqual(completed.returncode, 0, completed.stderr) - self.assertFalse((root / "oac/install.log").exists()) - self.assertFalse((root / "oac.staging").exists()) - - def test_retry_clears_staging_from_an_interrupted_process(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - staging = root / "oac.staging" - staging.mkdir() - (staging / ".oac-installer").symlink_to((root / "oac").resolve()) - (staging / "partial").write_text("unfinished download") - completed, _ = self.install(root) - self.assertEqual(completed.returncode, 0, completed.stderr) - self.assertFalse(staging.exists()) - self.assertFalse((root / "oac/partial").exists()) - - def test_unrecognized_staging_directory_is_never_deleted(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - staging = root / "oac.staging" - staging.mkdir() - (staging / "keep").write_text("user data") - completed, recorded = self.install(root) - self.assertNotEqual(completed.returncode, 0) - self.assertIn("Unrecognized staging", completed.stderr) - self.assertEqual((staging / "keep").read_text(), "user data") - self.assertNotIn("compose up", recorded) - - def test_existing_unrelated_directory_is_never_deleted(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - (root / "oac").mkdir() - (root / "oac/keep").write_text("user data") - completed, recorded = self.install(root) - self.assertNotEqual(completed.returncode, 0) - self.assertEqual((root / "oac/keep").read_text(), "user data") - self.assertNotIn("compose down", recorded) - - def test_another_installation_holds_the_lock(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - with (root / "oac.install.lock").open("w") as lock: - fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) - completed, recorded = self.install(root) - self.assertNotEqual(completed.returncode, 0) - self.assertIn("Another installation", completed.stderr) - self.assertFalse((root / "oac").exists()) - self.assertNotIn("pull --platform", recorded) - - def test_retry_does_not_overlap_an_oac_mutation(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - completed, _ = self.install(root) - self.assertEqual(completed.returncode, 0, completed.stderr) - (root / "docker.log").unlink() - with (root / "oac/.oac.lock").open("r+") as lock: - fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) - completed, recorded = self.install(root) - self.assertNotEqual(completed.returncode, 0) - self.assertIn("Another oac command", completed.stderr) - self.assertNotIn("compose up", recorded) - - def test_rerun_does_not_silently_replace_settings(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - completed, _ = self.install(root) - self.assertEqual(completed.returncode, 0, completed.stderr) - before = (root / "oac/.env").read_bytes() - completed, _ = self.install(root, "--web-port", "9000") - self.assertEqual(completed.returncode, 0, completed.stderr) - self.assertIn("Using saved settings", completed.stdout) - self.assertIn("only apply to new directories", completed.stdout) - self.assertEqual((root / "oac/.env").read_bytes(), before) - - def test_help_does_not_need_docker(self): - help_text = subprocess.run(["bash", str(INSTALL), "--help"], capture_output=True, text=True, check=True) - self.assertIn("--web-port", help_text.stdout) - self.assertNotIn("--external-proxy", help_text.stdout) - - def write_executable(self, path, text): - path.write_text(text) - path.chmod(path.stat().st_mode | stat.S_IEXEC) - - -if __name__ == "__main__": +SCRIPT = Path(__file__).with_name('install.sh').resolve() + + +class LauncherTests(unittest.TestCase): + def run_launcher(self, system='Darwin', machine='arm64', corrupt=False): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + binary = b'#!/bin/sh\nprintf "%s\\n" "$@"\n' + digest = hashlib.sha256(binary).hexdigest() + scripts = { + 'uname': '#!/bin/sh\nif [ "$1" = -s ]; then echo ' + system + '; else echo ' + machine + '; fi\n', + 'curl': '''#!/usr/bin/env python3 +import pathlib,sys +args=sys.argv +url=args[-3]; target=pathlib.Path(args[-1]) +asset=url.rsplit('/',1)[1] +if asset.endswith('.sha256'): + target.write_text(DIGEST + ' ' + asset.removesuffix('.sha256') + '\\n') +else: + target.write_bytes(BINARY) +'''.replace('DIGEST', repr('0' * 64 if corrupt else digest)).replace('BINARY', repr(binary)), + } + for name, contents in scripts.items(): + (root / name).write_text(contents); (root / name).chmod(0o700) + return subprocess.run(['bash', str(SCRIPT), '--install-dir', '/path with spaces/core', '--version', 'v1.2.3'], + env={**os.environ, 'PATH': str(root) + os.pathsep + os.environ['PATH']}, capture_output=True, text=True) + + def test_linux_and_mac_share_argument_forwarding(self): + for system, machine in [('Linux', 'x86_64'), ('Linux', 'aarch64'), ('Darwin', 'arm64'), ('Darwin', 'x86_64')]: + with self.subTest(system=system, machine=machine): + result = self.run_launcher(system, machine) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.splitlines(), ['install', '--install-dir', '/path with spaces/core', '--version', 'v1.2.3']) + + def test_corrupt_binary_is_never_executed(self): + result = self.run_launcher(corrupt=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('checksum mismatch', result.stderr) + self.assertEqual(result.stdout, '') + + def test_unsupported_host_fails_before_downloading(self): + result = self.run_launcher(system='FreeBSD') + self.assertNotEqual(result.returncode, 0) + + +if __name__ == '__main__': unittest.main() diff --git a/docs/configuration.md b/docs/configuration.md index f91a35562..fcdc5b4a4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -9,7 +9,7 @@ Every setting of a Core installation has exactly one home. There are two kinds: | [Process settings](#process-settings-configjson) | Public URL, ports, logging, harnesses, execution concurrency, audit retention, OAuth origins, Runtime history export | `.env` in the installation directory (default `~/.oac/core`) | Edit `.env`, then run `oac apply` | `oac apply` recreates the services that read the changed settings | | [Runtime settings](#runtime-settings-web) | Sandbox backend and size, nodes, Projects and keys, default models, executor credentials | Core's PostgreSQL database | Web, or the Core API (`/core/v1`) with the Core key | Saved without a Core restart; nodes prepare Runtime changes asynchronously | -Web's **System** page shows the installation's addresses, the default models, the sandbox configuration and, under **Startup settings**, the process settings Core loaded. Secrets live in [`data/secrets/`](#installation-directory), one copy each. No configuration file defines Projects or API keys. +Web's **System** page shows the installation's addresses, the default models, the sandbox configuration and, under **Startup settings**, the process settings Core loaded. Secrets live in [`secrets/`](#compose-installations), one copy each. No configuration file defines Projects or API keys. ## Process settings {#process-settings-configjson} @@ -23,9 +23,8 @@ Installer flags in [installation options](./getting-started/install-options.md) 1. It runs `oac-core check-config` with the `.env` you edited and changes nothing if a value is invalid. 2. It runs `docker compose up -d --wait`. Compose recreates only the services whose configuration changed. -3. If the check fails, no container is recreated. See [stop and restart](./getting-started/operations.md#stop-and-restart) for what a restart interrupts. -`docker compose ps` shows the services. Domain state is `data/domain/status.json`. +Use `docker compose ps` to check the services. See [stop and restart](./getting-started/operations.md#stop-and-restart) for what a restart interrupts. ### Changing the public URL {#changing-the-public-url} @@ -44,7 +43,7 @@ To change it, point the reverse proxy at the new address first, then edit `OAC_P | Variable | Default | Meaning | | --- | --- | --- | | `OAC_PUBLIC_URL` | `http://localhost:8080` | Origin applications, nodes, sandboxes and self-hosted executors use. See [changing the public URL](#changing-the-public-url) | -| `OAC_HOST` | `127.0.0.1` | Web bind address published by `compose.yaml`. `install.sh` sets `0.0.0.0` | +| `OAC_HOST` | `127.0.0.1` | Web bind address published by `compose.yaml`. The installer sets `0.0.0.0` | | `OAC_WEB_PORT` | `8080` | Host port of Web | | `OAC_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` | | `OAC_LOG_FORMAT` | `auto` | `auto`, `text` or `json` | @@ -81,7 +80,7 @@ Core approves a node's capacity when you generate its Add node command: **Sandbo ### Default models -Set a default in **System** → **Default model configuration**, or use `PUT /core/v1/harnesses/{harness}/model-configuration`. Core encrypts provider keys with `secrets/credential.key` and never returns them. [Model execution](../contracts/agents-api/model-execution.md#deployment-defaults) owns the request fields and replacement rules, and [precedence](../contracts/agents-api/model-execution.md#saved-defaults-and-precedence) says which Sessions use a default. +Set a default in **System** → **Default model configuration**, or use `PUT /core/v1/harnesses/{harness}/model-configuration`. Core encrypts provider keys with `secrets/core/credential.key` and never returns them. [Model execution](../contracts/agents-api/model-execution.md#deployment-defaults) owns the request fields and replacement rules, and [precedence](../contracts/agents-api/model-execution.md#saved-defaults-and-precedence) says which Sessions use a default. ## Compose installations @@ -100,7 +99,7 @@ The initialization service generates secrets and the installation ID once, then Initialization prepares this directory; application services receive their secret directories read-only. `docker compose exec web oac-web core-key` prints the Core key to the operator terminal without writing it to container logs. Database passwords and credential encryption keys are never printed. -`OAC_DATA_DIR` selects the directory and defaults to `./data` beside the Compose file. Preserve it together with that project's definition and public URL. Removing only the secret directories does not reset an installation; initialization refuses to start over an existing database. Core also binds the installation ID to its database. Runtime settings continue to live in [Core's database](#runtime-settings-web). +The named Docker volume `_data` contains these paths. Docker manages Linux ownership on every host; each service mounts only its required subdirectories. Preserve this volume together with the project definition and public URL. Removing only the secret directories does not reset an installation; initialization refuses to start over an existing database. Core also binds the installation ID to its database. Runtime settings continue to live in [Core's database](#runtime-settings-web). ## Docker node configuration @@ -118,37 +117,31 @@ The [Docker adapter](./sandbox-provider.md#docker-adapter) owns container isolat ## Installation directory -The installer creates the installation directory, `~/.oac/core` by default, with mode `0700`. Secret files are `0600`. +The installer creates `~/.oac/core` by default (`$HOME/.oac/core` on Windows). Its files contain process settings and the native operator command; persistent service data lives in the [Compose data volume](#compose-installations). | Path | Content | Changed by | | --- | --- | --- | -| `.env` | [Process settings](#process-settings-configjson). The file you edit | You, then `oac apply` | -| `compose.yaml` | The release's service definition. Do not edit them | The release | -| `oac` | The [management command](./getting-started/operations.md#the-oac-command), copied from the Core image | The installer | -| `data/secrets/web/core.key` | The [Core key](./getting-started/operations.md#core-key) | `oac rotate-core-key` | -| `data/secrets/core/credential.key` | Encryption key for what Core stores sealed in the database | Nothing. Keep it with the database | -| `data/secrets/core/core-key-digests.json` | SHA-256 of the Core key | `oac rotate-core-key` | -| `data/secrets/database/password` | PostgreSQL password | Nothing. PostgreSQL reads it only when the database is created | -| `data/database/` | PostgreSQL data | PostgreSQL | -| `data/node-payload/` | Node files Web serves at `/node-install/` | Initialization | -| `data/state/` | Private Provider state, including E2B receipts | Core | -| `.oac.lock` | The installation lock | Mutating `oac` commands | - -The Compose project is named `oac-<10 hex digits>`. Its services are `init`, `database`, `core` and `web`. Core applies database migrations when it starts. `web` serves the console and forwards `/v1` and `/api/v1` to Core, and it is the only service with a published port, `OAC_WEB_PORT`. No service receives a Docker socket. Apart from Docker's storage, nothing is written outside the installation directory. +| `.env` | Process settings and the stable Compose project name | You, then `oac apply` | +| `compose.yaml`, `compose-sha256sums.txt` | Verified release service definition | The release | +| `oac` (`oac.exe` on Windows) | Native management command | The installer | + +The sibling `.lock` directory remains for synchronization; `.staging` holds unpublished installation files. Neither contains service data. On Unix the installer creates private directories with mode `0700` and configuration files with mode `0600`. + +The Compose project is named `oac-<10 hex digits>`. Its services are `init`, `database`, `core` and `web`. Core applies database migrations when it starts. Web serves the console and forwards `/v1` and `/api/v1` to Core; it is the only service with a published port, `OAC_WEB_PORT`. No service receives a Docker socket. ## Appendix: Core environment without the installer -Core reads only its environment. Compose interpolates `.env` into the service environment. Compose must be 2.26.0 or newer. If you run Core yourself, set these variables; see the [service guide](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md). +Core reads its process environment. Compose interpolates `.env` into it and mounts secrets at the container paths below. When running Core directly, set the file variables to absolute paths readable by the Core process; see the [service guide](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md). | Variable | Set from | | --- | --- | | `OAC_PUBLIC_URL` | The public origin. Core derives the daemon WebSocket URL, the self-hosted `remote_url`, the hosted sandbox address and the deployment's read-only `core_url` from it, never from request headers. Without it, Core runs no Runtime gateway and executes no Sessions | | `OAC_ADDR` | The image sets `:8091`. Independently started Core defaults to `127.0.0.1:8091` when unset or empty | | `OAC_DATABASE_URL` | PostgreSQL without a password | -| `OAC_DATABASE_PASSWORD_FILE` | `data/secrets/database/password`. The URL must then carry no password | -| `OAC_CREDENTIAL_KEY_FILE` | `data/secrets/core/credential.key` | -| `OAC_CORE_KEY_DIGESTS_FILE` | `data/secrets/core/core-key-digests.json`: a JSON array with the SHA-256 of the Core key | -| `OAC_INSTALLATION_ID_FILE` | `data/secrets/core/installation.id`: the installation ID, a canonical UUID. It enables the sandbox deployment and node routes and requires `OAC_PUBLIC_URL` and `OAC_CORE_KEY_DIGESTS_FILE`. Core refuses an ID other than the one its database recorded | +| `OAC_DATABASE_PASSWORD_FILE` | `/run/database/password`. The URL must then carry no password | +| `OAC_CREDENTIAL_KEY_FILE` | `/run/oac/credential.key` | +| `OAC_CORE_KEY_DIGESTS_FILE` | `/run/oac/core-key-digests.json`: a JSON array with the SHA-256 of the Core key | +| `OAC_INSTALLATION_ID_FILE` | `/run/oac/installation.id`: the installation ID, a canonical UUID. It enables the sandbox deployment and node routes and requires `OAC_PUBLIC_URL` and `OAC_CORE_KEY_DIGESTS_FILE`. Core refuses an ID other than the one its database recorded | | `OAC_EXECUTION_CONCURRENCY`, `OAC_DEFAULT_HARNESS`, `OAC_HARNESSES`, `OAC_WRITE_AUDIT_RETENTION`, `OAC_OAUTH_TRUSTED_ORIGINS` | The matching [process settings](#settings). `oac-core check-config` validates them without starting Core | | `OAC_HISTORY_SETTINGS_FILE` | Optional Runtime history file. Sensitive; the installation report says only whether it is set | | `OAC_LOG_LEVEL`, `OAC_LOG_FORMAT`, `OAC_LOG_ADD_SOURCE` | Logging; Web reads the same three | @@ -162,7 +155,7 @@ Invalid explicit OAuth trusted origins stop Core at startup. Entries must be HTT ## Appendix: Web environment without the installer -Compose sets these for Web. Set them yourself only when you run the console without Compose. Of the installation's secrets, Web receives only `data/secrets/web/core.key`. +Compose sets these for Web. Set them yourself only when you run the console without Compose. Compose mounts the data volume's `secrets/web/` at `/run/oac` and sets `OAC_WEB_CORE_KEY_FILE=/run/oac/core.key`. | Variable | Default | Meaning | | --- | --- | --- | diff --git a/docs/getting-started/install-options.md b/docs/getting-started/install-options.md index 41c4e50e7..08cdd34e5 100644 --- a/docs/getting-started/install-options.md +++ b/docs/getting-started/install-options.md @@ -2,7 +2,7 @@ title: "Installation options and advanced deployments" --- -The [default installation](./install.md) needs no options. Use this page to run behind an existing reverse proxy or install without internet access. +The [default installation](./install.md) needs no options. Use this page to set installation options, deploy with Compose or configure a reverse proxy. Pass options to the downloaded script: @@ -10,11 +10,11 @@ Pass options to the downloaded script: ./install.sh --public-url https://core.example ``` -With the one-line command, append them after `bash -s --`. `--version TAG` selects a published release; otherwise the script selects the latest stable release and verifies each Compose file's SHA-256. A failed step stops installation without a success message. +On Windows, download `install.ps1` and pass the same flags with `& ./install.ps1 --public-url https://core.example`. With the Unix one-line command, append them after `bash -s --`. `--version TAG` selects a published release; otherwise the script selects the latest stable release and verifies the native command and Compose files against their SHA-256 checksums. ## Docker Compose and hosting platforms -Use the `compose.yaml` from a release with Docker Compose 2.26 or newer on Linux amd64. The release pins its initialization image and source revision in the [Compose template](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/compose/compose.yaml). Core and Web use the `latest` images, and PostgreSQL uses `postgres:16-alpine`. It starts PostgreSQL, Core and Web. Web forwards `/v1` and `/api/v1` to Core. Data is bind-mounted from a directory. The one-time initialization service generates random secrets there and prepares the node installer; Core applies database migrations when it starts. [Compose configuration](../configuration.md#compose-installations) owns the settings and the data directory. +Use the `compose.yaml` from a release on any [supported Core host](./install.md#prerequisites). The release pins its initialization image and source revision in the [Compose template](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/compose/compose.yaml). Core and Web use the `latest` images, and PostgreSQL uses `postgres:16-alpine`. It starts PostgreSQL, Core and Web. Web forwards `/v1` and `/api/v1` to Core. Data lives in a named Docker volume. The one-time initialization service generates random secrets there and prepares the node installer; Core applies database migrations when it starts. [Compose configuration](../configuration.md#compose-installations) owns the settings and the data directory. For a local trial, download `compose.yaml` from a release into an empty directory, then run: @@ -23,7 +23,7 @@ docker compose up -d --wait --wait-timeout 900 docker compose exec web oac-web core-key ``` -`oac-web core-key` prints the generated Core key to your terminal without writing it to container logs. Open `http://localhost:8080` and use that key to sign in. All installation secrets are generated automatically; keep the same Compose project and its data directory when restarting. +`oac-web core-key` prints the generated Core key to your terminal without writing it to container logs. Open `http://localhost:8080` and use that key to sign in. All installation secrets are generated automatically; keep the same Compose project and its data volume when restarting. The initialization image contains only the small node installation metadata alongside the initialization command. First startup verifies and copies that metadata without downloading the control archive or requiring access to GitHub Releases. Later starts verify the saved files. Container images still need to be pulled. An interrupted first initialization can be rerun; an existing database with missing installation secrets is refused. @@ -41,7 +41,7 @@ On either platform, open its server terminal and run `docker compose ls` to find After signing in, choose the sandbox backend and add nodes using [Nodes](./nodes.md). The Compose stack deploys the control plane; execution machines remain separate. -Stop with `docker compose stop` using the same files and environment. Back up the data directory together while the services are stopped. Follow the [installation version policy](./operations.md#installation-version-policy): a different release needs a new Compose project and a fresh data directory. +Stop with `docker compose stop` using the same files and environment. Back up the data volume and configuration together while the services are stopped. Follow the [installation version policy](./operations.md#installation-version-policy): a different release needs a new Compose project and a fresh data volume. ## Process settings @@ -60,7 +60,7 @@ These flags are written to `.env` once. After installation, edit that file and r | Option | Purpose | | --- | --- | -| `--install-dir DIR` | Absolute installation directory; defaults to `~/.oac/core`. A new installation requires an empty or missing directory, or one holding an [installation that never started](./install.md#install) | +| `--install-dir DIR` | Absolute installation directory; defaults to `~/.oac/core`. Use an empty or missing directory for a new installation, or an existing installation directory to [retry](./install.md#install) | Several installations can share a machine when they use distinct installation directories and ports. Use distinct IP addresses or a shared reverse proxy for more. Each installation has its own database, Core key and nodes. @@ -131,4 +131,4 @@ The address changes whenever `cloudflared` restarts; nodes bound to the old addr ## Offline hosts -This installer does not install from an offline bundle. It downloads Compose files and container images from the release. +Core installation requires access to GitHub Releases and the container registries to download release files and images. diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index acf59bd48..9e1b17e73 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -2,7 +2,7 @@ title: "Install Core and Web" --- -One command installs Core, the Web console and PostgreSQL on a Linux host. Sign in to Web with the Core key, set a default model and issue Project API keys. Applications call Core with those keys. Sessions run in sandboxes on nodes you add, or on E2B. +One command installs Core, the Web console and PostgreSQL on Linux, macOS or Windows. Sign in to Web with the Core key, set a default model and issue Project API keys. Applications call Core with those keys. Sessions run in sandboxes on nodes you add, or on E2B. 1. [Check the prerequisites](#prerequisites). 2. [Run the installer](#install). @@ -16,21 +16,29 @@ This page follows the default path. Every flag, existing reverse proxies and off ## Prerequisites -- Linux amd64 and curl. -- Docker Engine with Docker Compose 2.26.0 or newer (`docker compose version`). +- Linux amd64/arm64 or macOS Intel/Apple Silicon with curl; Windows x64 with PowerShell. +- Docker Engine 26 or newer and Docker Compose 2.26.0 or newer. On macOS and Windows, install and start Docker Desktop using Linux containers. - An account that can run `docker` and write to its home directory. Ordinary users and root both work; the installer never calls sudo. - Free port 8080 for Web. See [ports](./install-options.md#ports). Docker must be able to publish it; the installer does not change host policy. - For anything off this machine, the origin in `OAC_PUBLIC_URL` must be the address browsers, nodes and executors use. You can sign in on this machine first. -The Core host needs no KVM; nodes that run microsandbox do. +Sandbox nodes run on Linux amd64. When Core runs on macOS or Windows, connect a Linux node or use E2B. ## Install +Linux and macOS: + ```sh curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash ``` -On a host whose default route has a private-network address, the installer sets the public URL to `http://:8080`, so machines on the same network can open Web and add nodes; otherwise only this machine can. If a reverse proxy already serves this host, pass its HTTPS address: +Windows PowerShell: + +```powershell +irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.ps1 | iex +``` + +When binding to all IPv4 addresses, the installer uses the private address of the default route if available; otherwise the console address is `http://localhost:8080`. To choose another reachable origin, pass `--public-url`; if a reverse proxy already serves this host, use its HTTPS address: ```sh curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash -s -- --public-url https://core.example @@ -38,13 +46,13 @@ curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/ The script downloads that release's Compose files, checks their SHA-256, and: -1. checks Linux amd64, Docker Compose 2.26 or newer, and that the ports it will publish are free; -2. creates the [installation directory](../configuration.md#installation-directory), `~/.oac/core`, writes `.env`, and copies the `oac` command out of the Core image; +1. checks Docker is running Linux containers, meets the required versions, and can publish the chosen port; +2. prepares the [installation directory](../configuration.md#installation-directory), `~/.oac/core`, with `.env` and the native `oac` command (`oac.exe` on Windows); 3. starts the services with Docker Compose. Web serves the console on port 8080 and forwards `/v1`, `/api/v1` and `/docs` to Core. Core and PostgreSQL are not published. -It saves no sandbox backend, adds no node, creates no Project or key and makes no model request. It ends by printing the console address and the Core key. +The installer prints the console address and Core key. After signing in, configure execution resources and create Projects in Web. -Downloads and configuration checks happen before the installation directory is published. After that, failures preserve the configuration and data and show the service logs. Fix the reported cause and rerun the same command, or specify the installation directory: +Downloads and configuration checks happen before the installation directory is published. After that, failures preserve the configuration and data and report the failed step; inspect it with `docker compose logs --tail 100` in the installation directory. Fix the reported cause and rerun the same command, or specify the installation directory: ```bash curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash -s -- --install-dir "$HOME/.oac/core" @@ -63,6 +71,8 @@ For insufficient space or quota, free space on the filesystem named by the error ~/.oac/core/oac core-key --show ``` + On Windows, use `& "$HOME/.oac/core/oac.exe" core-key --show`. The same command arguments work on every platform. + ## Configure the public address {#configure-the-domain-and-https} Applications, nodes and sandboxes reach Core at one address, the public URL. HTTP is enough on the local network. When you expose Core beyond it, put a reverse proxy in front and set the public URL to the HTTPS origin it serves. E2B guests reach Core from the internet, so they need a public URL that is not loopback. diff --git a/docs/getting-started/operations.md b/docs/getting-started/operations.md index 83e63c196..a15d9174d 100644 --- a/docs/getting-started/operations.md +++ b/docs/getting-started/operations.md @@ -6,7 +6,7 @@ The installation operator owns the Core host, its storage and its availability. ## The oac command -Each installation has its own management command in its directory. It needs neither the bundle nor root: +Each installation has its own native management command in its directory: `oac` on Unix, `oac.exe` on Windows. It needs Docker access and no root privileges: ```sh docker compose -f ~/.oac/core/compose.yaml ps @@ -18,11 +18,11 @@ docker compose -f ~/.oac/core/compose.yaml ps | `docker compose start` | Starts the services | | `docker compose stop` | Stops the services. Data, nodes and sandboxes are kept | | `oac apply` | Runs `oac-core check-config`, then `docker compose up -d --wait`. A failed check changes no service | -| `oac core-key [--show]` | Prints the Core key path, or the key itself with `--show` | +| `oac core-key [--show]` | Identifies the key location in the data volume, or prints the key with `--show` | | `oac rotate-core-key` | Replaces the Core key and restarts Core and Web | | `docker compose down` | Removes the containers. Data is kept; to delete it, [uninstall](#uninstall) | -For a second installation, use its directory, such as `~/.oac/second`. +The examples use the default installation directory. On Windows, invoke the management command with `& "$HOME/.oac/core/oac.exe"` followed by the same arguments. For a custom installation directory, replace the path in each command. ## Service health @@ -63,13 +63,13 @@ A Web restart, including one caused by `oac apply`, signs everyone out of the co ## Core key -Each installation has one administrator credential, the Core key. The installer generates a key with the `oac_admin_` prefix followed by 64 random lowercase hexadecimal characters in `data/secrets/web/core.key`. Read it with `oac core-key --show`; the file is owned by the container user. The Core key: +Each installation has one administrator credential, the Core key. The installer generates a key with the `oac_admin_` prefix followed by 64 random lowercase hexadecimal characters in `secrets/web/core.key`. Read it with `oac core-key --show`; the file is owned by the container user. The Core key: - signs in to Web. The browser gets an HttpOnly session cookie, never the key; - authorizes Core API (`/core/v1`) requests sent as `Authorization: Bearer `; - never authorizes the Agents API (`/v1`). Applications use Project API keys, which in turn can't call `/core/v1`. -Keep it private. Web reads `data/secrets/web/core.key`. Core reads only its SHA-256 from `data/secrets/core/core-key-digests.json`. A Core key has at least 32 characters and no whitespace. Web limits failed sign-ins. +Keep it private. Web reads `secrets/web/core.key`. Core reads only its SHA-256 from `secrets/core/core-key-digests.json`. A Core key has at least 32 characters and no whitespace. Web limits failed sign-ins. ### Script the Core API @@ -103,7 +103,7 @@ The [Core administration API](../../contracts/agents-api/admin-api.md) lists eve ~/.oac/core/oac rotate-core-key ``` -It writes a new key to `data/secrets/web/core.key`, regenerates `data/secrets/core/core-key-digests.json`, and restarts Core and Web. The old key stops working as soon as Core restarts, and every console session ends: sign in again and update your scripts. +It runs in the initialization container, updates `secrets/web/core.key` and `secrets/core/core-key-digests.json` in the data volume, and restarts Core and Web. The old key stops working as soon as Core restarts, and every console session ends: sign in again and update your scripts. ## Projects and API keys @@ -123,37 +123,33 @@ Core records which key made each public resource write; the retention of that hi Back up these together; a restore needs all of them: -- the PostgreSQL volume `_database`. It holds Projects, key digests, nodes, default models, encrypted credentials and all execution history, including large objects. A logical dump: +- the Docker volume `_data`, including its `database/`, `secrets/` and `state/` directories. It holds Projects, key digests, nodes, default models, encrypted credentials and all execution history, including large objects. A logical dump: ```sh docker compose -f "$HOME/.oac/core/compose.yaml" exec -T database \ pg_dump -U agents_api agents_api > oac-backup.sql ``` -- the installation directory, especially `data/`. `data/secrets/core/credential.key` must stay with the database, or stored credentials can't be decrypted. +- the installation directory containing `.env`, `compose.yaml` and the command. The data volume's `secrets/core/credential.key` must stay with the database, or stored credentials cannot be decrypted. - each node's state directory on its host, `/var/lib/oac-node/.oac/nodes//`, with its provider storage: Docker volumes or microsandbox's store. See [when a node host fails](./nodes.md#when-a-node-host-fails) for restoring them. -Stop with `docker compose stop`, archive the installation directory, then `docker compose start`. - -Never prune Docker volumes or delete native harness history to make a retry pass. A deleted Session does not prove that all provider resources were reclaimed. +Stop with `docker compose stop`, export the complete data volume and archive the installation directory, then `docker compose start`. Docker Desktop supports volume export from its **Volumes** view. A SQL dump alone does not include the encryption key or Provider state. ## Uninstall ```sh cd ~/.oac/core -docker compose down --remove-orphans -docker compose run --rm --no-deps --entrypoint find init /data -mindepth 1 -delete -docker compose down --rmi all +docker compose down --volumes --remove-orphans --rmi all cd && rm -rf ~/.oac/core ``` -The containers own `data/`, so the `init` image deletes its contents; then `down --rmi all` removes the images and `rm` removes the installation directory. Run these only when you mean to delete the data. +`down --volumes` deletes the installation data volume. Remove the installation directory afterward; on Windows use `Remove-Item -Recurse "$HOME/.oac/core"`. All data goes with it: Projects and API keys, Session history, stored credentials and the Core key. To keep the data, stop the installation with `docker compose stop` instead, or [back it up](#back-up) first. Uninstall stops no sandbox: node sandboxes keep running on their nodes, and E2B sandboxes keep running, and billing, at E2B. While Core is still up, archive their Sessions or [reset the deployment](./nodes.md#change-the-sandbox-configuration) and let it complete; the command shows how many sandboxes Core has in use. -Nodes on other hosts keep running. To uninstall them the usual way, remove them in Web first, as in [Remove a node](./nodes.md#remove-a-node). After the installation directory is gone, their Core is gone: on each node host, run the node uninstall command with `--force`, using `node-install.pyz` from the release that installed them. The installation ID is `data/secrets/core/installation.id`. +Nodes on other hosts keep running. To uninstall them the usual way, remove them in Web first, as in [Remove a node](./nodes.md#remove-a-node). After the installation directory is gone, their Core is gone: on each node host, run the node uninstall command with `--force`, using `node-install.pyz` from the release that installed them. The installation ID is `secrets/core/installation.id` in the data volume. ## Installation version policy @@ -163,7 +159,7 @@ To move to a new release, install it into a new, empty directory, with its own d An interrupted installation can [resume with its saved configuration](./install.md#install). An unrelated nonempty directory is refused. -The installer and mutating `oac` commands hold `.oac.lock`. The installer also holds a sibling `.install.lock` while preparing the directory. If another command holds either lock, retry after it finishes. Never delete a lock file to get past a busy installation. +Installation and mutating `oac` commands share the [installation lock](../configuration.md#installation-directory). If another command is running, wait for it to finish before retrying. ## Troubleshooting diff --git a/docs/maintainers.md b/docs/maintainers.md index 47d162b70..166c39feb 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -6,7 +6,10 @@ This guide is for maintainers who build and publish OpenAgentCore. To install Co ## Build a distribution -A distribution is the matched set of Linux amd64 release assets built from one commit: the control archive (the installer, the `oac` command, and the Core, Web, ingress and PostgreSQL images), the Runtime image and node artifacts as separate files, and the native installers. +A distribution is a matched set of release assets built from one commit: the control archive (the installer, the `oac` command, and the Core, Web, ingress and PostgreSQL images), the Runtime image and node artifacts as separate files, and the native installers. + +Core, Web and ingress images are published as verified Linux amd64/arm64 indexes. The arm64 control archive contains those three images; Node, hosted Runtime and offline payloads use Linux amd64. Release builders use QEMU for ARM image steps, including the E2B helper. Host `oac` binaries are built from the same command for Linux amd64/arm64, macOS amd64/arm64 and Windows amd64; launchers only select, verify and invoke them. Each version index is checked against its platform archives before floating tags move. + Build on Linux x86_64 with a glibc compatible with Debian 12, Docker, the Go version in `go.mod`, a C compiler (the microsandbox helper is a CGO build), Node, pnpm, Python 3.9 or newer, curl, tar, pigz and sha256sum. The source must be clean and committed. First prepare the pinned Codex package and MiniMax Code companion, then build: @@ -96,7 +99,7 @@ The distribution combines the three Harness images into one Runtime image (`depl make build-e2b-provider ``` -Docker builds the Linux amd64 helper with the pinned CPython and Debian 12 image. The Python dependency closure, including PyInstaller, is hash-locked in `services/core/tools/e2b-provider/requirements.lock`; no E2B account key is needed. Set `E2B_PROVIDER_BUILD_DIR` for another output directory. The build is a pure function of the helper sources, `LICENSE` and the build script, so it is cached under `~/.oac/cache/e2b-provider/` by their hash and rebuilt only when they change. The output is `oac-e2b-provider-linux-amd64.tar.gz` with its `.sha256`; it extracts to `oac-e2b-provider/` with the executable, `_internal/`, `licenses/`, `requirements.lock` and `manifest.json`. The Core image uses that tree; the host needs a compatible glibc and CA certificates, not Python. +Docker builds the Linux helper for `GOARCH=amd64` (default) or `GOARCH=arm64` with the pinned CPython and Debian 12 image. The Python dependency closure, including PyInstaller, is hash-locked in `services/core/tools/e2b-provider/requirements.lock`; no E2B account key is needed. Set `E2B_PROVIDER_BUILD_DIR` for another output directory. The build is a pure function of the helper sources, `LICENSE` and the build script, so it is cached under `~/.oac/cache/e2b-provider/` by their hash and rebuilt only when they change. The output is `oac-e2b-provider-linux-.tar.gz` with its `.sha256`; it extracts to `oac-e2b-provider/` with the executable, `_internal/`, `licenses/`, `requirements.lock` and `manifest.json`. The Core image uses that tree; the host needs a compatible glibc and CA certificates, not Python. **microsandbox helper.** Linux only, with a C compiler: @@ -111,9 +114,9 @@ The helper is written to `~/.oac/build/microsandbox-provider/oac-microsandbox-pr ### Standalone Core builds -`make build-core` builds `oac-core`, `oac-core-device`, `oac-core-environment-key` and `oac-node` into `${OAC_DEV_HOME:-$HOME/.oac}/build/oac-core` (`OAC_DEV_CORE_BUILD_DIR` selects another absolute directory). The build copies only the source set listed in `scripts/build-core.sh` (the Core service, its contracts, the shared packages it needs and the root Go module files) into a temporary context and builds with CGO disabled, read-only modules and trimmed paths. It needs no Node, Docker or other application. When Core gains a shared dependency, add that package to the list; never copy the whole repository to make it compile. +`make build-core` builds `oac-core`, `oac-core-device`, `oac-core-environment-key` `oac-node` and `oac` into `${OAC_DEV_HOME:-$HOME/.oac}/build/oac-core` (`OAC_DEV_CORE_BUILD_DIR` selects another absolute directory). The build copies only the source set listed in `scripts/build-core.sh` (the Core service, its contracts, the shared packages it needs and the root Go module files) into a temporary context and builds with CGO disabled, read-only modules and trimmed paths. It needs no Node, Docker or other application. When Core gains a shared dependency, add that package to the list; never copy the whole repository to make it compile. -`make docker-build-core` builds the image `oac-core:dev` (`OAC_DEV_CORE_IMAGE` selects another name) from those five commands and the E2B helper. The base is the digest-pinned `debian:bookworm-slim` with CA certificates and the glibc runtime the helper needs; the default user is UID/GID 65532 and Core listens on `:8091`. The image is Linux amd64 only and is not pushed to a registry. Changes to the image or its build need `make check-core-container` in addition to the relevant source checks: it runs the official-client suite against the image with a read-only root filesystem and needs Linux Docker, a non-root user, and the [test database and pinned SDK](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#official-client-verification) of the service checks (`OAC_TEST_DATABASE_URL` naming an `oac_*_tests` database with the migrations applied, and `OAC_TEST_OFFICIAL_SDK_PYTHON`). +`make docker-build-core` builds the image `oac-core:dev` (`OAC_DEV_CORE_IMAGE` selects another name) from those five commands and the E2B helper. The base is the digest-pinned `debian:bookworm-slim` with CA certificates and the glibc runtime the helper needs; the default user is UID/GID 65532 and Core listens on `:8091`. This local target builds Linux amd64; the [distribution build](#build-a-distribution) builds both architectures. Changes to the image or its build need `make check-core-container` in addition to the relevant source checks: it runs the official-client suite against the image with a read-only root filesystem and needs Linux Docker, a non-root user, and the [test database and pinned SDK](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#official-client-verification) of the service checks (`OAC_TEST_DATABASE_URL` naming an `oac_*_tests` database with the migrations applied, and `OAC_TEST_OFFICIAL_SDK_PYTHON`). ## Publish a version @@ -132,13 +135,13 @@ Distribution and Runtime archives use `pigz` level 6 with at most four compressi ### Container registry -Version releases and manual `build-` drafts publish Linux amd64 images as `ghcr.io/minimax-ai/openagentcore/:`, where `` is `core`, `web`, `runtime` or `ingress`. For example, `ghcr.io/minimax-ai/openagentcore/core:v1.2.3`. A draft uses the tag `build-`. PostgreSQL uses its upstream image and is not republished. The registry images are loaded from the release archives without rebuilding. Existing version tags are reused only when their image config digest matches the release; a different image stops publication. A stable release also moves each component's `latest` tag to that image. Prereleases and drafts leave `latest` unchanged. SemVer build metadata uses `_` in place of `+` in container tags; version strings longer than 128 characters cannot be published to GHCR. After the images are verified, the publisher uploads the single `compose.yaml` and its checksum list, rendered for that release. Compose pins the ingress image by its registry digest; initialization rejects an image whose build revision differs from the Compose revision. A draft Release stays unpublished. +Version releases and manual `build-` drafts publish `ghcr.io/minimax-ai/openagentcore/:`, where `` is `core`, `web`, `runtime` or `ingress`. Core, Web and ingress indexes contain Linux amd64 and arm64 images; Runtime contains Linux amd64. Platform images use `-` tags and are loaded from the release archives. Existing version tags must match the release images and platform set. The publisher verifies every version index before updating `latest` for a stable release; prereleases and drafts leave `latest` unchanged. PostgreSQL uses its upstream image. SemVer build metadata uses `_` in place of `+` in container tags; version strings are limited to 128 characters. After verifying the images, the publisher uploads the release's `compose.yaml` and checksum list. Compose pins ingress by its index digest, and initialization checks its build revision against the Compose revision. The combined build/publication job uses `GITHUB_TOKEN` with `packages: write`. On the first publication, GitHub creates each container package as private: a package administrator must change all four packages to **Public** in their package settings before users can pull anonymously. See [GitHub container visibility](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry). Verify an unauthenticated pull after changing visibility. Repository visibility alone does not make a new container package public. GHCR and GitHub Releases do not share a transaction. A failed release may leave some matching version tags in GHCR; preserve those images and follow the draft recovery procedure below using the original artifacts. Registry failures other than a missing manifest stop publication. The job summary records digest-pinned references. These images and the rendered Compose files still require the configuration, secrets and routing described in [Configuration](./configuration.md). -`install.sh` downloads the Compose files for the latest stable release, or the release named by `--version`, verifies their SHA-256 and starts that release. The [installation guide](./getting-started/install.md#install) covers its use. +The [installation guide](./getting-started/install.md#install) covers release selection and the platform launchers. Go check and build jobs share Go module and compiler-cache directories under `~/.oac/cache/`, keyed by runner OS and architecture, all Go module files, the check/build partition and the commit. Partitioned keys prevent concurrent jobs from saving different compiler subsets under one key. Release builds can seed their cache from backend checks as well as earlier release builds. An older cache only seeds downloads and compilation; every check still runs. Release jobs also cache npm package downloads and the pinned microsandbox archive, whose checksum is verified on every build. Actions cache visibility follows GitHub ref scoping; a tag-specific cache is not shared with other release tags. New keys are saved only after a successful job. @@ -179,7 +182,7 @@ The planner compares the PR event's tested merge commit with its verified first `.github/actionlint.yaml` selects hygiene and lint. Known workflow changes select their consumers: the CI review and actionlint workflows run hygiene and lint; native workflow changes add native checks; API acceptance workflow changes add API checks with container acceptance enabled; website workflow changes add website checks. The shared Node action selects every job that uses it plus lint. A new or unclassified workflow/action selects the full gate until its consumers are declared in the planner. Planner tests and CI measurement scripts run hygiene; changing the planner itself runs the full gate. -Compose template and Compose test changes select both `distribution` fixtures and the `compose` smoke job; Core, Web, shared Go packages and the image Dockerfiles also select the smoke job. Run `python3 scripts/compose-smoke.py` locally with Docker available to repeat it. The script uses a unique project, an automatically assigned loopback port and artifacts under `~/.oac/tests/`; it removes its containers and volumes on exit. CI also performs cleanup after a failed or interrupted smoke step. Diagnostics show container status without printing HTTP response bodies or sign-in keys. Core, Web and the ingress image are built from the checkout; Web serves a placeholder page instead of the console build. Build-time node metadata comes from the release pinned in `deploy/compose/smoke-pins.json`; the initialization container runs with networking disabled. This checks generic Compose behavior; it does not run a Dokploy/Coolify instance or execute a model. +Compose template and Compose test changes select both `distribution` fixtures and the `compose` smoke job; Core, Web, shared Go packages and the image Dockerfiles also select the smoke job. Run `python3 scripts/compose-smoke.py` locally with Docker available to repeat it. The script uses a unique project, an automatically assigned loopback port and artifacts under `~/.oac/tests/`; it removes its containers and volumes on exit. CI also performs cleanup after a failed or interrupted smoke step. Diagnostics show container status without printing HTTP response bodies or sign-in keys. Core, Web and the ingress image are built from the checkout; Web serves a placeholder page instead of the console build. Build-time node metadata comes from the release pinned in `deploy/compose/smoke-pins.json`; the initialization container runs with networking disabled. The smoke matrix runs on native Linux amd64 and arm64 runners; the native matrix builds and tests the shared Core installer on Linux, macOS and Windows. Go module and workspace inputs select backend, API (including the container), native and distribution checks. Each Node module owns its manifest and lockfile. Website dependencies select website checks; Web dependencies select Web and browser checks; example dependencies select example checks; shared TypeScript client dependencies select Web, browser and example checks; Claude adapter dependencies select Harness, native and distribution checks. Shared package-manager configuration selects all Node consumers. The root TypeScript configuration selects Web and example checks; the adapter TypeScript configuration selects Harness and native checks. Each selected set includes hygiene. Mixed changes accumulate their consumers, and every job reads the same plan instead of maintaining its own path list. For example, a notification-only PR skips database, browser and native jobs, while a notification plus Core change adds backend and API checks. diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 2ef6abe1b..79465aa1f 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -1,7 +1,7 @@ --- title: "配置参考" source: docs/configuration.md -source_hash: e697e320a2df0c130cb004f5e8bbda50deff6515ef7727bea066fc0d4f688b7d +source_hash: 8eeef9a9742c5fd8a0bf27a5a31870a77dec59a436518dbe9d34770527c021a2 --- Core 安装的每项设置都恰好只有一个归属位置。共有两类: @@ -11,7 +11,7 @@ Core 安装的每项设置都恰好只有一个归属位置。共有两类: | [进程设置](#process-settings-configjson) | 公共 URL、端口、日志、Harness、执行并发度、审计保留期、OAuth 来源、Runtime 历史记录导出 | 安装目录中的 `.env`(默认 `~/.oac/core`) | 编辑 `.env`,然后运行 `oac apply` | `oac apply` 会重新创建读取了这些已更改设置的服务 | | [运行时设置](#runtime-settings-web) | 沙箱后端和大小、节点、项目和密钥、默认模型、执行器凭据 | Core 的 PostgreSQL 数据库 | 在 Web 中修改,或使用 Core 密钥调用 Core API(`/core/v1`) | 保存时无需重启 Core;节点会异步准备 Runtime 变更 | -Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置,并在 **Startup settings** 下以只读方式显示 Core 加载的进程设置。机密信息存放在 [`data/secrets/`](#installation-directory) 中,每项仅保存一份。没有任何配置文件定义项目或 API 密钥。 +Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置,并在 **Startup settings** 下以只读方式显示 Core 加载的进程设置。机密信息存放在 [`secrets/`](#compose-installations) 中,每项仅保存一份。没有任何配置文件定义项目或 API 密钥。 ## 进程设置 {#process-settings-configjson} @@ -25,9 +25,8 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置 1. 它用你改过的 `.env` 运行 `oac-core check-config`。值无效时什么都不改。 2. 它运行 `docker compose up -d --wait`。Compose 只重新创建配置有变化的服务。 -3. 校验失败时,不会重新创建任何容器。重启会中断哪些操作,见[停止和重启](getting-started/operations.md#stop-and-restart)。 -`docker compose ps` 展示服务。域名状态在 `data/domain/status.json`。 +用 `docker compose ps` 检查服务。重启会中断哪些操作,见[停止和重启](getting-started/operations.md#stop-and-restart)。 ### 更改公共 URL {#changing-the-public-url} @@ -48,7 +47,7 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置 | Variable | Default | Meaning | | --- | --- | --- | | `OAC_PUBLIC_URL` | `http://localhost:8080` | 应用、节点、沙箱和自托管执行器使用的源地址。参阅[修改公开 URL](#changing-the-public-url) | -| `OAC_HOST` | `127.0.0.1` | `compose.yaml` 发布的 Web 绑定地址。`install.sh` 设置为 `0.0.0.0` | +| `OAC_HOST` | `127.0.0.1` | `compose.yaml` 发布的 Web 绑定地址。安装器设置为 `0.0.0.0` | | `OAC_WEB_PORT` | `8080` | Host port of Web | | `OAC_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` | | `OAC_LOG_FORMAT` | `auto` | `auto`, `text` or `json` | @@ -85,7 +84,7 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置 ### 默认模型 {#default-models} -在 **System** → **Default model configuration** 中设置默认值,或使用 `PUT /core/v1/harnesses/{harness}/model-configuration`。Core 使用 `secrets/credential.key` 加密提供商密钥,并且绝不返回这些密钥。[模型执行](../../contracts/agents-api/zh/model-execution.md#deployment-defaults) 定义了请求字段和替换规则,[优先级](../../contracts/agents-api/zh/model-execution.md#saved-defaults-and-precedence)说明了哪些 Session 使用默认值。 +在 **System** → **Default model configuration** 中设置默认值,或使用 `PUT /core/v1/harnesses/{harness}/model-configuration`。Core 使用 `secrets/core/credential.key` 加密提供商密钥,并且绝不返回这些密钥。[模型执行](../../contracts/agents-api/zh/model-execution.md#deployment-defaults) 定义了请求字段和替换规则,[优先级](../../contracts/agents-api/zh/model-execution.md#saved-defaults-and-precedence)说明了哪些 Session 使用默认值。 ## Compose 安装 {#compose-installations} @@ -104,7 +103,7 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置 初始化会准备该目录;应用服务以只读方式接收各自的机密目录。`docker compose exec web oac-web core-key` 把 Core 密钥打印到运维人员终端,不写入容器日志。数据库密码和凭据加密密钥绝不打印。 -`OAC_DATA_DIR` 选择该目录,默认是 Compose 文件旁的 `./data`。必须将该项目的定义和公共 URL 与该目录一同保留。仅删除机密目录不会重置安装;如果数据库已经存在,初始化会拒绝重新开始。Core 还会将安装 ID 与其数据库绑定。运行时设置仍存储在 [Core 的数据库](#runtime-settings-web)中。 +上述路径位于 Docker 命名卷 `_data` 中。所有宿主机平台都由 Docker 管理 Linux 文件权限;每个服务只挂载需要的子目录。数据卷必须和项目定义、公共 URL 一同保留。仅删除机密目录不会重置安装;数据库已存在时初始化会拒绝重建。Core 也会校验安装 ID 与数据库的绑定。运行时设置仍保存在 [Core 数据库](#runtime-settings-web)中。 ## Docker 节点配置 {#docker-node-configuration} @@ -122,37 +121,31 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置 ## 安装目录 {#installation-directory} -安装程序会创建安装目录,默认路径为 `~/.oac/core`,权限模式为 `0700`。机密文件为 `0600`。 +安装目录默认为 `~/.oac/core`(Windows 为 `$HOME/.oac/core`)。其中保存进程设置和原生管理命令;服务持久数据位于 [Compose 数据卷](#compose-installations)。 | 路径 | 内容 | 修改者 | | --- | --- | --- | -| `.env` | [进程设置](#process-settings-configjson)。由你编辑的文件 | 你,然后运行 `oac apply`;托管域名设置写入 `OAC_PUBLIC_URL` | -| `compose.yaml` | 发行版的服务定义。不要编辑 | 发行版 | -| `oac` | [管理命令](getting-started/operations.md#the-oac-command),从 Core 镜像复制 | 安装程序 | -| `data/secrets/web/core.key` | [Core 密钥](getting-started/operations.md#core-key) | `oac rotate-core-key` | -| `data/secrets/core/credential.key` | 加密 Core 在数据库中封存内容的密钥 | 无。必须与数据库一同保留 | -| `data/secrets/core/core-key-digests.json` | Core 密钥的 SHA-256 | `oac rotate-core-key` | -| `data/secrets/database/password` | PostgreSQL 密码 | 无。PostgreSQL 仅在创建数据库时读取 | -| `data/database/` | PostgreSQL 数据 | PostgreSQL | -| `data/node-payload/` | Web 在 `/node-install/` 提供的节点文件 | 初始化 | -| `data/state/` | 私有 Provider 状态,包括 E2B 回执 | Core | -| `.oac.lock` | 安装锁 | 会修改安装状态的 `oac` 命令 | - -Compose 项目名为 `oac-<10 hex digits>`。服务包括 `init`、`database`、`core` 和 `web`。Core 启动时执行数据库迁移。`web` 提供控制台并把 `/v1` 和 `/api/v1` 转发到 Core,是唯一发布端口(`OAC_WEB_PORT`)的服务。没有服务持有 Docker 套接字。除 Docker 存储外,不会向安装目录之外写入任何内容。 +| `.env` | 进程设置和固定的 Compose 项目名 | 用户修改后运行 `oac apply` | +| `compose.yaml`、`compose-sha256sums.txt` | 已校验的发行版服务定义 | 发行流程 | +| `oac`(Windows 为 `oac.exe`) | 原生管理命令 | 安装程序 | + +同级 `.lock` 目录用于同步操作并一直保留;`.staging` 保存尚未就位的安装文件。两者都不保存服务数据。Unix 上安装程序以 `0700` 创建私有目录,以 `0600` 创建配置文件。 + +Compose 项目名为 `oac-<10 hex digits>`,服务包括 `init`、`database`、`core` 和 `web`。Core 启动时执行数据库迁移。Web 提供控制台并把 `/v1`、`/api/v1` 转发到 Core,是唯一发布端口(`OAC_WEB_PORT`)的服务。没有服务持有 Docker 套接字。 ## 附录:没有安装程序时的 Core 环境 {#appendix-core-environment-without-the-installer} -Core 只读取其环境。Compose 把 `.env` 插值进服务环境。Compose 必须为 2.26.0 或更高版本。如果你自行运行 Core,请设置这些变量;见[服务指南](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md)。 +Core 读取进程环境。Compose 将 `.env` 插值到环境中,并把机密文件挂载到下表中的容器路径。直接运行 Core 时,将文件变量设为 Core 进程可读取的绝对路径;见[服务指南](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md)。 | 变量 | 设置来源 | | --- | --- | | `OAC_PUBLIC_URL` | `public_url`,或 Core 的回环源地址。Core 从中派生守护进程 WebSocket URL、自托管 `remote_url`、托管沙箱地址和部署的只读 `core_url`,绝不从请求标头派生。未设置时,Core 不运行 Runtime 网关,也不执行任何 Session | | `OAC_ADDR` | 安装程序在容器中设置为 `:8091`。独立启动的 Core 在未设置或为空时,默认使用 `127.0.0.1:8091` | | `OAC_DATABASE_URL` | 该安装不含密码的 PostgreSQL URL,并将 `core.database_pool` 作为 `pool_*` 查询参数附加到其中 | -| `OAC_DATABASE_PASSWORD_FILE` | `data/secrets/database/password`。此时 URL 不得包含密码 | -| `OAC_CREDENTIAL_KEY_FILE` | `data/secrets/core/credential.key` | -| `OAC_CORE_KEY_DIGESTS_FILE` | `data/secrets/core/core-key-digests.json`:一个包含 Core 密钥 SHA-256 的 JSON 数组 | -| `OAC_INSTALLATION_ID_FILE` | `data/secrets/core/installation.id`:安装 ID,采用规范 UUID 格式。它会启用沙箱部署和节点路由,并要求设置 `OAC_PUBLIC_URL` 和 `OAC_CORE_KEY_DIGESTS_FILE`。如果 ID 与数据库记录的 ID 不一致,Core 会拒绝它,因此必须将两者一同保留 | +| `OAC_DATABASE_PASSWORD_FILE` | `/run/database/password`。此时 URL 不得包含密码 | +| `OAC_CREDENTIAL_KEY_FILE` | `/run/oac/credential.key` | +| `OAC_CORE_KEY_DIGESTS_FILE` | `/run/oac/core-key-digests.json`:一个包含 Core 密钥 SHA-256 的 JSON 数组 | +| `OAC_INSTALLATION_ID_FILE` | `/run/oac/installation.id`:安装 ID,采用规范 UUID 格式。它会启用沙箱部署和节点路由,并要求设置 `OAC_PUBLIC_URL` 和 `OAC_CORE_KEY_DIGESTS_FILE`。如果 ID 与数据库记录的 ID 不一致,Core 会拒绝它,因此必须将两者一同保留 | | `OAC_EXECUTION_CONCURRENCY`、`OAC_DEFAULT_HARNESS`、`OAC_HARNESSES`、`OAC_WRITE_AUDIT_RETENTION`、`OAC_OAUTH_TRUSTED_ORIGINS` | 对应的[进程设置](#settings)。`oac-core check-config` 会在不启动 Core 的情况下校验它们 | | `OAC_HISTORY_SETTINGS_FILE` | 可选的 Runtime 历史文件。敏感;安装报告只说明它是否已设置 | | `OAC_LOG_LEVEL`、`OAC_LOG_FORMAT`、`OAC_LOG_ADD_SOURCE` | `log.*`;Web 也读取这三个设置 | @@ -166,7 +159,7 @@ Core 会记录所加载文件的路径,但绝不记录环境变量的值或文 ## 附录:没有安装程序时的 Web 环境 {#appendix-web-environment-without-the-installer} -Compose 为 Web 设置这些变量。仅在不使用 Compose 运行控制台时才自行设置。该安装的机密信息中,Web 只收到 `data/secrets/web/core.key`。 +Compose 为 Web 设置这些变量。仅在不使用 Compose 运行控制台时才自行设置。Compose 把数据卷的 `secrets/web/` 挂载到 `/run/oac`,并设置 `OAC_WEB_CORE_KEY_FILE=/run/oac/core.key`。 | 变量 | 默认值 | 含义 | | --- | --- | --- | diff --git a/docs/zh/getting-started/install-options.md b/docs/zh/getting-started/install-options.md index b0faa317d..fe55a0610 100644 --- a/docs/zh/getting-started/install-options.md +++ b/docs/zh/getting-started/install-options.md @@ -1,10 +1,10 @@ --- title: "安装选项与高级部署" source: docs/getting-started/install-options.md -source_hash: e063d7dcd615ef5d6ba8b43f1cbe5a11c75325925605f376161f50b9383297c0 +source_hash: 2e7717f76a46694af3c1ef33a56b195c0a321f54fd7318b488179b4b4c61f336 --- -[默认安装](install.md)无需任何选项。使用本页可以在现有反向代理后运行,或者在无法访问互联网时进行安装。 +[默认安装](install.md)无需任何选项。本页介绍安装选项、Compose 部署和反向代理配置。 向下载的脚本传递选项: @@ -12,13 +12,11 @@ source_hash: e063d7dcd615ef5d6ba8b43f1cbe5a11c75325925605f376161f50b9383297c0 ./install.sh --public-url https://core.example ``` -使用单行命令时,请将选项追加在 `bash -s --` 之后。发布包下载器还接受 `--version TAG` 来选择已发布的版本;否则会选择最新的稳定版本。它会在解压前验证捆绑包的 SHA-256,并保留已验证的捆绑包以供[修复](operations.md#installation-version-policy)。 - -安装程序会打印每个阶段,然后打印地址、登录信息和后续步骤的摘要。设置 `NO_COLOR=1` 可禁用彩色输出。任一步骤失败都会停止安装,并且不会显示成功消息。 +Windows 可下载 `install.ps1`,然后使用相同参数,例如 `& ./install.ps1 --public-url https://core.example`。Unix 单行命令的参数追加在 `bash -s --` 后。`--version TAG` 选择已发布的版本;默认选择最新稳定版,并校验原生命令和 Compose 文件的 SHA-256。 ## Docker Compose 与托管平台 {#docker-compose-and-hosting-platforms} -在 Linux amd64 上使用发行版中的 `compose.yaml` 和 Docker Compose 2.26 或更高版本。发行流程会在 [Compose 模板](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/compose/compose.yaml)中固定初始化镜像及源码版本。Core 和 Web 使用 `latest` 镜像,PostgreSQL 使用 `postgres:16-alpine`。它会启动 PostgreSQL、Core 和 Web。Web 把 `/v1` 和 `/api/v1` 转发到 Core。数据通过目录 bind mount 挂载。一次性初始化服务会在该目录中生成随机机密信息并准备节点安装程序;Core 启动时执行数据库迁移。[Compose 配置](../configuration.md#compose-installations)负责管理各项设置和数据目录。 +在任一[支持的 Core 主机](install.md#prerequisites)上使用发行版中的 `compose.yaml`。发行流程会在 [Compose 模板](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/compose/compose.yaml)中固定初始化镜像及源码版本。Core 和 Web 使用 `latest` 镜像,PostgreSQL 使用 `postgres:16-alpine`。它会启动 PostgreSQL、Core 和 Web。Web 把 `/v1` 和 `/api/v1` 转发到 Core。数据保存在 Docker 命名卷中。一次性初始化服务会在该目录中生成随机机密信息并准备节点安装程序;Core 启动时执行数据库迁移。[Compose 配置](../configuration.md#compose-installations)负责管理各项设置和数据目录。 进行本地试用时,请将发行版的 `compose.yaml` 下载到一个空目录,然后运行: @@ -27,7 +25,7 @@ docker compose up -d --wait --wait-timeout 900 docker compose exec web oac-web core-key ``` -`oac-web core-key` 会将生成的 Core 密钥打印到终端,而不会将其写入容器日志。打开 `http://localhost:8080` 并使用该密钥登录。所有安装机密信息都会自动生成;重启时请保留同一个 Compose 项目及其数据目录。 +`oac-web core-key` 会将生成的 Core 密钥打印到终端,而不会将其写入容器日志。打开 `http://localhost:8080` 并使用该密钥登录。所有安装机密信息都会自动生成;重启时请保留同一个 Compose 项目及其数据卷。 初始化镜像除初始化命令外,仅包含较小的节点安装元数据。首次启动会验证并复制这些元数据,无需下载控制归档或访问 GitHub Releases。后续启动会验证已保存的文件。容器镜像仍需拉取。首次初始化中断后可以重新运行;如果现有数据库缺少安装机密信息,初始化会被拒绝。 @@ -45,7 +43,7 @@ docker compose exec web oac-web core-key 登录后,使用 [Nodes](nodes.md)选择沙箱后端并添加节点。Compose 堆栈部署控制平面;执行机器仍需单独部署。 -使用相同的文件和环境运行 `docker compose stop` 以停止服务。停止服务后,备份数据目录。请遵循[安装版本策略](operations.md#installation-version-policy):使用不同发布版本时,需要创建新的 Compose 项目并使用全新的数据目录。 +使用相同的文件和环境运行 `docker compose stop` 以停止服务。停止服务后,备份数据卷。请遵循[安装版本策略](operations.md#installation-version-policy):使用不同发布版本时,需要创建新的 Compose 项目并使用全新的数据卷。 ## 进程设置 {#process-settings} @@ -64,7 +62,7 @@ docker compose exec web oac-web core-key | 选项 | 用途 | | --- | --- | -| `--install-dir DIR` | 绝对安装目录;默认为 `~/.oac/core`。新安装要求目录为空或不存在,或者包含一个[从未启动过的安装](install.md#install) | +| `--install-dir DIR` | 绝对安装目录;默认为 `~/.oac/core`。新安装使用空目录或不存在的目录;[重试](install.md#install)使用已有安装目录 | 只要使用不同的安装目录和端口,多个安装就可以共用一台机器。需要更多安装时,请使用不同的 IP 地址或共享反向代理。每个安装都有自己的数据库、Core 密钥和节点。 @@ -137,4 +135,4 @@ http://:8443 { ## 离线主机 {#offline-hosts} -本安装程序不支持从离线捆绑包安装。它从发布版本下载 Compose 文件和容器镜像。 +Core 安装需要访问 GitHub Releases 和容器注册表,以下载发行文件和镜像。 diff --git a/docs/zh/getting-started/install.md b/docs/zh/getting-started/install.md index da6dbe184..656ada863 100644 --- a/docs/zh/getting-started/install.md +++ b/docs/zh/getting-started/install.md @@ -1,10 +1,10 @@ --- title: "安装 Core 和 Web" source: docs/getting-started/install.md -source_hash: d034ab565564ae9447c6e8b31b3d13d8868cae738f623a7471ba56ba517d67c8 +source_hash: 1366a76858085e015480c48c18891987397cdc1da0d859182da3685ed77355be --- -一条命令即可在 Linux 主机上安装 Core、Web 控制台和 PostgreSQL。用 Core 密钥登录 Web,设置默认模型并签发 Project API 密钥。应用使用这些密钥调用 Core。Session 在你添加的节点上的沙箱中运行,也可以在 E2B 上运行。 +一条命令即可在 Linux、macOS 或 Windows 上安装 Core、Web 控制台和 PostgreSQL。用 Core 密钥登录 Web,设置默认模型并签发 Project API 密钥。应用使用这些密钥调用 Core。Session 在你添加的节点上的沙箱中运行,也可以在 E2B 上运行。 1. [检查前置条件](#prerequisites)。 2. [运行安装程序](#install)。 @@ -18,21 +18,29 @@ source_hash: d034ab565564ae9447c6e8b31b3d13d8868cae738f623a7471ba56ba517d67c8 ## 前置条件 {#prerequisites} -- Linux amd64 和 curl。 -- Docker Engine 和 Docker Compose 2.26.0 或更高版本(`docker compose version`)。 +- Linux amd64/arm64 或 macOS Intel/Apple Silicon,需安装 curl;Windows x64 需 PowerShell。 +- Docker Engine 26 或更高版本,以及 Docker Compose 2.26.0 或更高版本。macOS 和 Windows 使用已启动的 Docker Desktop,并选择 Linux 容器。 - 能运行 `docker` 并向自己的主目录写入文件的账号。普通用户和 root 均可;安装程序不会调用 sudo。 - Web 的 8080 端口空闲。参阅[端口](install-options.md#ports)。Docker 必须能发布该端口;安装程序不会修改主机策略。 - 本机以外的访问要求 `OAC_PUBLIC_URL` 就是浏览器、节点和执行器使用的地址。可以先在本机登录。 -Core 主机不需要 KVM;运行 microsandbox 的节点需要。 +沙箱节点运行在 Linux amd64 上。在 macOS 或 Windows 上部署 Core 时,可连接 Linux 节点,或使用 E2B。 ## 安装 {#install} +Linux 和 macOS: + ```sh curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash ``` -如果主机默认路由的源地址是私有网络地址,安装程序把公开 URL 设为 `http://:8080`,同一网络的机器即可打开 Web 并添加节点;否则只有本机可以访问。反向代理已经提供这台主机时,传入它的 HTTPS 地址: +Windows PowerShell: + +```powershell +irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.ps1 | iex +``` + +绑定所有 IPv4 地址时,安装器会使用默认路由的私网地址;如果没有可用私网地址,则使用 `http://localhost:8080`。也可以通过 `--public-url` 指定可访问的源地址;如果已有反向代理,就使用它的 HTTPS 地址: ```sh curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash -s -- --public-url https://core.example @@ -40,13 +48,13 @@ curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/ 脚本下载该发布版的 Compose 文件,校验 SHA-256,然后: -1. 检查 Linux amd64、Docker Compose 2.26 或更高版本,以及将要发布的端口是否空闲; -2. 创建[安装目录](../configuration.md#installation-directory) `~/.oac/core`,写入 `.env`,并从 Core 镜像复制 `oac` 命令; +1. 检查 Docker 使用 Linux 容器、符合版本要求,并确认所选端口空闲; +2. 准备[安装目录](../configuration.md#installation-directory) `~/.oac/core`,写入 `.env` 和原生 `oac` 命令(Windows 为 `oac.exe`); 3. 用 Docker Compose 启动服务。Web 在 8080 端口提供控制台,并把 `/v1`、`/api/v1` 和 `/docs` 转发到 Core。Core 和 PostgreSQL 不发布端口。 -安装程序不保存沙箱后端,不添加节点,不创建 Project 或密钥,也不发起模型请求。完成后输出控制台地址和 Core 密钥。 +安装器会打印控制台地址和 Core 密钥。登录后,在 Web 中配置执行资源并创建 Project。 -安装目录就位前,先完成下载和配置检查。之后的失败会保留配置与数据,并显示服务日志。修复报错后,重新执行同一命令,或指定安装目录即可继续: +安装目录就位前,先完成下载和配置检查。之后的失败会保留配置与数据,并报告失败步骤;在安装目录运行 `docker compose logs --tail 100` 查看日志。修复报错后,重新执行同一命令,或指定安装目录即可继续: ```bash curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash -s -- --install-dir "$HOME/.oac/core" @@ -65,6 +73,8 @@ curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/ ~/.oac/core/oac core-key --show ``` + Windows 使用 `& "$HOME/.oac/core/oac.exe" core-key --show`。各平台的命令参数相同。 + ## 配置公开地址 {#configure-the-domain-and-https} 应用、节点和沙箱通过同一个地址访问 Core,即公开 URL。局域网上用 HTTP 即可。对外暴露时,在前面放反向代理,并把公开 URL 设为它提供的 HTTPS 源地址。E2B 客户机从互联网访问 Core,因此需要非回环的公开 URL。 diff --git a/docs/zh/getting-started/operations.md b/docs/zh/getting-started/operations.md index 9a2a87cc0..545f99a7b 100644 --- a/docs/zh/getting-started/operations.md +++ b/docs/zh/getting-started/operations.md @@ -1,14 +1,14 @@ --- title: "管理你的安装" source: docs/getting-started/operations.md -source_hash: 8b5ec8893b3e844a2c4173a4122cee17bf08350cd1ea2d5128e9a6c72f590907 +source_hash: f63789e0f3982b9f6633381d3c93441e5185b04398541b95c3e1d0505de588eb --- 安装运维人员负责 Core 主机、存储和可用性。节点主机运行各自的服务;参阅[节点](nodes.md)。设置见[配置参考](../configuration.md)。 ## oac 命令 {#the-oac-command} -每个安装目录中都有自己的管理命令,无需发行包或 root: +每个安装目录都有自己的原生管理命令:Unix 使用 `oac`,Windows 使用 `oac.exe`。命令需要 Docker 访问权限,不需要 root: ```sh docker compose -f ~/.oac/core/compose.yaml ps @@ -20,11 +20,11 @@ docker compose -f ~/.oac/core/compose.yaml ps | `docker compose start` | 启动服务 | | `docker compose stop` | 停止服务。保留数据、节点和沙箱 | | `oac apply` | 先运行 `oac-core check-config`,再执行 `docker compose up -d --wait`。校验失败时不改动任何服务 | -| `oac core-key [--show]` | 打印 Core 密钥路径;加上 `--show` 时打印密钥本身 | +| `oac core-key [--show]` | 指出 Core 密钥在数据卷中的位置;加上 `--show` 时打印密钥本身 | | `oac rotate-core-key` | 替换 Core 密钥并重启 Core 和 Web | | `docker compose down` | 移除容器。数据保留;要删除数据,请[卸载](#uninstall) | -第二个安装使用自己的目录,例如 `~/.oac/second`。 +示例使用默认安装目录。Windows 上使用 `& "$HOME/.oac/core/oac.exe"` 调用管理命令,后接相同参数。使用自定义安装目录时,替换各命令中的路径。 ## 服务健康状态 {#service-health} @@ -65,13 +65,13 @@ Web 重启(包括 `oac apply` 引起的重启)会让所有控制台用户退 ## Core 密钥 {#core-key} -每个安装有一个管理员凭据,即 Core 密钥。安装程序在 `data/secrets/web/core.key` 生成以 `oac_admin_` 为前缀、后接 64 个随机小写十六进制字符的密钥。用 `oac core-key --show` 读取;该文件属于容器用户。Core 密钥: +每个安装有一个管理员凭据,即 Core 密钥。安装程序在 `secrets/web/core.key` 生成以 `oac_admin_` 为前缀、后接 64 个随机小写十六进制字符的密钥。用 `oac core-key --show` 读取;该文件属于容器用户。Core 密钥: - 用于登录 Web。浏览器获得 HttpOnly 会话 cookie,不持有密钥; - 通过 `Authorization: Bearer ` 授权 Core API(`/core/v1`)请求; - 不授权 Agents API(`/v1`)。应用使用 Project API 密钥,后者也不能调用 `/core/v1`。 -请保密。Web 读取 `data/secrets/web/core.key`。Core 只读取 `data/secrets/core/core-key-digests.json` 中的 SHA-256。Core 密钥至少 32 字符且不含空白。Web 限制失败登录。 +请保密。Web 读取 `secrets/web/core.key`。Core 只读取 `secrets/core/core-key-digests.json` 中的 SHA-256。Core 密钥至少 32 字符且不含空白。Web 限制失败登录。 ### 用脚本调用 Core API {#script-the-core-api} @@ -105,7 +105,7 @@ core() ( # core METHOD PATH [JSON body] ~/.oac/core/oac rotate-core-key ``` -它把新密钥写入 `data/secrets/web/core.key`,重新生成 `data/secrets/core/core-key-digests.json`,并重启 Core 和 Web。Core 重启后旧密钥立即失效,所有控制台会话结束:重新登录并更新脚本。 +它在初始化容器中更新数据卷内的 `secrets/web/core.key` 和 `secrets/core/core-key-digests.json`,然后重启 Core 与 Web。Core 重启后旧密钥立即失效,控制台会话也会结束;请重新登录并更新脚本。 ## Project 和 API 密钥 {#projects-and-api-keys} @@ -125,38 +125,34 @@ Core 记录每次公开资源写入所使用的密钥;历史保留策略为 [` 一起备份这些内容;恢复时全部需要: -- PostgreSQL 卷 `_database`。其中包含 Project、密钥摘要、节点、默认模型、加密凭据和全部执行历史(含大对象)。逻辑备份: +- Docker 卷 `_data`,包括其中的 `database/`、`secrets/` 和 `state/` 目录。其中包含 Project、密钥摘要、节点、默认模型、加密凭据和全部执行历史(含大对象)。逻辑备份: ```sh docker compose -f "$HOME/.oac/core/compose.yaml" exec -T database \ pg_dump -U agents_api agents_api > oac-backup.sql ``` -- 安装目录,尤其是 `data/`。`data/secrets/core/credential.key` 必须与数据库一起保留,否则无法解密存储的凭据。 +- 安装目录中的 `.env`、`compose.yaml` 和管理命令。数据卷内的 `secrets/core/credential.key` 必须与数据库一起保留,否则存储的凭据无法解密。 -先 `docker compose stop`,打包安装目录,再 `docker compose start`。 - 各节点主机上的状态目录 `/var/lib/oac-node/.oac/nodes//` 及提供商存储:Docker 卷或 microsandbox 存储。恢复方法见[节点主机故障时](nodes.md#when-a-node-host-fails)。 -- 安装所使用的发行包,用于修复同一版本。 -不要通过清理 Docker 卷或删除原生 Harness 历史来让重试成功。Session 已删除不证明所有提供商资源已回收。 +运行 `docker compose stop`,导出完整数据卷并归档安装目录,再运行 `docker compose start`。Docker Desktop 的 **Volumes** 页面支持导出数据卷。SQL 转储不包含加密密钥和 Provider 状态。 ## 卸载 {#uninstall} ```sh cd ~/.oac/core -docker compose down --remove-orphans -docker compose run --rm --no-deps --entrypoint find init /data -mindepth 1 -delete -docker compose down --rmi all +docker compose down --volumes --remove-orphans --rmi all cd && rm -rf ~/.oac/core ``` -`data/` 归容器所有,因此由 `init` 镜像删除其内容;随后 `down --rmi all` 移除镜像,`rm` 删除安装目录。只有确定要删数据时才执行这些命令。 +`down --volumes` 会删除安装数据卷。之后删除安装目录;Windows 使用 `Remove-Item -Recurse "$HOME/.oac/core"`。 全部数据随之删除:Project 和 API 密钥、Session 历史、存储的凭据和 Core 密钥。要保留数据,请用 `docker compose stop` 停止安装,或先[备份](#back-up)。 卸载不停止沙箱:节点沙箱在节点继续运行,E2B 沙箱在 E2B 继续运行并计费。Core 仍运行时,归档它们的 Session,或[重置部署](nodes.md#change-the-sandbox-configuration)并等待完成;命令展示 Core 正在使用的沙箱数量。 -其他主机上的节点继续运行。按常规方式卸载时,先在 Web 移除,见[移除节点](nodes.md#remove-a-node)。安装目录删除后,它们的 Core 已不存在:在各节点主机使用当时发布版的 `node-install.pyz`,执行带 `--force` 的节点卸载命令。安装 ID 在 `data/secrets/core/installation.id`。 +其他主机上的节点继续运行。按常规方式卸载时,先在 Web 移除,见[移除节点](nodes.md#remove-a-node)。安装目录删除后,它们的 Core 已不存在:在各节点主机使用当时发布版的 `node-install.pyz`,执行带 `--force` 的节点卸载命令。安装 ID 在数据卷的 `secrets/core/installation.id` 中。 ## 安装版本策略 {#installation-version-policy} @@ -166,7 +162,7 @@ cd && rm -rf ~/.oac/core 中断的安装可以[沿用已保存配置继续](install.md#install)。与本安装无关的非空目录会被拒绝。 -安装程序和修改状态的 `oac` 命令持有 `.oac.lock`。安装程序准备目录时还持有同级的 `.install.lock`。其他命令持有锁时,等待其结束后重试。不要删除锁文件来绕过忙碌安装。 +安装程序和修改状态的 `oac` 命令共用[安装锁](../configuration.md#installation-directory)。其他命令正在运行时,等待其结束后重试。 ## 问题排查 {#troubleshooting} diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index 3b553a3da..aebb1a172 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,14 +1,17 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: 679fb7cf8af9fc6aa2adfb9b04e1766497a32937fd184162614ae813707cb90b +source_hash: 4f9fd38af5afd47e21589160930347f6693dd4786f56fb2dff2abd4853b3f72f --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请使用 [安装指南](getting-started/install.md)。安装器代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装器](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 ## 构建分发包 {#build-a-distribution} -分发包是从同一个提交构建的一组相互匹配的 Linux amd64 发布资源:控制归档(安装器、`oac` 命令,以及 Core、Web、ingress 和 PostgreSQL 镜像)、作为独立文件的 Runtime 镜像和节点构件,以及原生安装器。 +分发包是从同一个提交构建的一组相互匹配的发布资源:控制归档(安装器、`oac` 命令,以及 Core、Web、ingress 和 PostgreSQL 镜像)、作为独立文件的 Runtime 镜像和节点构件,以及原生安装器。 + +Core、Web 和 ingress 镜像发布为经过校验的 Linux amd64/arm64 多架构索引。arm64 控制归档包含这三个镜像;Node、托管 Runtime 和离线包使用 Linux amd64。发行构建使用 QEMU 执行 ARM 镜像步骤,包括 E2B helper。宿主机 `oac` 从同一份实现构建为 Linux amd64/arm64、macOS amd64/arm64 和 Windows amd64 二进制;启动脚本只选择、校验并运行它们。所有版本索引校验通过后才更新浮动标签。 + 请在 Linux x86_64 上构建,所需环境包括与 Debian 12 兼容的 glibc、Docker、`go.mod` 中指定的 Go 版本、C 编译器(microsandbox 辅助程序使用 CGO 构建)、Node、pnpm、Python 3.9 或更高版本、curl、tar、pigz 和 sha256sum。源代码必须保持干净并已提交。请先准备固定版本的 Codex 包和 MiniMax Code 配套程序,然后执行构建: @@ -98,7 +101,7 @@ docker build --platform linux/amd64 -t oac-runtime:mcode "${OAC_DEV_HOME:-$HOME/ make build-e2b-provider ``` -Docker 使用固定版本的 CPython 和 Debian 12 镜像构建 Linux amd64 辅助程序。Python 依赖闭包(including PyInstaller)在 `services/core/tools/e2b-provider/requirements.lock` 中按哈希锁定;不需要 E2B 账户密钥。要使用其他输出目录,请设置 `E2B_PROVIDER_BUILD_DIR`。构建结果完全由辅助程序源代码、`LICENSE` 和构建脚本决定,因此会按它们的哈希缓存在 `~/.oac/cache/e2b-provider/` 下,仅在它们变化时重新构建。输出为 `oac-e2b-provider-linux-amd64.tar.gz` 及其 `.sha256`;解压后会得到 `oac-e2b-provider/`,其中包含可执行文件、`_internal/`、`licenses/`、`requirements.lock` 和 `manifest.json`。Core 镜像使用该目录树;主机需要兼容的 glibc 和 CA 证书,而不需要 Python。 +Docker 使用固定版本的 CPython 和 Debian 12 镜像按 `GOARCH=amd64`(默认)或 `GOARCH=arm64` 构建 Linux 辅助程序。Python 依赖闭包(including PyInstaller)在 `services/core/tools/e2b-provider/requirements.lock` 中按哈希锁定;不需要 E2B 账户密钥。要使用其他输出目录,请设置 `E2B_PROVIDER_BUILD_DIR`。构建结果完全由辅助程序源代码、`LICENSE` 和构建脚本决定,因此会按它们的哈希缓存在 `~/.oac/cache/e2b-provider/` 下,仅在它们变化时重新构建。输出为 `oac-e2b-provider-linux-.tar.gz` 及其 `.sha256`;解压后会得到 `oac-e2b-provider/`,其中包含可执行文件、`_internal/`、`licenses/`、`requirements.lock` 和 `manifest.json`。Core 镜像使用该目录树;主机需要兼容的 glibc 和 CA 证书,而不需要 Python。 **microsandbox 辅助程序。** 仅支持 Linux,并且需要 C 编译器: @@ -113,9 +116,9 @@ make check-microsandbox-provider ### 独立 Core 构建 {#standalone-core-builds} -`make build-core` 会将 `oac-core`、`oac-core-device`、`oac-core-environment-key` 和 `oac-node` 构建到 `${OAC_DEV_HOME:-$HOME/.oac}/build/oac-core`(`OAC_DEV_CORE_BUILD_DIR` 可选择其他绝对目录)。构建过程仅将 `scripts/build-core.sh` 中列出的源文件集(Core 服务、其契约、所需的共享软件包以及根 Go 模块文件)复制到临时上下文,并使用禁用 CGO、只读模块和裁剪路径的方式构建。它不需要 Node、Docker 或其他应用程序。Core 新增共享依赖时,请将该软件包加入列表;绝不能复制整个仓库来使其完成编译。 +`make build-core` 会将 `oac-core`、`oac-core-device`、`oac-core-environment-key`、`oac-node` 和 `oac` 构建到 `${OAC_DEV_HOME:-$HOME/.oac}/build/oac-core`(`OAC_DEV_CORE_BUILD_DIR` 可选择其他绝对目录)。构建过程仅将 `scripts/build-core.sh` 中列出的源文件集(Core 服务、其契约、所需的共享软件包以及根 Go 模块文件)复制到临时上下文,并使用禁用 CGO、只读模块和裁剪路径的方式构建。它不需要 Node、Docker 或其他应用程序。Core 新增共享依赖时,请将该软件包加入列表;绝不能复制整个仓库来使其完成编译。 -`make docker-build-core` 会根据这五个命令和 E2B 辅助程序构建 `oac-core:dev` 镜像(`OAC_DEV_CORE_IMAGE` 可选择其他名称)。基础镜像是通过摘要固定的 `debian:bookworm-slim`,包含 CA 证书以及辅助程序所需的 glibc 运行时;默认用户的 UID/GID 为 65532,Core 监听 `:8091`。该镜像仅支持 Linux amd64,并且不会推送到注册表。对镜像或其构建进行更改时,除了相关的源代码检查外,还必须运行 `make check-core-container`:它会在只读根文件系统上针对该镜像运行官方客户端测试套件,并且需要 Linux Docker、非 root 用户,以及服务检查中的[测试数据库和固定版本 SDK](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#official-client-verification)(`OAC_TEST_DATABASE_URL` 指向一个已应用迁移的 `oac_*_tests` 数据库,并设置 `OAC_TEST_OFFICIAL_SDK_PYTHON`)。 +`make docker-build-core` 会根据这五个命令和 E2B 辅助程序构建 `oac-core:dev` 镜像(`OAC_DEV_CORE_IMAGE` 可选择其他名称)。基础镜像是通过摘要固定的 `debian:bookworm-slim`,包含 CA 证书以及辅助程序所需的 glibc 运行时;默认用户的 UID/GID 为 65532,Core 监听 `:8091`。此本地构建目标生成 Linux amd64 镜像;[分发构建](#build-a-distribution)生成两种架构的镜像。对镜像或其构建进行更改时,除了相关的源代码检查外,还必须运行 `make check-core-container`:它会在只读根文件系统上针对该镜像运行官方客户端测试套件,并且需要 Linux Docker、非 root 用户,以及服务检查中的[测试数据库和固定版本 SDK](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#official-client-verification)(`OAC_TEST_DATABASE_URL` 指向一个已应用迁移的 `oac_*_tests` 数据库,并设置 `OAC_TEST_OFFICIAL_SDK_PYTHON`)。 ## 发布版本 {#publish-a-version} @@ -134,13 +137,13 @@ git push origin v1.2.3 ### 容器注册表 {#container-registry} -版本发布和手动的 `build-` 草稿都会将 Linux amd64 镜像发布为 `ghcr.io/minimax-ai/openagentcore/:`,其中 `` 为 `core`、`web`、`runtime` 或 `ingress`。例如,`ghcr.io/minimax-ai/openagentcore/core:v1.2.3`。草稿使用标签 `build-`。PostgreSQL 使用其上游镜像,不会重新发布。注册表镜像从发布归档中加载,不会重新构建。仅当现有版本标签的镜像配置摘要与本次发布相同时才复用该标签;如果镜像不同,则停止发布。稳定版还会把每个组件的 `latest` 标签移到该镜像。预发布和草稿不会改动 `latest`。SemVer 构建元数据在容器标签中使用 `_` 代替 `+`;长度超过 128 个字符的版本字符串无法发布到 GHCR。镜像验证之后,发布器会上传为该发行版渲染的单个 `compose.yaml` 及其校验和清单。Compose 使用注册表摘要固定 ingress 镜像;如果镜像构建版本与 Compose 版本不同,初始化会拒绝运行。草稿 Release 保持未发布。 +版本发布和手动 `build-` 草稿使用 `ghcr.io/minimax-ai/openagentcore/:`,其中 `` 为 `core`、`web`、`runtime` 或 `ingress`。Core、Web 和 ingress 索引包含 Linux amd64 和 arm64 镜像,Runtime 包含 Linux amd64。各平台镜像使用 `-` 标签,从发行归档加载。已有版本标签必须与发行镜像及平台集合一致。发布器校验全部版本索引后,才为稳定版更新 `latest`;预发布版和草稿保持 `latest` 不变。PostgreSQL 使用上游镜像。容器标签中的 SemVer 构建元数据用 `_` 替换 `+`,版本字符串上限为 128 个字符。镜像校验后,发布器上传该版本的 `compose.yaml` 和校验和清单。Compose 用索引摘要固定 ingress,初始化时检查其构建版本与 Compose 版本一致。 合并的构建/发布作业使用具有 `packages: write` 权限的 `GITHUB_TOKEN`。首次发布时,GitHub 会将每个容器软件包创建为私有:软件包管理员必须先在各自的软件包设置中将全部四个软件包改为 **Public**,用户才能匿名拉取。请参阅 [GitHub container visibility](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)。更改可见性后,请验证未认证拉取。仅更改仓库可见性并不会使新的容器软件包变为公开。 GHCR 和 GitHub Releases 不共享事务。发布失败后,GHCR 中可能仍会保留一些匹配的版本标签;请保留这些镜像,并使用原始构件按照下文的草稿恢复流程操作。除清单缺失以外,注册表故障都会停止发布。作业摘要会记录按摘要固定的引用。这些镜像和渲染后的 Compose 文件仍需要[配置](configuration.md)中描述的配置、机密和路由。 -`install.sh` 会下载最新稳定版的 Compose 文件,或 `--version` 指定的发布版,校验 SHA-256 后启动该发布版。用法见[安装指南](getting-started/install.md#install)。 +[安装指南](getting-started/install.md#install)介绍版本选择和各平台的启动命令。 Go 检查和构建作业共享 `~/.oac/cache/` 下的 Go 模块和编译器缓存目录,缓存键由运行器 OS 和架构、全部 Go 模块文件、检查/构建分区以及提交确定。分区键可防止并发作业在同一个键下保存不同的编译器子集。发布构建既可以使用后端检查的缓存,也可以使用更早发布构建的缓存。较旧的缓存只会为下载和编译提供初始内容;每项检查仍会运行。发布作业还会缓存 npm 软件包下载内容和固定版本的 microsandbox 归档,并在每次构建时验证后者的校验和。Actions 缓存可见性遵循 GitHub ref 的作用域;特定标签的缓存不会与其他发布标签共享。只有作业成功后才会保存新键。 @@ -181,7 +184,9 @@ gh workflow run core-release --repo MiniMax-AI/OpenAgentCore --ref main \ `.github/actionlint.yaml` 会选择 hygiene 和 lint。已知工作流变更会选择其使用方:CI review 和 actionlint 工作流运行 hygiene 和 lint;原生工作流变更会添加原生检查;API 验收工作流变更会添加启用容器验收的 API 检查;网站工作流变更会添加网站检查。共享 Node 操作会选择使用它的每个作业以及 lint。新工作流或未分类的工作流/操作会选择完整门禁,直至在计划器中声明其使用方。计划器测试和 CI 测量脚本运行 hygiene;更改计划器本身会运行完整门禁。 -Compose 模板和 Compose 测试发生变更时,会同时选择 `distribution` 固定数据和 `compose` 冒烟作业;Core、Web、共享 Go 软件包和镜像 Dockerfile 的变更也会选择冒烟作业。安装 Docker 后,可在本地运行 `python3 scripts/compose-smoke.py` 重复该测试。该脚本使用唯一的项目、自动分配的回环端口,并将在 `~/.oac/tests/` 下生成构件;退出时移除其容器和数据卷。CI 还会在冒烟步骤失败或中断后执行清理。诊断信息会显示容器状态,但不会打印 HTTP 响应正文或登录密钥。Core、Web 和 ingress 镜像都从当前检出构建;Web 提供占位页面而不是控制台构建。构建时的节点元数据来自 `deploy/compose/smoke-pins.json` 固定的发布版本;初始化容器禁用网络运行。该测试检查通用 Compose 行为;它不会运行 Dokploy/Coolify 实例,也不会执行模型。 +Core 安装器在 Linux、macOS 和 Windows 原生 CI 中构建并测试。Compose 冒烟测试分别使用 Linux amd64 和 arm64 原生 runner。 + +Compose 模板和 Compose 测试发生变更时,会同时选择 `distribution` 固定数据和 `compose` 冒烟作业;Core、Web、共享 Go 软件包和镜像 Dockerfile 的变更也会选择冒烟作业。安装 Docker 后,可在本地运行 `python3 scripts/compose-smoke.py` 重复该测试。该脚本使用唯一的项目、自动分配的回环端口,并将在 `~/.oac/tests/` 下生成构件;退出时移除其容器和数据卷。CI 还会在冒烟步骤失败或中断后执行清理。诊断信息会显示容器状态,但不会打印 HTTP 响应正文或登录密钥。Core、Web 和 ingress 镜像都从当前检出构建;Web 提供占位页面而不是控制台构建。构建时的节点元数据来自 `deploy/compose/smoke-pins.json` 固定的发布版本;初始化容器禁用网络运行。 Go 模块和工作区输入会选择后端、API(包括容器)、原生和分发检查。每个 Node 模块都拥有自己的清单和锁文件。网站依赖项会选择网站检查;Web 依赖项会选择 Web 和浏览器检查;示例依赖项会选择示例检查;共享 TypeScript 客户端依赖项会选择 Web、浏览器和示例检查;Claude 适配器依赖项会选择 Harness、原生和分发检查。共享包管理器配置会选择所有 Node 使用方。根 TypeScript 配置会选择 Web 和示例检查;适配器 TypeScript 配置会选择 Harness 和原生检查。每个所选集合都包含 hygiene。混合变更会累加其使用方,并且每个作业都读取同一计划,而不是维护各自的路径列表。例如,仅修改通知的 PR 会跳过数据库、浏览器和原生作业,而同时修改通知和 Core 的 PR 会添加后端和 API 检查。 diff --git a/scripts/build-core-distribution.sh b/scripts/build-core-distribution.sh index e6635f418..e2db4d6dd 100755 --- a/scripts/build-core-distribution.sh +++ b/scripts/build-core-distribution.sh @@ -44,7 +44,7 @@ case "$build_network" in default|host|none) ;; *) printf 'CORE_DISTRIBUTION_BUILD_NETWORK must be default, host, or none\n' >&2; exit 1 ;; esac -# Build one Linux amd64 image and write the ID the local image store gives it to +# Build one selected Linux architecture image and write the ID the local image store gives it to # $stage/NAME.id. BuildKit's --iidfile reports the config digest, which is the # image ID only in Docker's classic store; the containerd store (Docker 29's # default) uses the manifest digest and cannot resolve the config digest. The @@ -58,14 +58,14 @@ build_image() { # declaration persists the operator's network configuration in the images. # The metadata file omits build provenance, so it does not record them either. BUILDX_METADATA_PROVENANCE=disabled docker build --network "$build_network" \ - --platform linux/amd64 --provenance=false --metadata-file "$stage/$name.build.json" \ + --platform "linux/$GOARCH" --provenance=false --metadata-file "$stage/$name.build.json" \ --label "org.opencontainers.image.revision=$revision" \ --build-arg HTTP_PROXY --build-arg HTTPS_PROXY --build-arg ALL_PROXY --build-arg NO_PROXY \ --build-arg "http_proxy=${http_proxy:-${HTTP_PROXY:-}}" \ --build-arg "https_proxy=${https_proxy:-${HTTPS_PROXY:-}}" \ --build-arg "all_proxy=${all_proxy:-${ALL_PROXY:-}}" \ --build-arg "no_proxy=${no_proxy:-${NO_PROXY:-}}" "$@" - python3 scripts/core-distribution-manifest.py built-image "$stage/$name.build.json" > "$stage/$name.id" + python3 scripts/core-distribution-manifest.py built-image "$stage/$name.build.json" "$GOARCH" > "$stage/$name.id" } require_clean_source() { @@ -255,3 +255,28 @@ fi mv "$stage/artifacts/"* "$output_dir/" if [[ -d "$stage/native-artifacts" ]]; then mv "$stage/native-artifacts/"* "$output_dir/"; fi mv "$bundle" "$output_dir/" + +# Core, Web and initialization also run natively in ARM64 Linux containers. +# Node and hosted Runtime payloads above remain linux/amd64. +export GOARCH=arm64 +arm_bundle="$stage/oac-$revision-linux-arm64" +mkdir -p "$arm_bundle/images" +OAC_DEV_BUILD_REVISION="$revision" scripts/build-core-image-context.sh "$stage/core" +OAC_DEV_WEB_BUILD_DIR="$stage/web" scripts/build-web.sh +cp "$stage/core/bin/oac" "$stage/ingress/oac" +for name in core web ingress; do + build_image "$name" "$stage/$name" + docker image save --output "$arm_bundle/images/$name.tar" "$(cat "$stage/$name.id")" +done +python3 scripts/core-distribution-manifest.py control-archive "$arm_bundle" "$stage" "$revision" arm64 +mv "$arm_bundle.tar.gz" "$arm_bundle.tar.gz.sha256" "$output_dir/" + +# The launchers and operator commands use this same portable implementation. +for platform in linux-amd64 linux-arm64 darwin-amd64 darwin-arm64 windows-amd64; do + extension="" + if [[ "$platform" == windows-* ]]; then extension=.exe; fi + asset="oac-$platform$extension" + GOOS="${platform%-*}" GOARCH="${platform#*-}" CGO_ENABLED=0 go build -mod=readonly -trimpath \ + -ldflags "-X main.buildRevision=$revision" -o "$output_dir/$asset" ./services/core/cmd/oac + (cd "$output_dir" && sha256sum "$asset" > "$asset.sha256") +done diff --git a/scripts/build-core-image-context.sh b/scripts/build-core-image-context.sh index 76d73eb32..fa8d3f0ee 100755 --- a/scripts/build-core-image-context.sh +++ b/scripts/build-core-image-context.sh @@ -13,8 +13,8 @@ if [[ "$context" != /* ]]; then fi mkdir -p "$context/bin" "$context/e2b" "$context/native-installers" -GOOS=linux GOARCH=amd64 OAC_DEV_CORE_BUILD_DIR="$context/bin" "$repo_root/scripts/build-core.sh" +GOOS=linux GOARCH="${GOARCH:-amd64}" OAC_DEV_CORE_BUILD_DIR="$context/bin" "$repo_root/scripts/build-core.sh" E2B_PROVIDER_BUILD_DIR="$context/e2b-build" "$repo_root/scripts/build-e2b-provider.sh" -tar -xzf "$context/e2b-build/oac-e2b-provider-linux-amd64.tar.gz" --strip-components=1 -C "$context/e2b" +tar -xzf "$context/e2b-build/oac-e2b-provider-linux-${GOARCH:-amd64}.tar.gz" --strip-components=1 -C "$context/e2b" rm -rf "$context/e2b-build" cp "$repo_root/deploy/distribution/Dockerfile" "$context/Dockerfile" diff --git a/scripts/build-e2b-provider.sh b/scripts/build-e2b-provider.sh index cd91fb5ec..b17f5e071 100755 --- a/scripts/build-e2b-provider.sh +++ b/scripts/build-e2b-provider.sh @@ -8,23 +8,25 @@ case "$output_dir" in *) printf 'E2B_PROVIDER_BUILD_DIR must be absolute\n' >&2; exit 1 ;; esac mkdir -p "$output_dir" -archive=oac-e2b-provider-linux-amd64.tar.gz +architecture="${GOARCH:-amd64}" +case "$architecture" in amd64|arm64) ;; *) echo "Unsupported E2B architecture" >&2; exit 1;; esac +archive="oac-e2b-provider-linux-$architecture.tar.gz" # The helper is a pure function of these files, so a build is reused by their hash. inputs="$(cd "$repo_root" && { find services/core/tools/e2b-provider -type f ! -path '*/__pycache__/*' -print0 | sort -z | xargs -0 sha256sum sha256sum LICENSE scripts/build-e2b-provider.sh } | sha256sum | cut -c1-64)" -cache="${OAC_DEV_HOME:-$HOME/.oac}/cache/e2b-provider/$inputs" +cache="${OAC_DEV_HOME:-$HOME/.oac}/cache/e2b-provider/$architecture-$inputs" if [[ ! -f "$cache/$archive.sha256" ]]; then mkdir -p "${cache%/*}" build="$(mktemp -d "${cache%/*}/.build.XXXXXX")" - image="oac-e2b-provider-build:${inputs:0:12}" + image="oac-e2b-provider-build:$architecture-${inputs:0:12}" # Proxy values are build-only operator settings; no account key is needed. - docker build --platform linux/amd64 --build-arg HTTP_PROXY --build-arg HTTPS_PROXY \ + docker build --platform "linux/$architecture" --build-arg HTTP_PROXY --build-arg HTTPS_PROXY \ --build-arg ALL_PROXY --build-arg NO_PROXY \ --file "$repo_root/services/core/tools/e2b-provider/Build.Dockerfile" \ --tag "$image" "$repo_root/services/core/tools/e2b-provider" - docker run --rm --platform linux/amd64 \ + docker run --rm --platform "linux/$architecture" \ --env HTTP_PROXY --env HTTPS_PROXY --env ALL_PROXY --env NO_PROXY \ --mount "type=bind,src=$repo_root,dst=/source,readonly" \ --mount "type=bind,src=$build,dst=/output" "$image" diff --git a/scripts/ci_plan.py b/scripts/ci_plan.py index 7b45e8bbf..b8356b4fc 100644 --- a/scripts/ci_plan.py +++ b/scripts/ci_plan.py @@ -58,6 +58,8 @@ (("docs/", "contracts/"), ("",), ("website",)), (("website/",), (*WEB, ".vue", ".md"), ("website",)), (("services/core/",), CORE, ("backend", "api", "compose")), + (("services/core/cmd/oac/",), GO, ("native", "distribution")), + (("deploy/install.sh", "deploy/install.ps1", "deploy/test_install.ps1"), (".sh", ".ps1"), ("native", "distribution")), (("services/core/internal/nativeinstaller/",), GO, ("native", "distribution")), (("services/core/deploy/", "services/core/tools/"), CORE, ("distribution",)), (("apps/daemon/",), GO, ("backend", "native")), diff --git a/scripts/ci_plan_test.py b/scripts/ci_plan_test.py index 23def3412..d5ee84204 100644 --- a/scripts/ci_plan_test.py +++ b/scripts/ci_plan_test.py @@ -20,7 +20,7 @@ def test_published_documents_also_build_the_website(self): self.assertEqual(self.jobs("README.md"), {"hygiene"}) def test_installer_does_not_download_a_browser_or_run_database_tests(self): - self.assertEqual(self.jobs("deploy/install.sh", "deploy/node/node_payload.py"), {"hygiene", "distribution"}) + self.assertEqual(self.jobs("deploy/install.sh", "deploy/node/node_payload.py"), {"hygiene", "distribution", "native"}) def test_compose_inputs_select_live_and_fixture_checks_without_image_builds(self): for path in ("deploy/compose/compose.yaml", "deploy/compose/https.yaml", "deploy/compose/dokploy.toml", @@ -165,13 +165,13 @@ def test_every_job_has_a_plan_condition(self): def test_mixed_changes_accumulate(self): self.assertEqual(self.jobs("docs/maintainers.md", "deploy/install.sh", "apps/web/src/app.tsx"), - {"hygiene", "distribution", "web", "web-acceptance", "website"}) + {"hygiene", "distribution", "native", "web", "web-acceptance", "website"}) def test_installer_pr_300_replay(self): self.assertEqual(self.jobs( "deploy/install.sh", "deploy/README.md", "deploy/node/node_install.py", "deploy/node/install_display.py", "deploy/node/node_payload.py", - "docs/getting-started/install.md"), {"hygiene", "distribution", "website"}) + "docs/getting-started/install.md"), {"hygiene", "distribution", "native", "website"}) def test_workflow_graph_cannot_silently_omit_or_add_a_gate_dependency(self): workflow = (Path(__file__).resolve().parents[1] / ".github/workflows/check.yml").read_text().split("jobs:\n", 1)[1] diff --git a/scripts/compose-smoke.py b/scripts/compose-smoke.py index 2a6b27fa8..f2c4c9946 100644 --- a/scripts/compose-smoke.py +++ b/scripts/compose-smoke.py @@ -55,7 +55,7 @@ def prepare_pinned_payload(destination): def build_images(directory, tag): revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip() protocol = re.search(r'const Version = "([^"]+)"', (ROOT / 'internal/agentdaemon/proto/version.go').read_text()).group(1) - go_env = {**os.environ, 'CGO_ENABLED': '0', 'GOOS': 'linux', 'GOARCH': 'amd64'} + go_env = {**os.environ, 'CGO_ENABLED': '0', 'GOOS': 'linux', 'GOARCH': os.environ.get('GOARCH', 'amd64')} def go_build(package, output, build_revision=revision): output.parent.mkdir(parents=True, exist_ok=True) @@ -88,7 +88,7 @@ def go_build(package, output, build_revision=revision): images = {} for name, context in contexts.items(): images[name] = f'oac-smoke/{name}:{tag}' - subprocess.run(['docker', 'build', '-q', '--platform', 'linux/amd64', '-t', images[name], str(context)], + subprocess.run(['docker', 'build', '-q', '--platform', 'linux/' + go_env['GOARCH'], '-t', images[name], str(context)], check=True, stdout=subprocess.DEVNULL) return images @@ -113,7 +113,7 @@ def main(): images = build_images(directory, project.removeprefix('oac-smoke-')) override.write_text(json.dumps({'services': {'init': {'network_mode': 'none'}}})) - env = {**os.environ, 'COMPOSE_PROGRESS': 'plain', 'OAC_DATA_DIR': str(data), + env = {**os.environ, 'COMPOSE_PROGRESS': 'plain', 'OAC_HOST': '127.0.0.1', 'OAC_WEB_PORT': '0', **{'OAC_IMAGE_' + name.upper(): image for name, image in images.items()}} env.pop('OAC_PUBLIC_URL', None) @@ -212,7 +212,18 @@ def terminate(_signum, _frame): assert any(p['id'] == project_data['id'] for p in get('/core/v1/projects')['data']), 'Project was lost' assert get('/v1/files/' + uploaded['id'], headers=api)['bytes'] == len(content), 'Uploaded file metadata was lost' assert 'Bundled node installation metadata verified' not in private_logs(key, project_key), 'Completed initialization recopied metadata' - print('PASS: startup, origin validation, sign-in, API, upload, node installer and persistent installation', flush=True) + compose('run', '--rm', '--no-deps', 'init', '/usr/local/bin/oac', 'rotate-volume-key') + compose('restart', 'core', 'web') + compose('up', '-d', '--wait', '--wait-timeout', '120', timeout=180) + rotated = compose('exec', '-T', 'web', '/usr/local/bin/oac-web', 'core-key').decode().strip() + assert rotated != key, 'Core key was not rotated' + browser = client() + request('/console/auth/login', {'core_key': rotated}) + compose('down') + compose('up', '-d', '--wait', '--wait-timeout', '120', timeout=180) + assert compose('exec', '-T', 'web', '/usr/local/bin/oac-web', 'core-key').decode().strip() == rotated, 'Rotated key was not retained' + private_logs(key, rotated, project_key) + print('PASS: startup, origin validation, sign-in, API, upload, node installer, key rotation and persistent installation', flush=True) except BaseException: # Service status identifies failed containers without dumping secret-bearing logs. status = subprocess.run(command + ['ps', '--all'], env=env, capture_output=True, timeout=30) @@ -220,9 +231,6 @@ def terminate(_signum, _frame): raise finally: compose('down', '--volumes', '--remove-orphans', timeout=60) - # Match the host installer's cleanup without requiring tools in scratch init. - compose('run', '--rm', '--no-deps', '--volume', str(data) + ':/data', - '--entrypoint', 'find', 'database', '/data', '-mindepth', '1', '-delete') subprocess.run(['docker', 'image', 'rm', '-f', *images.values()], capture_output=True, timeout=60) diff --git a/scripts/core-distribution-manifest.py b/scripts/core-distribution-manifest.py index c00f363b1..3529fb8fe 100644 --- a/scripts/core-distribution-manifest.py +++ b/scripts/core-distribution-manifest.py @@ -70,26 +70,26 @@ def sha256(path): return digest.hexdigest() -def verify_image(image): +def verify_image(image, architecture="amd64"): if not DIGEST.fullmatch(image): raise ValueError("Distribution image inputs must be immutable sha256 image IDs") details = json.loads(subprocess.check_output(["docker", "image", "inspect", image], text=True))[0] - if details["Id"] != image or details["Os"] != "linux" or details["Architecture"] != "amd64": - raise ValueError("Distribution images must be the selected Linux amd64 image") + if details["Id"] != image or details["Os"] != "linux" or details["Architecture"] != architecture: + raise ValueError("Distribution images must match the selected Linux architecture") return details -def built_image(metadata_file): +def built_image(metadata_file, architecture="amd64"): """Print the local store ID of the image one BuildKit build just produced.""" metadata = json.loads(pathlib.Path(metadata_file).read_text()) # The classic store names an image by its config digest, the containerd store # by its manifest digest; the other value never resolves to itself there. config = metadata.get("containerimage.config.digest") manifest = metadata.get("containerimage.digest", config) - print(resolve_image(config, manifest)) + print(resolve_image(config, manifest, architecture)) -def resolve_image(config, manifest): +def resolve_image(config, manifest, architecture="amd64"): """Resolve the archive identities in either supported Docker image store.""" if not all(isinstance(value, str) and DIGEST.fullmatch(value) for value in (config, manifest)): raise ValueError("Build metadata lacks valid image digests") @@ -101,11 +101,11 @@ def resolve_image(config, manifest): resolved.append(candidate) if len(resolved) != 1: raise ValueError("The local image store does not identify the built image by exactly one of its digests") - verify_image(resolved[0]) + verify_image(resolved[0], architecture) return resolved[0] -def image_identities(archive, build_id): +def image_identities(archive, build_id, architecture="amd64"): """Bind both Docker store identities to one exported Linux amd64 image.""" if not DIGEST.fullmatch(build_id): raise ValueError("Missing immutable distribution image identity") @@ -161,7 +161,7 @@ def blob(descriptor, parse=False): config_descriptor = image.get("config", {}) config = blob(config_descriptor, parse=True) config_digest = config_descriptor["digest"] - if config.get("os") != "linux" or config.get("architecture") != "amd64": + if config.get("os") != "linux" or config.get("architecture") != architecture: raise ValueError("Image archive contains an unexpected platform") for layer in image.get("layers", []): blob(layer) @@ -170,6 +170,21 @@ def blob(descriptor, parse=False): return config_digest, manifest_digest +def control_archive(bundle, stage, revision, architecture): + bundle, stage = pathlib.Path(bundle), pathlib.Path(stage) + manifest = {"source_commit": revision, "platform": "linux/" + architecture, + "images": {}, "image_manifest_digests": {}} + for name in ("core", "web", "ingress"): + config, digest = image_identities(bundle / "images" / (name + ".tar"), + (stage / (name + ".id")).read_text().strip(), architecture) + manifest["images"][name], manifest["image_manifest_digests"][name] = config, digest + (bundle / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") + target = bundle.with_name(bundle.name + ".tar.gz") + with tarfile.open(target, "w:gz") as output: + output.add(bundle, arcname=bundle.name) + target.with_name(target.name + ".sha256").write_text(sha256(target) + " " + target.name + "\n") + + def verify_runtime(image, daemon, source): details = verify_image(image) source = pathlib.Path(source) @@ -583,7 +598,7 @@ def check(image, link): if __name__ == "__main__": - commands = {"extract-runtime": extract_runtime, "verify-runtime": verify_runtime, "verify-image": verify_image, + commands = {"control-archive": control_archive, "extract-runtime": extract_runtime, "verify-runtime": verify_runtime, "verify-image": verify_image, "built-image": built_image, "node-payload": node_payload, "manifest": manifest, "archive": archive, "bootstraps": bootstraps, "release-base": release_base, "docs": docs, "native-catalog": native_catalog, "native-offline": native_offline} try: diff --git a/scripts/core-distribution-manifest.test.py b/scripts/core-distribution-manifest.test.py index 4719f05a1..cb4b78ce8 100644 --- a/scripts/core-distribution-manifest.test.py +++ b/scripts/core-distribution-manifest.test.py @@ -143,6 +143,13 @@ def test_oci_manifest_identity_is_distinct_from_docker_config_identity(self): digest, name = line.split(" ", 1) self.assertEqual(digest, distribution.sha256(self.bundle / name)) + def test_arm_archive_uses_explicit_platform_validation(self): + path = self.stage / "arm.tar" + config, digest = image_archive(path, "core", architecture="arm64") + self.assertEqual(distribution.image_identities(path, config, "arm64"), (config, digest)) + with self.assertRaisesRegex(ValueError, "unexpected platform"): + distribution.image_identities(path, config, "amd64") + def test_built_image_records_the_id_each_docker_store_resolves(self): config, manifest, other = ("sha256:" + digit * 64 for digit in "123") metadata = self.stage / "build.json" @@ -163,7 +170,7 @@ def test_built_image_records_the_id_each_docker_store_resolves(self): mock.patch("builtins.print") as output: if expected.startswith("sha256:"): distribution.built_image(metadata) - verify.assert_called_once_with(expected) + verify.assert_called_once_with(expected, "amd64") output.assert_called_once_with(expected) else: with self.assertRaisesRegex(ValueError, expected): diff --git a/scripts/publish-core-release.py b/scripts/publish-core-release.py index 7f27e0306..8ae61caa6 100644 --- a/scripts/publish-core-release.py +++ b/scripts/publish-core-release.py @@ -97,7 +97,7 @@ def registry_image(reference): if manifest is not None and "manifests" in manifest: descriptors = manifest["manifests"] if len(descriptors) != 1: - raise ValueError("Expected one Linux amd64 registry image: " + reference) + raise ValueError("Expected one platform registry image: " + reference) digest = descriptors[0]["digest"] if not distribution.DIGEST.fullmatch(digest): raise ValueError("Invalid registry image descriptor") @@ -109,87 +109,112 @@ def registry_image(reference): def publish_images(assets, repository, revision, tag, floating_latest=False): - """Load the checked release archives; never rebuild or replace another image.""" + """Verify both architectures before publishing immutable platform tags and indexes.""" image_tag = tag.replace("+", "_") - if not re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}", image_tag): + if not re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,120}", image_tag): raise ValueError("Release version exceeds the container tag format") - stem = "oac-" + revision + "-linux-amd64" - # Extract named regular members only, never archive-controlled paths. with tempfile.TemporaryDirectory(prefix="oac-ghcr-") as directory: directory = pathlib.Path(directory) - with tarfile.open(assets / (stem + ".tar.gz"), "r:gz") as archive: - manifest = json.load(archive.extractfile(stem + "/manifest.json")) - if manifest["source_commit"] != revision or manifest["platform"] != "linux/amd64": - raise ValueError("Registry images do not match the release") - for name in IMAGE_NAMES: - if name == "runtime": - continue - member = archive.getmember(stem + "/images/" + name + ".tar") - if not member.isfile(): - raise ValueError("Expected a regular image archive") - with archive.extractfile(member) as source, (directory / (name + ".tar")).open("wb") as target: - shutil.copyfileobj(source, target) - runtime = manifest["artifacts"]["images/runtime.tar.gz"] - filename = runtime["filename"] - if pathlib.Path(filename).name != filename: - raise ValueError("Invalid Runtime asset filename") - runtime_path = assets / filename - if runtime_path.is_symlink() or distribution.sha256(runtime_path) != runtime["sha256"]: - raise ValueError("Runtime image checksum mismatch") - with gzip.open(runtime_path, "rb") as source, (directory / "runtime.tar").open("wb") as target: - shutil.copyfileobj(source, target) - for name in IMAGE_NAMES: - expected = (manifest["images"][name], manifest["image_manifest_digests"][name]) - if distribution.image_identities(directory / (name + ".tar"), expected[0]) != expected: - raise ValueError("Release image identity mismatch: " + name) + entries = {} + for architecture in ("amd64", "arm64"): + stem = "oac-" + revision + "-linux-" + architecture + with tarfile.open(assets / (stem + ".tar.gz"), "r:gz") as archive: + manifest = json.load(archive.extractfile(stem + "/manifest.json")) + if manifest["source_commit"] != revision or manifest["platform"] != "linux/" + architecture: + raise ValueError("Registry images do not match the release") + names = IMAGE_NAMES if architecture == "amd64" else ("core", "web", "ingress") + for name in names: + path = directory / (name + "-" + architecture + ".tar") + if name == "runtime": + artifact = manifest["artifacts"]["images/runtime.tar.gz"] + filename = artifact["filename"] + if pathlib.Path(filename).name != filename: + raise ValueError("Invalid Runtime asset filename") + compressed = assets / filename + if compressed.is_symlink() or distribution.sha256(compressed) != artifact["sha256"]: + raise ValueError("Runtime image checksum mismatch") + with gzip.open(compressed, "rb") as source, path.open("wb") as target: + shutil.copyfileobj(source, target) + else: + member = archive.getmember(stem + "/images/" + name + ".tar") + if not member.isfile(): + raise ValueError("Expected a regular image archive") + with archive.extractfile(member) as source, path.open("wb") as target: + shutil.copyfileobj(source, target) + expected = (manifest["images"][name], manifest["image_manifest_digests"][name]) + if distribution.image_identities(path, expected[0], architecture) != expected: + raise ValueError("Release image identity mismatch: " + name) + entries[name, architecture] = (path, *expected) references = {} - # Validate every local image and every existing tag before the first push. - for name in IMAGE_NAMES: - path = directory / (name + ".tar") + for (name, architecture), (path, config, digest) in entries.items(): subprocess.run(["docker", "load", "--input", str(path)], check=True) - config = manifest["images"][name] - local = distribution.resolve_image(config, manifest["image_manifest_digests"][name]) - reference = "ghcr.io/" + repository.lower() + "/" + name + ":" + image_tag - remote, selected = registry_image(reference) + local = distribution.resolve_image(config, digest, architecture) + reference = "ghcr.io/" + repository.lower() + "/" + name + ":" + image_tag + "-" + architecture + remote, _ = registry_image(reference) if remote is not None and remote.get("config", {}).get("digest") != config: raise ValueError("Registry tag already names a different image: " + reference) - references[name] = (reference, config, local, remote) + references[name, architecture] = (reference, config, local, remote) + # Version indexes are immutable too. Check every existing one before pushing. + bases = {name: "ghcr.io/" + repository.lower() + "/" + name + ":" + image_tag for name in IMAGE_NAMES} + for name, reference in bases.items(): + current = registry_manifest(reference) + if current is not None: + verify_index(reference, current, {arch: entry[1] for (n, arch), entry in entries.items() if n == name}) + def push_image(item): - name, (reference, config, local, remote) = item - print("Publishing registry image " + name, flush=True) + key, (reference, config, local, remote) = item if remote is None: subprocess.run(["docker", "tag", local, reference], check=True) subprocess.run(["docker", "push", reference], check=True) remote, selected = registry_image(reference) if remote is None or remote.get("config", {}).get("digest") != config: raise ValueError("Registry image verification failed: " + reference) - # Inspect the registry's descriptor, not the local Docker image ID. details = json.loads(subprocess.check_output( ["docker", "manifest", "inspect", "--verbose", selected], text=True)) digest = details["Descriptor"]["digest"] if not distribution.DIGEST.fullmatch(digest): raise ValueError("Invalid registry manifest digest") - print("Verified registry image " + name, flush=True) - return name, {"tag": reference, "digest": reference.rsplit(":", 1)[0] + "@" + digest} - result = dict(sorted(parallel_each(push_image, references.items()))) + return key, reference.rsplit(":", 1)[0] + "@" + digest + platforms = dict(parallel_each(push_image, references.items())) + result = {} + for name, reference in bases.items(): + sources = [value for (n, _), value in platforms.items() if n == name] + expected = {arch: entry[1] for (n, arch), entry in entries.items() if n == name} + if registry_manifest(reference) is None: + subprocess.run(["docker", "buildx", "imagetools", "create", "--tag", reference, *sources], check=True) + verify_index(reference, registry_manifest(reference), expected) + digest = subprocess.check_output(["docker", "buildx", "imagetools", "inspect", reference, + "--format", "{{.Manifest.Digest}}"], text=True).strip() + if not distribution.DIGEST.fullmatch(digest): + raise ValueError("Invalid registry index digest") + pinned = reference.rsplit(":", 1)[0] + "@" + digest + result[name] = {"tag": reference, "digest": pinned} + # Advance floating tags only after all version indexes are verified. if floating_latest: - def push_latest(item): - name, (reference, config, local, remote) = item - latest = reference.rsplit(":", 1)[0] + ":latest" - current, _selected = registry_image(latest) - if current is not None and current.get("config", {}).get("digest") == config: - return name, latest - print("Publishing registry image " + name + ":latest", flush=True) - subprocess.run(["docker", "tag", local, latest], check=True) - subprocess.run(["docker", "push", latest], check=True) - current, selected = registry_image(latest) - if current is None or current.get("config", {}).get("digest") != config: - raise ValueError("Registry image verification failed: " + latest) - return name, latest - parallel_each(push_latest, references.items()) + for name, entry in result.items(): + latest = bases[name].rsplit(":", 1)[0] + ":latest" + expected = {arch: value[1] for (n, arch), value in entries.items() if n == name} + subprocess.run(["docker", "buildx", "imagetools", "create", "--tag", latest, entry["digest"]], check=True) + verify_index(latest, registry_manifest(latest), expected) return result +def verify_index(reference, index, expected): + if not index or len(index.get("manifests", [])) != len(expected): + raise ValueError("Registry index has unexpected platforms: " + reference) + actual = {} + for descriptor in index["manifests"]: + platform = descriptor.get("platform", {}) + architecture = platform.get("architecture") + digest = descriptor.get("digest", "") + if platform.get("os") != "linux" or architecture in actual or not distribution.DIGEST.fullmatch(digest): + raise ValueError("Invalid registry platform descriptor") + child = registry_manifest(reference.rsplit(":", 1)[0] + "@" + digest) + actual[architecture] = (child or {}).get("config", {}).get("digest") + if actual != expected: + raise ValueError("Registry index names a different image: " + reference) + + def publish(assets, repository, revision, tag, mode): if not REPOSITORY.fullmatch(repository): raise ValueError("Expected an owner/repository") @@ -209,7 +234,8 @@ def publish(assets, repository, revision, tag, mode): # The builder validates the manifest and Runtime assets. Verify archives again # after the Actions artifact transfer between jobs. stem = "oac-" + revision + "-linux-amd64" - archives = [assets / (stem + ".tar.gz"), assets / "install.sh"] + archives = [assets / (stem + ".tar.gz"), assets / ("oac-" + revision + "-linux-arm64.tar.gz"), assets / "install.sh", assets / "install.ps1"] + archives.extend(assets / name for name in ("oac-linux-amd64", "oac-linux-arm64", "oac-darwin-amd64", "oac-darwin-arm64", "oac-windows-amd64.exe")) if mode == "publish" or (assets / (stem + "-offline.tar.gz")).exists(): archives.append(assets / (stem + "-offline.tar.gz")) for archive in archives: @@ -244,7 +270,7 @@ def publish(assets, repository, revision, tag, mode): release = api(repository, "releases", "--method", "POST", "-f", "tag_name=" + tag, "-f", "target_commitish=" + revision, "-f", "name=OpenAgentCore " + tag, - "-f", "body=Linux amd64 distribution from commit " + revision + ".", + "-f", "body=Core for Linux, macOS and Windows; Linux amd64 Node distribution from commit " + revision + ".", "-F", "draft=true", "-F", "prerelease=" + str(prerelease).lower()) verify_draft(release, tag, revision) release_id = release["id"] diff --git a/scripts/publish-core-release.test.py b/scripts/publish-core-release.test.py index 29ff5258b..175bc9cd6 100644 --- a/scripts/publish-core-release.test.py +++ b/scripts/publish-core-release.test.py @@ -29,7 +29,7 @@ def setUp(self): self.assets = pathlib.Path(self.temp.name) self.revision = "a" * 40 self.stem = "oac-" + self.revision + "-linux-amd64" - for name in (self.stem + ".tar.gz", self.stem + "-offline.tar.gz", "install.sh"): + for name in (self.stem + ".tar.gz", self.stem + "-offline.tar.gz", "install.sh", "install.ps1", "oac-" + self.revision + "-linux-arm64.tar.gz", "oac-linux-amd64", "oac-linux-arm64", "oac-darwin-amd64", "oac-darwin-arm64", "oac-windows-amd64.exe"): (self.assets / name).write_bytes(b"archive fixture") (self.assets / (name + ".sha256")).write_text( hashlib.sha256(b"archive fixture").hexdigest() + " " + name + "\n") @@ -68,7 +68,7 @@ def test_draft_publishes_images_and_stays_unpublished(self): self.publish(tag="build-" + self.revision, mode="draft") self.images.assert_called_once() self.assertTrue(self.release["draft"]) - self.assertEqual(len(self.release["assets"]), 14) + self.assertEqual(len(self.release["assets"]), 28) def test_missing_native_asset_refuses_release_creation(self): (self.assets / f"oac-native-{self.revision}-windows-amd64.tar.gz").unlink() @@ -142,7 +142,7 @@ def response(repo, endpoint, *args): return result if endpoint == "releases/7": self.assertEqual(active, 0) - self.assertIn(len(self.release["assets"]), (12, 14)) + self.assertIn(len(self.release["assets"]), (26, 28)) return self.response(repo, endpoint, *args) self.api.side_effect = response self.publish() @@ -153,7 +153,7 @@ def test_version_tag_publishes_complete_fixed_id(self): self.publish() self.assertFalse(self.release["draft"]) self.assertFalse(self.release["prerelease"]) - self.assertEqual(len(self.release["assets"]), 14) + self.assertEqual(len(self.release["assets"]), 28) self.assertEqual({a["name"] for a in self.release["assets"] if a["name"].endswith(".yaml")}, {"compose.yaml"}) self.assertEqual(self.api.call_args.args[1:], ("releases/7", "--method", "PATCH", "-F", "draft=false")) @@ -343,142 +343,112 @@ def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.assets = pathlib.Path(self.temp.name) - self.revision = "a" * 40 - self.config = "sha256:" + "b" * 64 - self.digest = "sha256:" + "c" * 64 - runtime = self.assets / "runtime.tar.gz" - runtime.write_bytes(gzip.compress(b"runtime")) - self.manifest = { - "source_commit": self.revision, "platform": "linux/amd64", - "images": dict.fromkeys(publisher.IMAGE_NAMES, self.config), - "image_manifest_digests": dict.fromkeys(publisher.IMAGE_NAMES, self.digest), - "artifacts": {"images/runtime.tar.gz": { - "filename": runtime.name, "sha256": publisher.distribution.sha256(runtime)}}} - with tarfile.open(self.assets / ("oac-" + self.revision + "-linux-amd64.tar.gz"), "w:gz") as archive: - for name, data in [("manifest.json", json.dumps(self.manifest).encode())] + [ - ("images/" + name + ".tar", b"image") for name in ("core", "web", "ingress")]: - member = tarfile.TarInfo("oac-" + self.revision + "-linux-amd64/" + name) - member.size = len(data) - archive.addfile(member, io.BytesIO(data)) - stack = contextlib.ExitStack() - self.addCleanup(stack.close) - self.identities = stack.enter_context(mock.patch.object(publisher.distribution, "image_identities", return_value=(self.config, self.digest))) - stack.enter_context(mock.patch.object(publisher.distribution, "resolve_image", return_value=self.digest)) - self.run = stack.enter_context(mock.patch.object(publisher.subprocess, "run")) - stack.enter_context(mock.patch.object(publisher.subprocess, "check_output", return_value=json.dumps({"Descriptor": {"digest": self.digest}}))) - self.remote = stack.enter_context(mock.patch.object(publisher, "registry_manifest", return_value={"config": {"digest": self.config}})) - - def publish(self, tag="v1.2.3"): - return publisher.publish_images(self.assets, "MiniMax-AI/OpenAgentCore", self.revision, tag) + self.revision = 'a' * 40 + self.configs = {'amd64': 'sha256:' + '1' * 64, 'arm64': 'sha256:' + '2' * 64} + self.digests = {'amd64': 'sha256:' + '3' * 64, 'arm64': 'sha256:' + '4' * 64} + self.index_digest = 'sha256:' + '5' * 64 + self.remote_images = {} + for arch in ('amd64', 'arm64'): + names = publisher.IMAGE_NAMES if arch == 'amd64' else ('core', 'web', 'ingress') + manifest = {'source_commit': self.revision, 'platform': 'linux/' + arch, + 'images': dict.fromkeys(names, self.configs[arch]), + 'image_manifest_digests': dict.fromkeys(names, self.digests[arch])} + if arch == 'amd64': + runtime = self.assets / 'runtime.tar.gz' + runtime.write_bytes(gzip.compress(b'runtime')) + manifest['artifacts'] = {'images/runtime.tar.gz': {'filename': runtime.name, 'sha256': publisher.distribution.sha256(runtime)}} + stem = 'oac-' + self.revision + '-linux-' + arch + with tarfile.open(self.assets / (stem + '.tar.gz'), 'w:gz') as archive: + for name, raw in [('manifest.json', json.dumps(manifest).encode())] + [('images/' + n + '.tar', b'image') for n in names if n != 'runtime']: + member = tarfile.TarInfo(stem + '/' + name); member.size = len(raw) + archive.addfile(member, io.BytesIO(raw)) + stack = contextlib.ExitStack(); self.addCleanup(stack.close) + self.identities = stack.enter_context(mock.patch.object(publisher.distribution, 'image_identities', side_effect=lambda path, config, arch: (self.configs[arch], self.digests[arch]))) + stack.enter_context(mock.patch.object(publisher.distribution, 'resolve_image', side_effect=lambda config, digest, arch: digest)) + self.run = stack.enter_context(mock.patch.object(publisher.subprocess, 'run', side_effect=self.execute)) + stack.enter_context(mock.patch.object(publisher.subprocess, 'check_output', side_effect=self.output)) + self.remote = stack.enter_context(mock.patch.object(publisher, 'registry_manifest', side_effect=self.lookup)) + + def lookup(self, reference): + if '@' in reference: + for arch, digest in self.digests.items(): + if reference.endswith(digest): return {'config': {'digest': self.configs[arch]}} + return self.remote_images.get(reference) + + def execute(self, command, **kwargs): + if command[1] == 'push': + ref = command[-1]; arch = ref.rsplit('-', 1)[1] + self.remote_images[ref] = {'config': {'digest': self.configs[arch]}} + if command[1:4] == ['buildx', 'imagetools', 'create']: + ref = command[5]; name = ref.rsplit('/', 1)[1].split(':')[0] + arches = ('amd64',) if name == 'runtime' else ('amd64', 'arm64') + self.remote_images[ref] = {'manifests': [{'platform': {'os': 'linux', 'architecture': arch}, 'digest': self.digests[arch]} for arch in arches]} + + def output(self, command, **kwargs): + if command[1] == 'buildx': return self.index_digest + ref = command[-1] + arch = 'arm64' if ref.endswith('arm64') else 'amd64' + return json.dumps({'Descriptor': {'digest': self.digests[arch]}}) + + def publish(self, **kwargs): + return publisher.publish_images(self.assets, 'MiniMax-AI/OpenAgentCore', self.revision, 'v1.2.3', **kwargs) def pushes(self): - return [c.args[0] for c in self.run.call_args_list if c.args[0][1] == "push"] - - def test_matching_tags_are_reused_and_receipts_use_registry_digest(self): - result = self.publish() - self.assertEqual(self.pushes(), []) - self.assertEqual(result["core"]["digest"], "ghcr.io/minimax-ai/openagentcore/core@" + self.digest) - - def test_stable_release_moves_latest_after_the_version_tags(self): - seen = {} - def remote(reference): - seen[reference] = seen.get(reference, 0) + 1 - if seen[reference] == 1: - return None - return {"config": {"digest": self.config}} - self.remote.side_effect = remote - publisher.publish_images(self.assets, "MiniMax-AI/OpenAgentCore", self.revision, "v1.2.3", floating_latest=True) - pushed = [command[-1].rsplit("/", 1)[-1] for command in self.pushes()] - self.assertEqual(sorted(name for name in pushed if name.endswith(":v1.2.3")), - sorted(name + ":v1.2.3" for name in publisher.IMAGE_NAMES)) - self.assertEqual(sorted(name for name in pushed if name.endswith(":latest")), - sorted(name + ":latest" for name in publisher.IMAGE_NAMES)) - - def test_matching_latest_tag_is_reused(self): - publisher.publish_images(self.assets, "MiniMax-AI/OpenAgentCore", self.revision, "v1.2.3", floating_latest=True) - self.assertEqual(self.pushes(), []) + return [call.args[0] for call in self.run.call_args_list if call.args[0][1] == 'push'] - def test_new_images_use_resolved_store_identity_and_version_only(self): - self.remote.side_effect = [None] * 4 + [{"config": {"digest": self.config}}] * 4 - result = self.publish("v1.2.3-rc.1+build.2") - self.assertEqual(len(self.pushes()), 4) - self.assertTrue(all(c[-1].endswith(":v1.2.3-rc.1_build.2") for c in self.pushes())) - tags = [c.args[0] for c in self.run.call_args_list if c.args[0][1] == "tag"] - self.assertTrue(all(c[2] == self.digest for c in tags)) - self.assertEqual(len(result), 4) - - def test_single_platform_indexes_are_verified_by_child_config(self): - index = {"manifests": [{"digest": self.digest}]} - image = {"config": {"digest": self.config}} - self.remote.side_effect = lambda reference: image if "@" in reference else index + def test_publishes_and_reuses_verified_multiarch_indexes(self): result = self.publish() + self.assertEqual(len(self.pushes()), 7) + self.assertEqual(result['ingress']['digest'], 'ghcr.io/minimax-ai/openagentcore/ingress@' + self.index_digest) + self.assertEqual(len(self.remote_images['ghcr.io/minimax-ai/openagentcore/core:v1.2.3']['manifests']), 2) + self.run.reset_mock(); self.publish(); self.assertEqual(self.pushes(), []) + + def test_latest_updates_after_all_version_indexes(self): + self.publish(floating_latest=True) + creates = [c.args[0][5] for c in self.run.call_args_list if c.args[0][1:4] == ['buildx', 'imagetools', 'create']] + self.assertTrue(all(ref.endswith(':v1.2.3') for ref in creates[:4])) + self.assertTrue(all(ref.endswith(':latest') for ref in creates[4:])) + + def test_conflicting_platform_prevents_every_push(self): + self.remote_images['ghcr.io/minimax-ai/openagentcore/web:v1.2.3-arm64'] = {'config': {'digest': 'different'}} + with self.assertRaisesRegex(ValueError, 'different image'): self.publish() self.assertEqual(self.pushes(), []) - self.assertEqual(result["core"]["digest"], "ghcr.io/minimax-ai/openagentcore/core@" + self.digest) - self.assertTrue(any("@" in call.args[0] for call in self.remote.call_args_list)) - def test_multi_image_index_is_rejected(self): - self.remote.return_value = {"manifests": [{"digest": self.digest}] * 2} - with self.assertRaisesRegex(ValueError, "Expected one"): - self.publish() + def test_conflicting_index_prevents_every_push(self): + self.remote_images['ghcr.io/minimax-ai/openagentcore/core:v1.2.3'] = {'manifests': []} + with self.assertRaisesRegex(ValueError, 'unexpected platforms'): self.publish() self.assertEqual(self.pushes(), []) - def test_conflicting_tag_prevents_all_pushes(self): - self.remote.side_effect = [None, {"config": {"digest": "different"}}] - with self.assertRaisesRegex(ValueError, "different image"): - self.publish() - self.assertEqual(self.pushes(), []) + def test_missing_arm_archive_prevents_loading(self): + (self.assets / ('oac-' + self.revision + '-linux-arm64.tar.gz')).unlink() + with self.assertRaises(FileNotFoundError): self.publish() + self.run.assert_not_called() def test_corrupt_runtime_prevents_loading(self): - (self.assets / "runtime.tar.gz").write_bytes(b"corrupt") - with self.assertRaisesRegex(ValueError, "checksum"): - self.publish() + (self.assets / 'runtime.tar.gz').write_bytes(b'corrupt') + with self.assertRaisesRegex(ValueError, 'checksum'): self.publish() self.run.assert_not_called() - def test_archive_identity_mismatch_prevents_loading(self): - self.identities.return_value = (self.config, "different") - with self.assertRaisesRegex(ValueError, "identity mismatch"): - self.publish() + def test_wrong_archive_identity_prevents_loading(self): + self.identities.side_effect = None; self.identities.return_value = ('wrong', 'wrong') + with self.assertRaisesRegex(ValueError, 'identity mismatch'): self.publish() self.run.assert_not_called() - def test_failed_push_stops_publication(self): - self.remote.return_value = None - def run(command, **kwargs): - if command[1] == "push": - raise subprocess.CalledProcessError(1, command) - self.run.side_effect = run - with self.assertRaises(subprocess.CalledProcessError): - self.publish() - self.assertGreaterEqual(len(self.pushes()), 1) - self.assertEqual(len({command[-1] for command in self.pushes()}), len(self.pushes())) - - def test_registry_pushes_overlap_after_all_preflight_checks(self): - barrier = threading.Barrier(4, timeout=5) - pushed = set() - lock = threading.Lock() - def remote(reference): - with lock: - return {"config": {"digest": self.config}} if reference in pushed else None - def run(command, **kwargs): - if command[1] == "push": - self.assertEqual(sum(c.args[0][1] == "load" for c in self.run.call_args_list), 4) - barrier.wait() - with lock: - pushed.add(command[-1]) - self.remote.side_effect = remote - self.run.side_effect = run - self.assertEqual(len(self.publish()), 4) - self.assertEqual(len(pushed), 4) + def test_push_failure_stops_index_publication(self): + def fail(command, **kwargs): + if command[1] == 'push': raise subprocess.CalledProcessError(1, command) + self.run.side_effect = fail + with self.assertRaises(subprocess.CalledProcessError): self.publish() + self.assertFalse(any(c.args[0][1] == 'buildx' for c in self.run.call_args_list)) def test_registry_auth_failure_is_not_missing_image(self): - # Test the actual inspection function separately from the publication fixture. - with mock.patch.object(publisher.subprocess, "run", return_value=subprocess.CompletedProcess([], 1, "", "unauthorized")): - with self.assertRaisesRegex(RuntimeError, "Cannot inspect"): - REAL_REGISTRY_MANIFEST("ghcr.io/example/core:v1") + with mock.patch.object(publisher.subprocess, 'run', return_value=subprocess.CompletedProcess([], 1, '', 'unauthorized')): + with self.assertRaisesRegex(RuntimeError, 'Cannot inspect'): REAL_REGISTRY_MANIFEST('ghcr.io/example/core:v1') def test_registry_missing_manifest(self): - with mock.patch.object(publisher.subprocess, "run", return_value=subprocess.CompletedProcess([], 1, "", "manifest unknown")): - self.assertIsNone(REAL_REGISTRY_MANIFEST("ghcr.io/example/core:v1")) + with mock.patch.object(publisher.subprocess, 'run', return_value=subprocess.CompletedProcess([], 1, '', 'manifest unknown')): + self.assertIsNone(REAL_REGISTRY_MANIFEST('ghcr.io/example/core:v1')) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/services/core/cmd/oac/init.go b/services/core/cmd/oac/init.go index 7c8f6c8b4..432d9da60 100644 --- a/services/core/cmd/oac/init.go +++ b/services/core/cmd/oac/init.go @@ -12,7 +12,6 @@ import ( "os" "path/filepath" "strings" - "syscall" "time" "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" @@ -45,7 +44,6 @@ func initCommand() error { log.Bg().Error("Initialization failed", "step", "validate_revision", "revision", revision, "error", err) return err } - syscall.Umask(0o077) release := releaseIdentity{revision} return initialize("/data", release, func() (map[string][]byte, error) { return readRelease("/opt/oac/node-payload", release) @@ -173,131 +171,129 @@ func initialize(root string, release releaseIdentity, fetch func() (map[string][ } } nextStep("acquire_lock") - lock, err := os.OpenFile(filepath.Join(root, "secrets", ".init.lock"), os.O_CREATE|os.O_WRONLY, 0o600) - if err != nil { - return err - } - defer lock.Close() - if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil { - return err - } - nextStep("verify_existing_installation") - marker := filepath.Join(root, "installation.json") - if raw, err := os.ReadFile(marker); err == nil { - var receipt installReceipt - if err := json.Unmarshal(raw, &receipt); err != nil { + return withLock(filepath.Join(root, "secrets", "init"), func() error { + + nextStep("verify_existing_installation") + marker := filepath.Join(root, "installation.json") + if raw, err := os.ReadFile(marker); err == nil { + var receipt installReceipt + if err := json.Unmarshal(raw, &receipt); err != nil { + return err + } + if receipt.SourceCommit != release.revision { + return errors.New("this data directory belongs to another release; create a new installation") + } + for name, checksum := range receipt.Files { + actual, err := fileDigest(filepath.Join(root, name)) + if err != nil { + return err + } + if actual != checksum { + return errors.New("installation files changed; restore the matching data directory") + } + } + if err := syncCoreKeyDigest(root); err != nil { + return err + } + logger.InfoContext(ctx, "Existing installation verified", "file_count", len(receipt.Files)) + return nil + } else if !errors.Is(err, fs.ErrNotExist) { return err } - if receipt.SourceCommit != release.revision { - return errors.New("this data directory belongs to another release; create a new installation") - } - for name, checksum := range receipt.Files { - actual, err := fileDigest(filepath.Join(root, name)) + nextStep("verify_empty_data") + for _, name := range []string{"database", "state"} { + entries, err := os.ReadDir(filepath.Join(root, name)) if err != nil { return err } - if actual != checksum { - return errors.New("installation files changed; restore the matching data directory") + if len(entries) > 0 { + return errors.New("existing data requires its original installation files") } } - logger.InfoContext(ctx, "Existing installation verified", "file_count", len(receipt.Files)) - return nil - } else if !errors.Is(err, fs.ErrNotExist) { - return err - } - nextStep("verify_empty_data") - for _, name := range []string{"database", "state"} { - entries, err := os.ReadDir(filepath.Join(root, name)) + nextStep("verify_bundled_metadata") + files, err := fetch() if err != nil { return err } - if len(entries) > 0 { - return errors.New("existing data requires its original installation files") + logger.InfoContext(ctx, "Bundled node installation metadata verified", "file_count", len(files)) + nextStep("publish_node_metadata") + prefix := "node-payload/releases/" + release.revision + "/" + names := []string{} + for _, name := range releaseMembers { + if err := writeOwned(filepath.Join(root, prefix+name), files[name]); err != nil { + return err + } + logger.InfoContext(ctx, "Node metadata file copied", "file", name) + names = append(names, prefix+name) } - } - nextStep("verify_bundled_metadata") - files, err := fetch() - if err != nil { - return err - } - logger.InfoContext(ctx, "Bundled node installation metadata verified", "file_count", len(files)) - nextStep("publish_node_metadata") - prefix := "node-payload/releases/" + release.revision + "/" - names := []string{} - for _, name := range releaseMembers { - if err := writeOwned(filepath.Join(root, prefix+name), files[name]); err != nil { + active, _ := json.Marshal(map[string]string{"source_commit": release.revision}) + if err := writeOwned(filepath.Join(root, "node-payload", "active.json"), active); err != nil { return err } - logger.InfoContext(ctx, "Node metadata file copied", "file", name) - names = append(names, prefix+name) - } - active, _ := json.Marshal(map[string]string{"source_commit": release.revision}) - if err := writeOwned(filepath.Join(root, "node-payload", "active.json"), active); err != nil { - return err - } - if err := filepath.WalkDir(filepath.Join(root, "node-payload"), func(path string, entry fs.DirEntry, err error) error { - if err != nil || !entry.IsDir() { + if err := filepath.WalkDir(filepath.Join(root, "node-payload"), func(path string, entry fs.DirEntry, err error) error { + if err != nil || !entry.IsDir() { + return err + } + if err := os.Chmod(path, 0o755); err != nil { + return err + } + return chown(path, 65532, 65532) + }); err != nil { return err } - if err := os.Chmod(path, 0o755); err != nil { + nextStep("prepare_credentials") + generators := []struct { + name string + generate func() string + }{ + {"secrets/web/core.key", func() string { + key, err := generateCoreKey() + if err != nil { + panic(err) + } + return key + }}, + {"secrets/database/password", func() string { return randomHex(32) }}, + {"secrets/core/credential.key", func() string { return base64.StdEncoding.EncodeToString(randomBytes(32)) }}, + {"secrets/core/installation.id", func() string { return uuid.NewString() }}, + } + for _, secret := range generators { + path := filepath.Join(root, secret.name) + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + if err := writeOwned(path, []byte(secret.generate()+"\n")); err != nil { + return err + } + logger.InfoContext(ctx, "Credential file generated", "file", secret.name) + } else if err != nil { + return err + } else { + logger.InfoContext(ctx, "Credential file retained", "file", secret.name) + } + if secret.name != "secrets/web/core.key" { + names = append(names, secret.name) + } + } + if err := syncCoreKeyDigest(root); err != nil { return err } - return chown(path, 65532, 65532) - }); err != nil { - return err - } - nextStep("prepare_credentials") - generators := []struct { - name string - generate func() string - }{ - {"secrets/web/core.key", func() string { - key, err := generateCoreKey() + + names = append(names, "node-payload/active.json") + nextStep("write_installation_receipt") + receipt := installReceipt{SourceCommit: release.revision, Files: map[string]string{}} + for _, name := range names { + checksum, err := fileDigest(filepath.Join(root, name)) if err != nil { - panic(err) - } - return key - }}, - {"secrets/database/password", func() string { return randomHex(32) }}, - {"secrets/core/credential.key", func() string { return base64.StdEncoding.EncodeToString(randomBytes(32)) }}, - {"secrets/core/installation.id", func() string { return uuid.NewString() }}, - } - for _, secret := range generators { - path := filepath.Join(root, secret.name) - if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { - if err := writeOwned(path, []byte(secret.generate()+"\n")); err != nil { return err } - logger.InfoContext(ctx, "Credential file generated", "file", secret.name) - } else if err != nil { - return err - } else { - logger.InfoContext(ctx, "Credential file retained", "file", secret.name) + receipt.Files[name] = checksum } - names = append(names, secret.name) - } - key, err := coreKey(root) - if err != nil { - return err - } - digests, _ := json.Marshal([]string{keyDigest(key)}) - if err := writeOwned(filepath.Join(root, "secrets", "core", "core-key-digests.json"), digests); err != nil { - return err - } - names = append(names, "secrets/core/core-key-digests.json", "node-payload/active.json") - nextStep("write_installation_receipt") - receipt := installReceipt{SourceCommit: release.revision, Files: map[string]string{}} - for _, name := range names { - if receipt.Files[name], err = fileDigest(filepath.Join(root, name)); err != nil { + raw, _ := json.Marshal(receipt) + if err := writeOwned(marker, raw); err != nil { return err } - } - raw, _ := json.Marshal(receipt) - if err := writeOwned(marker, raw); err != nil { - return err - } - logger.InfoContext(ctx, "Installation initialized", "sign_in_key_command", "docker compose exec web oac-web core-key") - return nil + logger.InfoContext(ctx, "Installation initialized", "sign_in_key_command", "docker compose exec web oac-web core-key") + return nil + }) } func randomBytes(n int) []byte { diff --git a/services/core/cmd/oac/init_test.go b/services/core/cmd/oac/init_test.go index b0164d682..c005a2fc3 100644 --- a/services/core/cmd/oac/init_test.go +++ b/services/core/cmd/oac/init_test.go @@ -12,6 +12,7 @@ import ( "maps" "os" "path/filepath" + "runtime" "strings" "testing" @@ -42,7 +43,7 @@ func snapshot(t *testing.T, root string) map[string]string { } data, err := os.ReadFile(path) relative, _ := filepath.Rel(root, path) - saved[relative] = string(data) + saved[filepath.ToSlash(relative)] = string(data) return err }); err != nil { t.Fatal(err) @@ -81,7 +82,7 @@ func TestInitializeKeepsIdentityAndKeysAcrossRestarts(t *testing.T) { } for _, name := range []string{"secrets/web/core.key", "secrets/database/password", "secrets/core/credential.key"} { info, err := os.Stat(filepath.Join(root, name)) - if err != nil || info.Mode().Perm() != 0o600 { + if err != nil || (runtime.GOOS != "windows" && info.Mode().Perm() != 0o600) { t.Fatalf("%s: %v %v", name, info.Mode(), err) } } @@ -301,3 +302,29 @@ func TestInitializationLogsFailureStep(t *testing.T) { t.Fatal("failure logged success or generated credentials") } } + +func TestInitializationRetainsRotatedKeyAndRepairsItsDerivedDigest(t *testing.T) { + root, release, files := initFixture(t) + if err := initialize(root, release, fixed(files)); err != nil { + t.Fatal(err) + } + key, err := generateCoreKey() + if err != nil { + t.Fatal(err) + } + // Simulate interruption after publishing the new key but before its digest. + if err := writeOwned(filepath.Join(root, "secrets", "web", "core.key"), []byte(key+"\n")); err != nil { + t.Fatal(err) + } + if err := initialize(root, release, refuseDownload(t)); err != nil { + t.Fatal(err) + } + after, _ := coreKey(root) + if after != key { + t.Fatal("rotated key replaced") + } + digest, _ := os.ReadFile(filepath.Join(root, "secrets", "core", "core-key-digests.json")) + if !strings.Contains(string(digest), keyDigest(key)) { + t.Fatal("derived digest not repaired") + } +} diff --git a/services/core/cmd/oac/install.go b/services/core/cmd/oac/install.go new file mode 100644 index 000000000..efe7d5955 --- /dev/null +++ b/services/core/cmd/oac/install.go @@ -0,0 +1,358 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "flag" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" +) + +// Installation is host-independent. Only the launchers select a native binary; +// Docker owns the Linux filesystem, service identities and persistent data. +type installOptions struct { + dir, version, publicURL, host string + port int +} +type installer struct { + docker func(context.Context, string, ...string) ([]byte, error) + download func(context.Context, string, string) error + executable string +} + +func installCommand(ctx context.Context, args []string) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + flags := flag.NewFlagSet("oac install", flag.ContinueOnError) + var o installOptions + flags.StringVar(&o.dir, "install-dir", filepath.Join(home, ".oac", "core"), "absolute installation directory") + flags.StringVar(&o.version, "version", "latest", "release tag") + flags.StringVar(&o.publicURL, "public-url", "", "URL reachable by browsers and nodes") + flags.StringVar(&o.host, "host", "0.0.0.0", "Web bind address") + flags.IntVar(&o.port, "web-port", 8080, "Web port") + if err := flags.Parse(args); err != nil { + return err + } + if flags.NArg() != 0 { + return errors.New("unexpected installation arguments") + } + exe, err := os.Executable() + if err != nil { + return err + } + i := installer{docker: dockerOutput, download: downloadAsset, executable: exe} + return i.install(ctx, o) +} + +func dockerOutput(ctx context.Context, dir string, args ...string) ([]byte, error) { + cmd := exec.CommandContext(ctx, "docker", args...) + cmd.Dir = dir + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("docker %s: %w\n%s", args[0], err, output) + } + return output, nil +} + +func downloadAsset(ctx context.Context, address, path string) error { + client := &http.Client{Timeout: 2 * time.Minute, CheckRedirect: func(req *http.Request, via []*http.Request) error { + if req.URL.Scheme != "https" || len(via) >= 10 { + return errors.New("invalid release redirect") + } + return nil + }} + req, err := http.NewRequestWithContext(ctx, http.MethodGet, address, nil) + if err != nil { + return err + } + res, err := client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return fmt.Errorf("download %s: HTTP %d", address, res.StatusCode) + } + raw, err := io.ReadAll(io.LimitReader(res.Body, (1<<20)+1)) + if err != nil { + return err + } + if len(raw) > 1<<20 { + return errors.New("release configuration exceeds size limit") + } + return os.WriteFile(path, raw, 0o600) +} + +func (i installer) install(ctx context.Context, o installOptions) error { + if !filepath.IsAbs(o.dir) || filepath.Clean(o.dir) == filepath.VolumeName(o.dir)+string(filepath.Separator) { + return errors.New("--install-dir must name an absolute directory other than the filesystem root") + } + if o.port < 1 || o.port > 65535 { + return errors.New("--web-port must be between 1 and 65535") + } + if net.ParseIP(o.host) == nil { + return errors.New("--host must be an IP address") + } + if o.publicURL == "" { + o.publicURL = "http://localhost:" + strconv.Itoa(o.port) + if o.host == "0.0.0.0" { + // UDP connect selects a route without transmitting a packet. + if route, err := net.DialTimeout("udp4", "1.1.1.1:53", time.Second); err == nil { + address := route.LocalAddr().(*net.UDPAddr).IP + route.Close() + if address.IsPrivate() { + o.publicURL = "http://" + net.JoinHostPort(address.String(), strconv.Itoa(o.port)) + } + } + } + } + u, err := url.Parse(o.publicURL) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") { + return errors.New("--public-url must be an HTTP(S) origin") + } + for _, value := range []string{o.publicURL, o.version} { + if strings.ContainsAny(value, "\r\n'\\") { + return errors.New("installation options contain invalid characters") + } + } + o.dir = filepath.Clean(o.dir) + if err := os.MkdirAll(filepath.Dir(o.dir), 0o700); err != nil { + return err + } + parent, err := filepath.EvalSymlinks(filepath.Dir(o.dir)) + if err != nil { + return err + } + o.dir = filepath.Join(parent, filepath.Base(o.dir)) + if info, err := os.Lstat(o.dir); err == nil && !info.IsDir() { + return errors.New("installation path must be a directory, not a symbolic link or file") + } else if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + return withLock(o.dir, func() error { return i.installLocked(ctx, o) }) +} + +func (i installer) installLocked(ctx context.Context, o installOptions) error { + info, err := i.docker(ctx, "", "info", "--format", "{{.OSType}}/{{.Architecture}}") + if err != nil { + return fmt.Errorf("start Docker and check this account's access: %w", err) + } + switch strings.TrimSpace(string(info)) { + case "linux/x86_64", "linux/amd64", "linux/aarch64", "linux/arm64": + default: + return errors.New("Docker must run Linux amd64 or arm64 containers; on Windows select Linux containers in Docker Desktop") + } + version, err := i.docker(ctx, "", "compose", "version", "--short") + if err != nil { + return err + } + var major, minor int + if _, err := fmt.Sscanf(strings.TrimPrefix(strings.TrimSpace(string(version)), "v"), "%d.%d", &major, &minor); err != nil || major < 2 || (major == 2 && minor < 26) { + return errors.New("Docker Compose 2.26 or newer is required") + } + engine, err := i.docker(ctx, "", "version", "--format", "{{.Server.APIVersion}}") + if err != nil { + return err + } + if _, err := fmt.Sscanf(strings.TrimSpace(string(engine)), "%d.%d", &major, &minor); err != nil || major < 1 || (major == 1 && minor < 45) { + return errors.New("Docker Engine 26 or newer is required for data volume subdirectories") + } + stage := o.dir + ".staging" + if err := cleanStage(stage, o.dir); err != nil { + return err + } + entries, err := os.ReadDir(o.dir) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + resume := len(entries) > 0 + working := o.dir + if !resume { + listener, err := net.Listen("tcp", net.JoinHostPort(o.host, strconv.Itoa(o.port))) + if err != nil { + return fmt.Errorf("Web port unavailable; choose --web-port: %w", err) + } + listener.Close() + if err := os.Mkdir(stage, 0o700); err != nil { + return err + } + defer os.RemoveAll(stage) + // Directory creation publishes ownership atomically, including on Windows. + if err := os.Mkdir(filepath.Join(stage, stageMarker(o.dir)), 0o700); err != nil { + return err + } + working = stage + repository := os.Getenv("OAC_REPOSITORY") + if repository == "" { + repository = "MiniMax-AI/OpenAgentCore" + } + base := "https://github.com/" + repository + "/releases/latest/download/" + if o.version != "latest" { + base = "https://github.com/" + repository + "/releases/download/" + url.PathEscape(o.version) + "/" + } + fmt.Println("Downloading release configuration...") + for _, name := range []string{"compose-sha256sums.txt", "compose.yaml"} { + if err := i.download(ctx, base+name, filepath.Join(stage, name)); err != nil { + return err + } + } + contents := fmt.Sprintf("COMPOSE_PROJECT_NAME=oac-%s\nOAC_HOST='%s'\nOAC_WEB_PORT='%d'\nOAC_PUBLIC_URL='%s'\n", randomHex(5), o.host, o.port, o.publicURL) + if err := os.WriteFile(filepath.Join(stage, ".env"), []byte(contents), 0o600); err != nil { + return err + } + source, err := os.Open(i.executable) + if err != nil { + return err + } + defer source.Close() + target, err := os.OpenFile(filepath.Join(stage, cliName()), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o700) + if err != nil { + return err + } + _, copyErr := io.Copy(target, source) + closeErr := target.Close() + if copyErr != nil { + return copyErr + } + if closeErr != nil { + return closeErr + } + } else { + fmt.Println("Using saved settings and retaining existing data.") + } + for _, name := range []string{"compose.yaml", "compose-sha256sums.txt", ".env", cliName()} { + info, err := os.Lstat(filepath.Join(working, name)) + if err != nil || !info.Mode().IsRegular() { + return fmt.Errorf("incomplete installation: preserve %s and choose another directory", o.dir) + } + } + if err := verifyCompose(working); err != nil { + return err + } + compose := func(args ...string) ([]byte, error) { + return i.docker(ctx, working, append([]string{"compose"}, args...)...) + } + if _, err := compose("config", "--quiet"); err != nil { + return err + } + images, err := compose("config", "--images") + if err != nil { + return err + } + fmt.Println("Checking and downloading images...") + for _, image := range strings.Fields(string(images)) { + if resume { + if _, err := i.docker(ctx, working, "image", "inspect", image); err == nil { + continue + } + } + if _, err := i.docker(ctx, working, "pull", image); err != nil { + return err + } + } + if !resume { + if _, err := os.Stat(o.dir); err == nil { + if err := os.Remove(o.dir); err != nil { + return err + } + } + if err := os.Rename(stage, o.dir); err != nil { + return err + } + working = o.dir + _ = os.Remove(filepath.Join(o.dir, stageMarker(o.dir))) + } + fmt.Println("Starting services...") + if _, err := compose("up", "-d", "--wait", "--wait-timeout", "180", "--pull", "never", "--no-recreate"); err != nil { + return fmt.Errorf("startup failed; data retained, rerun the installer: %w", err) + } + key, err := compose("exec", "-T", "web", "/usr/local/bin/oac-web", "core-key") + if err != nil { + return err + } + environment, err := compose("config", "--environment") + if err != nil { + return err + } + for _, line := range strings.Split(string(environment), "\n") { + if strings.HasPrefix(line, "OAC_PUBLIC_URL=") { + o.publicURL = strings.TrimPrefix(line, "OAC_PUBLIC_URL=") + } + } + fmt.Printf("\nOpenAgentCore is running.\n\nConsole: %s\nCore key: %s\nCommand: %s\n", o.publicURL, strings.TrimSpace(string(key)), filepath.Join(o.dir, cliName())) + if u, _ := url.Parse(o.publicURL); u != nil && (u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1") { + fmt.Println("For remote access, set OAC_PUBLIC_URL in .env to a reachable origin and run oac apply.") + } + return nil +} + +func cliName() string { + if runtime.GOOS == "windows" { + return "oac.exe" + } + return "oac" +} + +func cleanStage(stage, owner string) error { + info, err := os.Lstat(stage) + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return err + } + if !info.IsDir() { + return errors.New("staging path must be a directory, not a link or file") + } + entries, err := os.ReadDir(stage) + if err != nil { + return err + } + if len(entries) > 0 { + marker := filepath.Join(stage, stageMarker(owner)) + info, err := os.Lstat(marker) + if err != nil || !info.IsDir() { + return errors.New("unrecognized staging directory; preserve it and choose another installation directory") + } + contents, err := os.ReadDir(marker) + if err != nil || len(contents) != 0 { + return errors.New("invalid staging ownership marker") + } + + } + return os.RemoveAll(stage) +} + +func verifyCompose(dir string) error { + raw, err := os.ReadFile(filepath.Join(dir, "compose.yaml")) + if err != nil { + return err + } + sums, err := os.ReadFile(filepath.Join(dir, "compose-sha256sums.txt")) + if err != nil { + return err + } + digest := sha256.Sum256(raw) + if strings.TrimSpace(string(sums)) != hex.EncodeToString(digest[:])+" compose.yaml" { + return errors.New("Compose checksum mismatch; restore the matching release configuration") + } + return nil +} + +func stageMarker(owner string) string { + return fmt.Sprintf(".oac-installer-%x", sha256.Sum256([]byte(owner))) +} diff --git a/services/core/cmd/oac/install_test.go b/services/core/cmd/oac/install_test.go new file mode 100644 index 000000000..be48b1bfc --- /dev/null +++ b/services/core/cmd/oac/install_test.go @@ -0,0 +1,245 @@ +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +type installFixture struct { + installer + options installOptions + calls []string + failure string + cached bool +} + +func newInstallFixture(t *testing.T) *installFixture { + t.Helper() + root, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + port := listener.Addr().(*net.TCPAddr).Port + listener.Close() + exe := filepath.Join(root, "source") + if err := os.WriteFile(exe, []byte("binary"), 0o700); err != nil { + t.Fatal(err) + } + f := &installFixture{options: installOptions{dir: filepath.Join(root, "installation with spaces"), version: "latest", host: "127.0.0.1", port: port}} + f.executable = exe + compose := []byte("services: {}\n") + f.download = func(_ context.Context, address, path string) error { + if f.failure == "download" { + return os.ErrPermission + } + raw := compose + if strings.HasSuffix(address, "sums.txt") { + raw = []byte(fmt.Sprintf("%x compose.yaml\n", sha256.Sum256(compose))) + } + return os.WriteFile(path, raw, 0o600) + } + f.docker = func(_ context.Context, dir string, args ...string) ([]byte, error) { + command := strings.Join(args, " ") + f.calls = append(f.calls, command) + if f.failure != "" && strings.HasPrefix(command, f.failure) { + return nil, os.ErrPermission + } + switch command { + case "info --format {{.OSType}}/{{.Architecture}}": + return []byte("linux/aarch64"), nil + case "compose version --short": + return []byte("v2.26.0"), nil + case "version --format {{.Server.APIVersion}}": + return []byte("1.45"), nil + case "compose config --images": + return []byte("example/core:latest\nexample/web:latest"), nil + case "compose config --environment": + return []byte("OAC_PUBLIC_URL=http://localhost:8080"), nil + } + if strings.HasPrefix(command, "image inspect") && !f.cached { + return nil, os.ErrNotExist + } + if strings.HasPrefix(command, "compose up") { + if _, err := os.Stat(filepath.Join(f.options.dir, ".env")); err != nil { + t.Fatal("services started before configuration publication") + } + } + return nil, nil + } + return f +} +func (f *installFixture) run() error { return f.install(context.Background(), f.options) } + +func TestInstallFailureBeforePublicationAndRetry(t *testing.T) { + for _, failure := range []string{"download", "compose config --quiet", "pull"} { + t.Run(failure, func(t *testing.T) { + f := newInstallFixture(t) + f.failure = failure + if err := f.run(); err == nil { + t.Fatal("expected failure") + } + for _, path := range []string{f.options.dir, f.options.dir + ".staging"} { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("unpublished state remains: %s", path) + } + } + f.failure = "" + if err := f.run(); err != nil { + t.Fatal(err) + } + }) + } +} +func TestInstallResumePreservesConfigAndUsesCachedImages(t *testing.T) { + f := newInstallFixture(t) + f.failure = "compose up" + if err := f.run(); err == nil { + t.Fatal("expected startup failure") + } + saved, err := os.ReadFile(filepath.Join(f.options.dir, ".env")) + if err != nil { + t.Fatal(err) + } + f.failure = "" + f.cached = true + f.calls = nil + f.download = func(context.Context, string, string) error { + t.Fatal("resume downloaded replacement release") + return nil + } + f.options.publicURL = "https://replacement.invalid" + if err := f.run(); err != nil { + t.Fatal(err) + } + after, _ := os.ReadFile(filepath.Join(f.options.dir, ".env")) + if string(saved) != string(after) { + t.Fatal("saved configuration replaced") + } + for _, call := range f.calls { + if strings.HasPrefix(call, "pull") || strings.Contains(call, "down") { + t.Fatal(call) + } + } + if !strings.Contains(strings.Join(f.calls, "\n"), "--pull never --no-recreate") { + t.Fatal(f.calls) + } +} +func TestInstallRecoversRecognizedStageAndPreservesUnknownData(t *testing.T) { + for _, owned := range []bool{true, false} { + t.Run(fmt.Sprint(owned), func(t *testing.T) { + f := newInstallFixture(t) + stage := f.options.dir + ".staging" + os.Mkdir(stage, 0o700) + os.WriteFile(filepath.Join(stage, "partial"), []byte("untouched"), 0o600) + if owned { + os.Mkdir(filepath.Join(stage, stageMarker(f.options.dir)), 0o700) + } + err := f.run() + if owned && err != nil { + t.Fatal(err) + } + if !owned { + if err == nil { + t.Fatal("unrecognized staging accepted") + } + if raw, _ := os.ReadFile(filepath.Join(stage, "partial")); string(raw) != "untouched" { + t.Fatal("unknown contents removed") + } + } + }) + } + f := newInstallFixture(t) + os.Mkdir(f.options.dir, 0o700) + os.WriteFile(filepath.Join(f.options.dir, "user-file"), []byte("keep"), 0o600) + if err := f.run(); err == nil { + t.Fatal("unrelated directory accepted") + } + if raw, _ := os.ReadFile(filepath.Join(f.options.dir, "user-file")); string(raw) != "keep" { + t.Fatal("user data lost") + } +} +func TestInstallRejectsBusyPortAndLock(t *testing.T) { + f := newInstallFixture(t) + listener, err := net.Listen("tcp", net.JoinHostPort(f.options.host, fmt.Sprint(f.options.port))) + if err != nil { + t.Fatal(err) + } + if err := f.run(); err == nil { + t.Fatal("busy port accepted") + } + listener.Close() + if err := withLock(f.options.dir, func() error { + if err := f.run(); err == nil { + t.Fatal("concurrent installation accepted") + } + return nil + }); err != nil { + t.Fatal(err) + } +} +func TestInstallRefusesModifiedCompose(t *testing.T) { + f := newInstallFixture(t) + if err := f.run(); err != nil { + t.Fatal(err) + } + os.WriteFile(filepath.Join(f.options.dir, "compose.yaml"), []byte("changed"), 0o600) + f.calls = nil + if err := f.run(); err == nil { + t.Fatal("changed compose accepted") + } + for _, call := range f.calls { + if strings.HasPrefix(call, "compose up") { + t.Fatal("started modified compose") + } + } +} +func TestInstallerInterruptedProcess(t *testing.T) { + if root := os.Getenv("OAC_TEST_INSTALL_KILL"); root != "" { + err := withLock(root, func() error { + stage := root + ".staging" + os.Mkdir(stage, 0o700) + os.Mkdir(filepath.Join(stage, stageMarker(root)), 0o700) + os.WriteFile(filepath.Join(stage, "ready"), nil, 0o600) + time.Sleep(time.Minute) + return nil + }) + if err != nil { + os.Exit(2) + } + return + } + f := newInstallFixture(t) + cmd := exec.Command(os.Args[0], "-test.run=^TestInstallerInterruptedProcess$") + cmd.Env = append(os.Environ(), "OAC_TEST_INSTALL_KILL="+f.options.dir) + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = cmd.Process.Kill() }) + deadline := time.Now().Add(15 * time.Second) + for { + if _, err := os.Stat(filepath.Join(f.options.dir+".staging", "ready")); err == nil { + break + } + if time.Now().After(deadline) { + t.Fatal("child never acquired lock") + } + time.Sleep(20 * time.Millisecond) + } + cmd.Process.Kill() + cmd.Wait() + if err := f.run(); err != nil { + t.Fatalf("killed installer left unrecoverable state: %v", err) + } +} diff --git a/services/core/cmd/oac/install_unix_test.go b/services/core/cmd/oac/install_unix_test.go new file mode 100644 index 000000000..73e875d08 --- /dev/null +++ b/services/core/cmd/oac/install_unix_test.go @@ -0,0 +1,46 @@ +//go:build unix + +package main + +import ( + "os" + "os/exec" + "os/signal" + "syscall" + "testing" +) + +func TestInstallWriteFailureLeavesRetryableState(t *testing.T) { + if os.Getenv("OAC_TEST_FILE_LIMIT") != "1" { + cmd := exec.Command(os.Args[0], "-test.run=^TestInstallWriteFailureLeavesRetryableState$") + cmd.Env = append(os.Environ(), "OAC_TEST_FILE_LIMIT=1") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("write failure probe: %v\n%s", err, output) + } + return + } + f := newInstallFixture(t) + var original syscall.Rlimit + if err := syscall.Getrlimit(syscall.RLIMIT_FSIZE, &original); err != nil { + t.Fatal(err) + } + limited := original + limited.Cur = 0 + signal.Ignore(syscall.SIGXFSZ) + if err := syscall.Setrlimit(syscall.RLIMIT_FSIZE, &limited); err != nil { + t.Fatal(err) + } + err := f.run() + if restore := syscall.Setrlimit(syscall.RLIMIT_FSIZE, &original); restore != nil { + t.Fatal(restore) + } + if err == nil { + t.Fatal("installation succeeded without writable files") + } + if _, err := os.Stat(f.options.dir + ".staging"); !os.IsNotExist(err) { + t.Fatal("failed installation retained staging") + } + if err := f.run(); err != nil { + t.Fatalf("installation did not recover after restoring writes: %v", err) + } +} diff --git a/services/core/cmd/oac/main.go b/services/core/cmd/oac/main.go index 0d1318c78..f2b3393d3 100644 --- a/services/core/cmd/oac/main.go +++ b/services/core/cmd/oac/main.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "flag" "fmt" "os" "os/signal" @@ -15,6 +16,7 @@ import ( "syscall" "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" + "github.com/MiniMax-AI/OpenAgentCore/internal/runtimefs" ) func main() { @@ -28,6 +30,9 @@ func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() if err := run(ctx, os.Args[1], os.Args[2:]); err != nil { + if errors.Is(err, flag.ErrHelp) { + return + } if os.Args[1] != "init" { fmt.Fprintln(os.Stderr, err.Error()) } @@ -39,27 +44,35 @@ func main() { var buildRevision = "development" func usage() { - fmt.Fprintf(os.Stderr, "oac (%s)\nUsage: oac apply|core-key|rotate-core-key|init\n", buildRevision) + fmt.Fprintf(os.Stderr, "oac (%s)\nUsage: oac install|apply|core-key|rotate-core-key\n", buildRevision) } func run(ctx context.Context, command string, args []string) error { switch command { + case "install": + return installCommand(ctx, args) case "init": return initCommand() + case "rotate-volume-key": + return withLock("/data/secrets/init", func() error { return rotateVolumeKey("/data") }) } root, err := installDir() if err != nil { return err } - in := installation{root: root, data: dataDir(root)} runner := execRunner{dir: root} switch command { case "apply": return withLock(root, func() error { return apply(ctx, runner) }) case "core-key": - return coreKeyCommand(ctx, in, runner, args) + return coreKeyCommand(ctx, runner, args) case "rotate-core-key": - return withLock(root, func() error { return rotateCoreKey(ctx, in, runner) }) + return withLock(root, func() error { + if err := runner.Run(ctx, "run", "--rm", "--no-deps", "init", "/usr/local/bin/oac", "rotate-volume-key"); err != nil { + return err + } + return runner.Run(ctx, "restart", "core", "web") + }) default: usage() return errors.New("unknown command") @@ -81,25 +94,24 @@ func installDir() (string, error) { return "", errors.New("run oac from an installation directory that contains compose.yaml") } -type installation struct{ root, data string } - -func dataDir(root string) string { - if dir := os.Getenv("OAC_DATA_MOUNT"); dir != "" { - return dir - } - return filepath.Join(root, "data") -} - func withLock(root string, fn func() error) error { - file, err := os.OpenFile(filepath.Join(root, ".oac.lock"), os.O_CREATE|os.O_RDWR, 0o600) - if err != nil { + dir := root + ".lock" + if info, err := os.Lstat(dir); err == nil && !info.IsDir() { + return errors.New("invalid installation lock directory") + } + if err := os.MkdirAll(dir, 0o700); err != nil { return err } - defer file.Close() - if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil { + handle, err := os.OpenRoot(dir) + if err != nil { return err } - defer syscall.Flock(int(file.Fd()), syscall.LOCK_UN) + defer handle.Close() + unlock, err := runtimefs.LockDirectory(handle) + if err != nil { + return fmt.Errorf("another operation is using this installation: %w", err) + } + defer unlock() return fn() } @@ -110,8 +122,8 @@ func apply(ctx context.Context, runner Runner) error { return runner.Run(ctx, "up", "-d", "--wait") } -func coreKeyCommand(ctx context.Context, in installation, runner Runner, args []string) error { - path := filepath.Join(in.data, "secrets", "web", "core.key") +func coreKeyCommand(ctx context.Context, runner Runner, args []string) error { + path := "Docker volume: secrets/web/core.key (use oac core-key --show)" if len(args) == 0 { fmt.Println(path) return nil @@ -130,24 +142,33 @@ func generateCoreKey() (string, error) { return "oac_admin_" + hex.EncodeToString(buf), nil } -func rotateCoreKey(ctx context.Context, in installation, runner Runner) error { +func rotateVolumeKey(data string) error { key, err := generateCoreKey() if err != nil { return err } - keyPath := filepath.Join(in.data, "secrets", "web", "core.key") - digestPath := filepath.Join(in.data, "secrets", "core", "core-key-digests.json") - if err := writeSecret(keyPath, key+"\n"); err != nil { + keyPath := filepath.Join(data, "secrets", "web", "core.key") + if err := writeOwned(keyPath, []byte(key+"\n")); err != nil { return err } - raw, err := json.Marshal([]string{keyDigest(key)}) + return syncCoreKeyDigest(data) +} + +func syncCoreKeyDigest(data string) error { + key, err := coreKey(data) if err != nil { return err } - if err := writeSecret(digestPath, string(raw)+"\n"); err != nil { - return err + value, err := hex.DecodeString(strings.TrimPrefix(key, "oac_admin_")) + if err != nil || !strings.HasPrefix(key, "oac_admin_") || len(value) != 32 { + return errors.New("invalid saved Core key") } - return runner.Run(ctx, "restart", "core", "web") + raw, _ := json.Marshal([]string{keyDigest(key)}) + path := filepath.Join(data, "secrets", "core", "core-key-digests.json") + if saved, err := os.ReadFile(path); err == nil && strings.TrimSpace(string(saved)) == string(raw) { + return nil + } + return writeOwned(path, raw) } func coreKey(data string) (string, error) { @@ -162,23 +183,3 @@ func keyDigest(key string) string { sum := sha256.Sum256([]byte(key)) return hex.EncodeToString(sum[:]) } - -func writeSecret(path, contents string) error { - info, err := os.Stat(path) - if err != nil { - return err - } - stat, ok := info.Sys().(*syscall.Stat_t) - if !ok { - return os.ErrInvalid - } - temporary := path + ".tmp" - if err := os.WriteFile(temporary, []byte(contents), 0o600); err != nil { - return err - } - if err := os.Chown(temporary, int(stat.Uid), int(stat.Gid)); err != nil { - _ = os.Remove(temporary) - return err - } - return os.Rename(temporary, path) -} diff --git a/services/core/cmd/oac/oac_test.go b/services/core/cmd/oac/oac_test.go index ee865c213..d3ccd058d 100644 --- a/services/core/cmd/oac/oac_test.go +++ b/services/core/cmd/oac/oac_test.go @@ -28,40 +28,36 @@ func TestApplyDoesNotStartWhenTheConfigurationCheckFails(t *testing.T) { func TestRotateCoreKeyDigestDoesNotEchoTheKey(t *testing.T) { root := t.TempDir() - in := installation{root: root, data: filepath.Join(root, "data")} - if err := os.MkdirAll(filepath.Join(in.data, "secrets", "web"), 0o755); err != nil { + data := filepath.Join(root, "data") + if err := os.MkdirAll(filepath.Join(data, "secrets", "web"), 0o755); err != nil { t.Fatal(err) } - if err := os.MkdirAll(filepath.Join(in.data, "secrets", "core"), 0o755); err != nil { + if err := os.MkdirAll(filepath.Join(data, "secrets", "core"), 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(in.data, "secrets", "web", "core.key"), []byte("old\n"), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(data, "secrets", "web", "core.key"), []byte("old\n"), 0o600); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(in.data, "secrets", "core", "core-key-digests.json"), []byte("[]\n"), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(data, "secrets", "core", "core-key-digests.json"), []byte("[]\n"), 0o600); err != nil { t.Fatal(err) } - var restarted bool - runner := scriptedRunner{run: func(args ...string) error { - restarted = args[0] == "restart" - return nil - }} - if err := rotateCoreKey(context.Background(), in, runner); err != nil { + previous := chown + chown = func(string, int, int) error { return nil } + t.Cleanup(func() { chown = previous }) + if err := rotateVolumeKey(data); err != nil { t.Fatal(err) } - raw, err := os.ReadFile(filepath.Join(in.data, "secrets", "web", "core.key")) + + raw, err := os.ReadFile(filepath.Join(data, "secrets", "web", "core.key")) if err != nil { t.Fatal(err) } key := strings.TrimSpace(string(raw)) assertCoreKeyFormat(t, key) - digest, err := os.ReadFile(filepath.Join(in.data, "secrets", "core", "core-key-digests.json")) + digest, err := os.ReadFile(filepath.Join(data, "secrets", "core", "core-key-digests.json")) if err != nil || !strings.Contains(string(digest), keyDigest(key)) || strings.Contains(string(digest), key) { t.Fatalf("digest %s key leaked %v", digest, err) } - if !restarted { - t.Fatal("core was not restarted") - } } type scriptedRunner struct { diff --git a/services/core/tools/e2b-provider/Build.Dockerfile b/services/core/tools/e2b-provider/Build.Dockerfile index 446fa4cd2..84083cefd 100644 --- a/services/core/tools/e2b-provider/Build.Dockerfile +++ b/services/core/tools/e2b-provider/Build.Dockerfile @@ -1,5 +1,5 @@ # Fixed CPython and glibc baseline shared by native Core and the Debian Core image. -FROM python:3.12.12-slim-bookworm@sha256:2986c55feb36e6cae00fa1fefb454283e4b33f35e75ff8bdd123b134130be301 +FROM python:3.12.12-slim-bookworm@sha256:593bd06efe90efa80dc4eee3948be7c0fde4134606dd40d8dd8dbcade98e669c RUN apt-get update && apt-get install -y --no-install-recommends binutils \ && rm -rf /var/lib/apt/lists/* ENTRYPOINT ["python3", "/source/services/core/tools/e2b-provider/build.py"] diff --git a/services/core/tools/e2b-provider/build.py b/services/core/tools/e2b-provider/build.py index 8d6f9badf..8b69302d5 100644 --- a/services/core/tools/e2b-provider/build.py +++ b/services/core/tools/e2b-provider/build.py @@ -16,7 +16,7 @@ SOURCE = Path('/source/services/core/tools/e2b-provider') OUTPUT = Path('/output') NAME = 'oac-e2b-provider' -BASE = 'python:3.12.12-slim-bookworm@sha256:2986c55feb36e6cae00fa1fefb454283e4b33f35e75ff8bdd123b134130be301' +BASE = next(line.split()[1] for line in (SOURCE / 'Build.Dockerfile').read_text().splitlines() if line.startswith('FROM ')) def checked(args, **options): @@ -24,8 +24,9 @@ def checked(args, **options): def main(): - if sys.version_info[:3] != (3, 12, 12) or platform.system() != 'Linux' or platform.machine() != 'x86_64': - raise RuntimeError('Use the pinned Linux amd64 build image') + if sys.version_info[:3] != (3, 12, 12) or platform.system() != 'Linux' or platform.machine() not in ('x86_64', 'aarch64'): + raise RuntimeError('Use the pinned Linux amd64 or arm64 build image') + architecture = {'x86_64': 'amd64', 'aarch64': 'arm64'}[platform.machine()] os.umask(0o022) with tempfile.TemporaryDirectory(prefix='e2b-build-') as temporary: root = Path(temporary) @@ -56,7 +57,7 @@ def main(): if report != {'Version': PROTOCOL_VERSION, 'SDKVersion': SDK_VERSION}: raise RuntimeError('Unexpected helper readiness report') manifest = {'format_version': 1, 'sdk_version': report['SDKVersion'], 'python_version': platform.python_version(), - 'platform': 'linux-amd64', 'libc': platform.libc_ver(), 'build_image': BASE, + 'platform': 'linux-' + architecture, 'libc': platform.libc_ver(), 'build_image': BASE, 'entrypoint': NAME, 'source_sha256': {file.name: hashlib.sha256(file.read_bytes()).hexdigest() for file in sorted(source.glob('*.py'))}, @@ -66,7 +67,7 @@ def main(): if entry.is_symlink() or not (entry.is_file() or entry.is_dir()): raise RuntimeError('Unsupported artifact entry') entry.chmod(0o755 if entry.is_dir() or entry.stat().st_mode & 0o111 else 0o644) - destination = OUTPUT / (NAME + '-linux-amd64.tar.gz') + destination = OUTPUT / (NAME + '-linux-' + architecture + '.tar.gz') with tarfile.open(destination, 'w:gz', dereference=True) as archive: archive.add(exported, arcname=NAME) checksum = hashlib.sha256(destination.read_bytes()).hexdigest() diff --git a/services/web/Dockerfile b/services/web/Dockerfile index 642383d04..cea7c0df0 100644 --- a/services/web/Dockerfile +++ b/services/web/Dockerfile @@ -1,5 +1,5 @@ # The build context contains only oac-web and the existing Web dist. -FROM gcr.io/distroless/static-debian13:nonroot@sha256:e754765ad9e167b0677b41c617fd44afb7b9818a477f48f17bda08e12cfb98cb +FROM gcr.io/distroless/static-debian13:nonroot@sha256:e2e927ec666bae08560abb3c55d0659eceabb657f56b6782ab500a9fc7f555e3 COPY --chmod=0555 oac-web /usr/local/bin/oac-web COPY dist /www From 9ede93563eaeff54273f9d13ec4b614ba4aac849 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 20:11:55 +0800 Subject: [PATCH 02/14] Delete the direct-call Harness factory (#484) Runs start only through the Executor, so agent.Factory, Runtime.Session, Registry.Resolve, the factories map and each adapter's direct-call factory had no production caller. RegisterKind now registers only the descriptor and model configuration, and Register rejects factories on an unavailable runtime. Code that only the factories reached is deleted, and the adapter tests run through the Executor path. --- .../claudesdk/cancellation_live_linux_test.go | 2 +- .../agent/claudesdk/cancellation_test.go | 6 +- .../agent/claudesdk/commands_session_test.go | 2 +- .../internal/agent/claudesdk/declaration.go | 5 +- .../agent/claudesdk/declaration_test.go | 6 - .../claudesdk/error_classification_test.go | 2 +- .../claudesdk/execution_controls_test.go | 4 +- .../agent/claudesdk/executor_fixture_test.go | 22 ++ .../agent/claudesdk/functions_test.go | 2 +- .../agent/claudesdk/live_linux_test.go | 6 +- .../internal/agent/claudesdk/messages_test.go | 2 +- .../agent/claudesdk/readiness_test.go | 6 +- .../agent/claudesdk/restrictions_test.go | 2 +- .../internal/agent/claudesdk/session.go | 21 -- .../internal/agent/claudesdk/session_test.go | 4 +- .../internal/agent/claudesdk/steering_test.go | 2 +- .../internal/agent/claudesdk/usage_test.go | 2 +- .../agent/claudesdk/workspace_launch_test.go | 4 +- .../claudesdk/workspace_live_linux_test.go | 2 +- .../internal/agent/codex/declaration.go | 2 +- .../internal/agent/codex/declaration_test.go | 2 +- .../agent/codex/mcp_http_preflight_test.go | 3 +- .../internal/agent/codex/mcp_required_test.go | 3 +- .../internal/agent/codex/preparation.go | 14 -- .../agent/codex/preparation_router_test.go | 5 +- apps/daemon/internal/agent/codex/prepared.go | 8 - .../internal/agent/codex/prepared_test.go | 4 +- apps/daemon/internal/agent/codex/session.go | 11 +- .../internal/agent/configuration_test.go | 11 +- apps/daemon/internal/agent/harness.go | 41 ++-- .../internal/agent/mcode/declaration.go | 2 +- .../agent/mcode/environment_mcp_test.go | 2 +- apps/daemon/internal/agent/mcode/executor.go | 6 +- .../internal/agent/mcode/executor_test.go | 19 +- .../internal/agent/mcode/executor_turn.go | 15 +- .../agent/mcode/mcp_observations_test.go | 2 +- .../agent/mcode/native_history_test.go | 37 +--- .../internal/agent/mcode/native_test.go | 12 +- apps/daemon/internal/agent/mcode/options.go | 3 - .../internal/agent/mcode/options_test.go | 3 - .../internal/agent/mcode/preparation.go | 3 +- .../internal/agent/mcode/preparation_test.go | 12 +- .../internal/agent/mcode/questions_test.go | 2 +- apps/daemon/internal/agent/mcode/session.go | 191 ++++-------------- .../internal/agent/mcode/session_test.go | 99 ++++++--- apps/daemon/internal/agent/mcode/steering.go | 23 +-- .../internal/agent/mcode/subagent_cancel.go | 15 -- .../internal/agent/mcode/subagents_test.go | 4 +- .../agent/mcode/tool_observations_test.go | 2 +- apps/daemon/internal/agent/registry.go | 36 +--- apps/daemon/internal/agent/registry_test.go | 130 ++++-------- .../internal/cli/connect_cleanup_test.go | 5 +- .../internal/cli/native_discovery_test.go | 11 +- apps/daemon/internal/cli/preparation_test.go | 10 +- .../dispatch/executor_cancel_receipt_test.go | 4 +- .../dispatch/executor_handoff_test.go | 5 +- .../daemon/internal/dispatch/executor_test.go | 8 +- .../internal/dispatch/local_directory_test.go | 9 +- .../daemon/internal/dispatch/mcp_http_test.go | 5 +- .../internal/dispatch/preparation_test.go | 4 +- .../internal/wireconformance/wire_test.go | 5 +- apps/daemon/testdata/onboarding/main.go | 4 +- contracts/agents-api/harness-onboarding.md | 14 +- contracts/agents-api/zh/harness-onboarding.md | 16 +- packages/claude-sdk-adapter/README.md | 4 +- 65 files changed, 307 insertions(+), 621 deletions(-) diff --git a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go index 7a958ed72..4cbab1ca5 100644 --- a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go @@ -66,7 +66,7 @@ func TestLiveClaudeSDKCancelResume(t *testing.T) { defer cancel() out := make(chan proto.Envelope, 64) request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."}} - running, err := NewFactory(config)(ctx, request, out) + running, err := startSingleTurn(ctx, config, request, out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/cancellation_test.go b/apps/daemon/internal/agent/claudesdk/cancellation_test.go index e9d4d2eb2..2ef7efc62 100644 --- a/apps/daemon/internal/agent/claudesdk/cancellation_test.go +++ b/apps/daemon/internal/agent/claudesdk/cancellation_test.go @@ -27,7 +27,7 @@ func TestCancellationWaitsForDrainAndPublishesOutcome(t *testing.T) { defer cancel() // A stopped consumer must not prevent native output draining or cancellation. out := make(chan proto.Envelope) - running, err := NewFactory(config)(ctx, cancellationRequest(), out) + running, err := startSingleTurn(ctx, config, cancellationRequest(), out) if err != nil { t.Fatal(err) } @@ -89,7 +89,7 @@ func TestFailureKeepsOnlyVerifiedNativeIdentity(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 8) - running, err := NewFactory(cancellationConfig(root, mode))(ctx, cancellationRequest(), out) + running, err := startSingleTurn(ctx, cancellationConfig(root, mode), cancellationRequest(), out) if err != nil { t.Fatal(err) } @@ -125,7 +125,7 @@ func TestCancellationDrainsIntoReadyConsumer(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := NewFactory(config)(ctx, cancellationRequest(), out) + running, err := startSingleTurn(ctx, config, cancellationRequest(), out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/commands_session_test.go b/apps/daemon/internal/agent/claudesdk/commands_session_test.go index bc62c8540..9789776c0 100644 --- a/apps/daemon/internal/agent/claudesdk/commands_session_test.go +++ b/apps/daemon/internal/agent/claudesdk/commands_session_test.go @@ -77,7 +77,7 @@ func TestWorkspaceCommandCancellationAndBridgeFailuresCloseOnlyPendingCalls(t *t req := workspaceRequest() req.AgentSessionID = "native-session" out := make(chan proto.Envelope, 32) - s, err := NewFactory(config)(ctx, req, out) + s, err := startSingleTurn(ctx, config, req, out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/declaration.go b/apps/daemon/internal/agent/claudesdk/declaration.go index 21a643f88..bbe3bdd3a 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration.go +++ b/apps/daemon/internal/agent/claudesdk/declaration.go @@ -59,9 +59,7 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, d if entrypoint == "" { return nil } - out := &agent.Runtime{Info: descriptor, Session: func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, fmt.Errorf("claude_sdk: configured runtime is unavailable") - }} + out := &agent.Runtime{Info: descriptor} var config Config fail := func(err error) *agent.Runtime { @@ -141,7 +139,6 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, d out.Info.Capabilities.MCPHTTPTools, out.Info.Capabilities.MCPHTTPBearerAuth = proto.CapabilityUnsupported, proto.CapabilityUnsupported out.Info.Capabilities.MCPHTTPRequired = proto.CapabilityUnsupported } - out.Session = NewFactory(config) out.Executor = NewExecutorFactory(config) if out.Info.Capabilities.LocalEnvironment.IsSupported() { out.Preparation = NewPreparationFactory(config) diff --git a/apps/daemon/internal/agent/claudesdk/declaration_test.go b/apps/daemon/internal/agent/claudesdk/declaration_test.go index 149ef8f65..6ff1a6350 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration_test.go +++ b/apps/daemon/internal/agent/claudesdk/declaration_test.go @@ -125,12 +125,6 @@ func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) { if info.Capabilities.Preparation.IsSupported() != ready { t.Fatal(info) } - if !ready { - factory, _ := registry.Resolve("claude_sdk") - if _, err := factory(t.Context(), proto.PromptRequestPayload{}, nil); err == nil || !strings.Contains(err.Error(), "runtime is unavailable") { - t.Fatal(err) - } - } } } } diff --git a/apps/daemon/internal/agent/claudesdk/error_classification_test.go b/apps/daemon/internal/agent/claudesdk/error_classification_test.go index 737d3d173..6ebc1eab2 100644 --- a/apps/daemon/internal/agent/claudesdk/error_classification_test.go +++ b/apps/daemon/internal/agent/claudesdk/error_classification_test.go @@ -25,7 +25,7 @@ func TestClassifiedBridgeFailurePreservesTerminalEvidence(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - s, err := NewFactory(config)(ctx, req, out) + s, err := startSingleTurn(ctx, config, req, out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go index 4f79035ce..0381cf750 100644 --- a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go +++ b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go @@ -50,7 +50,7 @@ func TestExecutionControlsRejectUnsupportedProfilesBeforeLaunch(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: "must-not-run", Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Original input."), ExecutionControls: &controls, AgentOptions: map[string]any{"model": "native-model"}} - _, err := NewFactory(config)(t.Context(), request, make(chan proto.Envelope, 1)) + _, err := startSingleTurn(t.Context(), config, request, make(chan proto.Envelope, 1)) if err == nil || !strings.Contains(err.Error(), "execution controls require") { t.Fatal("unsupported controls did not fail at admission", err) } @@ -67,7 +67,7 @@ func TestMCPWithoutEnvironmentNoneRejectedBeforeSetup(t *testing.T) { config := Config{Node: "must-not-run", Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Input"), MCPHTTPServers: &servers} - _, err := NewFactory(config)(t.Context(), request, make(chan proto.Envelope, 1)) + _, err := startSingleTurn(t.Context(), config, request, make(chan proto.Envelope, 1)) if err == nil || !strings.Contains(err.Error(), "service-origin MCP requires a service execution host") { t.Fatal("MCP reached an unsupported environment", err) } diff --git a/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go b/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go index 55768099f..15d683ccf 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go @@ -4,10 +4,32 @@ package claudesdk import ( "bufio" + "context" "encoding/json" "os" + + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) +// startSingleTurn prepares an Executor for one Turn and closes it once that +// Turn settles, so each test observes the complete native lifecycle. +func startSingleTurn(ctx context.Context, config Config, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Turn, error) { + run, input := req.RunID, req.Input + req.RunID, req.Input = "", nil + resource, err := NewExecutorFactory(config)(ctx, req) + if err != nil { + return nil, err + } + turn, err := resource.StartTurn(ctx, run, input, out) + if turn == nil { + _ = resource.Close(context.Background()) + return nil, err + } + go func() { _, _ = turn.AwaitSettlement(context.Background()); _ = resource.Close(context.Background()) }() + return turn, err +} + func helperTurn(scanner *bufio.Scanner, request *startRequest) (func(bridgeEvent), func()) { _ = json.NewEncoder(os.Stdout).Encode(bridgeEvent{Type: "executor_ready", Protocol: 3}) if !scanner.Scan() { diff --git a/apps/daemon/internal/agent/claudesdk/functions_test.go b/apps/daemon/internal/agent/claudesdk/functions_test.go index a448790e3..d9c358b83 100644 --- a/apps/daemon/internal/agent/claudesdk/functions_test.go +++ b/apps/daemon/internal/agent/claudesdk/functions_test.go @@ -26,7 +26,7 @@ func TestFunctionFactoryNativeReceipts(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := NewFactory(config)(ctx, request, out) + running, err := startSingleTurn(ctx, config, request, out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/live_linux_test.go b/apps/daemon/internal/agent/claudesdk/live_linux_test.go index 72aff690d..1f534d1a4 100644 --- a/apps/daemon/internal/agent/claudesdk/live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/live_linux_test.go @@ -142,7 +142,7 @@ func TestLiveClaudeSDKTextResume(t *testing.T) { request.AgentOptions["system_prompt"] = "Call lookup exactly once as requested, then report both result parts and any prior verification value. Never retry a failed tool." request.FunctionTools = []proto.FunctionTool{{Name: "lookup", Description: "Return a synthetic verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}`)}} } - running, err := NewFactory(config)(ctx, request, out) + running, err := startSingleTurn(ctx, config, request, out) if err != nil { t.Fatal(err) } @@ -268,7 +268,7 @@ func TestLiveClaudeSDKTextResume(t *testing.T) { proof.SessionID, _ = payload.Metadata[proto.DoneMetaAgentSessionID].(string) } } - // Done reports the Turn outcome; the direct factory closes its Executor + // Done reports the Turn outcome; startSingleTurn closes its Executor // after output settlement. Verify release at that boundary. if _, err := s.AwaitSettlement(ctx); err != nil { t.Fatal(err) @@ -276,7 +276,7 @@ func TestLiveClaudeSDKTextResume(t *testing.T) { select { case <-s.process.Done(): case <-time.After(5 * time.Second): - t.Fatal("direct factory retained its process after settlement") + t.Fatal("single-Turn Executor retained its process after settlement") } if !steeringAt.IsZero() { receipt := <-steeringReply diff --git a/apps/daemon/internal/agent/claudesdk/messages_test.go b/apps/daemon/internal/agent/claudesdk/messages_test.go index c0591c668..feaf594e7 100644 --- a/apps/daemon/internal/agent/claudesdk/messages_test.go +++ b/apps/daemon/internal/agent/claudesdk/messages_test.go @@ -24,7 +24,7 @@ func TestMessageObservations(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := NewFactory(config)(ctx, request, out) + running, err := startSingleTurn(ctx, config, request, out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/readiness_test.go b/apps/daemon/internal/agent/claudesdk/readiness_test.go index 49b0d5260..466d6e38e 100644 --- a/apps/daemon/internal/agent/claudesdk/readiness_test.go +++ b/apps/daemon/internal/agent/claudesdk/readiness_test.go @@ -25,7 +25,7 @@ func TestRequiredMCPNeedsQualifiedRuntime(t *testing.T) { }} req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, AgentOptions: map[string]any{"model": "fixture"}, MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", Required: true}}} - if _, err := NewFactory(config)(t.Context(), req, make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support required HTTP MCP" { + if _, err := startSingleTurn(t.Context(), config, req, make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support required HTTP MCP" { t.Fatalf("unqualified runtime executed required MCP: %v", err) } } @@ -46,7 +46,7 @@ func TestHTTPMCPRejectsOldPackagedRuntime(t *testing.T) { } req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, AgentOptions: map[string]any{"model": "fixture"}, MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp"}}} - if _, err := NewFactory(config)(t.Context(), req, make(chan proto.Envelope, 1)); err == nil || !strings.Contains(err.Error(), "packaged runtime does not support HTTP MCP") { + if _, err := startSingleTurn(t.Context(), config, req, make(chan proto.Envelope, 1)); err == nil || !strings.Contains(err.Error(), "packaged runtime does not support HTTP MCP") { t.Fatalf("old runtime was not rejected before execution: %v", err) } } @@ -83,7 +83,7 @@ func TestMCPBearerRejectsAnonymousOnlyRuntimeWithoutProbeSecrets(t *testing.T) { token := "private-fixture-token" req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, AgentOptions: map[string]any{"model": "fixture"}, MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", BearerToken: &token}}} - if _, err := NewFactory(config)(t.Context(), req, make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support authenticated HTTP MCP" { + if _, err := startSingleTurn(t.Context(), config, req, make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support authenticated HTTP MCP" { t.Fatalf("old runtime executed authenticated request or readiness received its secret: %v", err) } } diff --git a/apps/daemon/internal/agent/claudesdk/restrictions_test.go b/apps/daemon/internal/agent/claudesdk/restrictions_test.go index e0b7392de..a574a5009 100644 --- a/apps/daemon/internal/agent/claudesdk/restrictions_test.go +++ b/apps/daemon/internal/agent/claudesdk/restrictions_test.go @@ -29,7 +29,7 @@ func TestTextFactoryAcceptsRestrictiveCapabilities(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := NewFactory(config)(ctx, request, out) + running, err := startSingleTurn(ctx, config, request, out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/session.go b/apps/daemon/internal/agent/claudesdk/session.go index e6dba3fde..cf241ef07 100644 --- a/apps/daemon/internal/agent/claudesdk/session.go +++ b/apps/daemon/internal/agent/claudesdk/session.go @@ -33,27 +33,6 @@ type session struct { outcome proto.DonePayload } -func NewFactory(config Config) agent.Factory { - prepareExecutor := NewExecutorFactory(config) - return func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { - run, input := req.RunID, req.Input - req.RunID, req.Input = "", nil - resource, err := prepareExecutor(ctx, req) - if err != nil { - return nil, err - } - turn, err := resource.StartTurn(ctx, run, input, out) - if turn == nil { - _ = resource.Close(context.Background()) - return nil, err - } - // Factory callers own one execution. Both entrypoints use the same - // Executor implementation; Runtime pooling uses NewExecutorFactory. - go func() { _, _ = turn.AwaitSettlement(context.Background()); _ = resource.Close(context.Background()) }() - return turn, err - } -} - type bridgeEvent struct { EngineErrorCode json.RawMessage `json:"engine_error_code"` TurnID string `json:"turn_id"` diff --git a/apps/daemon/internal/agent/claudesdk/session_test.go b/apps/daemon/internal/agent/claudesdk/session_test.go index 3ea42c618..30899cfc7 100644 --- a/apps/daemon/internal/agent/claudesdk/session_test.go +++ b/apps/daemon/internal/agent/claudesdk/session_test.go @@ -26,7 +26,7 @@ func TestTextFactoryCompletionAndFailures(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - s, err := NewFactory(config)(ctx, request, out) + s, err := startSingleTurn(ctx, config, request, out) if err != nil { t.Fatal(err) } @@ -88,7 +88,7 @@ func TestTextFactoryRejectsUnsupportedInput(t *testing.T) { case "outside": config.StateDir = filepath.Dir(root) } - _, err := NewFactory(config)(context.Background(), request, make(chan proto.Envelope, 1)) + _, err := startSingleTurn(context.Background(), config, request, make(chan proto.Envelope, 1)) if err == nil || !strings.HasPrefix(err.Error(), "claudesdk:") { t.Fatalf("expected pre-launch rejection, got %v", err) } diff --git a/apps/daemon/internal/agent/claudesdk/steering_test.go b/apps/daemon/internal/agent/claudesdk/steering_test.go index 5f83e13d4..b59f5107f 100644 --- a/apps/daemon/internal/agent/claudesdk/steering_test.go +++ b/apps/daemon/internal/agent/claudesdk/steering_test.go @@ -31,7 +31,7 @@ func TestSteeringReceiptsAndLifecycle(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := NewFactory(config)(ctx, request, out) + running, err := startSingleTurn(ctx, config, request, out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/usage_test.go b/apps/daemon/internal/agent/claudesdk/usage_test.go index 6780ef95d..0c3af818f 100644 --- a/apps/daemon/internal/agent/claudesdk/usage_test.go +++ b/apps/daemon/internal/agent/claudesdk/usage_test.go @@ -27,7 +27,7 @@ func TestUsageTransportPreservesSnapshotOnFailureAndDone(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - s, err := NewFactory(config)(ctx, request, out) + s, err := startSingleTurn(ctx, config, request, out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go b/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go index 1f310776d..861312450 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go @@ -41,7 +41,7 @@ esac t.Fatal("readiness did not receive replacement environment", err) } out := make(chan proto.Envelope, 8) - s, err := NewFactory(config)(t.Context(), workspaceRequest(), out) + s, err := startSingleTurn(t.Context(), config, workspaceRequest(), out) if err != nil { t.Fatal(err) } @@ -64,7 +64,7 @@ esac if err := os.WriteFile(config.Node, []byte(script), 0o700); err != nil { t.Fatal(err) } - if _, err := NewFactory(config)(t.Context(), workspaceRequest(), out); err == nil { + if _, err := startSingleTurn(t.Context(), config, workspaceRequest(), out); err == nil { t.Fatal("old packaged bridge accepted workspace execution") } if _, err := os.Stat(filepath.Join(config.StateDir, "unexpected-start")); !os.IsNotExist(err) { diff --git a/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go index 3acd44b81..d1c1f9417 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go @@ -138,7 +138,7 @@ func testLiveClaudeWorkspace(t *testing.T, explicitPreparation bool) { } } } else { - running, err = NewFactory(config)(ctx, req, out) + running, err = startSingleTurn(ctx, config, req, out) } if err != nil { if name == "missing-history" && running == nil && strings.Contains(err.Error(), "history_unavailable") { diff --git a/apps/daemon/internal/agent/codex/declaration.go b/apps/daemon/internal/agent/codex/declaration.go index 26bb61c4e..2869f9da0 100644 --- a/apps/daemon/internal/agent/codex/declaration.go +++ b/apps/daemon/internal/agent/codex/declaration.go @@ -50,7 +50,7 @@ func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.Su return discoverWithCheck(ctx, options, info, CheckCLIAvailable) } func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, info proto.SupportedAgentKind, check func(context.Context, string) (string, error)) *agent.Runtime { - runtime := &agent.Runtime{Info: info, Session: Factory} + runtime := &agent.Runtime{Info: info} ctx, cancel := context.WithTimeout(parent, 15*time.Second) defer cancel() version, err := check(ctx, "") diff --git a/apps/daemon/internal/agent/codex/declaration_test.go b/apps/daemon/internal/agent/codex/declaration_test.go index b26acf131..b57d13e9a 100644 --- a/apps/daemon/internal/agent/codex/declaration_test.go +++ b/apps/daemon/internal/agent/codex/declaration_test.go @@ -49,7 +49,7 @@ func TestDeclaredCapabilityBaseline(t *testing.T) { func TestUnavailableRuntimeHasNoExecutionFactories(t *testing.T) { runtime := discoverWithCheck(t.Context(), agent.DiscoveryOptions{Stdout: io.Discard, Stderr: io.Discard}, Declaration.Info, func(context.Context, string) (string, error) { return "", errors.New("missing") }) - if runtime.Info.Available || runtime.Executor != nil || runtime.Preparation != nil || runtime.Session == nil { + if runtime.Info.Available || runtime.Executor != nil || runtime.Preparation != nil { t.Fatalf("unavailable runtime: %+v", runtime) } } diff --git a/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go b/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go index 40c09abdd..20b42094b 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go @@ -173,10 +173,11 @@ func TestPublicMCPHTTPPreparationChecksBeforeNewAndResumedThread(t *testing.T) { if err != nil { t.Fatal(err) } - s, err := p.start(t.Context(), "actual-run", proto.TextInput("actual prompt"), make(chan proto.Envelope, 8)) + started, err := p.Start(t.Context(), "actual-run", proto.TextInput("actual prompt"), make(chan proto.Envelope, 8)) if err != nil { t.Fatal(err) } + s := started.(*Session) defer s.Cancel(context.Background()) frames := waitPreparationMethod(t, root, "turn/start") checked, statuses := false, 0 diff --git a/apps/daemon/internal/agent/codex/mcp_required_test.go b/apps/daemon/internal/agent/codex/mcp_required_test.go index 44b106a43..18084cc08 100644 --- a/apps/daemon/internal/agent/codex/mcp_required_test.go +++ b/apps/daemon/internal/agent/codex/mcp_required_test.go @@ -40,10 +40,11 @@ func TestRequiredMCPWaitsForNativeThreadAndNeverRestartsFailedResume(t *testing. } defer p.Close() out := make(chan proto.Envelope, 16) - s, err := p.start(t.Context(), "required-run", proto.TextInput("actual prompt"), out) + started, err := p.Start(t.Context(), "required-run", proto.TextInput("actual prompt"), out) if err != nil { t.Fatal(err) } + s := started.(*Session) defer s.Cancel(context.Background()) waitPreparationMethod(t, root, method) time.Sleep(100 * time.Millisecond) diff --git a/apps/daemon/internal/agent/codex/preparation.go b/apps/daemon/internal/agent/codex/preparation.go index 0d3a4bdf9..729934ae9 100644 --- a/apps/daemon/internal/agent/codex/preparation.go +++ b/apps/daemon/internal/agent/codex/preparation.go @@ -20,20 +20,6 @@ func Prepare(owner context.Context, req proto.PromptRequestPayload) (*Prepared, return newPreparation(owner, req, defaultSessionConfig()) } -func newSession(parent context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope, cfg sessionConfig) (*Session, error) { - if out == nil { - return nil, errors.New("codex: nil out channel") - } - runID, prompt := req.RunID, req.Input - req.RunID, req.Input = "", nil - prepared, err := newPreparation(parent, req, cfg) - if err != nil { - return nil, err - } - defer prepared.Close() - return prepared.start(parent, runID, prompt, out) -} - func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg sessionConfig) (*Prepared, error) { if req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil { return nil, errors.New("codex: structured output is not qualified") diff --git a/apps/daemon/internal/agent/codex/preparation_router_test.go b/apps/daemon/internal/agent/codex/preparation_router_test.go index 660c94504..35f2b64f0 100644 --- a/apps/daemon/internal/agent/codex/preparation_router_test.go +++ b/apps/daemon/internal/agent/codex/preparation_router_test.go @@ -4,7 +4,6 @@ import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" import ( "context" - "errors" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/google/uuid" @@ -59,9 +58,7 @@ func TestPreparationRouterRetainsActualNativeChild(t *testing.T) { req.DisableExecutionEnvironment = false req.LocalEnvironment = &proto.LocalEnvironment{ID: environment, WorkspaceDirectory: "/workspace", NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}} registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("ordinary Factory must not run") - }) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, harnessconfig.Configuration{}) prepared := make(chan *Prepared, 1) registry.RegisterExecutor("codex", func(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { e, err := newExecutor(ctx, req, cfg) diff --git a/apps/daemon/internal/agent/codex/prepared.go b/apps/daemon/internal/agent/codex/prepared.go index 1f4e049f0..0049ea8e1 100644 --- a/apps/daemon/internal/agent/codex/prepared.go +++ b/apps/daemon/internal/agent/codex/prepared.go @@ -30,14 +30,6 @@ var _ agent.PreparedCancellation = (*Prepared)(nil) // cancellation after return does not cancel the transferred Session. The original // owner context remains its lifetime context. On success the Session owns out. func (p *Prepared) Start(ctx context.Context, runID string, prompt proto.MessageInput, out chan<- proto.Envelope) (agent.Session, error) { - session, err := p.start(ctx, runID, prompt, out) - if err != nil { - return nil, err - } - return session, nil -} - -func (p *Prepared) start(ctx context.Context, runID string, prompt proto.MessageInput, out chan<- proto.Envelope) (*Session, error) { if out == nil || strings.TrimSpace(runID) == "" || prompt.Validate() != nil { return nil, errors.New("codex: start requires a run identity, prompt and output channel") } diff --git a/apps/daemon/internal/agent/codex/prepared_test.go b/apps/daemon/internal/agent/codex/prepared_test.go index e729c9613..89d9ab0bf 100644 --- a/apps/daemon/internal/agent/codex/prepared_test.go +++ b/apps/daemon/internal/agent/codex/prepared_test.go @@ -169,9 +169,9 @@ func TestPreparedSessionConcurrentStartAndClose(t *testing.T) { go func() { defer wg.Done() <-begin - s, err := p.start(t.Context(), "run", proto.TextInput("prompt"), make(chan proto.Envelope, 8)) + s, err := p.Start(t.Context(), "run", proto.TextInput("prompt"), make(chan proto.Envelope, 8)) if err == nil { - started <- s + started <- s.(*Session) } }() } diff --git a/apps/daemon/internal/agent/codex/session.go b/apps/daemon/internal/agent/codex/session.go index 6c72ec886..a69f3944b 100644 --- a/apps/daemon/internal/agent/codex/session.go +++ b/apps/daemon/internal/agent/codex/session.go @@ -20,8 +20,8 @@ import ( // adapter safety net. const terminalSendTimeout = 2 * time.Second -// sessionConfig is the cross-cutting knob bag — production callers go -// through Factory which uses defaults. +// sessionConfig is the cross-cutting knob bag; production callers use +// defaultSessionConfig. type sessionConfig struct { codexBinary string logger *slog.Logger @@ -36,13 +36,6 @@ func defaultSessionConfig() sessionConfig { } } -// Factory implements agent.Factory for agent_kind="codex". Spawns one -// codex app-server child for one Turn. The run stream closes when the turn -// completes; the child remains until the caller cancels the Session. -func Factory(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { - return newSession(ctx, req, out, defaultSessionConfig()) -} - // Session implements agent.Session. State lifecycle: // // 1. Preparation initializes RPC and verifies the selected environment. diff --git a/apps/daemon/internal/agent/configuration_test.go b/apps/daemon/internal/agent/configuration_test.go index a186755ce..19d5b8e22 100644 --- a/apps/daemon/internal/agent/configuration_test.go +++ b/apps/daemon/internal/agent/configuration_test.go @@ -17,10 +17,7 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { configuration := harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "responses"}}} calls := 0 expected := errors.New("native entry reached") - registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, configuration, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - calls++ - return nil, expected - }) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, configuration) registry.RegisterExecutor("fixture", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { calls++ return nil, expected @@ -30,11 +27,9 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { return nil, expected }) configuration.Providers[0].Protocol = "anthropic" - factory, _ := registry.Resolve("fixture") executor, _ := registry.ResolveExecutor("fixture") preparation, _ := registry.ResolvePreparation("fixture") entries := []func(proto.PromptRequestPayload) error{ - func(req proto.PromptRequestPayload) error { _, err := factory(t.Context(), req, nil); return err }, func(req proto.PromptRequestPayload) error { _, err := executor(t.Context(), req); return err }, func(req proto.PromptRequestPayload) error { _, err := preparation(t.Context(), req); return err }, } @@ -55,7 +50,7 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { t.Fatal("bound declaration was lost or mutated", err) } } - if calls != 3 { + if calls != 2 { t.Fatal("unexpected native calls", calls) } } @@ -66,5 +61,5 @@ func TestRegistryRejectsInvalidConfigurationDeclaration(t *testing.T) { t.Fatal("invalid configuration registered") } }() - agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "unknown"}}}, stubFactory("fixture")) + agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "unknown"}}}) } diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index d199aa71f..ee08be11e 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -14,8 +14,8 @@ // Registration: each adapter exports one Declaration. The Runtime discovers the // static declaration list and installs each resulting Runtime through Register. // Availability and factory selection belong to the adapter. RegisterKind resets -// the other factories, so Register installs it first. Preparation capabilities -// are derived from the declared factories. +// the factories, so Register installs it first. Preparation capabilities are +// derived from the declared factories. // // Runtime registration and Core service qualification remain separate. A public // Harness also needs a profile in services/core/internal/engine; advertising @@ -35,7 +35,7 @@ import ( // Declaration is the complete startup contract for a Harness implementation. // Discover returns nil when the adapter is not configured. An unavailable -// configured adapter returns a Runtime with Available=false and a session factory. +// configured adapter returns a Runtime with Available=false and no factories. // Discovery owns runtime-specific configuration, readiness and feature gates. type Declaration struct { Info proto.SupportedAgentKind @@ -52,7 +52,6 @@ type DiscoveryOptions struct { // Runtime binds one discovered descriptor to its native factories. type Runtime struct { Info proto.SupportedAgentKind - Session Factory Preparation PreparationFactory Executor ExecutorFactory WorkspaceReadPreparation bool @@ -63,7 +62,10 @@ func (r *Registry) Register(declaration Declaration, runtime Runtime) { if runtime.Info.Kind != declaration.Info.Kind { panic("agent.Registry.Register: discovery kind differs from declaration") } - r.RegisterKind(runtime.Info, declaration.Configuration, runtime.Session) + if !runtime.Info.Available && (runtime.Executor != nil || runtime.Preparation != nil) { + panic("agent.Registry.Register: unavailable runtime has factories") + } + r.RegisterKind(runtime.Info, declaration.Configuration) if runtime.Executor != nil { r.RegisterExecutor(runtime.Info.Kind, runtime.Executor) } @@ -119,8 +121,8 @@ type TurnSettlement struct { Reason string } -// Session is the cancellation and outcome surface shared by direct-call -// sessions and Turns. Every owner exposes observed state. +// Session is the cancellation and outcome surface shared by Turn and +// PreparedCancellation. Every owner exposes observed state. // For Executor-owned Turns, AwaitSettlement and Executor.Close define settlement // and resource retirement; Cancel alone does not transfer resource ownership. type Session interface { @@ -227,25 +229,16 @@ type PreparedCancellation interface { // cleanup remains unconfirmed. The caller must retain and close that resource. type PreparationFactory func(context.Context, proto.PromptRequestPayload) (Prepared, error) -// Kind registration and the direct-call factory. - -// Factory builds a Session that runs req.Input as req.RunID without an -// Executor; the router starts every Run through RegisterExecutor instead. out -// is the channel the agent writes into and closes exactly once after terminal -// output. ctx is cancelled to wind the session down. -type Factory func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (Session, error) +// Kind registration. -// RegisterKind installs f and the heartbeat descriptor for an -// agent_kind. Callers may set Available=false when an adapter exists -// but its underlying CLI is not usable. -func (r *Registry) RegisterKind(info proto.SupportedAgentKind, configuration harnessconfig.Configuration, f Factory) { +// RegisterKind installs the heartbeat descriptor and model configuration for an +// agent_kind. Callers may set Available=false when an adapter exists but its +// underlying CLI is not usable. +func (r *Registry) RegisterKind(info proto.SupportedAgentKind, configuration harnessconfig.Configuration) { kind := info.Kind if kind == "" { panic("agent.Registry.Register: empty kind") } - if f == nil { - panic("agent.Registry.Register: nil factory") - } if err := configuration.ValidateDeclaration(); err != nil { panic(err) } @@ -256,12 +249,6 @@ func (r *Registry) RegisterKind(info proto.SupportedAgentKind, configuration har r.mu.Lock() defer r.mu.Unlock() r.configurations[kind] = configuration - r.factories[kind] = func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (Session, error) { - if _, err := configuration.Prepare(req.AgentOptions); err != nil { - return nil, err - } - return f(ctx, req, out) - } delete(r.preparers, kind) delete(r.executors, kind) info.Capabilities.Preparation = proto.CapabilityUnsupported diff --git a/apps/daemon/internal/agent/mcode/declaration.go b/apps/daemon/internal/agent/mcode/declaration.go index c97e57bea..b3f257ad7 100644 --- a/apps/daemon/internal/agent/mcode/declaration.go +++ b/apps/daemon/internal/agent/mcode/declaration.go @@ -47,7 +47,7 @@ func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.Su return discoverWithCheck(ctx, options, info, CheckCLIAvailable) } func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, result proto.SupportedAgentKind, check func(context.Context, string) (string, error)) *agent.Runtime { - runtime := &agent.Runtime{Info: result, Session: Factory} + runtime := &agent.Runtime{Info: result} ctx, cancel := context.WithTimeout(parent, 15*time.Second) defer cancel() diff --git a/apps/daemon/internal/agent/mcode/environment_mcp_test.go b/apps/daemon/internal/agent/mcode/environment_mcp_test.go index aa143e5aa..564227bdb 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp_test.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp_test.go @@ -219,7 +219,7 @@ func TestEnvironmentHTTPMCPUsesEphemeralACPConfiguration(t *testing.T) { } else if len(server.Headers) != 0 { t.Fatal("anonymous MCP inherited credentials") } - dataDir := resource.(*prepared).session.opts.DataDir + dataDir := resource.(*prepared).executor.opts.DataDir for _, name := range []string{"config.yaml", "mcp.json", "workspace-profile.json"} { body, err := os.ReadFile(filepath.Join(dataDir, name)) if err != nil && !os.IsNotExist(err) { diff --git a/apps/daemon/internal/agent/mcode/executor.go b/apps/daemon/internal/agent/mcode/executor.go index 1f9ecf907..33ea18ed9 100644 --- a/apps/daemon/internal/agent/mcode/executor.go +++ b/apps/daemon/internal/agent/mcode/executor.go @@ -63,7 +63,7 @@ func NewExecutorFactory(config *WorkspaceConfig) agent.ExecutorFactory { } func newExecutor(ctx context.Context, req proto.PromptRequestPayload, opts launchOptions, binary string) (*executor, error) { - bootstrap, err := launch(ctx, req, opts, binary, nil) + bootstrap, err := launch(ctx, req, opts, binary) if err != nil { return nil, err } @@ -126,14 +126,14 @@ func (e *executor) StartTurn(ctx context.Context, runID string, input proto.Mess default: } req := e.req - req.RunID, req.Input = runID, proto.TextInput(text) + req.RunID = runID s := newTurnSession(e.connection.process.Context(), req, e.opts, e.connection, out) s.executor, s.sessionID, s.nativeModel = e, e.nativeSession, e.model s.settled, s.inputDone = make(chan struct{}), make(chan struct{}) s.outputContext, s.outputCancel = context.WithCancel(context.Background()) e.active = s e.connection.setCurrent(s) - go s.runExecutorTurn() + go s.runExecutorTurn(text) go func() { select { case <-ctx.Done(): diff --git a/apps/daemon/internal/agent/mcode/executor_test.go b/apps/daemon/internal/agent/mcode/executor_test.go index 52d7bdbd5..979958263 100644 --- a/apps/daemon/internal/agent/mcode/executor_test.go +++ b/apps/daemon/internal/agent/mcode/executor_test.go @@ -189,12 +189,21 @@ func TestExecutorCancellationRetiresOwnerAndLateCancelCannotRetarget(t *testing. func TestExecutorStartFailureOwnership(t *testing.T) { t.Run("validation", func(t *testing.T) { e, record := executorFixture(t, "executor-reuse", true) - out := make(chan proto.Envelope, 1) - turn, err := e.StartTurn(t.Context(), "", proto.TextInput("invalid"), out) - if err == nil || turn != nil { - t.Fatal("invalid Start acquired output") + image := "data:image/png;base64,iVBORw0KGgo=" + for _, invalid := range []struct { + id, reason string + input proto.MessageInput + }{ + {"", "requires live context, identity", proto.TextInput("invalid")}, + {"attachment", "does not support image input", proto.MessageInput{{Content: []proto.InputContent{{Type: "input_image", ImageURL: &image}}}}}, + } { + out := make(chan proto.Envelope, 1) + turn, err := e.StartTurn(t.Context(), invalid.id, invalid.input, out) + if err == nil || turn != nil || !strings.Contains(err.Error(), invalid.reason) { + t.Fatal("invalid Start acquired output", err) + } + close(out) } - close(out) raw, _ := os.ReadFile(record) if strings.Contains(string(raw), "session/prompt") { t.Fatal("invalid input was sent") diff --git a/apps/daemon/internal/agent/mcode/executor_turn.go b/apps/daemon/internal/agent/mcode/executor_turn.go index 1633f5f18..e9e2dcc17 100644 --- a/apps/daemon/internal/agent/mcode/executor_turn.go +++ b/apps/daemon/internal/agent/mcode/executor_turn.go @@ -10,10 +10,10 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func (s *Session) runExecutorTurn() { +func (s *Session) runExecutorTurn(prompt string) { err := s.captureSubagentBaseline() if err == nil { - err = s.executePrompt() + err = s.executePrompt(prompt) } if errors.Is(err, errTurnCancelled) { err = nil @@ -26,11 +26,7 @@ func (s *Session) runExecutorTurn() { // prompt completion. No successor starts until all callers have settled. s.operations.Wait() if !s.req.DisableSubagents && s.subagentHistoryReady { - childErr := s.settleSubagents() - s.mu.Lock() - s.subagentSettlementError = childErr - s.mu.Unlock() - if childErr != nil { + if childErr := s.settleSubagents(); childErr != nil { err = childErr } } @@ -99,9 +95,6 @@ func (s *Session) captureSubagentBaseline() error { } func (s *Session) AwaitSettlement(ctx context.Context) (agent.TurnSettlement, error) { - if s.settled == nil { - return agent.TurnSettlement{}, fmt.Errorf("mcode: Turn has no Executor owner") - } select { case <-s.settled: return s.settlement, s.settlementErr @@ -110,7 +103,7 @@ func (s *Session) AwaitSettlement(ctx context.Context) (agent.TurnSettlement, er } } -func (s *Session) cancelTurn(ctx context.Context) error { +func (s *Session) Cancel(ctx context.Context) error { e := s.executor e.mu.Lock() if e.active != s { diff --git a/apps/daemon/internal/agent/mcode/mcp_observations_test.go b/apps/daemon/internal/agent/mcode/mcp_observations_test.go index 61e090a6b..9f613f1a9 100644 --- a/apps/daemon/internal/agent/mcode/mcp_observations_test.go +++ b/apps/daemon/internal/agent/mcode/mcp_observations_test.go @@ -14,7 +14,7 @@ import ( func mcpObservationSession(t *testing.T) (*Session, chan proto.Envelope) { t.Helper() out := make(chan proto.Envelope, 16) - s := &Session{ctx: context.Background(), opts: launchOptions{DataDir: t.TempDir()}, + s := &Session{ctx: context.Background(), outputContext: context.Background(), opts: launchOptions{DataDir: t.TempDir()}, req: proto.PromptRequestPayload{RunID: "run", LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{environmentMCPFixture()}}}, out: out, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}, active: true, sessionID: "native-session"} diff --git a/apps/daemon/internal/agent/mcode/native_history_test.go b/apps/daemon/internal/agent/mcode/native_history_test.go index d49061554..dc34007b5 100644 --- a/apps/daemon/internal/agent/mcode/native_history_test.go +++ b/apps/daemon/internal/agent/mcode/native_history_test.go @@ -7,12 +7,11 @@ import ( "strings" "testing" "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) // A successful public real-model run supplies an actual foreign native ID. -// Neither that history nor a missing ID may silently become a new session. +// Neither that history nor a missing ID may prepare an Executor, so no Turn can +// run against it or silently replace it with a new session. func TestNativeMCodeHistoryIsolation(t *testing.T) { binary, options, foreign := os.Getenv("OAC_RUNTIME_MCODE_BIN"), os.Getenv("OAC_TEST_MCODE_REAL_OPTIONS"), os.Getenv("OAC_TEST_MCODE_FOREIGN_NATIVE_ID") if binary == "" || options == "" || foreign == "" { @@ -28,37 +27,11 @@ func TestNativeMCodeHistoryIsolation(t *testing.T) { if json.Unmarshal(raw, &req.AgentOptions) != nil { t.Fatal("invalid private options") } - req.AgentSessionID, req.Input = id, proto.TextInput("This input must never execute.") + req.AgentSessionID = id ctx, cancel := context.WithTimeout(t.Context(), 90*time.Second) defer cancel() - out := make(chan proto.Envelope, 64) - session, err := newSession(ctx, req, out, binary) - if err != nil { - t.Fatal(err) - } - defer func() { - cleanup, stop := context.WithTimeout(context.Background(), 10*time.Second) - defer stop() - if err := session.Cancel(cleanup); err != nil { - t.Error(err) - } - }() - rejected := false - for event := range out { - if event.Type == proto.TypeError { - var failure proto.ErrorPayload - _ = json.Unmarshal(event.Payload, &failure) - rejected = strings.Contains(failure.Error, "session/load:") && !strings.Contains(failure.Error, "deadline exceeded") - } - if event.Type == proto.TypeDone { - var done proto.DonePayload - _ = json.Unmarshal(event.Payload, &done) - if done.Content != "" || done.Metadata[proto.DoneMetaAgentSessionID] != nil { - t.Fatal("unowned history executed or silently replaced") - } - } - } - if !rejected { + e, err := prepareExecutor(t, ctx, req) + if e != nil || err == nil || !strings.Contains(err.Error(), "session/load:") || strings.Contains(err.Error(), "deadline exceeded") { t.Fatal("native history was not explicitly rejected") } }) diff --git a/apps/daemon/internal/agent/mcode/native_test.go b/apps/daemon/internal/agent/mcode/native_test.go index 042f577de..d1fcc2cd2 100644 --- a/apps/daemon/internal/agent/mcode/native_test.go +++ b/apps/daemon/internal/agent/mcode/native_test.go @@ -22,6 +22,7 @@ func TestNativeMCodeACP(t *testing.T) { t.Skip("set OAC_TEST_MCODE_INTEGRATION_BIN to run native ACP smoke test") } req := testRequest(t) + t.Setenv("OAC_RUNTIME_MCODE_BIN", binary) var mu sync.Mutex var requests []string model := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -44,18 +45,9 @@ func TestNativeMCodeACP(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) t.Cleanup(cancel) out := make(chan proto.Envelope, 64) - session, err := newSession(ctx, req, out, binary) - if err != nil { + if _, err := startTurn(t, ctx, req, out); err != nil { t.Fatal(err) } - t.Cleanup(func() { - _ = session.Cancel(context.Background()) - select { - case <-session.exited: - case <-time.After(5 * time.Second): - t.Error("native CLI did not stop") - } - }) var done proto.DonePayload for event := range out { if event.Type == proto.TypeError { diff --git a/apps/daemon/internal/agent/mcode/options.go b/apps/daemon/internal/agent/mcode/options.go index 50926a7c8..6fc7e3492 100644 --- a/apps/daemon/internal/agent/mcode/options.go +++ b/apps/daemon/internal/agent/mcode/options.go @@ -26,9 +26,6 @@ func prepareOptions(req proto.PromptRequestPayload) (launchOptions, error) { if err := validateExecutionRequest(req); err != nil { return result, err } - if req.Input.HasImages() { - return result, fmt.Errorf("mcode: ACP does not support attachments") - } dataDir, err := agent.StateDir("mcode", req.AgentStateKey) if err != nil { return result, err diff --git a/apps/daemon/internal/agent/mcode/options_test.go b/apps/daemon/internal/agent/mcode/options_test.go index e5d399ed1..1ae1d7780 100644 --- a/apps/daemon/internal/agent/mcode/options_test.go +++ b/apps/daemon/internal/agent/mcode/options_test.go @@ -68,9 +68,6 @@ func TestOptionsRejectDroppedContext(t *testing.T) { edit func(*proto.PromptRequestPayload) }{ {"oversized instructions", func(r *proto.PromptRequestPayload) { r.AgentOptions["system_prompt"] = strings.Repeat("x", 32*1024+1) }}, - {"attachment", func(r *proto.PromptRequestPayload) { - r.Input = proto.MessageInput{{Content: []proto.InputContent{{Type: "input_image"}}}} - }}, {"missing model", func(r *proto.PromptRequestPayload) { delete(r.AgentOptions, "model") }}, {"missing provider", func(r *proto.PromptRequestPayload) { delete(r.AgentOptions, "model_provider") }}, } diff --git a/apps/daemon/internal/agent/mcode/preparation.go b/apps/daemon/internal/agent/mcode/preparation.go index 1366cb5ba..f4b00ed1d 100644 --- a/apps/daemon/internal/agent/mcode/preparation.go +++ b/apps/daemon/internal/agent/mcode/preparation.go @@ -28,8 +28,7 @@ func NewPreparationFactory(config WorkspaceConfig) agent.PreparationFactory { } return nil, err } - e := value.(*executor) - return &prepared{executor: e, session: newTurnSession(ctx, req, e.opts, e.connection, nil)}, nil + return &prepared{executor: value.(*executor)}, nil } } diff --git a/apps/daemon/internal/agent/mcode/preparation_test.go b/apps/daemon/internal/agent/mcode/preparation_test.go index 3f0a42502..92b9a2aa0 100644 --- a/apps/daemon/internal/agent/mcode/preparation_test.go +++ b/apps/daemon/internal/agent/mcode/preparation_test.go @@ -47,7 +47,7 @@ func TestPreparedWorkspaceHasOneInputAndOutputOwner(t *testing.T) { if err != nil || strings.Contains(string(raw), "session/prompt") { t.Fatalf("preparation consumed input: %q %v", raw, err) } - if p.session.opts.Dir != r.LocalEnvironment.WorkspaceRoot || p.session.opts.DataDir == r.LocalEnvironment.WorkspaceRoot { + if p.executor.opts.Dir != r.LocalEnvironment.WorkspaceRoot || p.executor.opts.DataDir == r.LocalEnvironment.WorkspaceRoot { t.Fatal("native cwd must use the workspace without moving Session state") } out := make(chan proto.Envelope) @@ -86,7 +86,7 @@ func TestPreparedWorkspaceHasOneInputAndOutputOwner(t *testing.T) { if e.Type == proto.TypeDone { done++ select { - case <-p.session.exited: + case <-p.executor.connection.exited: t.Fatal("successful Turn disposed the reusable native owner") default: } @@ -136,9 +136,9 @@ func TestPreparedSubagentsReleaseUnusedOwner(t *testing.T) { t.Fatal(err) } p := resource.(*prepared) - // The fixture only implements preparation. Enable execution cancellation's - // child branch after initialization to verify unused owners never enter it. - p.session.req.DisableSubagents = false + // The fixture only implements preparation. Enable the owner's child + // branch after initialization to verify unused owners never enter it. + p.executor.req.DisableSubagents = false ended := make(chan error, 1) go func() { if method == "close" { @@ -153,7 +153,7 @@ func TestPreparedSubagentsReleaseUnusedOwner(t *testing.T) { t.Fatal(err) } case <-ctx.Done(): - p.session.process.Cancel() + p.executor.connection.process.Cancel() t.Fatal("unused owner did not close") } raw, err := os.ReadFile(record) diff --git a/apps/daemon/internal/agent/mcode/questions_test.go b/apps/daemon/internal/agent/mcode/questions_test.go index c46562977..858658e6c 100644 --- a/apps/daemon/internal/agent/mcode/questions_test.go +++ b/apps/daemon/internal/agent/mcode/questions_test.go @@ -19,7 +19,7 @@ func TestQuestionnaireOtherUsesOneQuestion(t *testing.T) { properties := map[string]formProperty{"region": property, "region__other": {Type: "string", Title: "Region? — Other"}} params, _ := json.Marshal(map[string]any{"sessionId": "native-1", "mode": "form", "requestedSchema": map[string]any{"type": "object", "properties": properties}}) out := make(chan proto.Envelope, 1) - session := &Session{ctx: t.Context(), sessionID: "native-1", out: out, questions: map[string]pendingQuestion{}} + session := &Session{ctx: t.Context(), outputContext: t.Context(), sessionID: "native-1", out: out, questions: map[string]pendingQuestion{}} if err := session.askQuestion(rpcFrame{ID: json.RawMessage(`1`), Params: params}); err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/mcode/session.go b/apps/daemon/internal/agent/mcode/session.go index 741f9a3d9..95a0c3d09 100644 --- a/apps/daemon/internal/agent/mcode/session.go +++ b/apps/daemon/internal/agent/mcode/session.go @@ -21,71 +21,47 @@ type Session struct { req proto.PromptRequestPayload opts launchOptions *connection - executor *executor - settlement agent.TurnSettlement - settlementErr error - settled chan struct{} - inputDone chan struct{} - outputCancel context.CancelFunc - operations sync.WaitGroup - closing bool - cancelled bool - inputUncertain bool - out chan<- proto.Envelope - frames chan rpcFrame - finished chan struct{} - mu sync.Mutex - sessionID string - nativeModel string - outputContext context.Context - outcome proto.DonePayload - permissions map[string]pendingPermission - questions map[string]pendingQuestion - steeringReady bool - steeringTurn string - sequence uint64 - active bool - content strings.Builder - tools map[string]toolUpdate - completedTools map[string]bool - previousNativeTurns map[string]bool - subagentSettlementError error - rootCompletedAtMS *int64 - subagentHistoryReady bool + executor *executor + settlement agent.TurnSettlement + settlementErr error + settled chan struct{} + inputDone chan struct{} + outputCancel context.CancelFunc + operations sync.WaitGroup + closing bool + cancelled bool + inputUncertain bool + out chan<- proto.Envelope + frames chan rpcFrame + finished chan struct{} + mu sync.Mutex + sessionID string + nativeModel string + outputContext context.Context + outcome proto.DonePayload + permissions map[string]pendingPermission + questions map[string]pendingQuestion + steeringReady bool + steeringTurn string + sequence uint64 + active bool + content strings.Builder + tools map[string]toolUpdate + completedTools map[string]bool + previousNativeTurns map[string]bool + rootCompletedAtMS *int64 + subagentHistoryReady bool } var _ agent.Session = (*Session)(nil) -func Factory(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { - return newSession(ctx, req, out, defaultBinary()) -} - -func newSession(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope, binary string) (*Session, error) { - if ctx == nil { - ctx = context.Background() - } - if out == nil { - return nil, fmt.Errorf("mcode: output channel is required") - } - opts, err := prepareOptions(req) - if err != nil { - return nil, err - } - s, err := launch(ctx, req, opts, binary, out) - if err != nil { - return nil, err - } - go s.run() - return s, nil -} - -func launch(ctx context.Context, req proto.PromptRequestPayload, opts launchOptions, binary string, out chan<- proto.Envelope) (*Session, error) { +func launch(ctx context.Context, req proto.PromptRequestPayload, opts launchOptions, binary string) (*Session, error) { process, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: binary, Args: []string{"acp"}, Dir: opts.Dir, Env: opts.Env, NeedStdin: true}) if err != nil { return nil, err } c := &connection{process: process, exited: make(chan struct{}), responses: map[string]chan rpcFrame{}} - s := newTurnSession(ctx, req, opts, c, out) + s := newTurnSession(ctx, req, opts, c, nil) c.current = s go c.read() return s, nil @@ -95,69 +71,6 @@ func newTurnSession(ctx context.Context, req proto.PromptRequestPayload, opts la return &Session{ctx: ctx, req: req, opts: opts, connection: c, out: out, frames: make(chan rpcFrame, 32), finished: make(chan struct{}), permissions: map[string]pendingPermission{}, questions: map[string]pendingQuestion{}, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}} } -func (s *Session) run() { - defer func() { - if s.out != nil { - close(s.out) - } - }() - defer close(s.finished) - err := s.prepareNative() - if err == nil { - if !s.req.DisableSubagents { - var snapshot nativeSubagentSnapshot - snapshot, err = s.readSubagents(s.ctx) - s.subagentHistoryReady = err == nil - s.previousNativeTurns = map[string]bool{} - for _, session := range snapshot.Sessions { - if session.ID == s.sessionID { - for _, turn := range session.Turns { - s.previousNativeTurns[turn.ID] = true - } - } - } - } - } - if err == nil { - err = s.executePrompt() - } - if !s.req.DisableSubagents && s.subagentHistoryReady && s.out != nil { - observationErr := s.settleSubagents() - s.mu.Lock() - s.subagentSettlementError = observationErr - s.mu.Unlock() - if observationErr != nil { - err = observationErr - } - } - if err != nil { - s.process.Cancel() - <-s.exited - } - s.finishEnvironmentMCP() - if s.out == nil { - return - } - s.mu.Lock() - s.steeringReady = false - s.mu.Unlock() - if err != nil { - s.process.Cancel() - s.emit(proto.TypeError, proto.ErrorPayload{Error: err.Error()}) - } - s.mu.Lock() - sessionID := s.sessionID - s.permissions = map[string]pendingPermission{} - s.questions = map[string]pendingQuestion{} - s.mu.Unlock() - metadata := map[string]any{proto.DoneMetaAgentSessionType: "mcode"} - if sessionID != "" { - metadata[proto.DoneMetaAgentSessionID] = sessionID - } - // ACP context usage is not per-turn token usage; do not record it as spend. - s.emit(proto.TypeDone, proto.DonePayload{Content: s.content.String(), Metadata: metadata, SourceCompletedAtMS: s.rootCompletedAtMS}) -} - func (s *Session) prepareNative() error { var initialized struct { ProtocolVersion int `json:"protocolVersion"` @@ -212,16 +125,12 @@ func (s *Session) prepareNative() error { return nil } -func (s *Session) executePrompt() error { - prompt, err := s.req.Input.TextOnly() - if err != nil { - return err - } +func (s *Session) executePrompt(prompt string) error { s.active = true var result struct { StopReason string `json:"stopReason"` } - err = s.call("session/prompt", map[string]any{"sessionId": s.sessionID, "prompt": promptContent(prompt)}, &result, true) + err := s.call("session/prompt", map[string]any{"sessionId": s.sessionID, "prompt": promptContent(prompt)}, &result, true) s.active = false s.mu.Lock() s.steeringReady = false @@ -275,7 +184,7 @@ func (s *Session) call(method string, params any, result any, prompt bool) error if err != nil { return err } - if prompt && s.executor != nil { + if prompt { // Serialize the admission check with the wire, but release the owner // mutex before a pipe write so cancellation and Close can stop it. s.connection.writeMu.Lock() @@ -337,10 +246,6 @@ func (s *Session) call(method string, params any, result any, prompt bool) error } func (s *Session) emit(kind string, payload any) { - ctx := s.ctx - if s.outputContext != nil { - ctx = s.outputContext - } env, err := proto.NewEnvelope(kind, s.req.RunID, payload) if err != nil { return @@ -352,31 +257,7 @@ func (s *Session) emit(kind string, payload any) { } select { case s.out <- env: - case <-ctx.Done(): - } -} - -func (s *Session) Cancel(ctx context.Context) error { - if s.executor != nil { - return s.cancelTurn(ctx) - } - if !s.req.DisableSubagents { - if err := s.cancelSubagents(ctx); err != nil { - s.process.Cancel() - return err - } - } - s.process.Cancel() - select { - case <-s.exited: - case <-ctx.Done(): - return ctx.Err() - } - select { - case <-s.finished: - return nil - case <-ctx.Done(): - return ctx.Err() + case <-s.outputContext.Done(): } } diff --git a/apps/daemon/internal/agent/mcode/session_test.go b/apps/daemon/internal/agent/mcode/session_test.go index 5012b1268..ae435c344 100644 --- a/apps/daemon/internal/agent/mcode/session_test.go +++ b/apps/daemon/internal/agent/mcode/session_test.go @@ -23,7 +23,8 @@ func testRequest(t *testing.T) proto.PromptRequestPayload { }, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}} } -func helperSession(t *testing.T, scenario string, resume bool) (*Session, <-chan proto.Envelope) { +// helperRequest selects a protocol fixture scenario as the native CLI. +func helperRequest(t *testing.T, scenario string, resume bool) proto.PromptRequestPayload { t.Helper() req := testRequest(t) if resume { @@ -39,21 +40,54 @@ func helperSession(t *testing.T, scenario string, resume bool) (*Session, <-chan if err := os.WriteFile(binary, []byte(script), 0700); err != nil { t.Fatal(err) } + t.Setenv("OAC_RUNTIME_MCODE_BIN", binary) + return req +} + +// prepareExecutor prepares req without its Turn input; cleanup closes the +// Executor and reaps its CLI. +func prepareExecutor(t *testing.T, ctx context.Context, req proto.PromptRequestPayload) (*executor, error) { + t.Helper() + req.RunID, req.Input = "", nil + value, err := NewExecutorFactory(nil)(ctx, req) + if value == nil { + return nil, err + } + e := value.(*executor) + t.Cleanup(func() { + cleanup, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if err := e.Close(cleanup); err != nil { + t.Error("CLI was not reaped:", err) + } + }) + return e, err +} + +// startTurn prepares an Executor for req and starts req.Input as its Turn. +func startTurn(t *testing.T, ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (*Session, error) { + t.Helper() + e, err := prepareExecutor(t, ctx, req) + if err != nil { + return nil, err + } + turn, err := e.StartTurn(ctx, req.RunID, req.Input, out) + if err != nil { + return nil, err + } + return turn.(*Session), nil +} + +func helperSession(t *testing.T, scenario string, resume bool) (*Session, <-chan proto.Envelope) { + t.Helper() + req := helperRequest(t, scenario, resume) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) t.Cleanup(cancel) out := make(chan proto.Envelope, 32) - session, err := newSession(ctx, req, out, binary) + session, err := startTurn(t, ctx, req, out) if err != nil { t.Fatal(err) } - t.Cleanup(func() { - _ = session.Cancel(context.Background()) - select { - case <-session.exited: - case <-time.After(3 * time.Second): - t.Error("CLI was not reaped") - } - }) return session, out } @@ -148,34 +182,41 @@ func TestResumeSelectsModelWhenNativeSelectorIsMissing(t *testing.T) { } func TestSessionFailuresAreReported(t *testing.T) { - for _, scenario := range []string{"malformed", "exit", "rpc-error", "unknown-model"} { + for _, scenario := range []string{"malformed", "exit", "unknown-model"} { t.Run(scenario, func(t *testing.T) { - _, out := helperSession(t, scenario, false) - reported := false - for event := range out { - if event.Type == proto.TypeError { - reported = true - } - } - if !reported { - t.Fatal("failure was not emitted") + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + if e, err := prepareExecutor(t, ctx, helperRequest(t, scenario, false)); err == nil || e != nil { + t.Fatal("preparation failure was not reported", err) } }) } + t.Run("rpc-error", func(t *testing.T) { + _, out := helperSession(t, "rpc-error", false) + reported := false + for event := range out { + if event.Type == proto.TypeError { + reported = true + } + } + if !reported { + t.Fatal("failure was not emitted") + } + }) } -func TestCancelStopsWaitingCLI(t *testing.T) { - session, out := helperSession(t, "hang", false) - if err := session.Cancel(context.Background()); err != nil { - t.Fatal(err) +func TestPreparationCancellationStopsWaitingCLI(t *testing.T) { + req := helperRequest(t, "hang", false) + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + time.AfterFunc(100*time.Millisecond, cancel) + started := time.Now() + if e, err := prepareExecutor(t, ctx, req); err == nil || e != nil { + t.Fatal("cancelled preparation retained the CLI", err) } - select { - case <-session.exited: - case <-time.After(3 * time.Second): + if time.Since(started) > 3*time.Second { t.Fatal("cancel hung") } - for range out { - } } func TestMCodeProcess(t *testing.T) { diff --git a/apps/daemon/internal/agent/mcode/steering.go b/apps/daemon/internal/agent/mcode/steering.go index bff6f079f..005f5c859 100644 --- a/apps/daemon/internal/agent/mcode/steering.go +++ b/apps/daemon/internal/agent/mcode/steering.go @@ -18,16 +18,14 @@ func (s *Session) Steer(ctx context.Context, input proto.PromptSteerPayload) err // Native acceptance belongs to the active ACP Turn; it does not promise model consumption. func (s *Session) SteerWithReceipt(ctx context.Context, input proto.PromptSteerPayload, written func()) error { - if s.executor != nil { - s.mu.Lock() - if s.closing || s.cancelled { - s.mu.Unlock() - return agent.ErrSteeringInactive - } - s.operations.Add(1) + s.mu.Lock() + if s.closing || s.cancelled { s.mu.Unlock() - defer s.operations.Done() + return agent.ErrSteeringInactive } + s.operations.Add(1) + s.mu.Unlock() + defer s.operations.Done() text, err := input.Input.TextOnly() if strings.TrimSpace(input.InputID) == "" || err != nil { return agent.ErrSteeringRejected @@ -62,7 +60,7 @@ func (s *Session) SteerWithReceipt(ctx context.Context, input proto.PromptSteerP var stopped error select { case frame = <-response: - case <-s.inputSettlementDone(): + case <-s.inputDone: stopped = fmt.Errorf("mcode: run ended with unknown input outcome") case <-s.exited: stopped = fmt.Errorf("mcode: input transport closed") @@ -103,13 +101,6 @@ func (s *Session) SteerWithReceipt(ctx context.Context, input proto.PromptSteerP return nil } -func (s *Session) inputSettlementDone() <-chan struct{} { - if s.inputDone != nil { - return s.inputDone - } - return s.finished -} - func (s *Session) CancellationOutcome() proto.DonePayload { s.mu.Lock() defer s.mu.Unlock() diff --git a/apps/daemon/internal/agent/mcode/subagent_cancel.go b/apps/daemon/internal/agent/mcode/subagent_cancel.go index bd1b88e68..5efa5a031 100644 --- a/apps/daemon/internal/agent/mcode/subagent_cancel.go +++ b/apps/daemon/internal/agent/mcode/subagent_cancel.go @@ -6,21 +6,6 @@ import ( "fmt" ) -func (s *Session) cancelSubagents(ctx context.Context) error { - if err := s.stopSubagents(ctx); err != nil { - return err - } - select { - case <-s.finished: - s.mu.Lock() - err := s.subagentSettlementError - s.mu.Unlock() - return err - case <-ctx.Done(): - return ctx.Err() - } -} - func (s *Session) stopSubagents(ctx context.Context) error { select { case <-s.finished: diff --git a/apps/daemon/internal/agent/mcode/subagents_test.go b/apps/daemon/internal/agent/mcode/subagents_test.go index bccb15597..25a1773af 100644 --- a/apps/daemon/internal/agent/mcode/subagents_test.go +++ b/apps/daemon/internal/agent/mcode/subagents_test.go @@ -27,7 +27,7 @@ func childSnapshot(t *testing.T) nativeSubagentSnapshot { func TestSubagentSnapshotsKeepOwnHistoryAndStableNativeIdentity(t *testing.T) { project := func(snapshot nativeSubagentSnapshot) []proto.Envelope { out := make(chan proto.Envelope, 32) - s := &Session{ctx: context.Background(), out: out, previousNativeTurns: map[string]bool{}} + s := &Session{ctx: context.Background(), outputContext: context.Background(), out: out, previousNativeTurns: map[string]bool{}} if err := s.projectSubagents(snapshot); err != nil { t.Fatal(err) } @@ -63,7 +63,7 @@ func TestSubagentSnapshotsKeepOwnHistoryAndStableNativeIdentity(t *testing.T) { func TestSubagentSnapshotRejectsMissingParentProvenance(t *testing.T) { snapshot := childSnapshot(t) snapshot.Sessions[0].Tasks = nil - s := &Session{ctx: context.Background(), out: make(chan proto.Envelope, 32)} + s := &Session{ctx: context.Background(), outputContext: context.Background(), out: make(chan proto.Envelope, 32)} if err := s.projectSubagents(snapshot); err == nil { t.Fatal("accepted child without original spawning provenance") } diff --git a/apps/daemon/internal/agent/mcode/tool_observations_test.go b/apps/daemon/internal/agent/mcode/tool_observations_test.go index c7d1fa6da..703e3cb90 100644 --- a/apps/daemon/internal/agent/mcode/tool_observations_test.go +++ b/apps/daemon/internal/agent/mcode/tool_observations_test.go @@ -12,7 +12,7 @@ func TestWorkspaceCommandObservationsWaitForArgumentsAndRetainOutcome(t *testing for _, status := range []string{"completed", "failed"} { t.Run(status, func(t *testing.T) { out := make(chan proto.Envelope, 8) - s := &Session{ctx: context.Background(), req: proto.PromptRequestPayload{RunID: "run"}, out: out, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}} + s := &Session{ctx: context.Background(), outputContext: context.Background(), req: proto.PromptRequestPayload{RunID: "run"}, out: out, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}} s.emitTool(toolUpdate{ID: "call", Name: "mcp__oac_workspace__workspace_bash"}) if len(out) != 0 { t.Fatal("command item emitted before native arguments") diff --git a/apps/daemon/internal/agent/registry.go b/apps/daemon/internal/agent/registry.go index 6584fb751..4a808b7ff 100644 --- a/apps/daemon/internal/agent/registry.go +++ b/apps/daemon/internal/agent/registry.go @@ -20,13 +20,10 @@ var ErrUnknownPermission = errors.New("agent: unknown permission id") // ErrUnknownPermission. var ErrUnknownAsk = errors.New("agent: unknown ask id") -var ErrUnsupportedKind = errors.New("agent: unsupported agent_kind") - -// Registry maps agent_kind → Factory and keeps the daemon-advertised -// capability descriptor for each kind. Safe for concurrent use. +// Registry keeps the daemon-advertised capability descriptor and execution +// factories for each agent_kind. Safe for concurrent use. type Registry struct { mu sync.RWMutex - factories map[string]Factory preparers map[string]PreparationFactory executors map[string]ExecutorFactory kinds map[string]proto.SupportedAgentKind @@ -35,7 +32,6 @@ type Registry struct { func NewRegistry() *Registry { return &Registry{ - factories: make(map[string]Factory), preparers: make(map[string]PreparationFactory), executors: make(map[string]ExecutorFactory), kinds: make(map[string]proto.SupportedAgentKind), @@ -43,37 +39,13 @@ func NewRegistry() *Registry { } } -// Resolve returns the factory for kind, or wraps ErrUnsupportedKind. -func (r *Registry) Resolve(kind string) (Factory, error) { - r.mu.RLock() - defer r.mu.RUnlock() - f, ok := r.factories[kind] - if !ok { - return nil, fmt.Errorf("%w: %q", ErrUnsupportedKind, kind) - } - return f, nil -} - -// Kinds returns the list of registered kinds sorted lexicographically. -func (r *Registry) Kinds() []string { - r.mu.RLock() - defer r.mu.RUnlock() - out := make([]string, 0, len(r.factories)) - for k := range r.factories { - out = append(out, k) - } - slices.Sort(out) - return out -} - // SupportedAgentKinds returns the daemon-advertised capability // descriptors sorted by kind so heartbeat payloads are stable. func (r *Registry) SupportedAgentKinds() []proto.SupportedAgentKind { r.mu.RLock() defer r.mu.RUnlock() - out := make([]proto.SupportedAgentKind, 0, len(r.factories)) - for kind := range r.factories { - info := r.kinds[kind] + out := make([]proto.SupportedAgentKind, 0, len(r.kinds)) + for _, info := range r.kinds { out = append(out, info) } slices.SortFunc(out, func(a, b proto.SupportedAgentKind) int { diff --git a/apps/daemon/internal/agent/registry_test.go b/apps/daemon/internal/agent/registry_test.go index c41b6269a..657326a10 100644 --- a/apps/daemon/internal/agent/registry_test.go +++ b/apps/daemon/internal/agent/registry_test.go @@ -6,7 +6,6 @@ import ( "context" "errors" "reflect" - "slices" "testing" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" @@ -14,77 +13,14 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" ) -func stubFactory(marker string) agent.Factory { - return func(_ context.Context, _ proto.PromptRequestPayload, _ chan<- proto.Envelope) (agent.Session, error) { - return stubSession{marker: marker}, nil - } -} - -type stubSession struct{ marker string } - -func (stubSession) CancellationOutcome() proto.DonePayload { return proto.DonePayload{} } - -func (stubSession) Cancel(context.Context) error { return nil } -func (stubSession) SubmitPermission(context.Context, string, proto.PermissionDecisionPayload) error { - return nil -} -func (stubSession) SubmitPromptForUserChoice(context.Context, string, proto.PromptForUserChoiceDecisionPayload) error { - return nil -} - -func TestRegistryResolveReturnsRegisteredFactory(t *testing.T) { - reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("cc")) - - f, err := reg.Resolve("fake_alpha") - if err != nil { - t.Fatalf("Resolve: %v", err) - } - sess, err := f(context.Background(), proto.PromptRequestPayload{AgentKind: "fake_alpha"}, nil) - if err != nil { - t.Fatalf("factory: %v", err) - } - stub, ok := sess.(stubSession) - if !ok || stub.marker != "cc" { - t.Errorf("resolved factory returned %#v, want stubSession{marker:\"cc\"}", sess) - } -} - -func TestRegistryResolveUnknownKindReturnsTypedError(t *testing.T) { +func TestRegistryRegisterOverwritesDescriptor(t *testing.T) { reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("cc")) - - _, err := reg.Resolve("fake_beta") - if !errors.Is(err, agent.ErrUnsupportedKind) { - t.Errorf("Resolve unknown = %v, want ErrUnsupportedKind chain", err) - } -} + reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Version: "v1", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Version: "v2", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}) -func TestRegistryRegisterOverwrites(t *testing.T) { - reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("v1")) - reg.RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("v2")) - - f, err := reg.Resolve("k") - if err != nil { - t.Fatalf("Resolve: %v", err) - } - sess, _ := f(context.Background(), proto.PromptRequestPayload{}, nil) - if got := sess.(stubSession).marker; got != "v2" { - t.Errorf("overwrite: marker = %q, want v2", got) - } -} - -func TestRegistryKindsReportsRegistered(t *testing.T) { - reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("cc")) - reg.RegisterKind(proto.SupportedAgentKind{Kind: "fake_beta", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("oc")) - - got := reg.Kinds() - slices.Sort(got) - want := []string{"fake_alpha", "fake_beta"} - if !slices.Equal(got, want) { - t.Errorf("Kinds = %v, want %v", got, want) + got := reg.SupportedAgentKinds() + if len(got) != 1 || got[0].Version != "v2" { + t.Errorf("overwrite: descriptors = %#v, want one v2 descriptor", got) } } @@ -94,16 +30,30 @@ func TestRegistryRegisterPanicsOnEmptyKind(t *testing.T) { t.Fatal("Register(\"\", ...) did not panic") } }() - agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("x")) + agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}) } -func TestRegistryRegisterPanicsOnNilFactory(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Fatal("Register(kind, nil) did not panic") - } - }() - agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "k", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, nil) +func TestRegistryRegisterRejectsFactoriesForUnavailableRuntime(t *testing.T) { + info := proto.SupportedAgentKind{Kind: "k", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})} + executor := func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, nil } + preparation := func(context.Context, proto.PromptRequestPayload) (agent.Prepared, error) { return nil, nil } + for name, runtime := range map[string]agent.Runtime{ + "executor": {Info: info, Executor: executor}, + "preparation": {Info: info, Preparation: preparation}, + } { + t.Run(name, func(t *testing.T) { + registry := agent.NewRegistry() + defer func() { + if recover() == nil { + t.Fatal("unavailable runtime registered factories") + } + if len(registry.SupportedAgentKinds()) != 0 { + t.Fatal("rejected runtime changed registry") + } + }() + registry.Register(agent.Declaration{Info: info}, runtime) + }) + } } func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { @@ -115,7 +65,7 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ Streaming: proto.CapabilitySupported, }), - }, harnessconfig.Configuration{}, stubFactory("oc")) + }, harnessconfig.Configuration{}) reg.RegisterKind(proto.SupportedAgentKind{ Kind: "fake_alpha", Available: true, @@ -126,7 +76,7 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { Usage: proto.CapabilitySupported, Resume: proto.CapabilitySupported, }), - }, harnessconfig.Configuration{}, stubFactory("cc")) + }, harnessconfig.Configuration{}) got := reg.SupportedAgentKinds() if len(got) != 2 { @@ -145,9 +95,9 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { func TestRegistryExecutorRequiresExplicitRegistration(t *testing.T) { registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("native")) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}) if _, err := registry.ResolveExecutor("native"); err == nil { - t.Fatal("legacy factory implied reusable execution") + t.Fatal("kind registration implied reusable execution") } expected := errors.New("executor factory") registry.RegisterExecutor("native", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, expected }) @@ -158,7 +108,7 @@ func TestRegistryExecutorRequiresExplicitRegistration(t *testing.T) { if _, err := factory(t.Context(), proto.PromptRequestPayload{}); !errors.Is(err, expected) { t.Fatal(err) } - registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, stubFactory("replacement")) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}) if _, err := registry.ResolveExecutor("native"); err == nil { t.Fatal("replacing a kind retained its old executor capability") } @@ -169,7 +119,10 @@ func TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement(t *testing.T) { for i := 0; i < reflect.TypeOf(valid).NumField(); i++ { t.Run(reflect.TypeOf(valid).Field(i).Name, func(t *testing.T) { registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: valid}, harnessconfig.Configuration{}, stubFactory("original")) + original := proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: valid} + registry.RegisterKind(original, harnessconfig.Configuration{}) + registry.RegisterExecutor("fixture", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, nil }) + original.Capabilities.Preparation = proto.CapabilitySupported missing := valid reflect.ValueOf(&missing).Elem().Field(i).Set(reflect.ValueOf(proto.CapabilityUnspecified)) func() { @@ -178,14 +131,9 @@ func TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement(t *testing.T) { t.Error("incomplete declaration registered") } }() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: false, Capabilities: missing}, harnessconfig.Configuration{}, stubFactory("replacement")) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: false, Capabilities: missing}, harnessconfig.Configuration{}) }() - factory, err := registry.Resolve("fixture") - if err != nil { - t.Fatal(err) - } - session, err := factory(t.Context(), proto.PromptRequestPayload{}, nil) - if err != nil || session.(stubSession).marker != "original" { + if _, err := registry.ResolveExecutor("fixture"); err != nil || !reflect.DeepEqual(registry.SupportedAgentKinds(), []proto.SupportedAgentKind{original}) { t.Fatal("failed declaration changed registry") } }) diff --git a/apps/daemon/internal/cli/connect_cleanup_test.go b/apps/daemon/internal/cli/connect_cleanup_test.go index c8f92a00e..ba6192874 100644 --- a/apps/daemon/internal/cli/connect_cleanup_test.go +++ b/apps/daemon/internal/cli/connect_cleanup_test.go @@ -115,10 +115,7 @@ func testDisconnectedPumpCleanup(t *testing.T, suspend bool) { } owner := &cleanupExecutor{retry: make(chan struct{}), confirm: make(chan struct{})} registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "cleanup", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, - harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("unexpected legacy factory") - }) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "cleanup", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}) var factories atomic.Int32 registry.RegisterExecutor("cleanup", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { factories.Add(1) diff --git a/apps/daemon/internal/cli/native_discovery_test.go b/apps/daemon/internal/cli/native_discovery_test.go index e6d9e0dee..5f3592c9d 100644 --- a/apps/daemon/internal/cli/native_discovery_test.go +++ b/apps/daemon/internal/cli/native_discovery_test.go @@ -21,9 +21,7 @@ func TestDiscoveryAndRegistration(t *testing.T) { called = append(called, info.Kind) info.Available = true info.Version = "test" - return &agent.Runtime{Info: info, Session: func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("test") - }} + return &agent.Runtime{Info: info} } } rc := &runContext{stdout: io.Discard, stderr: io.Discard} @@ -41,10 +39,11 @@ func TestDiscoveryAndRegistration(t *testing.T) { } registry := agent.NewRegistry() registerAgentKinds(registry, discovery) - if len(registry.Kinds()) != len(expected) { - t.Fatal(registry.Kinds()) + kinds := registry.SupportedAgentKinds() + if len(kinds) != len(expected) { + t.Fatal(kinds) } - for _, info := range registry.SupportedAgentKinds() { + for _, info := range kinds { if !info.Available || info.Version != "test" { t.Fatal(info) } diff --git a/apps/daemon/internal/cli/preparation_test.go b/apps/daemon/internal/cli/preparation_test.go index 7df3223e6..9518ae1b1 100644 --- a/apps/daemon/internal/cli/preparation_test.go +++ b/apps/daemon/internal/cli/preparation_test.go @@ -14,9 +14,7 @@ func TestPreparationRegistrationFollowsNativeSupport(t *testing.T) { for _, supported := range []bool{false, true} { reg := agent.NewRegistry() info := proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilityFromBool(supported)})} - runtime := agent.Runtime{Info: info, Session: func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, nil - }} + runtime := agent.Runtime{Info: info} if supported { runtime.Preparation = func(context.Context, proto.PromptRequestPayload) (agent.Prepared, error) { return nil, nil } runtime.WorkspaceReadPreparation = true @@ -35,11 +33,9 @@ func TestPreparationRegistrationFollowsNativeSupport(t *testing.T) { t.Fatal("heartbeat registry lost preparation") } } - reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, nil - }) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}) if _, err := reg.ResolvePreparation("codex"); err == nil { - t.Fatal("factory replacement retained stale preparation") + t.Fatal("kind replacement retained stale preparation") } } } diff --git a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go index b99a1ef84..d9cf60fac 100644 --- a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go +++ b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go @@ -127,9 +127,7 @@ func TestExecutorCancellationReachesNativeBeforeDurableReceiptJoin(t *testing.T) sender := &receiptCancelSender{recSender: &recSender{}, entered: make(chan struct{}), release: make(chan struct{})} owner := &receiptCancelExecutor{turn: make(chan *receiptCancelTurn, 2), cancelFails: mode == "cancel_failure" || mode == "close_failure_retry", closeFailsFirst: mode == "close_failure_retry", closeEntered: make(chan struct{}), closeRelease: make(chan struct{})} reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("legacy factory forbidden") - }) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}) reg.RegisterExecutor("reusable", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return owner, nil }) r, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender, IdleTimeout: time.Hour}) if err != nil { diff --git a/apps/daemon/internal/dispatch/executor_handoff_test.go b/apps/daemon/internal/dispatch/executor_handoff_test.go index 60f194351..0b679aa7f 100644 --- a/apps/daemon/internal/dispatch/executor_handoff_test.go +++ b/apps/daemon/internal/dispatch/executor_handoff_test.go @@ -93,10 +93,7 @@ func TestPreparedDonePublishesAfterExecutorHandoff(t *testing.T) { } owner := &terminalHandoffExecutor{turns: make(chan *terminalHandoffTurn, 3)} registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "handoff", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, - harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("legacy path forbidden") - }) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "handoff", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}) var creates atomic.Int32 registry.RegisterExecutor("handoff", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { creates.Add(1) diff --git a/apps/daemon/internal/dispatch/executor_test.go b/apps/daemon/internal/dispatch/executor_test.go index 211c2201d..a2324a78c 100644 --- a/apps/daemon/internal/dispatch/executor_test.go +++ b/apps/daemon/internal/dispatch/executor_test.go @@ -80,9 +80,7 @@ func executorRequest() proto.ExecutionPreparePayload { // registerExecutorKind registers info for prepared execution only. func registerExecutorKind(reg *agent.Registry, info proto.SupportedAgentKind, factory agent.ExecutorFactory) { - reg.RegisterKind(info, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("prepared execution must not use the direct Session factory") - }) + reg.RegisterKind(info, harnessconfig.Configuration{}) reg.RegisterExecutor(info.Kind, factory) } func executorRouter(t *testing.T, owner *reusableExecutor, idle time.Duration) (*dispatch.Router, *recSender, *atomic.Int32) { @@ -230,9 +228,7 @@ func TestExecutorPreInputFailureConfirmsCloseBeforeRetrySignal(t *testing.T) { func poolRouter(t *testing.T, factory agent.ExecutorFactory) (*dispatch.Router, *recSender) { t.Helper() registry := agent.NewRegistry() - registry.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("unexpected legacy factory") - }) + registry.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}) registry.RegisterExecutor("reusable", factory) sender := &recSender{} router, err := dispatch.New(dispatch.Config{Registry: registry, Sender: sender, IdleTimeout: time.Minute}) diff --git a/apps/daemon/internal/dispatch/local_directory_test.go b/apps/daemon/internal/dispatch/local_directory_test.go index 3f1692b07..6c79a12cd 100644 --- a/apps/daemon/internal/dispatch/local_directory_test.go +++ b/apps/daemon/internal/dispatch/local_directory_test.go @@ -29,10 +29,7 @@ func TestLocalDirectoryPreparationNeedsNoHarnessAndRejectsOtherOwners(t *testing } var harnessCalls atomic.Int32 reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - harnessCalls.Add(1) - return nil, errors.New("must not start a model") - }) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}) reg.RegisterPreparation("native", true, func(context.Context, proto.PromptRequestPayload) (agent.Prepared, error) { harnessCalls.Add(1) return nil, errors.New("must not prepare a harness") @@ -111,9 +108,7 @@ func TestLocalDirectoryKeepsNotDirectorySeparateFromFailures(t *testing.T) { t.Fatal(err) } reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("must not start a model") - }) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, harnessconfig.Configuration{}) reg.RegisterPreparation("native", true, func(context.Context, proto.PromptRequestPayload) (agent.Prepared, error) { return nil, errors.New("must not prepare a harness") }) diff --git a/apps/daemon/internal/dispatch/mcp_http_test.go b/apps/daemon/internal/dispatch/mcp_http_test.go index 794b596ec..3ba64f237 100644 --- a/apps/daemon/internal/dispatch/mcp_http_test.go +++ b/apps/daemon/internal/dispatch/mcp_http_test.go @@ -112,10 +112,7 @@ func TestLocalMCPOriginAndCapabilityAdmission(t *testing.T) { req.Configuration.MCPHTTPServers = nil } entered := make(chan struct{}, 1) - h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityFromBool(mode != "environment missing capability"), MCPHTTPBearerAuth: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - t.Error("ordinary factory called") - return nil, errors.New("unexpected") - }) + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityFromBool(mode != "environment missing capability"), MCPHTTPBearerAuth: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported})}, harnessconfig.Configuration{}) h.reg.RegisterExecutor("prepared", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { entered <- struct{}{} return nil, errors.New("controlled stop") diff --git a/apps/daemon/internal/dispatch/preparation_test.go b/apps/daemon/internal/dispatch/preparation_test.go index 57312e3b2..b01dbcca6 100644 --- a/apps/daemon/internal/dispatch/preparation_test.go +++ b/apps/daemon/internal/dispatch/preparation_test.go @@ -119,9 +119,7 @@ func preparationRequest() proto.ExecutionPreparePayload { func preparationRouter(t *testing.T, sender dispatch.Sender, timeout time.Duration, factory agent.PreparationFactory) *dispatch.Router { t.Helper() reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Permissions: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("ordinary Factory must not be used for preparation") - }) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Permissions: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}) reg.RegisterPreparation("prepared", true, factory) reg.RegisterExecutor("prepared", preparationExecutorFixture(factory)) r, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender, PreparationTimeout: timeout, LocalWorkspace: preparationWorkspace(t)}) diff --git a/apps/daemon/internal/wireconformance/wire_test.go b/apps/daemon/internal/wireconformance/wire_test.go index 9af45ecba..80b93e394 100644 --- a/apps/daemon/internal/wireconformance/wire_test.go +++ b/apps/daemon/internal/wireconformance/wire_test.go @@ -157,10 +157,7 @@ func connectRuntime(t *testing.T, peer *corePeer, setupErr error) *runtimeSide { peer.accept(t) rt := &runtimeSide{conn: conn, executor: &controlledExecutor{turn: make(chan *controlledTurn, 1)}, stopped: make(chan struct{})} kinds := agent.NewRegistry() - kinds.RegisterKind(proto.SupportedAgentKind{Kind: prototest.HarnessKind, Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, - harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("prepared execution must not use the direct Session factory") - }) + kinds.RegisterKind(proto.SupportedAgentKind{Kind: prototest.HarnessKind, Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, harnessconfig.Configuration{}) kinds.RegisterExecutor(prototest.HarnessKind, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return rt.executor, setupErr }) diff --git a/apps/daemon/testdata/onboarding/main.go b/apps/daemon/testdata/onboarding/main.go index 3252f0bdb..0a08375ae 100644 --- a/apps/daemon/testdata/onboarding/main.go +++ b/apps/daemon/testdata/onboarding/main.go @@ -163,9 +163,7 @@ func run() error { h := &harness{history: map[string]string{}} registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture_harness", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ Streaming: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableTurns: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported, ExecutionControls: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, SubagentControl: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported, - })}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, errors.New("fixture execution requires an Executor") - }) + })}, harnessconfig.Configuration{}) registry.RegisterExecutor("fixture_harness", h.prepare) sink := &sender{encoder: json.NewEncoder(os.Stdout)} router, err := dispatch.New(dispatch.Config{Registry: registry, Sender: sink, Log: slog.New(slog.NewTextHandler(io.Discard, nil))}) diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 8d5c7459d..8ba69107e 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -116,8 +116,8 @@ A Session owns one reusable Executor in its connected Runtime; a Turn owns one i - An error means settlement is unconfirmed and frees neither ownership nor capacity. Caller deadlines stop the wait, not the tracked cleanup. Retry the same cleanup target serially; a failed cleanup blocks replacement and keeps its resource slot. - `Executor.Close` confirms resource retirement independently of the Turn outcome: an immutable Turn error must not prevent closing the native transport once its work and output have stopped. - Include owned background work in settlement and keep the exact native cleanup target after a failure. Native termination belongs to the adapter; a bulk cleanup acknowledgement alone does not establish quiescence. -- Every `Session`, including a direct-call factory result, declares `CancellationOutcome`. `Turn` and `PreparedCancellation` inherit it. The snapshot keeps observed native identity, Usage and output and remains readable after cancellation. Missing evidence stays unset; an empty `DonePayload` means nothing has been observed, not that cancellation succeeded or is unsupported. Reading the snapshot does not wait for settlement. -- Direct-call `Session.Cancel` requests cancellation; output closure signals teardown. Executable `PreparedCancellation.Cancel` waits for local cleanup and output writes to stop. Turn settlement still requires `AwaitSettlement` and any required `Executor.Close`; neither a successful cancellation request nor its snapshot replaces those waits. +- Every `Session` declares `CancellationOutcome`. `Turn` and `PreparedCancellation` inherit it. The snapshot keeps observed native identity, Usage and output and remains readable after cancellation. Missing evidence stays unset; an empty `DonePayload` means nothing has been observed, not that cancellation succeeded or is unsupported. Reading the snapshot does not wait for settlement. +- `Session.Cancel` requests cancellation; output closure signals teardown. Executable `PreparedCancellation.Cancel` waits for local cleanup and output writes to stop. Turn settlement still requires `AwaitSettlement` and any required `Executor.Close`; neither a successful cancellation request nor its snapshot replaces those waits. **What the Runtime does around a Turn.** One output consumer starts before native Start, drains the bounded 64-frame channel and keeps the terminal observation until Start publication, Turn settlement and admitted operation receipts finish. Natural completion never calls Cancel. Input, function and interaction admission close before settlement; operations already admitted hold their barrier through native receipts and outbound acknowledgement. The Runtime sends cancellation to the Turn before waiting on that barrier, because a written input may need a native interrupt to produce its receipt. It joins native settlement, any required confirmed Executor close, output drain and all admitted operations before an applied acknowledgement or reuse, and only then forwards Done or an applied cancellation receipt. A failed Close can report failure while keeping the same Run and outstanding operations for retry; a closed caller wait cannot manufacture an applied input receipt. The Runtime commits native continuity and releases the old Run's admission before publishing Done, since the receiver may start another Turn at once; a late terminal-send failure belongs to the old Run and cannot invalidate a successor that already owns the Executor. Connection shutdown owns transport-loss cleanup. The settlement wait is ten seconds and the receipt send budget five seconds; a timeout is not proof of quiescence. @@ -149,18 +149,16 @@ A Harness that supports the Subagent reads implements the [neutral observation c ## Register the adapter -Registration is static and requires a build. Export one `agent.Declaration` from `apps/daemon/internal/agent//declaration.go`, then add it to `harnessDeclarations` in [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go). The declaration contains the kind and complete capability descriptor, the shared model `Configuration` and a `Discover` function. Discovery receives the profile and diagnostic writers, owns native configuration and availability checks, and returns the installed `agent.Runtime` with its descriptor and session, preparation and Executor factories. Return nil when the adapter is not configured; return an unavailable descriptor with a session factory when configured prerequisites fail. Keep version gates and factory-selection conditions inside the adapter. +Registration is static and requires a build. Export one `agent.Declaration` from `apps/daemon/internal/agent//declaration.go`, then add it to `harnessDeclarations` in [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go). The declaration contains the kind and complete capability descriptor, the shared model `Configuration` and a `Discover` function. Discovery receives the profile and diagnostic writers, owns native configuration and availability checks, and returns the installed `agent.Runtime` with its descriptor and its preparation and Executor factories. Return nil when the adapter is not configured; return an unavailable descriptor without factories when configured prerequisites fail. Keep version gates and factory-selection conditions inside the adapter. -[`cli/agent_registration.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_registration.go) iterates the discovered runtimes and calls `Registry.Register` from `agent/harness.go`. It verifies that discovery retained the declared kind and installs factories in this order: +[`cli/agent_registration.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_registration.go) iterates the discovered runtimes and calls `Registry.Register` from `agent/harness.go`. It verifies that discovery retained the declared kind and registers the Runtime in this order: | Order | Method | Registers | | --- | --- | --- | -| 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration, agent.Factory)` | Kind, availability, version, `AgentKindCapabilities`, the model configuration declaration and the direct-call factory. It resets the other registrations, so call it first. | +| 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind, availability, version, `AgentKindCapabilities` and the model configuration declaration. It resets the other registrations, so call it first. | | 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | The Executor and Turn lifecycle used for execution; derives the `Preparation` capability | | 3 | `RegisterPreparation(kind, workspaceRead, agent.PreparationFactory)` | Optional: separate read-only workspace preparation for qualified workspace operations | -The direct-call `agent.Factory` delegates to the same Executor implementation. - Every `proto.AgentKindCapabilities` field must be explicitly `proto.CapabilitySupported` or `proto.CapabilityUnsupported`, even for an unavailable Harness. `proto.CapabilityUnspecified` is invalid: zero values and omitted fields never mean Unsupported. An installation probe may set an individual field with `proto.CapabilityFromBool`; it must not populate unmentioned or future fields. Availability stays separate in `SupportedAgentKind.Available`. Registration validates the complete declaration before changing the registry, and the wire carries an explicit boolean for every field, so omitted and null fields are invalid. A new field requires a decision in every production declaration. Runtime consumers use `IsSupported()` and reject unsupported requests before native operations; an interface assertion verifies implementation, never support. Every declaration must match the behavior verified for that installation; the [Core–Runtime protocol](../../docs/runtime-protocol.md#capability-declarations) owns how declarations travel and are frozen. The admission mapping is explicit. `Steering` controls non-durable `Steerer` input. `DurableInputReceipts` controls `DurableSteerer` input and also requires the Turn settlement contract; neither implies the other, and Core's public text profile requires both. `Permissions` qualifies permission and user-choice responses together and requires both native response paths. Workspace declarations describe the authorized resource owner, including the common Runtime workspace implementation. Runtime registration does not grant Core qualification; the service profile does. @@ -200,7 +198,7 @@ Run the `engine` and `execution` tests for omission, policy, combination and err ## Native model configuration -[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) owns the shared configuration declaration and pure preparation contract. Each adapter supplies one `Configuration`, in `internal/harnessconfig/`, to Core's composition and to the Runtime's `RegisterKind`. The direct factory, preparation and Executor paths all validate through that declaration before native side effects. The wire object is `proto.HarnessConfig`. [Model execution](./model-execution.md#native-model-parameters) lists each Harness's accepted fields. +[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) owns the shared configuration declaration and pure preparation contract. Each adapter supplies one `Configuration`, in `internal/harnessconfig/`, to Core's composition and to the Runtime's `RegisterKind`. The preparation and Executor paths both validate through that declaration before native side effects. The wire object is `proto.HarnessConfig`. [Model execution](./model-execution.md#native-model-parameters) lists each Harness's accepted fields. A supplied `model` must be a nonempty string, and an explicit `model_provider` requires it. The native-owned connection path may omit both; explicit null is invalid. An explicitly empty declaration accepts no provider or nonempty native parameters and advertises no provider support. Unknown protocol formats and duplicate protocol declarations fail at registration. diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index e5252a97d..8f21103a4 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "将原生 Harness 添加到 OpenAgentCore" source: contracts/agents-api/harness-onboarding.md -source_hash: cad1a3666f3c0d4c460bb6213da4bcca89c156fc387b1355d683593b55d025a0 +source_hash: f9d04f91520968c71565aada8592f116bf794b0e094f8897cb21ef0d10e17793 --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、Core 资格认定和验收。[Harness capabilities](harness-capabilities.md) 记录了当前每个 Harness 支持的功能。 @@ -118,8 +118,8 @@ Session 在其已连接的 Runtime 中拥有一个可复用的 Executor;Turn - 错误表示结算尚未确认,既不释放所有权,也不释放容量。调用方截止时间只会停止等待,不会停止受跟踪的清理。必须串行重试同一个清理目标;清理失败会阻止替换并保留其资源槽位。 - `Executor.Close` 独立于 Turn 结果确认资源退役:不可变的 Turn 错误不得阻止在其工作和输出已经停止后关闭原生传输层。 - 结算必须包含所属的后台工作,并在失败后保留精确的原生清理目标。原生终止由适配器负责;仅有批量清理确认并不能证明已达到静默状态。 -- 每个 `Session`(包括直接调用工厂的结果)都要声明 `CancellationOutcome`。`Turn` 和 `PreparedCancellation` 继承该声明。快照保留已观察到的原生身份、Usage 和输出,并在取消后仍可读取。缺失的证据保持未设置;空的 `DonePayload` 表示未观察到任何内容,而不是表示取消成功或不受支持。读取快照不会等待结算。 -- 直接调用的 `Session.Cancel` 请求取消;输出关闭表示拆卸开始。可执行准备中的 `PreparedCancellation.Cancel` 会等待本地清理和输出写入停止。Turn 结算仍需要 `AwaitSettlement` 和所需的任何 `Executor.Close`;取消请求成功或其快照都不能替代这些等待。 +- 每个 `Session` 都要声明 `CancellationOutcome`。`Turn` 和 `PreparedCancellation` 继承该声明。快照保留已观察到的原生身份、Usage 和输出,并在取消后仍可读取。缺失的证据保持未设置;空的 `DonePayload` 表示未观察到任何内容,而不是表示取消成功或不受支持。读取快照不会等待结算。 +- `Session.Cancel` 请求取消;输出关闭表示拆卸开始。可执行准备中的 `PreparedCancellation.Cancel` 会等待本地清理和输出写入停止。Turn 结算仍需要 `AwaitSettlement` 和所需的任何 `Executor.Close`;取消请求成功或其快照都不能替代这些等待。 **Runtime 在 Turn 前后执行的工作。** 一个输出消费者会在原生 Start 之前启动,耗尽有界的 64 帧通道,并将终态观察保留到 Start 发布、Turn 结算和已准入操作回执完成为止。正常完成绝不调用 Cancel。输入、函数和交互准入会在结算前关闭;已准入的操作会持有其屏障,直至原生回执和出站确认完成。Runtime 会在等待该屏障之前向 Turn 发送取消,因为已写入的输入可能需要原生中断才能生成回执。Runtime 会汇合原生结算、所需的已确认 Executor 关闭、输出耗尽和所有已准入操作,然后应用确认或执行复用,之后才会转发 Done 或已应用的取消回执。Close 失败可以报告失败,同时保留同一 Run 和未完成操作以供重试;已关闭的调用方等待无法凭空生成已应用输入回执。Runtime 会在发布 Done 前提交原生连续性状态并释放旧 Run 的准入,因为接收方可能立即启动另一个 Turn;迟到的终态发送失败属于旧 Run,不能使已拥有 Executor 的后继对象失效。连接关闭负责传输丢失清理。结算等待时间为十秒,回执发送预算为五秒;超时不能证明已达到静默状态。 @@ -151,18 +151,16 @@ MCP、公共函数、延迟函数发现、结构化输出、图像输入、详 ## 注册适配器 {#register-the-adapter} -注册是静态的,并且需要构建。从 `apps/daemon/internal/agent//declaration.go` 导出一个 `agent.Declaration`,然后将其添加到 [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go) 的 `harnessDeclarations` 中。声明包含 kind、完整能力描述符、共享模型 `Configuration` 和 `Discover` 函数。发现过程接收 profile 和诊断写入器,负责原生配置和可用性检查,并返回已安装的 `agent.Runtime` 及其描述符、session 工厂、准备工厂和 Executor 工厂。未配置适配器时返回 nil;已配置的前置条件失败时,返回不可用描述符和 session 工厂。将版本门控和工厂选择条件保留在适配器内部。 +注册是静态的,并且需要构建。从 `apps/daemon/internal/agent//declaration.go` 导出一个 `agent.Declaration`,然后将其添加到 [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go) 的 `harnessDeclarations` 中。声明包含 kind、完整能力描述符、共享模型 `Configuration` 和 `Discover` 函数。发现过程接收 profile 和诊断写入器,负责原生配置和可用性检查,并返回已安装的 `agent.Runtime` 及其描述符、准备工厂和 Executor 工厂。未配置适配器时返回 nil;已配置的前置条件失败时,返回不带任何工厂的不可用描述符。将版本门控和工厂选择条件保留在适配器内部。 -[`cli/agent_registration.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_registration.go) 遍历已发现的 Runtime,并调用 `agent/harness.go` 中的 `Registry.Register`。它验证发现过程是否保留了声明的 kind,并按以下顺序安装工厂: +[`cli/agent_registration.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_registration.go) 遍历已发现的 Runtime,并调用 `agent/harness.go` 中的 `Registry.Register`。它验证发现过程是否保留了声明的 kind,并按以下顺序注册该 Runtime: | 顺序 | 方法 | 注册内容 | | --- | --- | --- | -| 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration, agent.Factory)` | Kind、可用性、版本、`AgentKindCapabilities`、模型配置声明和直接调用工厂。它会重置其他注册项,因此必须首先调用。 | +| 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind、可用性、版本、`AgentKindCapabilities` 和模型配置声明。它会重置其他注册项,因此必须首先调用。 | | 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | 执行所用的 Executor 和 Turn 生命周期;据此派生 `Preparation` 能力 | | 3 | `RegisterPreparation(kind, workspaceRead, agent.PreparationFactory)` | 可选:针对已认定合格的工作区操作的独立只读工作区准备 | -直接调用的 `agent.Factory` 委托给同一个 Executor 实现。 - 每个 `proto.AgentKindCapabilities` 字段都必须显式设为 `proto.CapabilitySupported` 或 `proto.CapabilityUnsupported`,即使 Harness 不可用也是如此。`proto.CapabilityUnspecified` 无效:零值和省略字段绝不表示 Unsupported。安装探测可以使用 `proto.CapabilityFromBool` 设置单个字段;但不得填充未提及字段或未来字段。可用性通过 `SupportedAgentKind.Available` 单独表示。注册会在更改 registry 之前验证完整声明;线协议会为每个字段携带显式布尔值,因此省略字段和 null 字段均无效。添加新字段时,每个生产声明都必须作出决定。Runtime 使用者应调用 `IsSupported()`,并在原生操作前拒绝不受支持的请求;接口断言用于验证实现,绝不表示支持。每个声明都必须与针对该安装验证的行为一致;[Core–Runtime protocol](../../../docs/zh/runtime-protocol.md#capability-declarations) 负责声明的传输方式和冻结方式。 准入映射是显式的。`Steering` 控制非持久化 `Steerer` 输入。`DurableInputReceipts` 控制 `DurableSteerer` 输入,并且还要求 Turn 结算契约;二者互不隐含,而且 Core 的公共文本 profile 要求同时具备二者。`Permissions` 一起认定权限响应和用户选择响应的资格,并要求两条原生响应路径均存在。工作区声明描述授权资源所有者,包括通用 Runtime 工作区实现。Runtime 注册不会授予 Core 资格;服务 profile 才会授予。 @@ -202,7 +200,7 @@ profile 是纯逻辑:它使用现有的公共类型和协议类型,声明受 ## 原生模型配置 {#native-model-configuration} -[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 负责共享配置声明和纯准备契约。每个适配器在 `internal/harnessconfig/` 中提供一个 `Configuration`,供 Core 组合和 Runtime 的 `RegisterKind` 使用。直接调用工厂、准备路径和 Executor 路径都会在产生原生副作用之前通过该声明进行验证。线协议对象是 `proto.HarnessConfig`。[Model execution](model-execution.md#native-model-parameters) 列出了每个 Harness 接受的字段。 +[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 负责共享配置声明和纯准备契约。每个适配器在 `internal/harnessconfig/` 中提供一个 `Configuration`,供 Core 组合和 Runtime 的 `RegisterKind` 使用。准备路径和 Executor 路径都会在产生原生副作用之前通过该声明进行验证。线协议对象是 `proto.HarnessConfig`。[Model execution](model-execution.md#native-model-parameters) 列出了每个 Harness 接受的字段。 提供的 `model` 必须是非空字符串,并且显式指定 `model_provider` 时必须提供它。原生所有权连接路径可以省略二者;显式 null 无效。显式为空的声明不接受任何 Provider 或非空原生参数,也不宣称支持 Provider。未知协议格式和重复协议声明会导致注册失败。 diff --git a/packages/claude-sdk-adapter/README.md b/packages/claude-sdk-adapter/README.md index e124b3dad..ad803ff49 100644 --- a/packages/claude-sdk-adapter/README.md +++ b/packages/claude-sdk-adapter/README.md @@ -36,7 +36,7 @@ The private bridge accepts `executor_prepare` without model input. It freezes va Each Turn ends with a result/error and `turn_settled`, independently of process exit. The outer input iterator remains open for later Turns. `turn_cancel` invokes the native interrupt control for that exact Turn. Unconfirmed input, native child work or queue state invalidates the Executor and requires close before replacement. EOF, owner signals and invalid control input close owned resources. Preparation may write native metadata and perform startup traffic; readiness does not prove provider authentication, complete sandbox health or placement authorization. -`claudesdk.NewExecutorFactory` binds this bridge to `agent.Executor`. Direct-call and read-only preparation wrappers delegate to the same implementation. Runtime execution uses the Executor registry for both none and workspace configurations. Its owner context spans all Turns; a Turn's caller cannot replace fixed resources. A failed preparation returns its Executor when cleanup remains unconfirmed. Installed runtime checks are cached by package/file identity, while capability and request validation still run for each Executor configuration. +`claudesdk.NewExecutorFactory` binds this bridge to `agent.Executor`. The read-only preparation wrapper delegates to the same implementation. Runtime execution uses the Executor registry for both none and workspace configurations. Its owner context spans all Turns; a Turn's caller cannot replace fixed resources. A failed preparation returns its Executor when cleanup remains unconfirmed. Installed runtime checks are cached by package/file identity, while capability and request validation still run for each Executor configuration. ### Workspace reads and directory listing @@ -64,7 +64,7 @@ Workspace deferred-function discovery uses native ToolSearch alongside the norma ### Registration and state -For unmanaged bootstrap, daemon `connect` optionally registers this factory as `claude_sdk` when the operator sets `OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT` to the absolute packaged `dist/main.js`. `OAC_RUNTIME_CLAUDE_SDK_NODE` selects Node (default: `node` on PATH). Discovery resolves Node once and checks that exact configuration before connecting; the SDK's bounded runtime check is independent of CLI version probes. A ready SDK alone is sufficient to start the daemon. No configuration means no SDK probe or descriptor; failed readiness reports an unavailable descriptor with a rejecting factory. Runtime checks establish local readiness, not provider authentication. Installed daemons use `start` and their verified installation manifest for adapter selection and activation; ambient activation variables cannot extend that selection. See [the native installation contract](../../deploy/README.md#native-daemon-installer). +For unmanaged bootstrap, daemon `connect` optionally registers this adapter as `claude_sdk` when the operator sets `OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT` to the absolute packaged `dist/main.js`. `OAC_RUNTIME_CLAUDE_SDK_NODE` selects Node (default: `node` on PATH). Discovery resolves Node once and checks that exact configuration before connecting; the SDK's bounded runtime check is independent of CLI version probes. A ready SDK alone is sufficient to start the daemon. No configuration means no SDK probe or descriptor; failed readiness reports an unavailable descriptor without factories. Runtime checks establish local readiness, not provider authentication. Installed daemons use `start` and their verified installation manifest for adapter selection and activation; ambient activation variables cannot extend that selection. See [the native installation contract](../../deploy/README.md#native-daemon-installer). SDK state lives under `paths.ProfileDir(profile)/runtime/claude-sdk`, independently of the replaceable runtime bundle. Both the entrypoint and managed state root must be absolute. Background re-execution inherits operator configuration; it does not persist provider credentials in credential profiles. It accepts no caller-supplied environment variables or business write authority. From 80dfaea49c968ac6593e6f2aec4807254d614564 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 20:35:16 +0800 Subject: [PATCH 03/14] ci: split code and docs reviews with one Feishu report (#485) * ci: review documentation consistency and ownership * ci: split reviews and send one combined Feishu report --- .github/workflows/ci-review.yml | 101 ++++++++++++++++------- docs/maintainers.md | 4 +- docs/zh/maintainers.md | 6 +- scripts/ci_review.py | 139 ++++++++++++++++++++++++++++++++ scripts/ci_review_test.py | 96 ++++++++++++++++++++++ 5 files changed, 315 insertions(+), 31 deletions(-) create mode 100644 scripts/ci_review.py create mode 100644 scripts/ci_review_test.py diff --git a/.github/workflows/ci-review.yml b/.github/workflows/ci-review.yml index 0cd33e664..c5a2da524 100644 --- a/.github/workflows/ci-review.yml +++ b/.github/workflows/ci-review.yml @@ -16,12 +16,28 @@ permissions: jobs: review: + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - kind: code + name: Code review + instructions: | + 检查本次代码改动的正确性、仓库设计规则及已有 CI 结果。先读取 AGENTS.md 和 CONTRIBUTING.md,再查看相关实现和测试。 + 用 gh pr checks 查询这个 PR 已有的检查结果,必要时读取 Actions 日志。检查失败、缺失或尚未完成时如实报告,不把合并当作检查通过的证据。 + 报告包括实际行为变化(1–3 条)、代码审查结论和 CI 状态。全部正常时保持简短;有问题时给出文件与行号、证据和最小修改建议。 + - kind: docs + name: Docs review + instructions: | + 每次都检查文档与本次代码变化是否一致,即使 PR 没改文档。读取相关实现、测试和完整文档,核对平台、命令、配置、路径、默认值、接口和操作流程,找出遗漏更新或已经过时的说明。 + 文档有修改(包括新增、删除和重命名)时,再检查文档内部及同主题文档间的矛盾、重复维护的事实、主题归属和中英文含义。按 AGENTS.md 和 CONTRIBUTING.md 的 Documentation ownership 表判断位置。必要的概述加链接、中英文对照和同一来源生成的文档不算重复。 + 分别报告“文档与代码一致性”和“文档矛盾、重复与归属”。未改文档时,第二项写“本次未修改文档”。只报告本次修改引入或使之过时的、有依据的问题,给出文件与行号、对应代码或文档依据,以及最小修改建议。测试或链接检查通过不代表语义一致。 if: github.event.pull_request.merged == true runs-on: ${{ vars.OAC_USE_GITHUB_RUNNERS == 'true' && 'ubuntu-24.04' || 'blacksmith-2vcpu-ubuntu-2404' }} timeout-minutes: 30 env: - # allowed_non_write_users turns on a subprocess secret scrub; opt out so Claude's - # commands can read GH_TOKEN and the Feishu webhook. + # Keep GH_TOKEN available to read the PR and Actions logs. CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: '0' steps: # The triggering revision is already on main, so its rules and code are trusted. @@ -30,14 +46,15 @@ jobs: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 2 persist-credentials: false - - name: Review and send Feishu card with Claude Code - # Notification is best effort: a failed review or delivery never fails the job. + - name: Prepare the report schema + id: schema + run: python3 scripts/ci_review.py schema >> "$GITHUB_OUTPUT" + - name: Review with Claude Code + id: llm continue-on-error: true timeout-minutes: 25 uses: anthropics/claude-code-action@12dd8d74c712f5f3669365b2369b558c495b1104 # v1 env: - FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }} - FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }} ANTHROPIC_BASE_URL: https://api.minimax.cn/anthropic ANTHROPIC_AUTH_TOKEN: ${{ secrets.MINIMAX_API_KEY }} CLAUDE_CODE_AUTO_COMPACT_WINDOW: '524288' @@ -54,29 +71,57 @@ jobs: allowed_bots: '*' allowed_non_write_users: '*' prompt: | - 你是 CI 的负责人,负责审核 CI 结果并给出结论。 - 仓库 ${{ github.repository }} 的 PR #${{ github.event.pull_request.number }} 已合入 main: - - PR:https://github.com/${{ github.repository }}/pull/${{ github.event.pull_request.number }} - - 合并 commit:${{ github.event.pull_request.merge_commit_sha }}(已 checkout 到当前目录) - - 用 gh pr checks ${{ github.event.pull_request.number }} --repo ${{ github.repository }} 查询这个 PR 已有的检查结果,必要时读取对应 Actions 日志。不要触发新的 CI,也不要把合并本身当作检查通过的证据;管理员可能绕过门禁。检查失败、缺失或尚未完成时如实报告。 + 你负责本次 PR 的 ${{ matrix.name }},在独立上下文中完成审查。 + 仓库:${{ github.repository }};PR:#${{ github.event.pull_request.number }}。 + 合并提交:${{ github.event.pull_request.merge_commit_sha }},已检出到当前目录。 + 先用 gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} 读取变更清单与完整差异,再检查相关文件。 - 任务:给飞书群发一张中文卡片(Card 2.0)总结这次 CI,尽量在 10 轮以内给出结论,工具调用尽可能的并行。 - 环境里有 gh(已登录,GH_TOKEN)、git、node 和完整的仓库代码。 + ${{ matrix.instructions }} - 卡片要让手机上的读者快速看懂,如果一切正常,表达的尽可能简单,如果没有问题,尽量简洁: - 1. 哪个 PR(github id + PR 标题 + 链接) - - 如:`github id: jing332, PR 标题: 添加一个新功能, 链接: https://github.com/org/repo/pull/123` - 2. 改了什么(实际行为变化,1–3 条) - - 如:`添加了一个新功能、修改了 web 和 backend 的 CI` - 3. 是否符合仓库规则(AGENTS.md、CONTRIBUTING.md 及相关文档):未发现明确违规 / 发现需修复问题 / 信息不足,无法确认 - - 如:`符合仓库规则` - 4. CI 哪里失败、可能的原因(可以看日志),全部通过就直接说通过,不要给出细节。如果 CI 失败,则详细说明。 - - 如:`CI 失败,具体是哪几个失败;全部通过 ✅` - - 发送:webhook 地址在环境变量 FEISHU_WEBHOOK_URL。 - 响应 code=0 才算成功,失败就排查并重试,直到发出去。最后用一句中文说明结果,如:`CI 结果已发送至飞书群`。 - 不要在输出里打印 webhook 地址和密钥。 + 按 JSON schema 返回中文报告。status 为 ok(未发现问题)、issues(发现有证据的问题)或 incomplete(审查失败、范围未检查完整或检查结果尚未完成)。有问题且仍有未检查项时,用 issues 并在 summary 中说明缺项。summary 无问题时简短,有问题时保留依据和建议,遵守 schema 的长度限制。 + 只读审查,不修改仓库,不触发新的 CI,不发送消息。两份报告会由后续 job 合并发送。 claude_args: >- - --allowedTools Bash Read Write Edit Glob Grep WebFetch WebSearch + --allowedTools Bash Read Glob Grep --max-turns 30 + --json-schema '${{ steps.schema.outputs.schema }}' + - name: Save the review result + if: always() + env: + REVIEW_OUTCOME: ${{ steps.llm.outcome }} + REVIEW_RESULT: ${{ steps.llm.outputs.structured_output }} + run: python3 scripts/ci_review.py collect "$RUNNER_TEMP/review-${{ matrix.kind }}.json" + - uses: actions/upload-artifact@v6 + if: always() + with: + name: ci-review-${{ matrix.kind }} + path: ${{ runner.temp }}/review-${{ matrix.kind }}.json + if-no-files-found: error + overwrite: true + retention-days: 7 + + notify: + name: Combined Feishu notification + needs: review + if: always() && github.event.pull_request.merged == true + runs-on: ${{ vars.OAC_USE_GITHUB_RUNNERS == 'true' && 'ubuntu-24.04' || 'blacksmith-2vcpu-ubuntu-2404' }} + timeout-minutes: 5 + permissions: + contents: read + actions: read + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + persist-credentials: false + - uses: actions/download-artifact@v6 + continue-on-error: true + with: + pattern: ci-review-* + merge-multiple: true + path: ${{ runner.temp }}/reviews + - name: Send one card with both results + continue-on-error: true + env: + FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }} + FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }} + run: python3 scripts/ci_review.py notify "$RUNNER_TEMP/reviews" diff --git a/docs/maintainers.md b/docs/maintainers.md index 166c39feb..8863ed7b3 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -192,7 +192,9 @@ The final `check` runs even when planning or a dependency fails. It requires a s Use **Actions → core-check → Run workflow** for a manual full check. For a transient failure, use GitHub's **Re-run failed jobs** so successful jobs remain completed. PR updates cancel the superseded run through Actions concurrency. Build and dependency caches speed execution; they do not stand in for successful tests. Native release installers are passed between jobs using Actions artifacts, within the same release workflow. -The `CI review and Feishu notification` workflow runs once after a PR merges into main. It checks out the merged commit, reads that PR's existing checks and logs, and reports their actual status. It does not trigger another test run. Closing an unmerged PR does not invoke the review. The workflow uses `pull_request_target` only for the merged event and never checks out an unmerged PR head with notification credentials. +The `CI review and Feishu notification` workflow runs after a PR merges into main. A matrix runs **Code review** and **Docs review** in independent LLM contexts with `fail-fast: false`. Both read the merged commit and PR diff. Code review checks implementation, repository rules and existing CI results; it does not start another test run. Docs review checks changed behavior against documentation even when no docs changed. When docs change, it also checks contradictions, duplicated facts, topic ownership under CONTRIBUTING, and English/Chinese agreement. Findings include file and line references, supporting evidence and a minimal correction. Reviews read the repository without editing it. + +Each review returns a structured result (`ok`, `issues` or `incomplete`) and a Chinese summary, retained as an Actions artifact for seven days. **Combined Feishu notification** waits for both jobs and sends one Card 2.0 message containing both results through the existing `FEISHU_WEBHOOK_URL`; `FEISHU_WEBHOOK_SECRET` optionally signs it. Only this notification job receives the webhook secrets. Failed, missing or invalid reports appear as incomplete, alongside any available result from the other review. The notification job runs even when a review fails. Delivery requires Feishu's `code=0` response; a failed or ambiguous request is not automatically retried, avoiding duplicate messages. Review and delivery failures do not block merges, and closing an unmerged PR does not trigger this workflow. The workflow checks out only the merged commit with notification credentials. Browser jobs own separate fixtures and servers; increasing workers against the shared mutable fixture is unsafe. Failed browser jobs retain reports/traces for seven days. Native failure phase summaries are retained for seven days and detailed output stays in the Actions logs; credentials and temporary installation trees are not uploaded. Successful native archives are uploaded only for explicit manual packaging or releases, without recompressing the compressed archive. Release distribution artifacts retain their existing recovery policy; failed publication can reuse the original build as described above. diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index aebb1a172..152722ce3 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: 4f9fd38af5afd47e21589160930347f6693dd4786f56fb2dff2abd4853b3f72f +source_hash: bfe6a890a27ff93640636197e72241c71f94c14ca62385be54c69f203ee563e1 --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请使用 [安装指南](getting-started/install.md)。安装器代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装器](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -196,7 +196,9 @@ Go 模块和工作区输入会选择后端、API(包括容器)、原生和 要手动执行完整检查,请使用 **Actions → core-check → Run workflow**。遇到暂时性故障时,请使用 GitHub 的 **Re-run failed jobs**,这样已成功的作业可保持完成状态。PR 更新后,Actions 并发机制会取消已被取代的运行。构建和依赖项缓存可加快执行,但不能替代成功的测试。原生发布安装器通过 Actions 构建产物在同一发布工作流的不同作业之间传递。 -`CI review and Feishu notification` 工作流仅在 PR 合并到 main 后运行一次。它检出合并后的提交,读取该 PR 已有的检查和日志,并报告实际状态,不会触发另一轮测试。关闭未合并 PR 不触发审查。该工作流只针对合并事件使用 `pull_request_target`,绝不在持有通知凭据时检出未合并 PR 的 head。 +`CI review and Feishu notification` 工作流在 PR 合入 main 后运行。矩阵中的 **Code review** 和 **Docs review** 使用独立的 LLM 上下文,并设置 `fail-fast: false`。两者读取合并后的提交和 PR 差异。代码审查检查实现、仓库规则和已有 CI 结果,不触发新一轮测试。文档审查会核对行为变化与文档是否一致,即使没有修改文档;修改文档时,还会检查矛盾、重复维护的事实、CONTRIBUTING 规定的主题归属及中英文含义。每项问题包含文件和行号、依据及最小修改建议。审查只读取仓库,不修改文件。 + +每项审查返回结构化结果(`ok`、`issues` 或 `incomplete`)及中文摘要,作为 Actions 构建产物保留七天。**Combined Feishu notification** 等待两项审查结束,通过已有的 `FEISHU_WEBHOOK_URL` 发送一张包含两份结果的 Card 2.0 卡片;可选的 `FEISHU_WEBHOOK_SECRET` 用于签名。只有通知 job 能读取 webhook 密钥。审查失败、报告缺失或格式无效时标为未完成,另一项已有的结果照常展示。某项审查失败时,通知 job 仍会运行。只有飞书返回 `code=0` 才确认送达;请求失败或送达状态不明时不自动重试,以免重复发消息。审查和发送失败不会阻止合并;关闭未合并的 PR 不触发此流程。持有通知凭据时,工作流只检出合并后的提交。 浏览器作业各自拥有独立的固定数据和服务;对共享可变固定数据增加 worker 数不安全。失败的浏览器作业保留报告与 trace 七天。原生失败阶段摘要保留七天,详细输出留在 Actions 日志中;凭据和临时安装目录不上传。成功的原生归档仅用于显式手动打包或发布时上传,不重新压缩已压缩的归档。发布分发产物保留现有恢复策略;失败发布可以按前述方式复用原构建。 diff --git a/scripts/ci_review.py b/scripts/ci_review.py new file mode 100644 index 000000000..a6b97b887 --- /dev/null +++ b/scripts/ci_review.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Collect independent review reports and deliver one Feishu card.""" + +import argparse +import base64 +import hashlib +import hmac +import json +import os +from pathlib import Path +import re +import time +import urllib.error +import urllib.request + + +STATUSES = {"ok": "未发现问题", "issues": "发现问题", "incomplete": "未完成"} +MAX_SUMMARY = 2000 +SCHEMA = { + "type": "object", + "properties": { + "status": {"type": "string", "enum": list(STATUSES)}, + "summary": {"type": "string", "minLength": 1, "maxLength": MAX_SUMMARY}, + }, + "required": ["status", "summary"], + "additionalProperties": False, +} + + +def incomplete(reason): + return {"status": "incomplete", "summary": reason} + + +def parse_report(raw): + try: + report = json.loads(raw) + except (ValueError, TypeError): + return incomplete("未收到有效报告,请查看审查日志。") + if (not isinstance(report, dict) or set(report) != set(SCHEMA["required"]) + or not isinstance(report["status"], str) or report["status"] not in STATUSES + or not isinstance(report["summary"], str) or not report["summary"].strip() + or len(report["summary"]) > MAX_SUMMARY): + return incomplete("报告格式不完整,请查看审查日志。") + return report + + +def collect(outcome, raw): + if outcome != "success": + return incomplete("审查未成功结束,请查看审查日志。") + return parse_report(raw) + + +def read_report(directory, kind): + try: + return parse_report((directory / f"review-{kind}.json").read_text()) + except (OSError, UnicodeError): + return incomplete("审查报告缺失,请查看审查日志。") + + +def markdown_text(value): + return re.sub(r"([\\`*_{}\[\]()<>!])", r"\\\1", value) + + +def build_card(event, reports, run_url): + pr = event["pull_request"] + statuses = {report["status"] for report in reports.values()} + color = "red" if "issues" in statuses else "yellow" if "incomplete" in statuses else "green" + elements = [{"tag": "markdown", "content": ( + f"**{markdown_text(pr['title'][:200])}**\n" + f"{markdown_text(pr['user']['login'])} · [PR #{pr['number']}]({pr['html_url']})" + )}] + for kind, title in (("code", "代码审查与 CI"), ("docs", "文档审查")): + report = reports[kind] + # Feishu mentions use HTML-like tags; reports are ordinary Markdown. + summary = report["summary"].replace("<", "<").replace(">", ">") + elements.append({"tag": "markdown", "content": f"**{title}:{STATUSES[report['status']]}**\n{summary}"}) + elements.append({"tag": "markdown", "content": f"[查看审查日志]({run_url})"}) + return { + "msg_type": "interactive", + "card": { + "schema": "2.0", + "header": {"template": color, "title": {"tag": "plain_text", "content": f"CI 审查 · PR #{pr['number']}"}}, + "body": {"elements": elements}, + }, + } + + +def send_card(webhook, secret, card): + if not webhook: + raise ValueError("FEISHU_WEBHOOK_URL is not configured") + payload = dict(card) + if secret: + timestamp = str(int(time.time())) + key = f"{timestamp}\n{secret}".encode() + payload.update(timestamp=timestamp, sign=base64.b64encode(hmac.new(key, b"", hashlib.sha256).digest()).decode()) + data = json.dumps(payload, ensure_ascii=False).encode() + if len(data) > 20000: + raise ValueError("Review card exceeds the webhook message size limit") + request = urllib.request.Request(webhook, data=data, headers={"Content-Type": "application/json"}, method="POST") + # A timeout may follow successful delivery; retrying could send a duplicate. + try: + with urllib.request.urlopen(request, timeout=30) as response: + result = json.load(response) + except (OSError, ValueError, urllib.error.URLError): + raise ValueError("Feishu delivery failed; delivery status is unknown") from None + if not isinstance(result, dict) or type(result.get("code")) is not int or result["code"] != 0: + raise ValueError("Feishu did not confirm delivery") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + commands.add_parser("schema") + commands.add_parser("collect").add_argument("output", type=Path) + commands.add_parser("notify").add_argument("directory", type=Path) + args = parser.parse_args() + if args.command == "schema": + print("schema=" + json.dumps(SCHEMA, separators=(",", ":"))) + elif args.command == "collect": + report = collect(os.environ.get("REVIEW_OUTCOME"), os.environ.get("REVIEW_RESULT", "")) + args.output.write_text(json.dumps(report, ensure_ascii=False)) + else: + event = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text()) + reports = {kind: read_report(args.directory, kind) for kind in ("code", "docs")} + run_url = f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}" + card = build_card(event, reports, run_url) + summary = os.environ.get("GITHUB_STEP_SUMMARY") + if summary: + with open(summary, "a") as output: + output.write("\n\n".join(element["content"] for element in card["card"]["body"]["elements"]) + "\n") + send_card(os.environ.get("FEISHU_WEBHOOK_URL"), os.environ.get("FEISHU_WEBHOOK_SECRET"), card) + print("代码与文档审查结果已合并发送至飞书群。") + + +if __name__ == "__main__": + try: + main() + except ValueError as error: + raise SystemExit(str(error)) from None diff --git a/scripts/ci_review_test.py b/scripts/ci_review_test.py new file mode 100644 index 000000000..05161c0a8 --- /dev/null +++ b/scripts/ci_review_test.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Exercise combined notification and incomplete-review behavior without sending messages.""" + +import io +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import ci_review as review + + +class ReviewTests(unittest.TestCase): + event = {"pull_request": {"number": 485, "title": "Review docs", "html_url": "https://github.com/org/repo/pull/485", "user": {"login": "author"}}} + run_url = "https://github.com/org/repo/actions/runs/123" + + def setUp(self): + self.ok = {"status": "ok", "summary": "未发现问题"} + + def card(self, code=None, docs=None): + return review.build_card(self.event, {"code": code or self.ok, "docs": docs or self.ok}, self.run_url) + + @patch("ci_review.urllib.request.urlopen") + def test_two_reports_make_one_delivery(self, post): + post.return_value = io.BytesIO(b'{"code":0}') + card = self.card(docs={"status": "issues", "summary": "docs/install.md:12 与代码不符"}) + review.send_card("https://example.invalid/webhook", "", card) + post.assert_called_once() + payload = json.loads(post.call_args.args[0].data) + self.assertEqual(payload["card"]["schema"], "2.0") + self.assertEqual(payload["card"]["header"]["template"], "red") + text = "\n".join(item["content"] for item in payload["card"]["body"]["elements"]) + self.assertIn("代码审查与 CI:未发现问题", text) + self.assertIn("文档审查:发现问题", text) + self.assertIn("docs/install.md:12", text) + self.assertIn(self.run_url, text) + self.assertNotIn("sign", payload) + + def test_failed_action_cannot_publish_a_success_report(self): + for outcome in ("failure", "cancelled", "skipped", None): + with self.subTest(outcome=outcome): + result = review.collect(outcome, json.dumps(self.ok)) + self.assertEqual(result["status"], "incomplete") + self.assertEqual(self.card(code=result)["card"]["header"]["template"], "yellow") + + def test_invalid_or_missing_reports_are_incomplete(self): + for raw in ("", "not json", "null", "[]", '{"status":"ok"}', '{"status":[],"summary":"x"}', '{"status":"ok","summary":" "}', json.dumps({"status": "ok", "summary": "x" * 2001})): + with self.subTest(raw=raw[:50]): + self.assertEqual(review.collect("success", raw)["status"], "incomplete") + root = Path.home() / ".oac/tests" + root.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=root) as directory: + path = Path(directory) + (path / "review-code.json").write_text(json.dumps(self.ok)) + self.assertEqual(review.read_report(path, "code"), self.ok) + self.assertEqual(review.read_report(path, "docs")["status"], "incomplete") + + @patch("ci_review.urllib.request.urlopen") + def test_http_success_requires_feishu_confirmation(self, post): + for body in (b'{"code":19021}', b'{}', b'{"code":false}', b'not json'): + with self.subTest(body=body): + post.reset_mock() + post.return_value = io.BytesIO(body) + with self.assertRaises(ValueError): + review.send_card("https://example.invalid/webhook", "", self.card()) + post.assert_called_once() + + @patch("ci_review.urllib.request.urlopen", side_effect=TimeoutError("private-webhook")) + def test_ambiguous_delivery_is_not_retried_or_logged_with_secrets(self, post): + with self.assertRaisesRegex(ValueError, "delivery status is unknown") as error: + review.send_card("https://example.invalid/private-webhook", "private-signing-key", self.card()) + self.assertNotIn("private", str(error.exception)) + post.assert_called_once() + + @patch("ci_review.time.time", return_value=1700000000) + @patch("ci_review.urllib.request.urlopen") + def test_optional_signing(self, post, _clock): + post.return_value = io.BytesIO(b'{"code":0}') + review.send_card("https://example.invalid/webhook", "test-secret", self.card()) + payload = json.loads(post.call_args.args[0].data) + self.assertEqual(payload["timestamp"], "1700000000") + self.assertEqual(payload["sign"], "mbm4Y4oluIPQ00qlBIhX8vAZ0EKv3nw0LuTb91jPL84=") + + @patch("ci_review.urllib.request.urlopen") + def test_bounded_unicode_reports_and_mentions(self, post): + post.return_value = io.BytesIO(b'{"code":0}') + report = {"status": "issues", "summary": "所有人" + "问" * 1950} + review.send_card("https://example.invalid/webhook", "", self.card(report, report)) + data = post.call_args.args[0].data + self.assertLess(len(data), 20000) + self.assertNotIn(b" Date: Wed, 7 Oct 2026 21:08:13 +0800 Subject: [PATCH 04/14] fix(ci): retain completed reviews within the execution timeout (#487) --- .github/workflows/ci-review.yml | 4 ++-- docs/maintainers.md | 2 +- docs/zh/maintainers.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-review.yml b/.github/workflows/ci-review.yml index c5a2da524..2f328e429 100644 --- a/.github/workflows/ci-review.yml +++ b/.github/workflows/ci-review.yml @@ -32,7 +32,7 @@ jobs: instructions: | 每次都检查文档与本次代码变化是否一致,即使 PR 没改文档。读取相关实现、测试和完整文档,核对平台、命令、配置、路径、默认值、接口和操作流程,找出遗漏更新或已经过时的说明。 文档有修改(包括新增、删除和重命名)时,再检查文档内部及同主题文档间的矛盾、重复维护的事实、主题归属和中英文含义。按 AGENTS.md 和 CONTRIBUTING.md 的 Documentation ownership 表判断位置。必要的概述加链接、中英文对照和同一来源生成的文档不算重复。 - 分别报告“文档与代码一致性”和“文档矛盾、重复与归属”。未改文档时,第二项写“本次未修改文档”。只报告本次修改引入或使之过时的、有依据的问题,给出文件与行号、对应代码或文档依据,以及最小修改建议。测试或链接检查通过不代表语义一致。 + 分别报告“文档与代码一致性”和“文档矛盾、重复与归属”。未改文档时,第二项写“本次未修改文档”。只报告本次修改引入或使之过时的、有依据的问题,给出文件与行号、对应代码或文档依据,以及最小修改建议。测试或链接检查通过不代表语义一致。只报告影响读者理解或操作的具体问题,不把未穷举实现细节当作文档缺陷。 if: github.event.pull_request.merged == true runs-on: ${{ vars.OAC_USE_GITHUB_RUNNERS == 'true' && 'ubuntu-24.04' || 'blacksmith-2vcpu-ubuntu-2404' }} timeout-minutes: 30 @@ -79,10 +79,10 @@ jobs: ${{ matrix.instructions }} 按 JSON schema 返回中文报告。status 为 ok(未发现问题)、issues(发现有证据的问题)或 incomplete(审查失败、范围未检查完整或检查结果尚未完成)。有问题且仍有未检查项时,用 issues 并在 summary 中说明缺项。summary 无问题时简短,有问题时保留依据和建议,遵守 schema 的长度限制。 + 围绕本次 diff 和受影响的文档展开,证据充分后输出结果,避免重复核对同一结论。 只读审查,不修改仓库,不触发新的 CI,不发送消息。两份报告会由后续 job 合并发送。 claude_args: >- --allowedTools Bash Read Glob Grep - --max-turns 30 --json-schema '${{ steps.schema.outputs.schema }}' - name: Save the review result if: always() diff --git a/docs/maintainers.md b/docs/maintainers.md index 8863ed7b3..7f3ac6626 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -194,7 +194,7 @@ Use **Actions → core-check → Run workflow** for a manual full check. For a t The `CI review and Feishu notification` workflow runs after a PR merges into main. A matrix runs **Code review** and **Docs review** in independent LLM contexts with `fail-fast: false`. Both read the merged commit and PR diff. Code review checks implementation, repository rules and existing CI results; it does not start another test run. Docs review checks changed behavior against documentation even when no docs changed. When docs change, it also checks contradictions, duplicated facts, topic ownership under CONTRIBUTING, and English/Chinese agreement. Findings include file and line references, supporting evidence and a minimal correction. Reviews read the repository without editing it. -Each review returns a structured result (`ok`, `issues` or `incomplete`) and a Chinese summary, retained as an Actions artifact for seven days. **Combined Feishu notification** waits for both jobs and sends one Card 2.0 message containing both results through the existing `FEISHU_WEBHOOK_URL`; `FEISHU_WEBHOOK_SECRET` optionally signs it. Only this notification job receives the webhook secrets. Failed, missing or invalid reports appear as incomplete, alongside any available result from the other review. The notification job runs even when a review fails. Delivery requires Feishu's `code=0` response; a failed or ambiguous request is not automatically retried, avoiding duplicate messages. Review and delivery failures do not block merges, and closing an unmerged PR does not trigger this workflow. The workflow checks out only the merged commit with notification credentials. +Each review returns a structured result (`ok`, `issues` or `incomplete`) and a Chinese summary, retained as an Actions artifact for seven days. **Combined Feishu notification** waits for both jobs and sends one Card 2.0 message containing both results through the existing `FEISHU_WEBHOOK_URL`; `FEISHU_WEBHOOK_SECRET` optionally signs it. Only this notification job receives the webhook secrets. Failed, missing or invalid reports appear as incomplete, alongside any available result from the other review. The notification job runs even when a review fails. Delivery requires Feishu's `code=0` response; a failed or ambiguous request is not automatically retried, avoiding duplicate messages. Each review has a 25-minute execution timeout. Review and delivery failures do not block merges, and closing an unmerged PR does not trigger this workflow. The workflow checks out only the merged commit with notification credentials. Browser jobs own separate fixtures and servers; increasing workers against the shared mutable fixture is unsafe. Failed browser jobs retain reports/traces for seven days. Native failure phase summaries are retained for seven days and detailed output stays in the Actions logs; credentials and temporary installation trees are not uploaded. Successful native archives are uploaded only for explicit manual packaging or releases, without recompressing the compressed archive. Release distribution artifacts retain their existing recovery policy; failed publication can reuse the original build as described above. diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index 152722ce3..6fb303605 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: bfe6a890a27ff93640636197e72241c71f94c14ca62385be54c69f203ee563e1 +source_hash: e3435d01696a172a0a0bcd5acecf4167410389f2c249730c9333f5d24027248d --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请使用 [安装指南](getting-started/install.md)。安装器代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装器](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -198,7 +198,7 @@ Go 模块和工作区输入会选择后端、API(包括容器)、原生和 `CI review and Feishu notification` 工作流在 PR 合入 main 后运行。矩阵中的 **Code review** 和 **Docs review** 使用独立的 LLM 上下文,并设置 `fail-fast: false`。两者读取合并后的提交和 PR 差异。代码审查检查实现、仓库规则和已有 CI 结果,不触发新一轮测试。文档审查会核对行为变化与文档是否一致,即使没有修改文档;修改文档时,还会检查矛盾、重复维护的事实、CONTRIBUTING 规定的主题归属及中英文含义。每项问题包含文件和行号、依据及最小修改建议。审查只读取仓库,不修改文件。 -每项审查返回结构化结果(`ok`、`issues` 或 `incomplete`)及中文摘要,作为 Actions 构建产物保留七天。**Combined Feishu notification** 等待两项审查结束,通过已有的 `FEISHU_WEBHOOK_URL` 发送一张包含两份结果的 Card 2.0 卡片;可选的 `FEISHU_WEBHOOK_SECRET` 用于签名。只有通知 job 能读取 webhook 密钥。审查失败、报告缺失或格式无效时标为未完成,另一项已有的结果照常展示。某项审查失败时,通知 job 仍会运行。只有飞书返回 `code=0` 才确认送达;请求失败或送达状态不明时不自动重试,以免重复发消息。审查和发送失败不会阻止合并;关闭未合并的 PR 不触发此流程。持有通知凭据时,工作流只检出合并后的提交。 +每项审查返回结构化结果(`ok`、`issues` 或 `incomplete`)及中文摘要,作为 Actions 构建产物保留七天。**Combined Feishu notification** 等待两项审查结束,通过已有的 `FEISHU_WEBHOOK_URL` 发送一张包含两份结果的 Card 2.0 卡片;可选的 `FEISHU_WEBHOOK_SECRET` 用于签名。只有通知 job 能读取 webhook 密钥。审查失败、报告缺失或格式无效时标为未完成,另一项已有的结果照常展示。某项审查失败时,通知 job 仍会运行。只有飞书返回 `code=0` 才确认送达;请求失败或送达状态不明时不自动重试,以免重复发消息。每项审查的执行超时为 25 分钟。审查和发送失败不会阻止合并;关闭未合并的 PR 不触发此流程。持有通知凭据时,工作流只检出合并后的提交。 浏览器作业各自拥有独立的固定数据和服务;对共享可变固定数据增加 worker 数不安全。失败的浏览器作业保留报告与 trace 七天。原生失败阶段摘要保留七天,详细输出留在 Actions 日志中;凭据和临时安装目录不上传。成功的原生归档仅用于显式手动打包或发布时上传,不重新压缩已压缩的归档。发布分发产物保留现有恢复策略;失败发布可以按前述方式复用原构建。 From 516fd3041132d47bb7ad45be537f68c6f37fa777 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 21:16:38 +0800 Subject: [PATCH 05/14] Run Harnesses unattended and delete the interaction protocol (#489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every Harness now runs unattended; a human in the loop goes only through function tools and requires_action, as in the pinned Agents API. - mcode advertises no elicitation and answers session/request_permission with the ACP cancelled outcome, which mcode treats as a denial; other client methods get -32601. - codex disables its blocking tools.experimental_request_user_input tool; under approval policy never it settles MCP elicitation itself, and any other server request gets the client's method-not-found reply. The approval policy and sandbox unions collapse to constants. - Delete permission_request, permission_cancel, permission_decision, prompt_for_user_choice, prompt_for_user_choice_decision, device_shutdown, the Permissions capability, the responder interfaces, the daemon interaction routing, the gateway interaction indexes and the interaction_not_supported diagnostic. - interaction_decision_ack stays for function results and cancellation; the daemon's applied-result replay now serves function results only. Bumps the Core–Runtime protocol to 0.12.0, which also covers the earlier agent_options, request-field and prompt_request removals. --- .../internal/agent/claudesdk/contracts.go | 2 - .../internal/agent/claudesdk/declaration.go | 1 - .../internal/agent/claudesdk/unsupported.go | 11 +- .../agent/claudesdk/unsupported_test.go | 4 - .../internal/agent/codex/approval_policy.go | 85 ---- .../agent/codex/approval_policy_test.go | 142 ------- apps/daemon/internal/agent/codex/contracts.go | 2 - .../internal/agent/codex/declaration.go | 1 - .../internal/agent/codex/declaration_test.go | 2 +- .../agent/codex/execution_controls_test.go | 2 +- apps/daemon/internal/agent/codex/executor.go | 2 +- .../internal/agent/codex/executor_turn.go | 9 +- .../agent/codex/function_receipt_test.go | 8 +- .../codex/function_write_receipt_test.go | 4 +- .../internal/agent/codex/functions_test.go | 12 +- apps/daemon/internal/agent/codex/options.go | 15 +- .../internal/agent/codex/options_test.go | 8 +- .../agent/codex/permission_profile_test.go | 3 - .../internal/agent/codex/preparation.go | 1 - apps/daemon/internal/agent/codex/protocol.go | 182 +-------- .../agent/codex/protocol_wire_test.go | 64 +--- .../internal/agent/codex/rpc_close_test.go | 2 +- .../internal/agent/codex/server_requests.go | 362 ------------------ .../agent/codex/server_requests_mcp.go | 38 -- .../agent/codex/server_requests_mcp_test.go | 100 ----- .../agent/codex/server_requests_test.go | 357 ----------------- apps/daemon/internal/agent/codex/session.go | 25 +- .../internal/agent/codex/session_cancel.go | 1 - .../agent/codex/session_cancel_test.go | 2 +- .../agent/codex/session_cancel_write_test.go | 2 +- .../internal/agent/codex/session_plan.go | 3 - .../internal/agent/codex/session_run.go | 1 - .../codex/session_steering_lifecycle_test.go | 2 +- .../internal/agent/codex/session_thread.go | 18 +- .../agent/codex/subagent_observations_test.go | 2 +- .../agent/contract_declarations_test.go | 2 - .../internal/agent/contracttest/text.go | 2 +- apps/daemon/internal/agent/harness.go | 18 +- apps/daemon/internal/agent/mcode/contracts.go | 2 - .../internal/agent/mcode/declaration.go | 1 - .../internal/agent/mcode/declaration_test.go | 2 +- apps/daemon/internal/agent/mcode/events.go | 44 +-- .../internal/agent/mcode/executor_turn.go | 3 +- .../internal/agent/mcode/options_test.go | 19 - apps/daemon/internal/agent/mcode/protocol.go | 15 - apps/daemon/internal/agent/mcode/questions.go | 217 ----------- .../internal/agent/mcode/questions_test.go | 55 --- apps/daemon/internal/agent/mcode/session.go | 32 +- .../internal/agent/mcode/session_test.go | 70 ++-- apps/daemon/internal/agent/registry.go | 11 - apps/daemon/internal/agent/registry_test.go | 9 +- .../dispatch/capability_admission_test.go | 47 --- apps/daemon/internal/dispatch/export_test.go | 23 -- apps/daemon/internal/dispatch/functions.go | 53 ++- .../dispatch/interaction_decisions.go | 328 ---------------- .../dispatch/optional_interactions_test.go | 43 --- .../dispatch/preparation_cancel_test.go | 14 +- .../preparation_executor_fixture_test.go | 12 - .../internal/dispatch/preparation_start.go | 2 +- .../internal/dispatch/preparation_test.go | 2 +- .../internal/dispatch/prepared_handoff.go | 15 - .../prepared_handoff_mutation_test.go | 53 +-- .../dispatch/prepared_handoff_test.go | 23 +- apps/daemon/internal/dispatch/router.go | 36 +- apps/daemon/internal/dispatch/router_test.go | 248 +----------- apps/daemon/internal/dispatch/shutdown.go | 37 +- apps/daemon/internal/dispatch/suspend.go | 2 +- apps/daemon/internal/dispatch/suspend_test.go | 14 +- contracts/agents-api/core-errors.md | 2 +- contracts/agents-api/harness-onboarding.md | 17 +- contracts/agents-api/zh/core-errors.md | 4 +- contracts/agents-api/zh/harness-onboarding.md | 19 +- docs/runtime-protocol.md | 14 +- docs/zh/runtime-protocol.md | 16 +- internal/agentdaemon/proto/envelope.go | 5 +- internal/agentdaemon/proto/inbound.go | 72 +--- internal/agentdaemon/proto/outbound.go | 49 --- .../proto/prototest/capabilities.go | 1 - internal/agentdaemon/proto/tool_call_test.go | 13 + internal/agentdaemon/proto/user_choice.go | 67 ---- .../agentdaemon/proto/user_choice_test.go | 46 --- internal/agentdaemon/proto/version.go | 2 +- .../internal/api/turn_diagnostic_failure.go | 2 +- .../api/turn_diagnostic_failure_test.go | 2 +- services/core/internal/execution/delivery.go | 3 - services/core/internal/runtimedevice/state.go | 1 - .../mcp_bearer_live_linux_test.go | 53 ++- .../core/internal/runtimegateway/registry.go | 111 +----- .../core/internal/runtimegateway/session.go | 36 +- .../internal/runtimegateway/session_test.go | 83 +--- .../runtime_compute_lifecycle_test.go | 3 - 91 files changed, 281 insertions(+), 3269 deletions(-) delete mode 100644 apps/daemon/internal/agent/codex/approval_policy.go delete mode 100644 apps/daemon/internal/agent/codex/approval_policy_test.go delete mode 100644 apps/daemon/internal/agent/codex/server_requests.go delete mode 100644 apps/daemon/internal/agent/codex/server_requests_mcp.go delete mode 100644 apps/daemon/internal/agent/codex/server_requests_mcp_test.go delete mode 100644 apps/daemon/internal/agent/codex/server_requests_test.go delete mode 100644 apps/daemon/internal/agent/mcode/questions.go delete mode 100644 apps/daemon/internal/agent/mcode/questions_test.go delete mode 100644 apps/daemon/internal/dispatch/interaction_decisions.go delete mode 100644 apps/daemon/internal/dispatch/optional_interactions_test.go create mode 100644 internal/agentdaemon/proto/tool_call_test.go delete mode 100644 internal/agentdaemon/proto/user_choice.go delete mode 100644 internal/agentdaemon/proto/user_choice_test.go diff --git a/apps/daemon/internal/agent/claudesdk/contracts.go b/apps/daemon/internal/agent/claudesdk/contracts.go index 4683fd3aa..72b94bdad 100644 --- a/apps/daemon/internal/agent/claudesdk/contracts.go +++ b/apps/daemon/internal/agent/claudesdk/contracts.go @@ -11,8 +11,6 @@ var ( _ agent.DurableSteerer = (*session)(nil) _ agent.Steerer = (*session)(nil) _ agent.FunctionResultSubmitter = (*session)(nil) - _ agent.PermissionResponder = (*session)(nil) - _ agent.UserChoiceResponder = (*session)(nil) _ agent.WorkspaceReader = (*session)(nil) _ agent.WorkspaceDirectoryLister = (*session)(nil) _ agent.WorkspaceWriter = (*session)(nil) diff --git a/apps/daemon/internal/agent/claudesdk/declaration.go b/apps/daemon/internal/agent/claudesdk/declaration.go index bbe3bdd3a..6459a635f 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration.go +++ b/apps/daemon/internal/agent/claudesdk/declaration.go @@ -22,7 +22,6 @@ const claudeSDKNodeEnv = "OAC_RUNTIME_CLAUDE_SDK_NODE" var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude_sdk", Capabilities: proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilityUnsupported, Streaming: proto.CapabilitySupported, - Permissions: proto.CapabilityUnsupported, Usage: proto.CapabilitySupported, Resume: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilityUnsupported, diff --git a/apps/daemon/internal/agent/claudesdk/unsupported.go b/apps/daemon/internal/agent/claudesdk/unsupported.go index 091e6d167..d66e3cd5f 100644 --- a/apps/daemon/internal/agent/claudesdk/unsupported.go +++ b/apps/daemon/internal/agent/claudesdk/unsupported.go @@ -2,19 +2,10 @@ package claudesdk import ( "context" - "fmt" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func (s *session) SubmitPermission(context.Context, string, proto.PermissionDecisionPayload) error { - return fmt.Errorf("%w: native permission responses are not exposed", agent.ErrUnsupportedOperation) -} - -func (s *session) SubmitPromptForUserChoice(context.Context, string, proto.PromptForUserChoiceDecisionPayload) error { - return fmt.Errorf("%w: native user-choice responses are not exposed", agent.ErrUnsupportedOperation) -} - func (s *executor) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported } diff --git a/apps/daemon/internal/agent/claudesdk/unsupported_test.go b/apps/daemon/internal/agent/claudesdk/unsupported_test.go index 82ee6cae2..9934a1038 100644 --- a/apps/daemon/internal/agent/claudesdk/unsupported_test.go +++ b/apps/daemon/internal/agent/claudesdk/unsupported_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) // Nil concrete receivers prove Unsupported needs no native owner, transport, @@ -25,9 +24,6 @@ func TestUnsupportedExtensionsHaveNoNativeEffects(t *testing.T) { t.Fatal("unsupported error disclosed input") } } - var turn *session - check(turn.SubmitPermission(ctx, secret, proto.PermissionDecisionPayload{})) - check(turn.SubmitPromptForUserChoice(ctx, secret, proto.PromptForUserChoiceDecisionPayload{})) for _, owner := range []agent.WorkspaceWriter{(*executor)(nil), (*session)(nil), (*prepared)(nil)} { result, err := owner.WriteWorkspaceFile(ctx, secret, []byte(secret)) check(err) diff --git a/apps/daemon/internal/agent/codex/approval_policy.go b/apps/daemon/internal/agent/codex/approval_policy.go deleted file mode 100644 index 9b084e4da..000000000 --- a/apps/daemon/internal/agent/codex/approval_policy.go +++ /dev/null @@ -1,85 +0,0 @@ -package codex - -import ( - "encoding/json" - "fmt" -) - -// SilentGranularPolicy disables every granular Codex approval gate. -func SilentGranularPolicy() AskForApproval { - g := GranularAskForApproval{} // zero value = every gate false - return AskForApproval{Granular: &g} -} - -// HumanApprovalPolicy lets Codex request sandbox escalation from Core. -func HumanApprovalPolicy() AskForApproval { - return AskForApproval{String: "on-request"} -} - -// IsSilent reports whether p suppresses every approval surface. The -// daemon logs this distinction when it starts a thread. Server-request -// handlers are always registered so explicit policy overrides cannot leave -// Codex waiting on an unhandled request. -// -// A nil pointer is treated as silent — the safe default when callers -// forget to wire a policy. -func IsSilent(p *AskForApproval) bool { - if p == nil { - return true - } - if p.Granular != nil { - g := p.Granular - return !g.SandboxApproval && !g.Rules && !g.SkillApproval && - !g.RequestPermissions && !g.MCPElicitations - } - // Bare string variants ("never" is silent; everything else surfaces). - return p.String == "never" -} - -// MarshalJSON emits codex-rs's discriminated union — either a bare -// string or the granular object. Producing both would deserialise to -// the granular branch on the codex side (it wins precedence in -// codex-rs/protocol.rs AskForApproval), but the wire would be wrong; we -// validate exclusivity here. -func (a AskForApproval) MarshalJSON() ([]byte, error) { - hasString := a.String != "" - hasGranular := a.Granular != nil - if hasString && hasGranular { - return nil, fmt.Errorf("codex: AskForApproval must set exactly one of String or Granular, got both") - } - if hasString { - return json.Marshal(a.String) - } - if hasGranular { - return json.Marshal(struct { - Granular *GranularAskForApproval `json:"granular"` - }{Granular: a.Granular}) - } - // Neither set — preserve the historical zero-value encoding. Production - // plans always set a policy explicitly; an empty marshal would - // produce `null`, which codex-rs rejects. - return json.Marshal(struct { - Granular GranularAskForApproval `json:"granular"` - }{}) -} - -// UnmarshalJSON is the inverse: codex's ThreadStartResult.approvalPolicy -// echoes back as either a string or {"granular":...}. Provided so tests -// can round-trip without surprises; production code only marshals. -func (a *AskForApproval) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err == nil { - a.String = s - a.Granular = nil - return nil - } - var obj struct { - Granular *GranularAskForApproval `json:"granular"` - } - if err := json.Unmarshal(data, &obj); err != nil { - return fmt.Errorf("codex: AskForApproval: %w", err) - } - a.Granular = obj.Granular - a.String = "" - return nil -} diff --git a/apps/daemon/internal/agent/codex/approval_policy_test.go b/apps/daemon/internal/agent/codex/approval_policy_test.go deleted file mode 100644 index 7a98f06cd..000000000 --- a/apps/daemon/internal/agent/codex/approval_policy_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package codex - -import ( - "encoding/json" - "testing" -) - -func TestSilentGranularPolicy_AllFalse(t *testing.T) { - p := SilentGranularPolicy() - if p.Granular == nil { - t.Fatal("SilentGranularPolicy must populate Granular") - } - g := *p.Granular - if g.SandboxApproval || g.Rules || g.SkillApproval || g.RequestPermissions || g.MCPElicitations { - t.Fatalf("silent policy must have every gate false, got %+v", g) - } - if !IsSilent(&p) { - t.Fatal("IsSilent must return true for the silent default") - } -} - -func TestHumanApprovalPolicy_UsesOnRequest(t *testing.T) { - p := HumanApprovalPolicy() - if p.String != "on-request" || p.Granular != nil { - t.Fatalf("HumanApprovalPolicy = %+v, want on-request string policy", p) - } - if IsSilent(&p) { - t.Fatal("HumanApprovalPolicy must surface app-server requests") - } -} - -func TestIsSilent_NilIsSilent(t *testing.T) { - if !IsSilent(nil) { - t.Fatal("nil policy must be treated as silent (safe default)") - } -} - -func TestIsSilent_StringPolicies(t *testing.T) { - cases := []struct { - name string - policy AskForApproval - want bool - }{ - {"never silences", AskForApproval{String: "never"}, true}, - {"on-request loud", AskForApproval{String: "on-request"}, false}, - {"untrusted loud", AskForApproval{String: "untrusted"}, false}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - if got := IsSilent(&tc.policy); got != tc.want { - t.Fatalf("IsSilent(%+v) = %v, want %v", tc.policy, got, tc.want) - } - }) - } -} - -func TestIsSilent_PartialGranularIsLoud(t *testing.T) { - // Flipping any single gate true must make IsSilent report false so - // the daemon registers the loud server-request handler. - for _, flip := range []func(*GranularAskForApproval){ - func(g *GranularAskForApproval) { g.SandboxApproval = true }, - func(g *GranularAskForApproval) { g.Rules = true }, - func(g *GranularAskForApproval) { g.SkillApproval = true }, - func(g *GranularAskForApproval) { g.RequestPermissions = true }, - func(g *GranularAskForApproval) { g.MCPElicitations = true }, - } { - g := GranularAskForApproval{} - flip(&g) - p := AskForApproval{Granular: &g} - if IsSilent(&p) { - t.Fatalf("IsSilent must be false when any gate is true: %+v", g) - } - } -} - -func TestAskForApproval_MarshalString(t *testing.T) { - p := AskForApproval{String: "never"} - raw, err := json.Marshal(p) - if err != nil { - t.Fatalf("marshal: %v", err) - } - if string(raw) != `"never"` { - t.Fatalf("marshal string variant = %s", raw) - } -} - -func TestAskForApproval_MarshalGranular(t *testing.T) { - g := GranularAskForApproval{RequestPermissions: true} - p := AskForApproval{Granular: &g} - raw, err := json.Marshal(p) - if err != nil { - t.Fatalf("marshal: %v", err) - } - want := `{"granular":{"sandbox_approval":false,"rules":false,"skill_approval":false,"request_permissions":true,"mcp_elicitations":false}}` - if string(raw) != want { - t.Fatalf("marshal granular = %s\nwant = %s", raw, want) - } -} - -func TestAskForApproval_MarshalEmpty_DefaultsSilent(t *testing.T) { - // Neither field set — must not produce `null` (codex rejects it). - var p AskForApproval - raw, err := json.Marshal(p) - if err != nil { - t.Fatalf("marshal: %v", err) - } - if string(raw) == "null" { - t.Fatalf("empty AskForApproval must not marshal to null: %s", raw) - } - if string(raw) != `{"granular":{"sandbox_approval":false,"rules":false,"skill_approval":false,"request_permissions":false,"mcp_elicitations":false}}` { - t.Fatalf("empty AskForApproval marshal = %s", raw) - } -} - -func TestAskForApproval_MarshalBothErrors(t *testing.T) { - g := GranularAskForApproval{} - p := AskForApproval{String: "never", Granular: &g} - if _, err := json.Marshal(p); err == nil { - t.Fatal("setting both String and Granular must error on marshal") - } -} - -func TestAskForApproval_RoundTripString(t *testing.T) { - var got AskForApproval - if err := json.Unmarshal([]byte(`"on-request"`), &got); err != nil { - t.Fatalf("unmarshal: %v", err) - } - if got.String != "on-request" || got.Granular != nil { - t.Fatalf("round-trip = %+v", got) - } -} - -func TestAskForApproval_RoundTripGranular(t *testing.T) { - src := `{"granular":{"sandbox_approval":true,"rules":false,"skill_approval":false,"request_permissions":false,"mcp_elicitations":false}}` - var got AskForApproval - if err := json.Unmarshal([]byte(src), &got); err != nil { - t.Fatalf("unmarshal: %v", err) - } - if got.Granular == nil || !got.Granular.SandboxApproval || got.String != "" { - t.Fatalf("round-trip = %+v", got) - } -} diff --git a/apps/daemon/internal/agent/codex/contracts.go b/apps/daemon/internal/agent/codex/contracts.go index 0ba51146f..a6f045a89 100644 --- a/apps/daemon/internal/agent/codex/contracts.go +++ b/apps/daemon/internal/agent/codex/contracts.go @@ -11,8 +11,6 @@ var ( _ agent.DurableSteerer = (*Session)(nil) _ agent.Steerer = (*Session)(nil) _ agent.FunctionResultSubmitter = (*Session)(nil) - _ agent.PermissionResponder = (*Session)(nil) - _ agent.UserChoiceResponder = (*Session)(nil) _ agent.WorkspaceReader = (*Session)(nil) _ agent.WorkspaceDirectoryLister = (*Session)(nil) _ agent.WorkspaceWriter = (*Session)(nil) diff --git a/apps/daemon/internal/agent/codex/declaration.go b/apps/daemon/internal/agent/codex/declaration.go index 2869f9da0..8e5aa004b 100644 --- a/apps/daemon/internal/agent/codex/declaration.go +++ b/apps/daemon/internal/agent/codex/declaration.go @@ -16,7 +16,6 @@ var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{ Capabilities: proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilitySupported, Streaming: proto.CapabilitySupported, - Permissions: proto.CapabilitySupported, Usage: proto.CapabilitySupported, Resume: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilityUnsupported, diff --git a/apps/daemon/internal/agent/codex/declaration_test.go b/apps/daemon/internal/agent/codex/declaration_test.go index b57d13e9a..8f5c7e757 100644 --- a/apps/daemon/internal/agent/codex/declaration_test.go +++ b/apps/daemon/internal/agent/codex/declaration_test.go @@ -29,7 +29,7 @@ func TestMCPRequiredDiscoveryRequiresPinnedNative(t *testing.T) { // The declaration must retain the complete baseline capability descriptor. func TestDeclaredCapabilityBaseline(t *testing.T) { - expected := map[string]bool{"SubagentObservations": true, "Streaming": true, "Permissions": true, "Usage": true, "Resume": true, "Steering": true, "MessageItems": true, "ToolObservations": true, "EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "WebSearchControl": true, "MessageImages": true, "FunctionResultImages": true, "SubagentControl": true, "DurableInputReceipts": true, "DurableTurns": true, "FunctionTools": true, "MCPHTTPTools": true, "MCPHTTPBearerAuth": true} + expected := map[string]bool{"SubagentObservations": true, "Streaming": true, "Usage": true, "Resume": true, "Steering": true, "MessageItems": true, "ToolObservations": true, "EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "WebSearchControl": true, "MessageImages": true, "FunctionResultImages": true, "SubagentControl": true, "DurableInputReceipts": true, "DurableTurns": true, "FunctionTools": true, "MCPHTTPTools": true, "MCPHTTPBearerAuth": true} expected["ExecutionControls"], expected["TextVerbosity"] = SupportsTextVerbosity, SupportsTextVerbosity value := reflect.ValueOf(Declaration.Info.Capabilities) for i := 0; i < value.NumField(); i++ { diff --git a/apps/daemon/internal/agent/codex/execution_controls_test.go b/apps/daemon/internal/agent/codex/execution_controls_test.go index 65f8a058f..e9d6f5451 100644 --- a/apps/daemon/internal/agent/codex/execution_controls_test.go +++ b/apps/daemon/internal/agent/codex/execution_controls_test.go @@ -16,7 +16,7 @@ func TestExecutionControlsSelectNativeSettings(t *testing.T) { t.Fatal(err) } plan.Cleanup() - want := [][2]string{{"web_search", `"` + search + `"`}, {"model_verbosity", `"` + verbosity + `"`}} + want := [][2]string{{"tools.experimental_request_user_input.enabled", "false"}, {"web_search", `"` + search + `"`}, {"model_verbosity", `"` + verbosity + `"`}} if !reflect.DeepEqual(plan.ExtraConfig, want) { t.Fatalf("config = %v, want %v", plan.ExtraConfig, want) } diff --git a/apps/daemon/internal/agent/codex/executor.go b/apps/daemon/internal/agent/codex/executor.go index 75403c57d..d8997e525 100644 --- a/apps/daemon/internal/agent/codex/executor.go +++ b/apps/daemon/internal/agent/codex/executor.go @@ -72,7 +72,7 @@ func (e *Executor) StartTurn(ctx context.Context, runID string, input proto.Mess functions: functions, observeMessages: base.observeMessages, observeSubagentIdentities: base.observeSubagentIdentities, cfg: base.cfg, rpc: base.rpc, cancelCtx: turnCtx, cancelFn: cancel, waitDone: make(chan struct{}), outputDone: make(chan struct{}), cleanup: func() {}, - bufs: NewItemBuffers(), resolvedModel: base.resolvedModel, interactions: newPendingCodexInteractions(), runID: runID, out: out} + bufs: NewItemBuffers(), resolvedModel: base.resolvedModel, runID: runID, out: out} if previous != nil { s.threadID = previous.currentThreadID() s.retiredTurns = make(map[string]bool, len(previous.retiredTurns)+1) diff --git a/apps/daemon/internal/agent/codex/executor_turn.go b/apps/daemon/internal/agent/codex/executor_turn.go index d02eb988c..60746293f 100644 --- a/apps/daemon/internal/agent/codex/executor_turn.go +++ b/apps/daemon/internal/agent/codex/executor_turn.go @@ -44,7 +44,6 @@ func (s *Session) settleExecutorTurn(startErr error) { s.operationMu.Unlock() s.stopSteering() s.stopFunctionCalls() - s.stopCodexInteractionTimers() if !s.nativeSettled.Load() || startErr != nil || !s.rpc.Alive() { s.cancelFn() s.settlementErr = errors.Join(s.settlementErr, s.rpc.Close()) @@ -120,7 +119,6 @@ func (s *Session) cancelExecutorTurn(ctx context.Context) error { defer close(s.cancelReady) s.stopFunctionCalls() turnID, active := s.stopSteering() - s.stopCodexInteractionTimers() if !s.terminal.Load() && active { if turnID == "" { s.cancelErr = errors.New("codex: cancellation has no native turn identity") @@ -150,9 +148,7 @@ func (s *Session) cancelExecutorTurn(ctx context.Context) error { var _ agent.Turn = (*Session)(nil) -// Server callbacks retain their originating Turn. MCP elicitation may be a -// standalone thread-scoped request in the native protocol; a supplied Turn ID -// must still match exactly. +// Server callbacks retain their originating Turn, whose ID must match exactly. func (s *Session) onServerRequest(method string, handler ServerRequestHandler) { s.rpc.OnServerRequest(method, func(raw json.RawMessage, id any) (any, error) { if s.executor != nil { @@ -161,8 +157,7 @@ func (s *Session) onServerRequest(method string, handler ServerRequestHandler) { TurnID string `json:"turnId"` } if json.Unmarshal(raw, &scope) != nil || !s.isRootThread(scope.ThreadID) || s.terminal.Load() || - (scope.TurnID != "" && !s.isRootTurn(scope.ThreadID, scope.TurnID)) || - (scope.TurnID == "" && method != "mcpServer/elicitation/request") { + scope.TurnID == "" || !s.isRootTurn(scope.ThreadID, scope.TurnID) { return nil, errors.New("codex: request does not belong to the active turn") } } diff --git a/apps/daemon/internal/agent/codex/function_receipt_test.go b/apps/daemon/internal/agent/codex/function_receipt_test.go index decbf235e..fd138a009 100644 --- a/apps/daemon/internal/agent/codex/function_receipt_test.go +++ b/apps/daemon/internal/agent/codex/function_receipt_test.go @@ -16,7 +16,7 @@ import ( func TestFunctionResultWaitsForNativeCompletion(t *testing.T) { client, server, cleanup := NewTestClient() defer cleanup() - session, output := newInteractionTestSession(client.JSONRPCClient) + session, output := newFunctionTestSession(client.JSONRPCClient) session.setThreadID("thread") session.startSteering("thread", "turn") var err error @@ -61,7 +61,7 @@ func pendingReceiptContext(t *testing.T, ctx context.Context) (*Session, ServerS t.Helper() client, server, cleanup := NewTestClient() t.Cleanup(cleanup) - s, out := newInteractionTestSession(client.JSONRPCClient) + s, out := newFunctionTestSession(client.JSONRPCClient) s.cfg.logger = slog.New(slog.NewTextHandler(io.Discard, nil)) s.setThreadID("thread") s.startSteering("thread", "turn") @@ -169,7 +169,7 @@ func TestFunctionReceiptShutdownDoesNotConfirmApplication(t *testing.T) { func TestFunctionReceiptDeadlineEndsUncertainExecution(t *testing.T) { client, server, cleanup := NewTestClient() defer cleanup() - s, out := newInteractionTestSession(client.JSONRPCClient) + s, out := newFunctionTestSession(client.JSONRPCClient) s.setThreadID("thread") s.startSteering("thread", "turn") s.functions, _ = prepareFunctionTools([]proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{}`)}}) @@ -207,7 +207,7 @@ func TestFunctionReceiptDeadlineEndsUncertainExecution(t *testing.T) { func TestFunctionReceiptWinsSimultaneousDeadline(t *testing.T) { for range 100 { client, _, cleanup := NewTestClient() - s, _ := newInteractionTestSession(client.JSONRPCClient) + s, _ := newFunctionTestSession(client.JSONRPCClient) s.setThreadID("thread") s.startSteering("thread", "turn") s.functions, _ = prepareFunctionTools(nil) diff --git a/apps/daemon/internal/agent/codex/function_write_receipt_test.go b/apps/daemon/internal/agent/codex/function_write_receipt_test.go index 49cdc6826..7e6730647 100644 --- a/apps/daemon/internal/agent/codex/function_write_receipt_test.go +++ b/apps/daemon/internal/agent/codex/function_write_receipt_test.go @@ -31,7 +31,7 @@ func (w *confirmedResultWriter) Close() error { w.once.Do(func() { close(w.close func TestFunctionConfirmedReceiptDuringWriteDeadline(t *testing.T) { client, _, cleanup := NewTestClient() defer cleanup() - s, out := newInteractionTestSession(client.JSONRPCClient) + s, out := newFunctionTestSession(client.JSONRPCClient) s.setThreadID("thread") s.startSteering("thread", "turn") s.functions, _ = prepareFunctionTools([]proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{}`)}}) @@ -64,7 +64,7 @@ func TestFunctionConfirmedReceiptDuringWriteDeadline(t *testing.T) { func TestFunctionUnconfirmedWriteDeadlineClosesNative(t *testing.T) { client, _, cleanup := NewTestClient() defer cleanup() - s, out := newInteractionTestSession(client.JSONRPCClient) + s, out := newFunctionTestSession(client.JSONRPCClient) s.setThreadID("thread") s.startSteering("thread", "turn") s.functions, _ = prepareFunctionTools([]proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{}`)}}) diff --git a/apps/daemon/internal/agent/codex/functions_test.go b/apps/daemon/internal/agent/codex/functions_test.go index 1fc34979f..a2fd9c752 100644 --- a/apps/daemon/internal/agent/codex/functions_test.go +++ b/apps/daemon/internal/agent/codex/functions_test.go @@ -12,12 +12,20 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) +func newFunctionTestSession(rpc *JSONRPCClient) (*Session, <-chan proto.Envelope) { + out := make(chan proto.Envelope, 1) + ctx, cancel := context.WithCancel(context.Background()) + s := &Session{runID: "run-test", out: out, rpc: rpc, cancelCtx: ctx, cancelFn: cancel} + s.registerHandlers() + return s, out +} + func TestFunctionCallWaitsAndRepliesOnce(t *testing.T) { for _, success := range []bool{true, false} { t.Run(map[bool]string{true: "success", false: "failure"}[success], func(t *testing.T) { tc, srv, cleanup := NewTestClient() defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) + s, out := newFunctionTestSession(tc.JSONRPCClient) var err error s.functions, err = prepareFunctionTools([]proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object"}`)}}) if err != nil { @@ -88,7 +96,7 @@ func TestFunctionCallWaitsAndRepliesOnce(t *testing.T) { func TestFunctionCallRejectsUnregisteredAndClosedRuns(t *testing.T) { tc, _, cleanup := NewTestClient() defer cleanup() - s, _ := newInteractionTestSession(tc.JSONRPCClient) + s, _ := newFunctionTestSession(tc.JSONRPCClient) s.setThreadID("thread") s.startSteering("thread", "turn") s.functions, _ = prepareFunctionTools([]proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{}`)}}) diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index 4b7b2d2d2..1f53a8d24 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -57,11 +57,6 @@ type SessionPlan struct { // ModelReasoningEffort is frozen for launch and every native Turn. ModelReasoningEffort string - // ApprovalPolicy + Sandbox apply to both new and resumed threads. - ApprovalPolicy AskForApproval - Sandbox SandboxMode - Permissions string - // Cleanup is the deferred housekeeping the session must run after the child exits. Cleanup func() } @@ -76,13 +71,11 @@ type SessionPlan struct { // // Execution controls select the native web_search and model_verbosity settings. // The codex binary itself is resolved via PATH only. -// -// Daemon-managed Codex sessions bypass approvals and the engine sandbox. func BuildSessionPlan(agentStateKey string, opts map[string]any, controls *proto.ExecutionControls) (SessionPlan, error) { plan := SessionPlan{ - ApprovalPolicy: AskForApproval{String: "never"}, - Sandbox: SandboxDangerFullAcces, - Cleanup: func() {}, + // Harnesses run unattended: Codex never offers its ask-the-user tool. + ExtraConfig: [][2]string{{"tools.experimental_request_user_input.enabled", "false"}}, + Cleanup: func() {}, } nativeConfig, err := harnessconfiguration.Configuration().PrepareHarnessConfig(opts) @@ -128,7 +121,7 @@ func BuildSessionPlan(agentStateKey string, opts map[string]any, controls *proto plan.Env = env if controls != nil { - plan.ExtraConfig = [][2]string{{"web_search", strconv(controls.WebSearch)}, {"model_verbosity", strconv(controls.TextVerbosity)}} + plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"web_search", strconv(controls.WebSearch)}, [2]string{"model_verbosity", strconv(controls.TextVerbosity)}) } if effort, ok := nativeConfig["model_reasoning_effort"].(string); ok { plan.ModelReasoningEffort = effort diff --git a/apps/daemon/internal/agent/codex/options_test.go b/apps/daemon/internal/agent/codex/options_test.go index b20123e2b..cb8910aaa 100644 --- a/apps/daemon/internal/agent/codex/options_test.go +++ b/apps/daemon/internal/agent/codex/options_test.go @@ -2,6 +2,7 @@ package codex import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "slices" "strings" "testing" ) @@ -11,11 +12,8 @@ func TestBuildSessionPlan_DefaultsToBypass(t *testing.T) { if err != nil { t.Fatalf("BuildSessionPlan: %v", err) } - if plan.ApprovalPolicy.String != "never" { - t.Fatalf("default policy must bypass approvals, got %+v", plan.ApprovalPolicy) - } - if plan.Sandbox != SandboxDangerFullAcces { - t.Fatalf("default sandbox = %s, want danger-full-access", plan.Sandbox) + if !slices.Contains(plan.ExtraConfig, [2]string{"tools.experimental_request_user_input.enabled", "false"}) { + t.Fatalf("default plan must disable the native ask-the-user tool, got %+v", plan.ExtraConfig) } if plan.Cleanup == nil { t.Fatal("Cleanup must be non-nil") diff --git a/apps/daemon/internal/agent/codex/permission_profile_test.go b/apps/daemon/internal/agent/codex/permission_profile_test.go index dc250c7be..e69720d6e 100644 --- a/apps/daemon/internal/agent/codex/permission_profile_test.go +++ b/apps/daemon/internal/agent/codex/permission_profile_test.go @@ -16,9 +16,6 @@ func TestRuntimeUsesHostPermissions(t *testing.T) { if err != nil { t.Fatal(err) } - if plan.Sandbox != "danger-full-access" || plan.Permissions != "" || plan.ApprovalPolicy.String != "never" { - t.Fatal("Runtime must bypass inner sandbox", plan) - } if plan.Cwd != req.LocalEnvironment.WorkspaceRoot { t.Fatal("native cwd is not the bound workspace root", plan.Cwd) } diff --git a/apps/daemon/internal/agent/codex/preparation.go b/apps/daemon/internal/agent/codex/preparation.go index 729934ae9..263178522 100644 --- a/apps/daemon/internal/agent/codex/preparation.go +++ b/apps/daemon/internal/agent/codex/preparation.go @@ -78,7 +78,6 @@ func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg cleanup: sync.OnceFunc(plan.Cleanup), bufs: NewItemBuffers(), resolvedModel: plan.Model, - interactions: newPendingCodexInteractions(), } plan.Cleanup = s.cleanup p := &Prepared{ diff --git a/apps/daemon/internal/agent/codex/protocol.go b/apps/daemon/internal/agent/codex/protocol.go index 462c10f3e..2daa4acb7 100644 --- a/apps/daemon/internal/agent/codex/protocol.go +++ b/apps/daemon/internal/agent/codex/protocol.go @@ -64,8 +64,8 @@ func (e *JsonRpcError) Error() string { return fmt.Sprintf("%d %s", e.Code, e.Me // --------------------------------------------------------------------------- // InitializeCapabilities mirrors codex-rs/app-server/src/protocol.rs. -// experimentalApi=true opts into the granular AskForApproval enum and -// the thread/* notification stream. +// experimentalApi=true opts into the experimental thread/* fields and +// notification stream. type InitializeCapabilities struct { ExperimentalAPI bool `json:"experimentalApi"` RequestAttestation bool `json:"requestAttestation"` @@ -93,83 +93,19 @@ type SkillsExtraRootsSetParams struct { ExtraRoots []string `json:"extraRoots"` } -// --------------------------------------------------------------------------- -// Approval / sandbox policies -// --------------------------------------------------------------------------- - -// GranularAskForApproval names each approval gate the codex agent can -// surface. All-false produces a fully silent run; turning any single -// field true makes the corresponding ServerRequest reach the daemon for -// human approval. -// -// JSON tags MUST stay snake_case — codex-rs deserialises this struct -// from a config TOML / RPC param with field names like sandbox_approval, -// and renaming silently breaks the wire. -type GranularAskForApproval struct { - SandboxApproval bool `json:"sandbox_approval"` - Rules bool `json:"rules"` - SkillApproval bool `json:"skill_approval"` - RequestPermissions bool `json:"request_permissions"` - MCPElicitations bool `json:"mcp_elicitations"` -} - -// AskForApproval is the discriminated union codex accepts on -// ThreadStartParams.approvalPolicy. The serialiser must emit EITHER -// {"granular": {...}} OR a bare string ("never" / "on-request" / -// "on-failure" / "untrusted"). See MarshalJSON in approval_policy.go. -type AskForApproval struct { - String string // "" when granular is set - Granular *GranularAskForApproval // nil when string is set -} - -// SandboxMode is the wire-level sandbox setting codex's v2 thread/start -// API accepts. Serializes to a kebab-case string per -// codex-rs/app-server-protocol/src/protocol/v2/shared.rs::SandboxMode. -// -// Earlier (~0.137-era) the field on ThreadStartParams was named -// `sandboxPolicy` and took a tagged object `{type: "dangerFullAccess"}`. -// 0.141.0 renamed it to `sandbox` and flattened it to one of these three -// strings. Sending the old shape no longer errors loudly — codex just -// silently falls back to read-only, which makes prompts terminate -// immediately with no agent output. Keep the constants pinned exactly -// to the kebab values upstream serializes. -type SandboxMode string - -const ( - SandboxReadOnly SandboxMode = "read-only" - SandboxWorkspaceWrite SandboxMode = "workspace-write" - SandboxDangerFullAcces SandboxMode = "danger-full-access" -) - -// SandboxPolicy is the legacy compound type kept for internal -// representation only — codex still echoes it back on some response -// shapes (turn_context inside rollout files, for example). It is NOT -// what ThreadStartParams.Sandbox takes on the wire. -type SandboxPolicy struct { - Type SandboxMode `json:"type"` - WritableRoots []string `json:"writableRoots,omitempty"` - NetworkAccess bool `json:"networkAccess,omitempty"` - ExcludeTmpdirEnvVar bool `json:"excludeTmpdirEnvVar,omitempty"` - ExcludeSlashTmp bool `json:"excludeSlashTmp,omitempty"` -} - // --------------------------------------------------------------------------- // thread/start, thread/resume, thread/list // --------------------------------------------------------------------------- type ThreadStartParams struct { - HistoryMode string `json:"historyMode,omitempty"` - Cwd string `json:"cwd"` - Model string `json:"model,omitempty"` - ModelProvider string `json:"modelProvider,omitempty"` - ApprovalPolicy AskForApproval `json:"approvalPolicy"` - // Sandbox is the v0.141+ field name; previously called sandboxPolicy - // and took a tagged-enum object. Wire format now is a kebab-case - // string: "read-only" / "workspace-write" / "danger-full-access". - // Sending the old object shape causes codex to silently default to - // read-only, which terminates the turn before the model can reply. - Sandbox SandboxMode `json:"sandbox,omitempty"` - Permissions string `json:"permissions,omitempty"` + HistoryMode string `json:"historyMode,omitempty"` + Cwd string `json:"cwd"` + Model string `json:"model,omitempty"` + ModelProvider string `json:"modelProvider,omitempty"` + ApprovalPolicy string `json:"approvalPolicy"` + // Sandbox is the kebab-case mode string. Codex silently falls back to + // read-only for the legacy sandboxPolicy object. + Sandbox string `json:"sandbox"` DeveloperInstructions string `json:"developerInstructions,omitempty"` RuntimeWorkspaceRoots []string `json:"runtimeWorkspaceRoots,omitempty"` DynamicTools []dynamicFunctionTool `json:"dynamicTools,omitempty"` @@ -187,19 +123,16 @@ type Thread struct { } type ThreadStartResult struct { - Thread Thread `json:"thread"` - Model string `json:"model,omitempty"` - ApprovalPolicy *AskForApproval `json:"approvalPolicy,omitempty"` - Sandbox *SandboxPolicy `json:"sandbox,omitempty"` + Thread Thread `json:"thread"` + Model string `json:"model,omitempty"` } type ThreadResumeParams struct { - Cwd string `json:"cwd,omitempty"` - DeveloperInstructions string `json:"developerInstructions"` - ThreadID string `json:"threadId"` - ApprovalPolicy AskForApproval `json:"approvalPolicy"` - Sandbox SandboxMode `json:"sandbox,omitempty"` - Permissions string `json:"permissions,omitempty"` + Cwd string `json:"cwd,omitempty"` + DeveloperInstructions string `json:"developerInstructions"` + ThreadID string `json:"threadId"` + ApprovalPolicy string `json:"approvalPolicy"` + Sandbox string `json:"sandbox"` } // --------------------------------------------------------------------------- @@ -381,84 +314,3 @@ type ErrorNotification struct { Error *TurnError `json:"error,omitempty"` Message string `json:"message,omitempty"` } - -// --------------------------------------------------------------------------- -// Approval and user-input ServerRequest params. Explicit requests defer their -// responses until Web or IM submits the decision, even with bypass defaults. -// --------------------------------------------------------------------------- - -type CommandExecutionRequestApprovalParams struct { - ThreadID string `json:"threadId"` - TurnID string `json:"turnId"` - ItemID string `json:"itemId"` - Command *string `json:"command,omitempty"` - Cwd *string `json:"cwd,omitempty"` - Reason *string `json:"reason,omitempty"` -} - -type FileChangeRequestApprovalParams struct { - ThreadID string `json:"threadId"` - TurnID string `json:"turnId"` - ItemID string `json:"itemId"` - Reason *string `json:"reason,omitempty"` - GrantRoot *string `json:"grantRoot,omitempty"` -} - -type PermissionsRequestApprovalParams struct { - ThreadID string `json:"threadId"` - TurnID string `json:"turnId"` - ItemID string `json:"itemId"` - Cwd string `json:"cwd"` - Reason *string `json:"reason,omitempty"` - Permissions map[string]any `json:"permissions,omitempty"` -} - -// CommandExecutionApprovalDecision is the verdict the daemon writes -// back to a Codex approval ServerRequest. "accept" / "decline" / "cancel" -// / "acceptForSession". -type CommandExecutionApprovalDecision = string - -// ApprovalDecisionResult is the result body for command-execution and -// file-change approvals. item/permissions/requestApproval uses the distinct -// PermissionsRequestApprovalResponse contract below. -type ApprovalDecisionResult struct { - Decision CommandExecutionApprovalDecision `json:"decision"` -} - -// PermissionsRequestApprovalResponse mirrors Codex app-server's dedicated -// response contract. Approving echoes the requested permission profile; -// denying returns an empty profile, which grants no additional capability. -type PermissionsRequestApprovalResponse struct { - Permissions map[string]any `json:"permissions"` - Scope string `json:"scope,omitempty"` -} - -type ToolRequestUserInputOption struct { - Label string `json:"label"` - Description string `json:"description"` -} - -type ToolRequestUserInputQuestion struct { - ID string `json:"id"` - Header string `json:"header"` - Question string `json:"question"` - Options []ToolRequestUserInputOption `json:"options,omitempty"` - IsOther bool `json:"isOther,omitempty"` - IsSecret bool `json:"isSecret,omitempty"` -} - -type ToolRequestUserInputParams struct { - ThreadID string `json:"threadId"` - TurnID string `json:"turnId"` - ItemID string `json:"itemId"` - Questions []ToolRequestUserInputQuestion `json:"questions"` - AutoResolutionMs *uint64 `json:"autoResolutionMs,omitempty"` -} - -type ToolRequestUserInputAnswer struct { - Answers []string `json:"answers"` -} - -type ToolRequestUserInputResponse struct { - Answers map[string]ToolRequestUserInputAnswer `json:"answers"` -} diff --git a/apps/daemon/internal/agent/codex/protocol_wire_test.go b/apps/daemon/internal/agent/codex/protocol_wire_test.go index d32590d7c..895888bab 100644 --- a/apps/daemon/internal/agent/codex/protocol_wire_test.go +++ b/apps/daemon/internal/agent/codex/protocol_wire_test.go @@ -6,66 +6,12 @@ import ( "testing" ) -// TestThreadStartParams_SandboxIsKebabString pins the v0.141+ wire -// format reviewer caught the hard way: codex's ThreadStartParams.sandbox -// is a kebab-case string ("read-only" / "workspace-write" / -// "danger-full-access"), NOT the legacy {"type":"dangerFullAccess"} -// tagged object on a sandboxPolicy field. -// -// Sending the legacy shape used to terminate every turn in <1s with an -// empty agent message body, because codex silently fell back to -// read-only when it didn't recognise the field name. -func TestThreadStartParams_SandboxIsKebabString(t *testing.T) { - cases := []struct { - name string - mode SandboxMode - want string - }{ - {"read-only", SandboxReadOnly, "read-only"}, - {"workspace-write", SandboxWorkspaceWrite, "workspace-write"}, - {"danger-full-access", SandboxDangerFullAcces, "danger-full-access"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - params := ThreadStartParams{ - Cwd: "/workspace", - Model: "gpt-5.5", - ApprovalPolicy: SilentGranularPolicy(), - Sandbox: tc.mode, - } - raw, err := json.Marshal(params) - if err != nil { - t.Fatalf("marshal: %v", err) - } - body := string(raw) - if !strings.Contains(body, `"sandbox":"`+tc.want+`"`) { - t.Fatalf("wire body missing sandbox=%q: %s", tc.want, body) - } - // The legacy field name MUST be gone. Even shipping it as - // "sandboxPolicy" alongside the new field would confuse - // future-version codex servers that strict-deserialise. - if strings.Contains(body, `"sandboxPolicy"`) { - t.Fatalf("legacy sandboxPolicy field leaked into wire: %s", body) - } - // And the kebab-case value must NOT collide with the old - // camelCase enum names. - for _, oldName := range []string{`"readOnly"`, `"workspaceWrite"`, `"dangerFullAccess"`} { - if strings.Contains(body, oldName) { - t.Fatalf("legacy camelCase enum value %s leaked: %s", oldName, body) - } - } - }) - } -} - // TestThreadStartParams_OmitsEmptyOptionalFields pins that model_provider // and other optional fields don't leak null/empty values into the wire. // codex would reject malformed enum values otherwise. func TestThreadStartParams_OmitsEmptyOptionalFields(t *testing.T) { params := ThreadStartParams{ - Cwd: "/workspace", - ApprovalPolicy: SilentGranularPolicy(), - Sandbox: SandboxDangerFullAcces, + Cwd: "/workspace", // Model, ModelProvider, DeveloperInstructions deliberately empty } raw, _ := json.Marshal(params) @@ -88,11 +34,9 @@ func TestThreadStartParams_OmitsEmptyOptionalFields(t *testing.T) { // override. Sending it on both paths is belt + suspenders. func TestThreadStartParams_ModelProviderIsCamelCaseField(t *testing.T) { params := ThreadStartParams{ - Cwd: "/workspace", - Model: "gpt-5.5", - ModelProvider: "oac", - ApprovalPolicy: SilentGranularPolicy(), - Sandbox: SandboxDangerFullAcces, + Cwd: "/workspace", + Model: "gpt-5.5", + ModelProvider: "oac", } raw, _ := json.Marshal(params) body := string(raw) diff --git a/apps/daemon/internal/agent/codex/rpc_close_test.go b/apps/daemon/internal/agent/codex/rpc_close_test.go index c7b345d33..7b387e23a 100644 --- a/apps/daemon/internal/agent/codex/rpc_close_test.go +++ b/apps/daemon/internal/agent/codex/rpc_close_test.go @@ -80,7 +80,7 @@ func TestJSONRPCClientCloseCanRetryUnreapedChild(t *testing.T) { closeConcurrently(true) ownerCtx, cancelOwner := context.WithCancel(t.Context()) s := &Session{rpc: client, cancelCtx: ownerCtx, cancelFn: cancelOwner, - cfg: defaultSessionConfig(), interactions: newPendingCodexInteractions(), bufs: NewItemBuffers()} + cfg: defaultSessionConfig(), bufs: NewItemBuffers()} ctx, cancelWait := context.WithTimeout(t.Context(), 20*time.Millisecond) defer cancelWait() if err := s.Cancel(ctx); !errors.Is(err, context.DeadlineExceeded) { diff --git a/apps/daemon/internal/agent/codex/server_requests.go b/apps/daemon/internal/agent/codex/server_requests.go deleted file mode 100644 index 5e77e3c9d..000000000 --- a/apps/daemon/internal/agent/codex/server_requests.go +++ /dev/null @@ -1,362 +0,0 @@ -package codex - -import ( - "crypto/rand" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "strings" - "sync" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -type pendingCodexPermission struct { - rpcID any - kind codexPermissionKind - permissions map[string]any - timeout time.Duration - timer *time.Timer -} - -type pendingCodexAsk struct { - rpcID any - questionIDs []string - timeout time.Duration - timer *time.Timer -} - -const codexInteractionTimeout = 10 * time.Minute - -type codexPermissionKind uint8 - -const ( - codexDecisionApproval codexPermissionKind = iota - codexPermissionsApproval - codexMCPApproval -) - -type pendingCodexInteractions struct { - mu sync.Mutex - permissions map[string]pendingCodexPermission - asks map[string]pendingCodexAsk -} - -func newPendingCodexInteractions() *pendingCodexInteractions { - return &pendingCodexInteractions{ - permissions: make(map[string]pendingCodexPermission), - asks: make(map[string]pendingCodexAsk), - } -} - -func codexInteractionID(prefix string) string { - var bytes [8]byte - if _, err := rand.Read(bytes[:]); err == nil { - return prefix + "_" + hex.EncodeToString(bytes[:]) - } - return fmt.Sprintf("%s_fallback", prefix) -} - -func (s *Session) handleCodexCommandApproval(raw json.RawMessage, rpcID any) (any, error) { - var params CommandExecutionRequestApprovalParams - if err := json.Unmarshal(raw, ¶ms); err != nil { - return nil, fmt.Errorf("decode command approval: %w", err) - } - title := stringPointer(params.Command) - if title == "" { - title = "Run command" - } - return s.deferCodexPermission(rpcID, codexDecisionApproval, "command_execution", title, stringPointer(params.Reason), raw, nil) -} - -func (s *Session) handleCodexFileApproval(raw json.RawMessage, rpcID any) (any, error) { - var params FileChangeRequestApprovalParams - if err := json.Unmarshal(raw, ¶ms); err != nil { - return nil, fmt.Errorf("decode file approval: %w", err) - } - title := stringPointer(params.GrantRoot) - if title == "" { - title = "Apply file changes" - } - return s.deferCodexPermission(rpcID, codexDecisionApproval, "file_change", title, stringPointer(params.Reason), raw, nil) -} - -func (s *Session) handleCodexPermissionsApproval(raw json.RawMessage, rpcID any) (any, error) { - var params PermissionsRequestApprovalParams - if err := json.Unmarshal(raw, ¶ms); err != nil { - return nil, fmt.Errorf("decode permissions approval: %w", err) - } - title := strings.TrimSpace(params.Cwd) - if title == "" { - title = "Grant additional permissions" - } - return s.deferCodexPermission(rpcID, codexPermissionsApproval, "permission_request", title, stringPointer(params.Reason), raw, params.Permissions) -} - -func (s *Session) deferCodexPermission(rpcID any, kind codexPermissionKind, tool, title, detail string, raw json.RawMessage, permissions map[string]any) (any, error) { - requestID := codexInteractionID("perm") - payload := map[string]any{} - _ = json.Unmarshal(raw, &payload) - pending := pendingCodexPermission{ - rpcID: rpcID, kind: kind, permissions: permissions, - timeout: codexInteractionTimeout, - } - s.interactions.mu.Lock() - // Start the timer while holding the table lock. Even a future very short - // timeout then blocks in expireCodexPermission until the entry is visible, - // rather than firing before insertion and leaving an immortal request. - pending.timer = time.AfterFunc(pending.timeout, func() { s.expireCodexPermission(requestID) }) - s.interactions.permissions[requestID] = pending - s.interactions.mu.Unlock() - env, err := proto.NewEnvelope(proto.TypePermissionRequest, s.runID, proto.PermissionRequestPayload{ - RequestID: requestID, Tool: tool, Title: title, Detail: detail, Payload: payload, - }) - if err != nil { - s.interactions.mu.Lock() - pending, ok := s.interactions.permissions[requestID] - if ok { - delete(s.interactions.permissions, requestID) - } - s.interactions.mu.Unlock() - if ok && pending.timer != nil { - pending.timer.Stop() - } - return nil, err - } - s.trySend(env) - return DeferReply, nil -} - -func (s *Session) handleCodexUserInput(raw json.RawMessage, rpcID any) (any, error) { - var params ToolRequestUserInputParams - if err := json.Unmarshal(raw, ¶ms); err != nil { - return nil, fmt.Errorf("decode requestUserInput: %w", err) - } - if len(params.Questions) == 0 { - return nil, errors.New("requestUserInput contains no questions") - } - askID := codexInteractionID("ask") - questions := make([]proto.PromptForUserChoiceQuestion, 0, len(params.Questions)) - questionIDs := make([]string, 0, len(params.Questions)) - for index, question := range params.Questions { - options := make([]proto.PromptForUserChoiceOption, 0, len(question.Options)) - for _, option := range question.Options { - options = append(options, proto.PromptForUserChoiceOption{Label: option.Label, Description: option.Description}) - } - header := strings.TrimSpace(question.Header) - if header == "" { - header = fmt.Sprintf("q%d", index) - } - questionID := strings.TrimSpace(question.ID) - if questionID == "" { - questionID = header - } - questionIDs = append(questionIDs, questionID) - questions = append(questions, proto.PromptForUserChoiceQuestion{ - ID: questionID, Header: header, Question: question.Question, Options: options, - IsOther: question.IsOther, IsSecret: question.IsSecret, - }) - } - timeout := codexInteractionTimeout - if params.AutoResolutionMs != nil && *params.AutoResolutionMs > 0 { - const maxMillis = uint64(^uint64(0)>>1) / uint64(time.Millisecond) - if *params.AutoResolutionMs <= maxMillis { - timeout = time.Duration(*params.AutoResolutionMs) * time.Millisecond - } - } - pending := pendingCodexAsk{rpcID: rpcID, questionIDs: questionIDs, timeout: timeout} - s.interactions.mu.Lock() - pending.timer = time.AfterFunc(pending.timeout, func() { s.expireCodexAsk(askID) }) - s.interactions.asks[askID] = pending - s.interactions.mu.Unlock() - env, err := proto.NewEnvelope(proto.TypePromptForUserChoice, s.runID, proto.PromptForUserChoicePayload{ - AskID: askID, Questions: questions, AutoResolutionMs: params.AutoResolutionMs, - }) - if err != nil { - s.interactions.mu.Lock() - pending, ok := s.interactions.asks[askID] - if ok { - delete(s.interactions.asks, askID) - } - s.interactions.mu.Unlock() - if ok && pending.timer != nil { - pending.timer.Stop() - } - return nil, err - } - s.trySend(env) - return DeferReply, nil -} - -func (s *Session) submitCodexPermission(requestID string, decision proto.PermissionDecisionPayload) error { - if !s.beginOperation() { - return agent.ErrSteeringInactive - } - defer s.endOperation() - s.interactions.mu.Lock() - pending, ok := s.interactions.permissions[requestID] - if ok { - delete(s.interactions.permissions, requestID) - } - s.interactions.mu.Unlock() - if !ok { - return agent.ErrUnknownPermission - } - if pending.timer != nil { - pending.timer.Stop() - } - if err := s.sendCodexPermissionReply(pending, decision.Approved); err != nil { - s.interactions.mu.Lock() - pending.timer = time.AfterFunc(pending.timeout, func() { s.expireCodexPermission(requestID) }) - s.interactions.permissions[requestID] = pending - s.interactions.mu.Unlock() - return err - } - return nil -} - -func (s *Session) sendCodexPermissionReply(pending pendingCodexPermission, approved bool) error { - if pending.kind == codexPermissionsApproval { - permissions := map[string]any{} - if approved && pending.permissions != nil { - permissions = pending.permissions - } - return s.rpc.SendServerReply(pending.rpcID, PermissionsRequestApprovalResponse{ - Permissions: permissions, - Scope: "turn", - }) - } - value := "decline" - if approved { - value = "accept" - } - if pending.kind == codexMCPApproval { - var content map[string]any - if approved { - content = map[string]any{} - } - return s.rpc.SendServerReply(pending.rpcID, mcpElicitationResponse{Action: value, Content: content}) - } - return s.rpc.SendServerReply(pending.rpcID, ApprovalDecisionResult{Decision: value}) -} - -func (s *Session) submitCodexUserInput(askID string, decision proto.PromptForUserChoiceDecisionPayload) error { - if !s.beginOperation() { - return agent.ErrSteeringInactive - } - defer s.endOperation() - s.interactions.mu.Lock() - pending, ok := s.interactions.asks[askID] - var answers map[string][]string - if ok { - var err error - answers, err = decision.AnswersFor(pending.questionIDs) - if err != nil { - s.interactions.mu.Unlock() - return err - } - delete(s.interactions.asks, askID) - } - s.interactions.mu.Unlock() - if !ok { - return agent.ErrUnknownAsk - } - if pending.timer != nil { - pending.timer.Stop() - } - if decision.Cancelled { - reason := strings.TrimSpace(decision.Reason) - if reason == "" { - reason = "user cancelled input request" - } - if err := s.rpc.SendServerError(pending.rpcID, -32001, reason, nil); err != nil { - s.interactions.mu.Lock() - pending.timer = time.AfterFunc(pending.timeout, func() { s.expireCodexAsk(askID) }) - s.interactions.asks[askID] = pending - s.interactions.mu.Unlock() - return err - } - return nil - } - result := ToolRequestUserInputResponse{Answers: make(map[string]ToolRequestUserInputAnswer, len(pending.questionIDs))} - for _, questionID := range pending.questionIDs { - values := answers[questionID] - if values == nil { - values = []string{} - } - result.Answers[questionID] = ToolRequestUserInputAnswer{Answers: values} - } - if err := s.rpc.SendServerReply(pending.rpcID, result); err != nil { - s.interactions.mu.Lock() - pending.timer = time.AfterFunc(pending.timeout, func() { s.expireCodexAsk(askID) }) - s.interactions.asks[askID] = pending - s.interactions.mu.Unlock() - return err - } - return nil -} - -func (s *Session) expireCodexPermission(requestID string) { - if !s.beginOperation() { - return - } - defer s.endOperation() - s.interactions.mu.Lock() - pending, ok := s.interactions.permissions[requestID] - if ok { - delete(s.interactions.permissions, requestID) - } - s.interactions.mu.Unlock() - if ok { - if pending.timer != nil { - pending.timer.Stop() - } - _ = s.sendCodexPermissionReply(pending, false) - } -} - -func (s *Session) expireCodexAsk(askID string) { - if !s.beginOperation() { - return - } - defer s.endOperation() - s.interactions.mu.Lock() - pending, ok := s.interactions.asks[askID] - if ok { - delete(s.interactions.asks, askID) - } - s.interactions.mu.Unlock() - if ok { - if pending.timer != nil { - pending.timer.Stop() - } - _ = s.rpc.SendServerError(pending.rpcID, -32001, "input request timed out", nil) - } -} - -func (s *Session) stopCodexInteractionTimers() { - s.interactions.mu.Lock() - defer s.interactions.mu.Unlock() - for id, pending := range s.interactions.permissions { - if pending.timer != nil { - pending.timer.Stop() - } - delete(s.interactions.permissions, id) - } - for id, pending := range s.interactions.asks { - if pending.timer != nil { - pending.timer.Stop() - } - delete(s.interactions.asks, id) - } -} - -func stringPointer(value *string) string { - if value == nil { - return "" - } - return strings.TrimSpace(*value) -} diff --git a/apps/daemon/internal/agent/codex/server_requests_mcp.go b/apps/daemon/internal/agent/codex/server_requests_mcp.go deleted file mode 100644 index 4cb4de00e..000000000 --- a/apps/daemon/internal/agent/codex/server_requests_mcp.go +++ /dev/null @@ -1,38 +0,0 @@ -package codex - -import ( - "encoding/json" - "errors" - "fmt" - "strings" -) - -type mcpElicitationParams struct { - ServerName string `json:"serverName"` - Mode string `json:"mode"` - Message string `json:"message"` - Schema struct { - Type string `json:"type"` - Properties map[string]json.RawMessage `json:"properties"` - Required []string `json:"required"` - } `json:"requestedSchema"` -} - -type mcpElicitationResponse struct { - Action string `json:"action"` - Content map[string]any `json:"content"` -} - -func (s *Session) handleCodexMCPElicitation(raw json.RawMessage, rpcID any) (any, error) { - var params mcpElicitationParams - if err := json.Unmarshal(raw, ¶ms); err != nil { - return nil, fmt.Errorf("decode MCP elicitation: %w", err) - } - if params.Mode != "form" || params.Schema.Type != "object" || params.Schema.Properties == nil || len(params.Schema.Properties) != 0 || len(params.Schema.Required) != 0 { - return nil, errors.New("MCP elicitation requires unsupported input; only empty confirmation forms are supported") - } - if strings.TrimSpace(params.ServerName) == "" || strings.TrimSpace(params.Message) == "" { - return nil, errors.New("MCP confirmation requires a server name and message") - } - return s.deferCodexPermission(rpcID, codexMCPApproval, "mcp:"+params.ServerName, params.Message, "", raw, nil) -} diff --git a/apps/daemon/internal/agent/codex/server_requests_mcp_test.go b/apps/daemon/internal/agent/codex/server_requests_mcp_test.go deleted file mode 100644 index a72ec3d9e..000000000 --- a/apps/daemon/internal/agent/codex/server_requests_mcp_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package codex - -import ( - "encoding/json" - "errors" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -func TestCodexMCPConfirmationUsesPermissionLifecycle(t *testing.T) { - for _, action := range []string{"approve", "deny", "expire"} { - t.Run(action, func(t *testing.T) { - tc, srv, cleanup := NewTestClient() - defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) - defer s.stopCodexInteractionTimers() - params := json.RawMessage(`{"threadId":"thread-1","turnId":"turn-1","serverName":"qa-service-desk","mode":"form","message":"Allow get_test_ticket?","requestedSchema":{"type":"object","properties":{}},"_meta":{"codex_approval_kind":"mcp_tool_call","persist":["session","always"]}}`) - if err := SendServerRequest(srv, "rpc-mcp", "mcpServer/elicitation/request", params); err != nil { - t.Fatal(err) - } - var env proto.Envelope - select { - case env = <-out: - case <-time.After(2 * time.Second): - t.Fatal("MCP confirmation was not surfaced") - } - var request proto.PermissionRequestPayload - if err := env.DecodePayload(&request); err != nil { - t.Fatal(err) - } - if env.Type != proto.TypePermissionRequest || env.ID != "run-test" || request.RequestID == "" || request.Tool != "mcp:qa-service-desk" || request.Title != "Allow get_test_ticket?" { - t.Fatalf("incorrect confirmation: %+v / %+v", env, request) - } - done := make(chan error, 1) - go func() { - if action == "expire" { - s.expireCodexPermission(request.RequestID) - done <- nil - return - } - done <- s.SubmitPermission(t.Context(), request.RequestID, proto.PermissionDecisionPayload{Approved: action == "approve"}) - }() - var reply struct { - ID string `json:"id"` - Result map[string]json.RawMessage `json:"result"` - } - decodeCodexReply(t, srv, &reply) - if err := <-done; err != nil { - t.Fatal(err) - } - wantAction, wantContent := `"decline"`, `null` - if action == "approve" { - wantAction, wantContent = `"accept"`, `{}` - } - if reply.ID != "rpc-mcp" || len(reply.Result) != 2 || string(reply.Result["action"]) != wantAction || string(reply.Result["content"]) != wantContent { - t.Fatalf("incorrect MCP response: %+v", reply) - } - if err := s.SubmitPermission(t.Context(), request.RequestID, proto.PermissionDecisionPayload{Approved: true}); !errors.Is(err, agent.ErrUnknownPermission) { - t.Fatalf("resolved confirmation accepted again: %v", err) - } - }) - } -} - -func TestCodexMCPElicitationRejectsUnsupportedInputWithoutApproval(t *testing.T) { - for _, params := range []string{ - `{"serverName":"qa","mode":"form","message":"Enter name","requestedSchema":{"type":"object","properties":{"name":{"type":"string"}}}}`, - `{"serverName":"qa","mode":"url","message":"Sign in","url":"https://example.test/login","elicitationId":"login"}`, - `{"serverName":"qa","mode":"form","message":"Confirm","requestedSchema":{"type":"object","properties":{},"required":["name"]}}`, - `{"serverName":"qa","mode":"form","message":"Confirm","requestedSchema":{"type":"object"}}`, - `{"mode":"form","requestedSchema":{"type":"object","properties":{}}}`, - } { - t.Run(params, func(t *testing.T) { - tc, srv, cleanup := NewTestClient() - defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) - defer s.stopCodexInteractionTimers() - if err := SendServerRequest(srv, "rpc-unsupported", "mcpServer/elicitation/request", json.RawMessage(params)); err != nil { - t.Fatal(err) - } - var reply struct { - Error *struct { - Code int `json:"code"` - } `json:"error"` - } - decodeCodexReply(t, srv, &reply) - if reply.Error == nil || reply.Error.Code != -32603 { - t.Fatalf("unsupported input was not rejected: %+v", reply) - } - select { - case env := <-out: - t.Fatalf("unsupported input became an approval: %+v", env) - default: - } - }) - } -} diff --git a/apps/daemon/internal/agent/codex/server_requests_test.go b/apps/daemon/internal/agent/codex/server_requests_test.go deleted file mode 100644 index 4bae35ed0..000000000 --- a/apps/daemon/internal/agent/codex/server_requests_test.go +++ /dev/null @@ -1,357 +0,0 @@ -package codex - -import ( - "context" - "encoding/json" - "errors" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -func newInteractionTestSession(rpc *JSONRPCClient) (*Session, <-chan proto.Envelope) { - out := make(chan proto.Envelope, 1) - ctx, cancel := context.WithCancel(context.Background()) - s := &Session{ - runID: "run-test", - out: out, - rpc: rpc, - cancelCtx: ctx, - cancelFn: cancel, - interactions: newPendingCodexInteractions(), - } - s.registerHandlers() - return s, out -} - -func TestCodexPermissionRequestWaitsForHumanDecision(t *testing.T) { - tc, srv, cleanup := NewTestClient() - defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) - - command := "go test ./..." - reason := "requires process execution" - if err := SendServerRequest(srv, "rpc-perm-1", "item/commandExecution/requestApproval", CommandExecutionRequestApprovalParams{ - ThreadID: "thread-1", TurnID: "turn-1", ItemID: "item-1", Command: &command, Reason: &reason, - }); err != nil { - t.Fatalf("send server request: %v", err) - } - - var env proto.Envelope - select { - case env = <-out: - case <-time.After(2 * time.Second): - t.Fatal("permission request was not surfaced to OpenAgentCore") - } - if env.Type != proto.TypePermissionRequest { - t.Fatalf("envelope type = %q, want %q", env.Type, proto.TypePermissionRequest) - } - var request proto.PermissionRequestPayload - if err := env.DecodePayload(&request); err != nil { - t.Fatalf("decode permission payload: %v", err) - } - if env.ID != "run-test" || request.RequestID == "" { - t.Fatalf("permission correlation = env.ID %q request.ID %q", env.ID, request.RequestID) - } - if request.Tool != "command_execution" || request.Title != command || request.Detail != reason { - t.Fatalf("permission payload = %+v", request) - } - - submitDone := make(chan error, 1) - go func() { - submitDone <- s.SubmitPermission(context.Background(), request.RequestID, proto.PermissionDecisionPayload{Approved: true}) - }() - reply := readCodexServerReply(t, srv) - if err := <-submitDone; err != nil { - t.Fatalf("submit permission: %v", err) - } - if reply.ID != "rpc-perm-1" || reply.Result.Decision != "accept" { - t.Fatalf("reply = %+v", reply) - } -} - -func TestCodexPermissionsApprovalUsesDedicatedResponseSchema(t *testing.T) { - for _, tt := range []struct { - name string - approved bool - wantGrants bool - }{ - {name: "approve echoes requested grants", approved: true, wantGrants: true}, - {name: "deny grants nothing", approved: false, wantGrants: false}, - } { - t.Run(tt.name, func(t *testing.T) { - tc, srv, cleanup := NewTestClient() - defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) - requested := map[string]any{ - "network": map[string]any{"enabled": true}, - "fileSystem": map[string]any{"write": []any{"/workspace"}}, - } - if err := SendServerRequest(srv, "rpc-permissions", "item/permissions/requestApproval", PermissionsRequestApprovalParams{ - Cwd: "/workspace", Permissions: requested, - }); err != nil { - t.Fatalf("send permissions request: %v", err) - } - env := <-out - var request proto.PermissionRequestPayload - if err := env.DecodePayload(&request); err != nil { - t.Fatalf("decode permission payload: %v", err) - } - done := make(chan error, 1) - go func() { - done <- s.SubmitPermission(context.Background(), request.RequestID, proto.PermissionDecisionPayload{Approved: tt.approved}) - }() - var reply struct { - ID string `json:"id"` - Result struct { - Permissions map[string]any `json:"permissions"` - Scope string `json:"scope"` - Decision string `json:"decision"` - } `json:"result"` - } - decodeCodexReply(t, srv, &reply) - if err := <-done; err != nil { - t.Fatalf("submit permissions: %v", err) - } - if reply.Result.Scope != "turn" || reply.Result.Decision != "" { - t.Fatalf("permissions reply = %+v", reply.Result) - } - _, granted := reply.Result.Permissions["network"] - if granted != tt.wantGrants { - t.Fatalf("permissions = %+v, want grants=%v", reply.Result.Permissions, tt.wantGrants) - } - }) - } -} - -func TestCodexUserInputMapsAnswersByQuestionID(t *testing.T) { - tc, srv, cleanup := NewTestClient() - defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) - - autoResolutionMs := uint64(120_000) - if err := SendServerRequest(srv, "rpc-ask-1", "item/tool/requestUserInput", ToolRequestUserInputParams{ - ThreadID: "thread-1", TurnID: "turn-1", ItemID: "item-ask", - AutoResolutionMs: &autoResolutionMs, - Questions: []ToolRequestUserInputQuestion{ - {ID: "deployment", Header: "Deploy", Question: "Where?", IsOther: true, Options: []ToolRequestUserInputOption{{Label: "Staging"}, {Label: "Production"}}}, - {ID: "checks", Header: "Checks", Question: "Which checks?", IsSecret: true, Options: []ToolRequestUserInputOption{{Label: "Unit"}, {Label: "E2E"}}}, - }, - }); err != nil { - t.Fatalf("send server request: %v", err) - } - - var env proto.Envelope - select { - case env = <-out: - case <-time.After(2 * time.Second): - t.Fatal("requestUserInput was not surfaced to OpenAgentCore") - } - if env.Type != proto.TypePromptForUserChoice { - t.Fatalf("envelope type = %q, want %q", env.Type, proto.TypePromptForUserChoice) - } - var request proto.PromptForUserChoicePayload - if err := env.DecodePayload(&request); err != nil { - t.Fatalf("decode user input payload: %v", err) - } - if len(request.Questions) != 2 || request.Questions[0].ID != "deployment" || request.Questions[1].ID != "checks" || request.Questions[0].Header != "Deploy" { - t.Fatalf("user input payload = %+v", request) - } - if !request.Questions[0].IsOther || !request.Questions[1].IsSecret || request.AutoResolutionMs == nil || *request.AutoResolutionMs != autoResolutionMs { - t.Fatalf("user input metadata = %+v", request) - } - s.interactions.mu.Lock() - pending := s.interactions.asks[request.AskID] - s.interactions.mu.Unlock() - if pending.timeout != 2*time.Minute { - t.Fatalf("ask timeout = %v, want 2m", pending.timeout) - } - - if err := s.SubmitPromptForUserChoice(context.Background(), request.AskID, proto.PromptForUserChoiceDecisionPayload{ - QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "foreign", Answers: []string{"yes"}}}, - }); err == nil { - t.Fatal("accepted a foreign question ID") - } - // Invalid identity must leave the original native request pending. - submitDone := make(chan error, 1) - go func() { - submitDone <- s.SubmitPromptForUserChoice(context.Background(), request.AskID, proto.PromptForUserChoiceDecisionPayload{ - QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{ - {QuestionID: "checks", Answers: []string{"Unit", "E2E"}}, - {QuestionID: "deployment", Answers: []string{"Staging"}}, - }, - }) - }() - - var reply struct { - ID string `json:"id"` - Result ToolRequestUserInputResponse `json:"result"` - } - decodeCodexReply(t, srv, &reply) - if err := <-submitDone; err != nil { - t.Fatalf("submit user input: %v", err) - } - if reply.ID != "rpc-ask-1" { - t.Fatalf("reply id = %q", reply.ID) - } - if got := reply.Result.Answers["deployment"].Answers; len(got) != 1 || got[0] != "Staging" { - t.Fatalf("deployment answers = %v", got) - } - if got := reply.Result.Answers["checks"].Answers; len(got) != 2 || got[0] != "Unit" || got[1] != "E2E" { - t.Fatalf("checks answers = %v", got) - } -} - -func TestCodexUserInputCancellationReturnsErrorInsteadOfEmptyAnswers(t *testing.T) { - tc, srv, cleanup := NewTestClient() - defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) - - if err := SendServerRequest(srv, "rpc-ask-cancel", "item/tool/requestUserInput", ToolRequestUserInputParams{ - Questions: []ToolRequestUserInputQuestion{{ID: "confirm", Header: "Confirm", Question: "Continue?"}}, - }); err != nil { - t.Fatalf("send server request: %v", err) - } - var env proto.Envelope - select { - case env = <-out: - case <-time.After(2 * time.Second): - t.Fatal("requestUserInput was not surfaced to OpenAgentCore") - } - var request proto.PromptForUserChoicePayload - if err := env.DecodePayload(&request); err != nil { - t.Fatalf("decode user input payload: %v", err) - } - submitDone := make(chan error, 1) - go func() { - submitDone <- s.SubmitPromptForUserChoice(context.Background(), request.AskID, proto.PromptForUserChoiceDecisionPayload{ - Cancelled: true, Reason: "cancelled by user", - }) - }() - - var reply struct { - ID string `json:"id"` - Error *struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` - } - decodeCodexReply(t, srv, &reply) - if err := <-submitDone; err != nil { - t.Fatalf("cancel user input: %v", err) - } - if reply.ID != "rpc-ask-cancel" || reply.Error == nil || reply.Error.Code != -32001 { - t.Fatalf("cancel reply = %+v", reply) - } -} - -func TestCodexInteractionExpiryUnblocksRuntime(t *testing.T) { - t.Run("permission declines", func(t *testing.T) { - tc, srv, cleanup := NewTestClient() - defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) - command := "deploy" - if err := SendServerRequest(srv, "rpc-perm-timeout", "item/commandExecution/requestApproval", CommandExecutionRequestApprovalParams{Command: &command}); err != nil { - t.Fatalf("send permission request: %v", err) - } - env := <-out - var request proto.PermissionRequestPayload - if err := env.DecodePayload(&request); err != nil { - t.Fatalf("decode permission payload: %v", err) - } - expireDone := make(chan struct{}) - go func() { s.expireCodexPermission(request.RequestID); close(expireDone) }() - reply := readCodexServerReply(t, srv) - <-expireDone - if reply.Result.Decision != "decline" { - t.Fatalf("expiry decision = %q", reply.Result.Decision) - } - if err := s.SubmitPermission(context.Background(), request.RequestID, proto.PermissionDecisionPayload{Approved: true}); !errors.Is(err, agent.ErrUnknownPermission) { - t.Fatalf("late permission error = %v, want ErrUnknownPermission", err) - } - }) - - t.Run("permission profile grants nothing", func(t *testing.T) { - tc, srv, cleanup := NewTestClient() - defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) - if err := SendServerRequest(srv, "rpc-profile-timeout", "item/permissions/requestApproval", PermissionsRequestApprovalParams{ - Permissions: map[string]any{"network": map[string]any{"enabled": true}}, - }); err != nil { - t.Fatalf("send permission profile request: %v", err) - } - env := <-out - var request proto.PermissionRequestPayload - if err := env.DecodePayload(&request); err != nil { - t.Fatalf("decode permission payload: %v", err) - } - done := make(chan struct{}) - go func() { s.expireCodexPermission(request.RequestID); close(done) }() - var reply struct { - Result PermissionsRequestApprovalResponse `json:"result"` - } - decodeCodexReply(t, srv, &reply) - <-done - if len(reply.Result.Permissions) != 0 || reply.Result.Scope != "turn" { - t.Fatalf("expiry permissions response = %+v", reply.Result) - } - }) - - t.Run("user input returns timeout error", func(t *testing.T) { - tc, srv, cleanup := NewTestClient() - defer cleanup() - s, out := newInteractionTestSession(tc.JSONRPCClient) - if err := SendServerRequest(srv, "rpc-ask-timeout", "item/tool/requestUserInput", ToolRequestUserInputParams{ - Questions: []ToolRequestUserInputQuestion{{ID: "q1", Header: "Confirm", Question: "Continue?"}}, - }); err != nil { - t.Fatalf("send input request: %v", err) - } - var request proto.PromptForUserChoicePayload - if err := (<-out).DecodePayload(&request); err != nil { - t.Fatalf("decode input request: %v", err) - } - expireDone := make(chan struct{}) - go func() { s.expireCodexAsk(request.AskID); close(expireDone) }() - var reply struct { - Error *struct { - Code int `json:"code"` - } `json:"error"` - } - decodeCodexReply(t, srv, &reply) - <-expireDone - if reply.Error == nil || reply.Error.Code != -32001 { - t.Fatalf("expiry reply = %+v", reply) - } - if err := s.SubmitPromptForUserChoice(context.Background(), request.AskID, proto.PromptForUserChoiceDecisionPayload{QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}}); !errors.Is(err, agent.ErrUnknownAsk) { - t.Fatalf("late input error = %v, want ErrUnknownAsk", err) - } - }) -} - -type approvalReply struct { - ID string `json:"id"` - Result ApprovalDecisionResult `json:"result"` -} - -func readCodexServerReply(t *testing.T, srv ServerSide) approvalReply { - t.Helper() - var reply approvalReply - decodeCodexReply(t, srv, &reply) - return reply -} - -func decodeCodexReply(t *testing.T, srv ServerSide, target any) { - t.Helper() - done := make(chan error, 1) - go func() { done <- json.NewDecoder(srv.FromClient).Decode(target) }() - select { - case err := <-done: - if err != nil { - t.Fatalf("decode Codex reply: %v", err) - } - case <-time.After(2 * time.Second): - t.Fatal("Codex reply timed out") - } -} diff --git a/apps/daemon/internal/agent/codex/session.go b/apps/daemon/internal/agent/codex/session.go index a69f3944b..9d3386609 100644 --- a/apps/daemon/internal/agent/codex/session.go +++ b/apps/daemon/internal/agent/codex/session.go @@ -103,24 +103,11 @@ type Session struct { finalText string lastErrText string - interactions *pendingCodexInteractions - outcome cancellationOutcomeState + outcome cancellationOutcomeState } var _ agent.Session = (*Session)(nil) -// SubmitPermission completes the deferred Codex app-server request that -// produced the Core permission envelope. -func (s *Session) SubmitPermission(_ context.Context, permID string, decision proto.PermissionDecisionPayload) error { - return s.submitCodexPermission(permID, decision) -} - -// SubmitPromptForUserChoice maps Core's header/answer pairs back to -// Codex's question-id keyed requestUserInput response. -func (s *Session) SubmitPromptForUserChoice(_ context.Context, askID string, decision proto.PromptForUserChoiceDecisionPayload) error { - return s.submitCodexUserInput(askID, decision) -} - // --------------------------------------------------------------------------- // notification handlers // --------------------------------------------------------------------------- @@ -142,14 +129,10 @@ func (s *Session) registerHandlers() { rpc.OnNotification("thread/tokenUsage/updated", s.onUsageUpdated) rpc.OnNotification("error", s.onErrorNotif) - s.onServerRequest("item/commandExecution/requestApproval", s.handleCodexCommandApproval) - s.onServerRequest("item/fileChange/requestApproval", s.handleCodexFileApproval) - s.onServerRequest("item/permissions/requestApproval", s.handleCodexPermissionsApproval) - // Older app-server releases used this unseparated method name. - s.onServerRequest("item/permissionsRequestApproval", s.handleCodexPermissionsApproval) - s.onServerRequest("item/tool/requestUserInput", s.handleCodexUserInput) + // Harnesses run unattended. Approval policy never and the session plan keep + // native asks from being raised; any other server request gets the + // client's method-not-found reply. s.onServerRequest("item/tool/call", s.handleFunctionCall) - s.onServerRequest("mcpServer/elicitation/request", s.handleCodexMCPElicitation) } func (s *Session) onTurnStarted(raw json.RawMessage) { diff --git a/apps/daemon/internal/agent/codex/session_cancel.go b/apps/daemon/internal/agent/codex/session_cancel.go index 25024d5b5..091eae119 100644 --- a/apps/daemon/internal/agent/codex/session_cancel.go +++ b/apps/daemon/internal/agent/codex/session_cancel.go @@ -42,7 +42,6 @@ func (s *Session) cancelNativeWork() { defer close(s.cancelReady) s.stopFunctionCalls() turnID, active := s.stopSteering() - s.stopCodexInteractionTimers() // Best effort: a known Turn must use its native identity. An explicit // empty ID invokes native startup cancellation before turn/started. if threadID := s.currentThreadID(); threadID != "" && active { diff --git a/apps/daemon/internal/agent/codex/session_cancel_test.go b/apps/daemon/internal/agent/codex/session_cancel_test.go index b5879197b..32e794b6a 100644 --- a/apps/daemon/internal/agent/codex/session_cancel_test.go +++ b/apps/daemon/internal/agent/codex/session_cancel_test.go @@ -116,7 +116,7 @@ func cancellationTestSession(t *testing.T) (*Session, *TestClient, ServerSide) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) s := &Session{rpc: client.JSONRPCClient, cancelCtx: ctx, cancelFn: cancel, - cfg: defaultSessionConfig(), interactions: newPendingCodexInteractions(), out: make(chan proto.Envelope, 8), bufs: NewItemBuffers()} + cfg: defaultSessionConfig(), out: make(chan proto.Envelope, 8), bufs: NewItemBuffers()} return s, client, server } diff --git a/apps/daemon/internal/agent/codex/session_cancel_write_test.go b/apps/daemon/internal/agent/codex/session_cancel_write_test.go index 7c0eed406..087b8e78f 100644 --- a/apps/daemon/internal/agent/codex/session_cancel_write_test.go +++ b/apps/daemon/internal/agent/codex/session_cancel_write_test.go @@ -146,7 +146,7 @@ func TestCancelReleasesBlockedNativeProcess(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() s := &Session{rpc: client, cancelCtx: ctx, cancelFn: cancel, - cfg: defaultSessionConfig(), interactions: newPendingCodexInteractions(), bufs: NewItemBuffers()} + cfg: defaultSessionConfig(), bufs: NewItemBuffers()} s.setThreadID("native-thread") s.onTurnStarted(json.RawMessage(`{"threadId":"native-thread","turn":{"id":"native-turn"}}`)) cancelDone := make(chan struct{}) diff --git a/apps/daemon/internal/agent/codex/session_plan.go b/apps/daemon/internal/agent/codex/session_plan.go index 6d8ae5d67..d8e7aede8 100644 --- a/apps/daemon/internal/agent/codex/session_plan.go +++ b/apps/daemon/internal/agent/codex/session_plan.go @@ -31,9 +31,6 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg disableProgrammaticTools(&plan, req.ExecutionControls) if req.LocalEnvironment != nil { plan.Cwd = req.LocalEnvironment.WorkspaceRoot - plan.Sandbox = "danger-full-access" - plan.Permissions = "" - plan.ApprovalPolicy = AskForApproval{String: "never"} } else { // environment:none has no workspace; the Session's private home is its cwd. plan.Cwd = nativeHomeFromPlan(plan) diff --git a/apps/daemon/internal/agent/codex/session_run.go b/apps/daemon/internal/agent/codex/session_run.go index 7517bfb98..b9f26c933 100644 --- a/apps/daemon/internal/agent/codex/session_run.go +++ b/apps/daemon/internal/agent/codex/session_run.go @@ -11,7 +11,6 @@ import ( func (s *Session) run(plan SessionPlan, req proto.PromptRequestPayload) { defer close(s.waitDone) - defer s.stopCodexInteractionTimers() defer s.stopFunctionCalls() defer s.cleanup() defer s.closeRunOutput() diff --git a/apps/daemon/internal/agent/codex/session_steering_lifecycle_test.go b/apps/daemon/internal/agent/codex/session_steering_lifecycle_test.go index 9b2bf38f0..7345d5dae 100644 --- a/apps/daemon/internal/agent/codex/session_steering_lifecycle_test.go +++ b/apps/daemon/internal/agent/codex/session_steering_lifecycle_test.go @@ -86,7 +86,7 @@ func TestBlockedSteeringWriteEndsRunWithTerminalFrames(t *testing.T) { s := &Session{ runID: "run", rpc: client.JSONRPCClient, cancelCtx: ctx, out: out, resolvedModel: "synthetic", cfg: sessionConfig{logger: obslog.Bg()}, waitDone: make(chan struct{}), cleanup: func() {}, - bufs: NewItemBuffers(), interactions: newPendingCodexInteractions(), + bufs: NewItemBuffers(), } s.registerHandlers() ready := make(chan error, 1) diff --git a/apps/daemon/internal/agent/codex/session_thread.go b/apps/daemon/internal/agent/codex/session_thread.go index 28681b400..35699b8a8 100644 --- a/apps/daemon/internal/agent/codex/session_thread.go +++ b/apps/daemon/internal/agent/codex/session_thread.go @@ -5,14 +5,20 @@ import ( "fmt" ) +// Harnesses run unattended with the launching user's permissions: Codex never +// raises a native approval request and applies no inner sandbox. +const ( + approvalPolicyNever = "never" + sandboxDangerFullAccess = "danger-full-access" +) + func (s *Session) startThread(plan SessionPlan) error { params := ThreadStartParams{ Cwd: plan.Cwd, Model: plan.Model, ModelProvider: plan.ModelProvider, - ApprovalPolicy: plan.ApprovalPolicy, - Sandbox: plan.Sandbox, - Permissions: plan.Permissions, + ApprovalPolicy: approvalPolicyNever, + Sandbox: sandboxDangerFullAccess, DeveloperInstructions: plan.SystemPrompt, } if s.observeSubagentIdentities { @@ -26,8 +32,7 @@ func (s *Session) startThread(plan SessionPlan) error { "cwd", params.Cwd, "model", params.Model, "model_provider", params.ModelProvider, - "sandbox", string(params.Sandbox), - "approval_silent", IsSilent(¶ms.ApprovalPolicy), + "sandbox", params.Sandbox, "developer_instructions_len", len(params.DeveloperInstructions)) _, err := s.rpc.requestWithResult(s.cancelCtx, "thread/start", params, func(raw json.RawMessage) error { return s.bindThreadResult(raw, "") @@ -37,8 +42,7 @@ func (s *Session) startThread(plan SessionPlan) error { func (s *Session) resumeThread(threadID string, plan SessionPlan) error { params := ThreadResumeParams{ - ThreadID: threadID, ApprovalPolicy: plan.ApprovalPolicy, Sandbox: plan.Sandbox, - Permissions: plan.Permissions, + ThreadID: threadID, ApprovalPolicy: approvalPolicyNever, Sandbox: sandboxDangerFullAccess, DeveloperInstructions: plan.SystemPrompt, } if plan.mcpServers != nil { diff --git a/apps/daemon/internal/agent/codex/subagent_observations_test.go b/apps/daemon/internal/agent/codex/subagent_observations_test.go index 6d10dd0a8..3e8f7241a 100644 --- a/apps/daemon/internal/agent/codex/subagent_observations_test.go +++ b/apps/daemon/internal/agent/codex/subagent_observations_test.go @@ -74,7 +74,7 @@ func observationSession(t *testing.T, status string) (*Session, *subagentFixture t.Fatal(err) } f.persist(t) - s := &Session{runID: "run", nativeHome: f.home, rpc: client.JSONRPCClient, out: out, cancelCtx: ctx, cancelFn: cancel, cfg: defaultSessionConfig(), bufs: NewItemBuffers(), interactions: newPendingCodexInteractions()} + s := &Session{runID: "run", nativeHome: f.home, rpc: client.JSONRPCClient, out: out, cancelCtx: ctx, cancelFn: cancel, cfg: defaultSessionConfig(), bufs: NewItemBuffers()} s.setThreadID("root") s.beginRootTurn("root", "root-turn") s.startSubagentObservations() diff --git a/apps/daemon/internal/agent/contract_declarations_test.go b/apps/daemon/internal/agent/contract_declarations_test.go index fc642d3f4..386cecee6 100644 --- a/apps/daemon/internal/agent/contract_declarations_test.go +++ b/apps/daemon/internal/agent/contract_declarations_test.go @@ -22,8 +22,6 @@ func TestPublicHarnessContractDeclarations(t *testing.T) { "DurableSteerer": {"session"}, "Steerer": {"session"}, "FunctionResultSubmitter": {"session"}, - "PermissionResponder": {"session"}, - "UserChoiceResponder": {"session"}, "WorkspaceReader": {"executor", "prepared", "session"}, "WorkspaceDirectoryLister": {"executor", "prepared", "session"}, "WorkspaceWriter": {"executor", "prepared", "session"}, diff --git a/apps/daemon/internal/agent/contracttest/text.go b/apps/daemon/internal/agent/contracttest/text.go index 5c0d76e4a..c389163a7 100644 --- a/apps/daemon/internal/agent/contracttest/text.go +++ b/apps/daemon/internal/agent/contracttest/text.go @@ -32,7 +32,7 @@ type TextFixture struct { // TextLifecycle verifies ordinary reuse, exact Turn ownership, durable active // input, confirmed cancellation and continuation of a healthy Executor. It asks -// for no optional tools, images, interactions, workspace or detailed Usage. +// for no optional tools, images, workspace or detailed Usage. // Native fault injection and history recovery have separate adapter tests. func TextLifecycle(t *testing.T, fixture TextFixture) { t.Helper() diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index ee08be11e..3a798043b 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -109,8 +109,8 @@ type Executor interface { type Turn interface { Session DurableSteerer - // Success confirms closed output and settled native input, function, - // interaction and child-work obligations. Errors cannot prove cancellation. + // Success confirms closed output and settled native input, function and + // child-work obligations. Errors cannot prove cancellation. AwaitSettlement(context.Context) (TurnSettlement, error) } @@ -158,20 +158,6 @@ type FunctionResultSubmitter interface { SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error } -// PermissionResponder accepts decisions for qualified permission requests. -// An adapter that never supports these interactions returns ErrUnsupportedOperation. -// Unknown or expired requests return ErrUnknownPermission. -type PermissionResponder interface { - SubmitPermission(context.Context, string, proto.PermissionDecisionPayload) error -} - -// UserChoiceResponder accepts answers for qualified user-choice requests. -// An adapter that never supports these interactions returns ErrUnsupportedOperation. -// Unknown or expired requests return ErrUnknownAsk. -type UserChoiceResponder interface { - SubmitPromptForUserChoice(context.Context, string, proto.PromptForUserChoiceDecisionPayload) error -} - // Workspace extensions, implemented by each owner explicitly. The common // Runtime may supply an authorized workspace owner independently of the adapter. // A resource without native access returns the corresponding Unsupported error; diff --git a/apps/daemon/internal/agent/mcode/contracts.go b/apps/daemon/internal/agent/mcode/contracts.go index d2ed2e6d4..025ef1890 100644 --- a/apps/daemon/internal/agent/mcode/contracts.go +++ b/apps/daemon/internal/agent/mcode/contracts.go @@ -11,8 +11,6 @@ var ( _ agent.DurableSteerer = (*Session)(nil) _ agent.Steerer = (*Session)(nil) _ agent.FunctionResultSubmitter = (*Session)(nil) - _ agent.PermissionResponder = (*Session)(nil) - _ agent.UserChoiceResponder = (*Session)(nil) _ agent.WorkspaceReader = (*Session)(nil) _ agent.WorkspaceDirectoryLister = (*Session)(nil) _ agent.WorkspaceWriter = (*Session)(nil) diff --git a/apps/daemon/internal/agent/mcode/declaration.go b/apps/daemon/internal/agent/mcode/declaration.go index b3f257ad7..5216634f5 100644 --- a/apps/daemon/internal/agent/mcode/declaration.go +++ b/apps/daemon/internal/agent/mcode/declaration.go @@ -14,7 +14,6 @@ import ( var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "mcode", Capabilities: proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilityUnsupported, Streaming: proto.CapabilitySupported, - Permissions: proto.CapabilitySupported, Usage: proto.CapabilityUnsupported, Resume: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilityUnsupported, diff --git a/apps/daemon/internal/agent/mcode/declaration_test.go b/apps/daemon/internal/agent/mcode/declaration_test.go index ff2f74f16..d39aada63 100644 --- a/apps/daemon/internal/agent/mcode/declaration_test.go +++ b/apps/daemon/internal/agent/mcode/declaration_test.go @@ -35,7 +35,7 @@ func TestMCodeExecutionOptInIsVersionBound(t *testing.T) { // The declaration must retain the complete baseline capability descriptor. func TestDeclaredCapabilityBaseline(t *testing.T) { - expected := map[string]bool{"Streaming": true, "Permissions": true, "Resume": true} + expected := map[string]bool{"Streaming": true, "Resume": true} value := reflect.ValueOf(Declaration.Info.Capabilities) for i := 0; i < value.NumField(); i++ { name := value.Type().Field(i).Name diff --git a/apps/daemon/internal/agent/mcode/events.go b/apps/daemon/internal/agent/mcode/events.go index 97bd1f8cd..a41789fd1 100644 --- a/apps/daemon/internal/agent/mcode/events.go +++ b/apps/daemon/internal/agent/mcode/events.go @@ -7,18 +7,18 @@ import ( "net/url" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" ) func decodeComponent(value string) (string, error) { return url.PathUnescape(value) } func (s *Session) handle(frame rpcFrame) error { if len(frame.ID) > 0 { - if frame.Method == "session/request_permission" && s.active { - return s.askPermission(frame) - } - if frame.Method == "elicitation/create" && s.active { - return s.askQuestion(frame) + // Harnesses run unattended, so no client method other than the + // permission answer is offered. Pinned mcode 0.4.12 maps the ACP + // cancelled outcome to deny: the tool call is blocked and the Turn + // continues. + if frame.Method == "session/request_permission" { + return s.write(rpcFrame{JSONRPC: "2.0", ID: frame.ID, Result: json.RawMessage(`{"outcome":{"outcome":"cancelled"}}`)}) } return s.write(rpcFrame{JSONRPC: "2.0", ID: frame.ID, Error: &rpcError{Code: -32601, Message: "ACP method not supported by OpenAgentCore"}}) } @@ -95,35 +95,3 @@ func (s *Session) emitTool(update toolUpdate) error { } return nil } - -func (s *Session) askPermission(frame rpcFrame) error { - var request permissionRequest - if err := json.Unmarshal(frame.Params, &request); err != nil { - return fmt.Errorf("mcode: invalid permission request") - } - if request.SessionID != s.sessionID { - return fmt.Errorf("mcode: permission request belongs to another session") - } - pending := pendingPermission{RPCID: frame.ID} - for _, option := range request.Options { - switch option.Kind { - case "allow_once": - pending.Allow = option.ID - case "reject_once": - pending.Deny = option.ID - } - } - if pending.Allow == "" || pending.Deny == "" { - return fmt.Errorf("mcode: permission request has no one-time allow/deny options") - } - id := "perm_" + uuid.NewString() - s.mu.Lock() - s.permissions[id] = pending - s.mu.Unlock() - tool := request.ToolCall.Name - if tool == "" { - tool = request.ToolCall.Title - } - s.emit(proto.TypePermissionRequest, proto.PermissionRequestPayload{RequestID: id, Tool: tool, Title: request.ToolCall.Title, Payload: request.ToolCall.RawInput}) - return nil -} diff --git a/apps/daemon/internal/agent/mcode/executor_turn.go b/apps/daemon/internal/agent/mcode/executor_turn.go index e9e2dcc17..dc24cd17a 100644 --- a/apps/daemon/internal/agent/mcode/executor_turn.go +++ b/apps/daemon/internal/agent/mcode/executor_turn.go @@ -38,7 +38,7 @@ func (s *Session) runExecutorTurn(prompt string) { s.mu.Lock() // ACP and native history cancellation do not prove detached tool cleanup. // Retire the owner and settle its workers before acknowledging cancellation. - if s.cancelled || s.inputUncertain || len(s.permissions) != 0 || len(s.questions) != 0 { + if s.cancelled || s.inputUncertain { reusable = false } if s.inputUncertain && err == nil { @@ -47,7 +47,6 @@ func (s *Session) runExecutorTurn(prompt string) { metadata := map[string]any{proto.DoneMetaAgentSessionType: "mcode", proto.DoneMetaAgentSessionID: s.sessionID} s.outcome = proto.DonePayload{Content: s.content.String(), Metadata: metadata, SourceCompletedAtMS: s.rootCompletedAtMS} outcome := s.outcome - s.permissions, s.questions = map[string]pendingPermission{}, map[string]pendingQuestion{} s.mu.Unlock() if !reusable { // Unknown quiescence invalidates this owner. A successful settlement diff --git a/apps/daemon/internal/agent/mcode/options_test.go b/apps/daemon/internal/agent/mcode/options_test.go index 1ae1d7780..70d736c86 100644 --- a/apps/daemon/internal/agent/mcode/options_test.go +++ b/apps/daemon/internal/agent/mcode/options_test.go @@ -81,22 +81,3 @@ func TestOptionsRejectDroppedContext(t *testing.T) { }) } } - -func TestQuestionContentPreservesTypesAndValidates(t *testing.T) { - pending := pendingQuestion{Properties: map[string]formProperty{"text": {Type: "string"}, "choice": {Type: "string", Options: []formOption{{Value: "eu", Title: "Europe"}}}}, Required: []string{"choice"}} - if _, err := questionContent(pending, proto.PromptForUserChoiceDecisionPayload{}); err == nil { - t.Fatal("required answer accepted empty") - } - decision := proto.PromptForUserChoiceDecisionPayload{QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "text", Answers: []string{"custom"}}, {QuestionID: "choice", Answers: []string{"Europe"}}}} - content, err := questionContent(pending, decision) - if err != nil { - t.Fatal(err) - } - if content["choice"] != "eu" || content["text"] != "custom" { - t.Fatalf("answers=%v", content) - } - decision.QuestionAnswers[1].Answers = []string{"unoffered"} - if _, err := questionContent(pending, decision); err == nil { - t.Fatal("unoffered choice accepted") - } -} diff --git a/apps/daemon/internal/agent/mcode/protocol.go b/apps/daemon/internal/agent/mcode/protocol.go index 95c9f629a..116d8c024 100644 --- a/apps/daemon/internal/agent/mcode/protocol.go +++ b/apps/daemon/internal/agent/mcode/protocol.go @@ -50,18 +50,3 @@ type sessionUpdate struct { toolUpdate } `json:"update"` } - -type permissionRequest struct { - SessionID string `json:"sessionId"` - ToolCall toolUpdate `json:"toolCall"` - Options []struct { - ID string `json:"optionId"` - Kind string `json:"kind"` - } `json:"options"` -} - -type pendingPermission struct { - RPCID json.RawMessage - Allow string - Deny string -} diff --git a/apps/daemon/internal/agent/mcode/questions.go b/apps/daemon/internal/agent/mcode/questions.go deleted file mode 100644 index b61721d97..000000000 --- a/apps/daemon/internal/agent/mcode/questions.go +++ /dev/null @@ -1,217 +0,0 @@ -package mcode - -import ( - "context" - "encoding/json" - "fmt" - "sort" - "strings" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/google/uuid" -) - -type formOption struct { - Value string `json:"const"` - Title string `json:"title"` - Description string `json:"description"` -} - -type formProperty struct { - Type string `json:"type"` - Title string `json:"title"` - Description string `json:"description"` - Options []formOption `json:"oneOf"` - Items struct { - Options []formOption `json:"anyOf"` - } `json:"items"` -} - -type pendingQuestion struct { - RPCID json.RawMessage - Properties map[string]formProperty - Required []string - OtherFields map[string]string -} - -func (s *Session) askQuestion(frame rpcFrame) error { - var request struct { - SessionID string `json:"sessionId"` - Mode string `json:"mode"` - Schema struct { - Type string `json:"type"` - Properties map[string]formProperty `json:"properties"` - Required []string `json:"required"` - } `json:"requestedSchema"` - } - if err := json.Unmarshal(frame.Params, &request); err != nil { - return fmt.Errorf("mcode: invalid input request") - } - if request.SessionID != s.sessionID { - return fmt.Errorf("mcode: input request belongs to another session") - } - if request.Mode != "form" || request.Schema.Type != "object" || len(request.Schema.Properties) == 0 { - return s.write(rpcFrame{JSONRPC: "2.0", ID: frame.ID, Error: &rpcError{Code: -32602, Message: "OpenAgentCore requires a nonempty input form"}}) - } - otherFields := questionnaireOtherFields(request.Schema.Properties) - for _, key := range otherFields { - delete(request.Schema.Properties, key) - } - keys := make([]string, 0, len(request.Schema.Properties)) - for key := range request.Schema.Properties { - keys = append(keys, key) - } - sort.Strings(keys) - questions := make([]proto.PromptForUserChoiceQuestion, 0, len(keys)) - for _, key := range keys { - property := request.Schema.Properties[key] - if property.Type != "string" && property.Type != "array" { - return s.write(rpcFrame{JSONRPC: "2.0", ID: frame.ID, Error: &rpcError{Code: -32602, Message: "OpenAgentCore supports text and choice input fields"}}) - } - question := proto.PromptForUserChoiceQuestion{ID: key, Question: property.Title, MultiSelect: property.Type == "array", Options: []proto.PromptForUserChoiceOption{}} - if question.Question == "" { - question.Question = key - } - if property.Description != "" { - question.Question += "\n" + property.Description - } - options := property.Options - if question.MultiSelect { - options = property.Items.Options - } - labels := map[string]bool{} - for _, option := range options { - label := option.Title - if label == "" { - label = option.Value - } - if labels[label] { - return fmt.Errorf("mcode: input choices have ambiguous labels") - } - labels[label] = true - question.Options = append(question.Options, proto.PromptForUserChoiceOption{Label: label, Description: option.Description}) - } - question.IsOther = len(options) == 0 || otherFields[key] != "" - questions = append(questions, question) - } - id := "ask_" + uuid.NewString() - s.mu.Lock() - s.questions[id] = pendingQuestion{RPCID: frame.ID, Properties: request.Schema.Properties, Required: request.Schema.Required, OtherFields: otherFields} - s.mu.Unlock() - s.emit(proto.TypePromptForUserChoice, proto.PromptForUserChoicePayload{AskID: id, Questions: questions}) - return nil -} - -// Native questionnaires encode Other as a companion field, not a second question. -func questionnaireOtherFields(properties map[string]formProperty) map[string]string { - fields := map[string]string{} - for key, property := range properties { - if len(property.Options) == 0 && len(property.Items.Options) == 0 { - continue - } - for candidate, other := range properties { - if strings.HasPrefix(candidate, key+"__other") && strings.Trim(strings.TrimPrefix(candidate, key+"__other"), "_") == "" && other.Type == "string" && len(other.Options) == 0 && other.Title == property.Title+" — Other" { - fields[key] = candidate - break - } - } - } - return fields -} - -func (s *Session) SubmitPromptForUserChoice(_ context.Context, id string, decision proto.PromptForUserChoiceDecisionPayload) error { - s.mu.Lock() - defer s.mu.Unlock() - pending, ok := s.questions[id] - if !ok { - return agent.ErrUnknownAsk - } - if err := decision.Validate(); err != nil { - return err - } - response := map[string]any{"action": "cancel"} - if !decision.Cancelled { - content, err := questionContent(pending, decision) - if err != nil { - return err - } - response = map[string]any{"action": "accept", "content": content} - } - raw, _ := json.Marshal(response) - if err := s.write(rpcFrame{JSONRPC: "2.0", ID: pending.RPCID, Result: raw}); err != nil { - return err - } - delete(s.questions, id) - return nil -} - -func questionContent(pending pendingQuestion, decision proto.PromptForUserChoiceDecisionPayload) (map[string]any, error) { - ids := make([]string, 0, len(pending.Properties)) - for id := range pending.Properties { - ids = append(ids, id) - } - if _, err := decision.AnswersFor(ids); err != nil { - return nil, err - } - content := map[string]any{} - for _, answer := range decision.QuestionAnswers { - property, ok := pending.Properties[answer.QuestionID] - if !ok { - return nil, fmt.Errorf("mcode: unknown input field") - } - values := append([]string{}, answer.Answers...) - if len(values) == 0 { - continue - } - options := property.Options - if property.Type == "array" { - options = property.Items.Options - } else if len(values) != 1 { - return nil, fmt.Errorf("mcode: input field requires a single answer") - } - selected := make([]string, 0, len(values)) - for _, value := range values { - if len(options) == 0 { - selected = append(selected, value) - continue - } - found := false - for _, option := range options { - label := option.Title - if label == "" { - label = option.Value - } - if value == label { - selected = append(selected, option.Value) - found = true - break - } - } - if !found { - other := pending.OtherFields[answer.QuestionID] - if other == "" { - return nil, fmt.Errorf("mcode: answer is not an offered choice") - } - if _, exists := content[other]; exists { - return nil, fmt.Errorf("mcode: input field requires a single custom answer") - } - content[other] = value - } - } - if len(selected) == 0 { - continue - } - if property.Type == "array" { - content[answer.QuestionID] = selected - } else { - content[answer.QuestionID] = selected[0] - } - } - for _, key := range pending.Required { - if _, ok := content[key]; !ok { - return nil, fmt.Errorf("mcode: required input is missing") - } - } - return content, nil -} diff --git a/apps/daemon/internal/agent/mcode/questions_test.go b/apps/daemon/internal/agent/mcode/questions_test.go deleted file mode 100644 index 858658e6c..000000000 --- a/apps/daemon/internal/agent/mcode/questions_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package mcode - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -func TestQuestionnaireOtherUsesOneQuestion(t *testing.T) { - for _, multiple := range []bool{false, true} { - property := formProperty{Type: "string", Title: "Region?", Options: []formOption{{Value: "eu", Title: "Europe"}}} - if multiple { - property.Type = "array" - property.Items.Options = property.Options - property.Options = nil - } - properties := map[string]formProperty{"region": property, "region__other": {Type: "string", Title: "Region? — Other"}} - params, _ := json.Marshal(map[string]any{"sessionId": "native-1", "mode": "form", "requestedSchema": map[string]any{"type": "object", "properties": properties}}) - out := make(chan proto.Envelope, 1) - session := &Session{ctx: t.Context(), outputContext: t.Context(), sessionID: "native-1", out: out, questions: map[string]pendingQuestion{}} - if err := session.askQuestion(rpcFrame{ID: json.RawMessage(`1`), Params: params}); err != nil { - t.Fatal(err) - } - var request proto.PromptForUserChoicePayload - _ = json.Unmarshal((<-out).Payload, &request) - if len(request.Questions) != 1 || !request.Questions[0].IsOther || request.Questions[0].MultiSelect != multiple { - t.Fatalf("unexpected questions: %#v", request.Questions) - } - pending := session.questions[request.AskID] - for _, custom := range []bool{false, true} { - answer := "Europe" - want := map[string]any{"region": "eu"} - if multiple { - want["region"] = []string{"eu"} - } - if custom { - answer = "Asia" - want = map[string]any{"region__other": "Asia"} - } - got, err := questionContent(pending, proto.PromptForUserChoiceDecisionPayload{QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "region", Answers: []string{answer}}}}) - if err != nil || !reflect.DeepEqual(got, want) { - t.Fatalf("multiple=%t custom=%t: got=%v err=%v", multiple, custom, got, err) - } - } - if multiple { - got, err := questionContent(pending, proto.PromptForUserChoiceDecisionPayload{QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "region", Answers: []string{"Europe", "Asia"}}}}) - want := map[string]any{"region": []string{"eu"}, "region__other": "Asia"} - if err != nil || !reflect.DeepEqual(got, want) { - t.Fatalf("combined choice and custom: got=%v err=%v", got, err) - } - } - } -} diff --git a/apps/daemon/internal/agent/mcode/session.go b/apps/daemon/internal/agent/mcode/session.go index 95a0c3d09..528d0338b 100644 --- a/apps/daemon/internal/agent/mcode/session.go +++ b/apps/daemon/internal/agent/mcode/session.go @@ -3,7 +3,6 @@ package mcode import ( "context" "encoding/json" - "errors" "fmt" "net/url" "slices" @@ -39,8 +38,6 @@ type Session struct { nativeModel string outputContext context.Context outcome proto.DonePayload - permissions map[string]pendingPermission - questions map[string]pendingQuestion steeringReady bool steeringTurn string sequence uint64 @@ -68,7 +65,7 @@ func launch(ctx context.Context, req proto.PromptRequestPayload, opts launchOpti } func newTurnSession(ctx context.Context, req proto.PromptRequestPayload, opts launchOptions, c *connection, out chan<- proto.Envelope) *Session { - return &Session{ctx: ctx, req: req, opts: opts, connection: c, out: out, frames: make(chan rpcFrame, 32), finished: make(chan struct{}), permissions: map[string]pendingPermission{}, questions: map[string]pendingQuestion{}, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}} + return &Session{ctx: ctx, req: req, opts: opts, connection: c, out: out, frames: make(chan rpcFrame, 32), finished: make(chan struct{}), tools: map[string]toolUpdate{}, completedTools: map[string]bool{}} } func (s *Session) prepareNative() error { @@ -81,7 +78,7 @@ func (s *Session) prepareNative() error { } `json:"oac/subagents"` } `json:"_meta"` } - if err := s.call("initialize", map[string]any{"protocolVersion": 1, "clientInfo": map[string]string{"name": "oac", "version": "1"}, "clientCapabilities": map[string]any{"elicitation": map[string]any{"form": map[string]any{}}}}, &initialized, false); err != nil { + if err := s.call("initialize", map[string]any{"protocolVersion": 1, "clientInfo": map[string]string{"name": "oac", "version": "1"}}, &initialized, false); err != nil { return err } if initialized.ProtocolVersion != 1 { @@ -260,28 +257,3 @@ func (s *Session) emit(kind string, payload any) { case <-s.outputContext.Done(): } } - -func (s *Session) SubmitPermission(_ context.Context, id string, decision proto.PermissionDecisionPayload) error { - s.mu.Lock() - defer s.mu.Unlock() - pending, ok := s.permissions[id] - if !ok { - return agent.ErrUnknownPermission - } - choice := pending.Deny - if decision.Approved { - choice = pending.Allow - } - if choice == "" { - return errors.New("mcode: ACP permission option is unavailable") - } - if len(decision.UpdatedInput) > 0 { - return errors.New("mcode: edited permission input is not supported") - } - raw, _ := json.Marshal(map[string]any{"outcome": map[string]string{"outcome": "selected", "optionId": choice}}) - if err := s.write(rpcFrame{JSONRPC: "2.0", ID: pending.RPCID, Result: raw}); err != nil { - return err - } - delete(s.permissions, id) - return nil -} diff --git a/apps/daemon/internal/agent/mcode/session_test.go b/apps/daemon/internal/agent/mcode/session_test.go index ae435c344..0dfc9e070 100644 --- a/apps/daemon/internal/agent/mcode/session_test.go +++ b/apps/daemon/internal/agent/mcode/session_test.go @@ -4,14 +4,12 @@ import ( "bufio" "context" "encoding/json" - "errors" "os" "path/filepath" "strings" "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) @@ -129,39 +127,20 @@ func TestSessionStreamsCurrentTurnAndResumes(t *testing.T) { } } -func TestSessionInteractionRoundTrip(t *testing.T) { - for _, approved := range []bool{true, false} { - t.Run(map[bool]string{true: "allow", false: "deny"}[approved], func(t *testing.T) { - session, out := helperSession(t, "interaction", false) - permissions, questions := 0, 0 - for event := range out { - switch event.Type { - case proto.TypeError: - t.Fatalf("error: %s", event.Payload) - case proto.TypePermissionRequest: - permissions++ - var p proto.PermissionRequestPayload - _ = json.Unmarshal(event.Payload, &p) - if err := session.SubmitPermission(context.Background(), p.RequestID, proto.PermissionDecisionPayload{Approved: approved}); err != nil { - t.Fatal(err) - } - if err := session.SubmitPermission(context.Background(), p.RequestID, proto.PermissionDecisionPayload{Approved: true}); !errors.Is(err, agent.ErrUnknownPermission) { - t.Fatalf("duplicate approval: %v", err) - } - case proto.TypePromptForUserChoice: - questions++ - var p proto.PromptForUserChoicePayload - _ = json.Unmarshal(event.Payload, &p) - decision := proto.PromptForUserChoiceDecisionPayload{QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "region", Answers: []string{"Europe"}}}} - if err := session.SubmitPromptForUserChoice(context.Background(), p.AskID, decision); err != nil { - t.Fatal(err) - } - } - } - if permissions != 1 || questions != 1 { - t.Fatalf("permissions=%d questions=%d", permissions, questions) - } - }) +// Harnesses run unattended: native asks are declined in the adapter and the turn completes. +func TestSessionDeclinesNativeAsks(t *testing.T) { + _, out := helperSession(t, "unattended", false) + done := 0 + for event := range out { + switch event.Type { + case proto.TypeError: + t.Fatalf("error: %s", event.Payload) + case proto.TypeDone: + done++ + } + } + if done != 1 { + t.Fatalf("done=%d", done) } } @@ -272,6 +251,9 @@ func TestMCodeProcess(t *testing.T) { result := any(map[string]any{}) switch frame.Method { case "initialize": + if scenario == "unattended" && strings.Contains(string(frame.Params), "elicitation") { + os.Exit(7) + } result = map[string]int{"protocolVersion": 1} case "session/new", "session/load": if scenario == "prepared-mcp-cancel" { @@ -356,9 +338,9 @@ func TestMCodeProcess(t *testing.T) { send(rpcFrame{JSONRPC: "2.0", ID: frame.ID, Error: &rpcError{Code: -32603, Message: "Fixture provider unavailable"}}) continue } - if scenario == "interaction" { + if scenario == "unattended" { promptID = frame.ID - send(map[string]any{"jsonrpc": "2.0", "id": "permission-1", "method": "session/request_permission", "params": map[string]any{"sessionId": "native-1", "toolCall": map[string]any{"toolCallId": "tool-1", "name": "Bash", "title": "Run fixture", "rawInput": map[string]any{"command": "echo fixture"}}, "options": []map[string]string{{"optionId": "once", "kind": "allow_once"}, {"optionId": "always", "kind": "allow_always"}, {"optionId": "deny", "kind": "reject_once"}}}}) + send(map[string]any{"jsonrpc": "2.0", "id": "permission-1", "method": "session/request_permission", "params": map[string]any{"sessionId": "native-1", "toolCall": map[string]any{"toolCallId": "tool-1", "name": "Bash", "title": "Run fixture", "rawInput": map[string]any{"command": "echo fixture"}}, "options": []map[string]string{{"optionId": "once", "kind": "allow_once"}, {"optionId": "deny", "kind": "reject_once"}}}}) continue } update("agent_message_chunk", map[string]any{"content": map[string]string{"type": "text", "text": "Hello "}}) @@ -397,21 +379,15 @@ func TestMCodeProcess(t *testing.T) { if string(frame.ID) == `"permission-1"` { var reply struct { Outcome struct { - Option string `json:"optionId"` + Outcome string `json:"outcome"` } `json:"outcome"` } - _ = json.Unmarshal(frame.Result, &reply) - if reply.Outcome.Option != "once" && reply.Outcome.Option != "deny" { + if json.Unmarshal(frame.Result, &reply) != nil || reply.Outcome.Outcome != "cancelled" { os.Exit(5) } - send(map[string]any{"jsonrpc": "2.0", "id": "question-1", "method": "elicitation/create", "params": map[string]any{"sessionId": "native-1", "mode": "form", "requestedSchema": map[string]any{"type": "object", "required": []string{"region"}, "properties": map[string]any{"region": map[string]any{"type": "string", "title": "Region?", "oneOf": []map[string]string{{"const": "eu", "title": "Europe"}, {"const": "us", "title": "America"}}}}}}}) + send(map[string]any{"jsonrpc": "2.0", "id": "question-1", "method": "elicitation/create", "params": map[string]any{"sessionId": "native-1", "mode": "form", "requestedSchema": map[string]any{"type": "object", "properties": map[string]any{"region": map[string]any{"type": "string"}}}}}) } else { - var reply struct { - Action string `json:"action"` - Content map[string]string `json:"content"` - } - _ = json.Unmarshal(frame.Result, &reply) - if reply.Action != "accept" || reply.Content["region"] != "eu" { + if frame.Error == nil || frame.Error.Code != -32601 { os.Exit(6) } raw, _ := json.Marshal(map[string]string{"stopReason": "end_turn"}) diff --git a/apps/daemon/internal/agent/registry.go b/apps/daemon/internal/agent/registry.go index 4a808b7ff..f6b02c0c9 100644 --- a/apps/daemon/internal/agent/registry.go +++ b/apps/daemon/internal/agent/registry.go @@ -1,7 +1,6 @@ package agent import ( - "errors" "fmt" "slices" "sync" @@ -10,16 +9,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" ) -// ErrUnknownPermission is returned by PermissionResponder.SubmitPermission when -// the permID doesn't match any outstanding request. The router uses -// this to distinguish a benign race from a real forwarding failure. -var ErrUnknownPermission = errors.New("agent: unknown permission id") - -// ErrUnknownAsk is returned by UserChoiceResponder.SubmitPromptForUserChoice when -// the askID doesn't match any outstanding ask. Same race semantics as -// ErrUnknownPermission. -var ErrUnknownAsk = errors.New("agent: unknown ask id") - // Registry keeps the daemon-advertised capability descriptor and execution // factories for each agent_kind. Safe for concurrent use. type Registry struct { diff --git a/apps/daemon/internal/agent/registry_test.go b/apps/daemon/internal/agent/registry_test.go index 657326a10..9720ab654 100644 --- a/apps/daemon/internal/agent/registry_test.go +++ b/apps/daemon/internal/agent/registry_test.go @@ -71,10 +71,9 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { Available: true, Version: "1.2.3", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ - Streaming: proto.CapabilitySupported, - Permissions: proto.CapabilitySupported, - Usage: proto.CapabilitySupported, - Resume: proto.CapabilitySupported, + Streaming: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, }), }, harnessconfig.Configuration{}) @@ -85,7 +84,7 @@ func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { if got[0].Kind != "fake_alpha" || got[1].Kind != "fake_beta" { t.Fatalf("SupportedAgentKinds sort = %#v, want fake_alpha then fake_beta", got) } - if !got[0].Available || got[0].Version != "1.2.3" || !got[0].Capabilities.Permissions.IsSupported() || !got[0].Capabilities.Resume.IsSupported() { + if !got[0].Available || got[0].Version != "1.2.3" || !got[0].Capabilities.Usage.IsSupported() || !got[0].Capabilities.Resume.IsSupported() { t.Fatalf("fake_alpha descriptor not preserved: %#v", got[0]) } if got[1].Available || got[1].Version != "missing" || !got[1].Capabilities.Streaming.IsSupported() { diff --git a/apps/daemon/internal/dispatch/capability_admission_test.go b/apps/daemon/internal/dispatch/capability_admission_test.go index 744d60ff6..7aaf78466 100644 --- a/apps/daemon/internal/dispatch/capability_admission_test.go +++ b/apps/daemon/internal/dispatch/capability_admission_test.go @@ -52,50 +52,3 @@ func TestSteeringUsesAdmittedDeclarationAndDoesNotReplayUnsupportedImplementatio }) } } - -func TestInteractionDeclarationPrecedesResponderMethods(t *testing.T) { - for _, supported := range []bool{false, true} { - for _, ask := range []bool{false, true} { - t.Run(fmt.Sprintf("supported=%v/ask=%v", supported, ask), func(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - var session *fakeSession - info := proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, Permissions: proto.CapabilityFromBool(supported)})} - registerExecutorKind(h.reg, info, sessionExecutor(func(_ context.Context, _ string, _ proto.MessageInput, out chan<- proto.Envelope) (agent.Session, error) { - session = &fakeSession{out: out, closeOutOnCancel: true, submitErr: agent.ErrUnsupportedOperation, askErr: agent.ErrUnsupportedOperation} - return session, nil - })) - startRun(t, h.router, h.sender, "run", proto.PromptRequestPayload{AgentKind: "fixture"}) - event := mustEnv(t, proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{RequestID: "interaction", Tool: "fixture"}) - decision := mustEnv(t, proto.TypePermissionDecision, "interaction", proto.PermissionDecisionPayload{DeliveryID: "decision", Approved: true}) - if ask { - event = mustEnv(t, proto.TypePromptForUserChoice, "run", proto.PromptForUserChoicePayload{AskID: "interaction"}) - decision = mustEnv(t, proto.TypePromptForUserChoiceDecision, "interaction", proto.PromptForUserChoiceDecisionPayload{DeliveryID: "decision"}) - } - session.out <- event - waitFor(t, func() bool { return hasFrame(h.sender, event.Type, "run") }, "interaction indexed") - if err := h.router.Handle(t.Context(), decision); err != nil { - t.Fatal(err) - } - code := "unsupported" - if supported { - code = "contract_violation" - } - assertDecisionAck(t, h.sender, "decision", false, code) - count := len(session.submissions()) - if ask { - session.askMu.Lock() - count = len(session.askCalls) - session.askMu.Unlock() - } - want := 0 - if supported { - want = 1 - } - if count != want { - t.Fatalf("native calls %d, want %d", count, want) - } - }) - } - } -} diff --git a/apps/daemon/internal/dispatch/export_test.go b/apps/daemon/internal/dispatch/export_test.go index 05d7011d7..6ee90d36e 100644 --- a/apps/daemon/internal/dispatch/export_test.go +++ b/apps/daemon/internal/dispatch/export_test.go @@ -7,29 +7,6 @@ func (r *Router) PreparationOwnershipForTest(handle string) (bool, bool) { return p != nil && p.owns, p != nil && p.busy } -// Test-only re-exports so router_test.go (package dispatch_test) can -// peek into internal state without widening the public surface. - -// AskIndexLenForTest returns the number of asks still indexed at the -// router level. Used to assert cleanup paths. -func (r *Router) AskIndexLenForTest() int { - r.mu.Lock() - defer r.mu.Unlock() - return len(r.askIndex) -} - -// PendingAsksLenForTest reports how many asks the named run still has -// outstanding. Returns -1 if the run is unknown. -func (r *Router) PendingAsksLenForTest(runID string) int { - r.mu.Lock() - defer r.mu.Unlock() - s, ok := r.sessions[runID] - if !ok { - return -1 - } - return len(s.pendingAsks) -} - func (r *Router) SteeringClosedForTest(runID string) bool { r.mu.Lock() defer r.mu.Unlock() diff --git a/apps/daemon/internal/dispatch/functions.go b/apps/daemon/internal/dispatch/functions.go index 89ee63a41..14252711b 100644 --- a/apps/daemon/internal/dispatch/functions.go +++ b/apps/daemon/internal/dispatch/functions.go @@ -5,6 +5,8 @@ import ( "crypto/sha256" "encoding/json" "errors" + "fmt" + "time" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" @@ -29,9 +31,15 @@ func (r *Router) handleFunctionResult(ctx context.Context, env proto.Envelope) e } fingerprint := sha256.Sum256(encoded) // Scope receipt replay to both identities, even when native call IDs repeat across Runs. - kind := proto.TypeFunctionResult + "\x00" + result.CallID - if handled, err := r.replayAppliedInteractionDecision(ctx, env.ID, result.DeliveryID, kind, fingerprint); handled { - return err + key := env.ID + "\x00" + result.CallID + r.mu.Lock() + applied, replay := r.applied[key] + r.mu.Unlock() + if replay { + if applied.fingerprint != fingerprint { + return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, false, "decision_conflict", "request was already applied with a different decision") + } + return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, true, "", "") } r.mu.Lock() state := r.sessions[env.ID] @@ -69,6 +77,43 @@ func (r *Router) handleFunctionResult(ctx context.Context, env proto.Envelope) e } return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, false, code, "function result was not applied") } - r.rememberAppliedInteractionDecision(env.ID, kind, fingerprint) + r.rememberAppliedFunctionResult(key, fingerprint) return r.sendInteractionDecisionAck(ctx, env.ID, result.DeliveryID, true, "", "") } + +func (r *Router) rememberAppliedFunctionResult(key string, fingerprint [32]byte) { + now := time.Now().UTC() + r.mu.Lock() + if len(r.applied) >= 1024 { + cutoff := now.Add(-time.Hour) + for id, entry := range r.applied { + if entry.recordedAt.Before(cutoff) { + delete(r.applied, id) + } + } + } + if len(r.applied) >= 1024 { + for id := range r.applied { + delete(r.applied, id) + break + } + } + r.applied[key] = appliedFunctionResult{fingerprint: fingerprint, recordedAt: now} + r.mu.Unlock() +} + +func (r *Router) sendInteractionDecisionAck(ctx context.Context, requestID, deliveryID string, applied bool, errorCode, message string) error { + env, err := proto.NewEnvelope(proto.TypeInteractionDecisionAck, requestID, proto.InteractionDecisionAckPayload{ + DeliveryID: deliveryID, + Applied: applied, + ErrorCode: errorCode, + Error: message, + }) + if err != nil { + return fmt.Errorf("dispatch: build interaction decision ack: %w", err) + } + if err := r.sender.Send(ctx, env); err != nil { + return fmt.Errorf("dispatch: send interaction decision ack: %w", err) + } + return nil +} diff --git a/apps/daemon/internal/dispatch/interaction_decisions.go b/apps/daemon/internal/dispatch/interaction_decisions.go deleted file mode 100644 index 1c4c4abb7..000000000 --- a/apps/daemon/internal/dispatch/interaction_decisions.go +++ /dev/null @@ -1,328 +0,0 @@ -package dispatch - -import ( - "context" - "crypto/sha256" - "encoding/json" - "errors" - "fmt" - "strings" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -func (r *Router) handlePermissionDecision(ctx context.Context, env proto.Envelope) error { - if env.ID == "" { - return errors.New("dispatch: permission_decision missing perm id (Envelope.ID empty)") - } - var payload proto.PermissionDecisionPayload - if err := env.DecodePayload(&payload); err != nil { - return fmt.Errorf("dispatch: decode permission_decision: %w", err) - } - if payload.DeliveryID == "" { - return errors.New("dispatch: permission_decision missing delivery_id") - } - fingerprint, err := interactionDecisionFingerprint(payload) - if err != nil { - return fmt.Errorf("dispatch: fingerprint permission_decision: %w", err) - } - if handled, err := r.replayAppliedInteractionDecision(ctx, env.ID, payload.DeliveryID, proto.TypePermissionDecision, fingerprint); handled { - return err - } - - r.mu.Lock() - runID, known := r.permIndex[env.ID] - var state *sessionState - var session agent.Session - var finishOperation func() - if known { - state = r.sessions[runID] - if state != nil { - session, finishOperation, _ = r.preparedOperationLocked(state) - } - } - r.mu.Unlock() - - if !known || state == nil { - // Server's perm timeout / cancel race; common enough that info - // is right. - r.log.InfoContext(ctx, "permission_decision for unknown perm (run gone)", "perm_id", env.ID) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "not_pending", "permission request is no longer pending") - } - if session == nil { - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "not_ready", "permission request is waiting for the native session") - } - defer finishOperation() - ctx, stop := r.shutdownContext(ctx) - defer stop() - - if !state.capabilities.Permissions.IsSupported() { - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "unsupported", "The runtime declaration does not support interaction decisions.") - } - responder, supported := session.(agent.PermissionResponder) - if !supported { - r.dropPermission(state, env.ID) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "contract_violation", "Declared runtime capability does not implement permission responses") - } - if err := responder.SubmitPermission(ctx, env.ID, payload); err != nil { - if errors.Is(err, agent.ErrUnsupportedOperation) { - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "contract_violation", "Declared permission capability has no implementation.") - } - if errors.Is(err, agent.ErrUnknownPermission) { - r.log.InfoContext(ctx, "agent reports unknown perm (race with cancel)", "perm_id", env.ID, "run_id", runID) - r.dropPermission(state, env.ID) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "not_pending", err.Error()) - } - r.log.WarnContext(ctx, "agent rejected permission decision", "perm_id", env.ID, "run_id", runID, "err", err) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "runtime_error", err.Error()) - } - r.dropPermission(state, env.ID) - r.rememberAppliedInteractionDecision(env.ID, proto.TypePermissionDecision, fingerprint) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, true, "", "") -} - -func (r *Router) dropPermission(s *sessionState, permissionID string) { - r.mu.Lock() - delete(r.permIndex, permissionID) - delete(s.pendingIDs, permissionID) - r.mu.Unlock() -} - -// handlePromptForUserChoiceDecision is the ask-side twin of -// handlePermissionDecision. The server forwards the human's answer -// here; we look up the owning session via askIndex and ask the agent -// to write a matching tool_result back into its CLI. -// -// On both success and ErrUnknownAsk we drop the ask from askIndex / -// pendingAsks so a stale retry can't waste cycles. Timer-fired cancels -// inside the session don't currently call back into the router, so -// those entries linger until cleanupSession — acceptable because they -// can't double-fire (the session's own pendingAskTable.Take already -// guards that); cleanupSession removes the routing entry when the run -// stream closes. -func (r *Router) handlePromptForUserChoiceDecision(ctx context.Context, env proto.Envelope) error { - if env.ID == "" { - return errors.New("dispatch: prompt_for_user_choice_decision missing ask id (Envelope.ID empty)") - } - var payload proto.PromptForUserChoiceDecisionPayload - if err := env.DecodePayload(&payload); err != nil { - return fmt.Errorf("dispatch: decode prompt_for_user_choice_decision: %w", err) - } - if payload.DeliveryID == "" { - return errors.New("dispatch: prompt_for_user_choice_decision missing delivery_id") - } - fingerprint, err := interactionDecisionFingerprint(payload) - if err != nil { - return fmt.Errorf("dispatch: fingerprint prompt_for_user_choice_decision: %w", err) - } - if handled, err := r.replayAppliedInteractionDecision(ctx, env.ID, payload.DeliveryID, proto.TypePromptForUserChoiceDecision, fingerprint); handled { - return err - } - - r.mu.Lock() - runID, known := r.askIndex[env.ID] - var state *sessionState - var session agent.Session - var finishOperation func() - if known { - state = r.sessions[runID] - if state != nil { - session, finishOperation, _ = r.preparedOperationLocked(state) - } - } - r.mu.Unlock() - - if !known || state == nil { - r.log.InfoContext(ctx, "prompt_for_user_choice_decision for unknown ask (run gone)", "ask_id", env.ID) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "not_pending", "user-input request is no longer pending") - } - if session == nil { - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "not_ready", "user-input request is waiting for the native session") - } - defer finishOperation() - ctx, stop := r.shutdownContext(ctx) - defer stop() - - if !state.capabilities.Permissions.IsSupported() { - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "unsupported", "The runtime declaration does not support interaction decisions.") - } - responder, supported := session.(agent.UserChoiceResponder) - if !supported { - r.dropAsk(state, env.ID) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "contract_violation", "Declared runtime capability does not implement user-choice responses") - } - err = responder.SubmitPromptForUserChoice(ctx, env.ID, payload) - if err != nil { - if errors.Is(err, agent.ErrUnsupportedOperation) { - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "contract_violation", "Declared user-choice capability has no implementation.") - } - if errors.Is(err, agent.ErrUnknownAsk) { - r.log.InfoContext(ctx, "agent reports unknown ask (race with cancel)", "ask_id", env.ID, "run_id", runID) - r.dropAsk(state, env.ID) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "not_pending", err.Error()) - } - // Keep the routing entry for transient runtime failures. Codex, for - // example, restores its pending request when a JSON-RPC reply write - // fails, so dropping the ask here would turn a retryable error into a - // permanent not_pending response on the next attempt. - r.log.WarnContext(ctx, "agent rejected user-input decision", "ask_id", env.ID, "run_id", runID, "err", err) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, false, "runtime_error", err.Error()) - } - r.dropAsk(state, env.ID) - r.rememberAppliedInteractionDecision(env.ID, proto.TypePromptForUserChoiceDecision, fingerprint) - return r.sendInteractionDecisionAck(ctx, env.ID, payload.DeliveryID, true, "", "") -} - -func (r *Router) replayAppliedInteractionDecision(ctx context.Context, requestID, deliveryID, kind string, fingerprint [32]byte) (bool, error) { - key := appliedInteractionDecisionKey(requestID, kind) - r.mu.Lock() - applied, ok := r.applied[key] - r.mu.Unlock() - if !ok { - return false, nil - } - if applied.requestID != requestID || applied.kind != kind || applied.fingerprint != fingerprint { - return true, r.sendInteractionDecisionAck(ctx, requestID, deliveryID, false, "decision_conflict", "request was already applied with a different decision") - } - return true, r.sendInteractionDecisionAck(ctx, requestID, deliveryID, true, "", "") -} - -func (r *Router) rememberAppliedInteractionDecision(requestID, kind string, fingerprint [32]byte) { - now := time.Now().UTC() - key := appliedInteractionDecisionKey(requestID, kind) - r.mu.Lock() - if len(r.applied) >= 1024 { - cutoff := now.Add(-time.Hour) - for id, entry := range r.applied { - if entry.recordedAt.Before(cutoff) { - delete(r.applied, id) - } - } - } - if len(r.applied) >= 1024 { - for id := range r.applied { - delete(r.applied, id) - break - } - } - r.applied[key] = appliedInteractionDecision{ - requestID: requestID, kind: kind, fingerprint: fingerprint, recordedAt: now, - } - r.mu.Unlock() -} - -func appliedInteractionDecisionKey(requestID, kind string) string { - return kind + "\x00" + requestID -} - -// interactionDecisionFingerprint excludes the transport delivery id. Each -// retry gets a fresh delivery id so a late ack cannot satisfy a newer waiter, -// while the request plus decision content remains stable for daemon replay. -func interactionDecisionFingerprint(payload any) ([32]byte, error) { - switch decision := payload.(type) { - case proto.PermissionDecisionPayload: - decision.DeliveryID = "" - encoded, err := json.Marshal(decision) - if err != nil { - return [32]byte{}, err - } - return sha256.Sum256(encoded), nil - case proto.PromptForUserChoiceDecisionPayload: - decision.DeliveryID = "" - encoded, err := json.Marshal(decision) - if err != nil { - return [32]byte{}, err - } - return sha256.Sum256(encoded), nil - default: - return [32]byte{}, fmt.Errorf("unsupported interaction decision %T", payload) - } -} - -func (r *Router) sendInteractionDecisionAck(ctx context.Context, requestID, deliveryID string, applied bool, errorCode, message string) error { - env, err := proto.NewEnvelope(proto.TypeInteractionDecisionAck, requestID, proto.InteractionDecisionAckPayload{ - DeliveryID: deliveryID, - Applied: applied, - ErrorCode: errorCode, - Error: message, - }) - if err != nil { - return fmt.Errorf("dispatch: build interaction decision ack: %w", err) - } - if err := r.sender.Send(ctx, env); err != nil { - return fmt.Errorf("dispatch: send interaction decision ack: %w", err) - } - return nil -} - -// dropAsk clears askID from both the router-level askIndex and the -// session's pendingAsks set. Safe to call with an askID that's already -// gone — both deletes are no-ops then. -func (r *Router) dropAsk(s *sessionState, askID string) { - r.mu.Lock() - delete(r.askIndex, askID) - delete(s.pendingAsks, askID) - r.mu.Unlock() -} - -// indexPermissionFrame records interaction identities before forwarding them. -// Prepared output may arrive before successful Session publication; decisions -// remain retryable until state.session becomes available. -func (r *Router) indexPermissionFrame(s *sessionState, env proto.Envelope) { - switch env.Type { - case proto.TypePermissionRequest: - var p proto.PermissionRequestPayload - if err := env.DecodePayload(&p); err != nil { - return - } - requestID := strings.TrimSpace(p.RequestID) - if requestID == "" { - return - } - r.mu.Lock() - if r.interactionRouteOpenLocked(s) { - r.permIndex[requestID] = s.runID - s.pendingIDs[requestID] = struct{}{} - } - r.mu.Unlock() - case proto.TypePermissionCancel: - if env.ID == "" { - return - } - r.mu.Lock() - delete(r.permIndex, env.ID) - delete(s.pendingIDs, env.ID) - r.mu.Unlock() - case proto.TypePromptForUserChoice: - // env.ID is the run id (so the server-side dispatch can fan - // this frame to the run's subscriber); the ask id rides on - // the payload. Decode just enough to seed the index. - var p proto.PromptForUserChoicePayload - if err := env.DecodePayload(&p); err != nil || p.AskID == "" { - return - } - r.mu.Lock() - if r.interactionRouteOpenLocked(s) { - r.askIndex[p.AskID] = s.runID - s.pendingAsks[p.AskID] = struct{}{} - } - r.mu.Unlock() - } -} - -func (r *Router) clearInteractionRoutesLocked(s *sessionState) { - for permissionID := range s.pendingIDs { - delete(r.permIndex, permissionID) - delete(s.pendingIDs, permissionID) - } - for askID := range s.pendingAsks { - delete(r.askIndex, askID) - delete(s.pendingAsks, askID) - } -} - -// drain consumes everything left on ch until the agent closes it. -// Events are dropped — by the time we're draining, either transport -// is dead or the router is shutting down. diff --git a/apps/daemon/internal/dispatch/optional_interactions_test.go b/apps/daemon/internal/dispatch/optional_interactions_test.go deleted file mode 100644 index 37d22eaae..000000000 --- a/apps/daemon/internal/dispatch/optional_interactions_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package dispatch_test - -import ( - "context" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" -) - -// Wrapping Session proves that no responder stubs are required for a Session. -type lifecycleOnly struct{ agent.Session } - -func TestOptionalInteractionResponders(t *testing.T) { - for _, ask := range []bool{false, true} { - t.Run(map[bool]string{false: "permission", true: "user choice"}[ask], func(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - var output chan<- proto.Envelope - registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "minimal", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, sessionExecutor(func(_ context.Context, _ string, _ proto.MessageInput, out chan<- proto.Envelope) (agent.Session, error) { - output = out - s := &fakeSession{out: out, closeOutOnCancel: true} - return lifecycleOnly{Session: s}, nil - })) - startRun(t, h.router, h.sender, "run", proto.PromptRequestPayload{AgentKind: "minimal"}) - event := mustEnv(t, proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{RequestID: "interaction", Tool: "fixture"}) - decision := mustEnv(t, proto.TypePermissionDecision, "interaction", proto.PermissionDecisionPayload{DeliveryID: "decision", Approved: true}) - if ask { - event = mustEnv(t, proto.TypePromptForUserChoice, "run", proto.PromptForUserChoicePayload{AskID: "interaction"}) - decision = mustEnv(t, proto.TypePromptForUserChoiceDecision, "interaction", proto.PromptForUserChoiceDecisionPayload{DeliveryID: "decision"}) - } - // An inconsistent adapter emitted an interaction it cannot answer: reject it, - // never acknowledge application or call a fabricated responder. - output <- event - waitFor(t, func() bool { return hasFrame(h.sender, event.Type, "run") }, "interaction indexed") - if err := h.router.Handle(t.Context(), decision); err != nil { - t.Fatal(err) - } - assertDecisionAck(t, h.sender, "decision", false, "unsupported") - }) - } -} diff --git a/apps/daemon/internal/dispatch/preparation_cancel_test.go b/apps/daemon/internal/dispatch/preparation_cancel_test.go index b21a5c21c..0a5f96c96 100644 --- a/apps/daemon/internal/dispatch/preparation_cancel_test.go +++ b/apps/daemon/internal/dispatch/preparation_cancel_test.go @@ -100,8 +100,6 @@ func TestPreparedCancellationWaitsForOutputAndCleanup(t *testing.T) { session = &fakeSession{out: out, closeOutOnCancel: true, postCancelEnvelopes: []proto.Envelope{mustEnv(t, proto.TypeDone, id, p.outcome)}} out <- mustEnv(t, proto.TypeDelta, id, proto.DeltaPayload{Delta: "observed"}) - out <- mustEnv(t, proto.TypePermissionRequest, id, proto.PermissionRequestPayload{RequestID: "permission"}) - out <- mustEnv(t, proto.TypePromptForUserChoice, id, proto.PromptForUserChoicePayload{AskID: "ask"}) out <- mustEnv(t, proto.TypeUsage, id, proto.UsagePayload{Usage: p.outcome.Usage}) close(startEntered) <-startReturn @@ -138,16 +136,6 @@ func TestPreparedCancellationWaitsForOutputAndCleanup(t *testing.T) { if len(cancellationAcks(sender.recSender)) != 0 || r.ActiveRuns() != 1 { t.Fatal("cleanup released ownership early") } - for _, decision := range []proto.Envelope{ - mustEnv(t, proto.TypePermissionDecision, "permission", proto.PermissionDecisionPayload{DeliveryID: "permission-reply", Approved: true}), - mustEnv(t, proto.TypePromptForUserChoiceDecision, "ask", proto.PromptForUserChoiceDecisionPayload{DeliveryID: "ask-reply", QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}}), - } { - if err := r.Handle(t.Context(), decision); err != nil { - t.Fatal(err) - } - } - assertDecisionAck(t, sender.recSender, "permission-reply", false, "not_pending") - assertDecisionAck(t, sender.recSender, "ask-reply", false, "not_pending") close(cleanupReturn) waitFor(t, func() bool { return len(cancellationAcks(sender.recSender)) == 2 && r.ActiveRuns() == 0 }, "prepared cancellation receipts") if p.calls.Load() != 1 || session.cancels() != 1 { @@ -159,7 +147,7 @@ func TestPreparedCancellationWaitsForOutputAndCleanup(t *testing.T) { } } got := sender.typesFor("run") - want := []string{proto.TypeDelta, proto.TypePermissionRequest, proto.TypePromptForUserChoice, proto.TypeUsage, proto.TypeDone, proto.TypeInteractionDecisionAck, proto.TypeInteractionDecisionAck} + want := []string{proto.TypeDelta, proto.TypeUsage, proto.TypeDone, proto.TypeInteractionDecisionAck, proto.TypeInteractionDecisionAck} if !reflect.DeepEqual(got, want) { t.Fatalf("output/receipt order = %v", got) } diff --git a/apps/daemon/internal/dispatch/preparation_executor_fixture_test.go b/apps/daemon/internal/dispatch/preparation_executor_fixture_test.go index a21823c6a..9da6a2d2e 100644 --- a/apps/daemon/internal/dispatch/preparation_executor_fixture_test.go +++ b/apps/daemon/internal/dispatch/preparation_executor_fixture_test.go @@ -113,18 +113,6 @@ func (t *preparationTurn) SteerWithReceipt(ctx context.Context, p proto.PromptSt } return agent.ErrSteeringInactive } -func (t *preparationTurn) SubmitPermission(ctx context.Context, id string, p proto.PermissionDecisionPayload) error { - if target, ok := t.Session.(agent.PermissionResponder); ok { - return target.SubmitPermission(ctx, id, p) - } - return agent.ErrUnknownPermission -} -func (t *preparationTurn) SubmitPromptForUserChoice(ctx context.Context, id string, p proto.PromptForUserChoiceDecisionPayload) error { - if target, ok := t.Session.(agent.UserChoiceResponder); ok { - return target.SubmitPromptForUserChoice(ctx, id, p) - } - return agent.ErrUnknownAsk -} func (t *preparationTurn) ReadWorkspaceFile(ctx context.Context, path string, limit int) (agent.WorkspaceReadResult, error) { if target, ok := t.Session.(agent.WorkspaceReader); ok { return target.ReadWorkspaceFile(ctx, path, limit) diff --git a/apps/daemon/internal/dispatch/preparation_start.go b/apps/daemon/internal/dispatch/preparation_start.go index af330553a..5b96f6369 100644 --- a/apps/daemon/internal/dispatch/preparation_start.go +++ b/apps/daemon/internal/dispatch/preparation_start.go @@ -70,7 +70,7 @@ func (r *Router) handleExecutionStart(_ context.Context, env proto.Envelope) err } p.status.State, p.status.RunID, p.status.Revision = "starting", input.RunID, p.status.Revision+1 p.startFingerprint, p.busy = fingerprint, true - state := &sessionState{capabilities: p.capabilities, runID: input.RunID, environmentID: p.environmentID, out: make(chan proto.Envelope, 64), ctx: p.ctx, pendingIDs: make(map[string]struct{}), pendingAsks: make(map[string]struct{}), traceparent: env.Trace} + state := &sessionState{capabilities: p.capabilities, runID: input.RunID, environmentID: p.environmentID, out: make(chan proto.Envelope, 64), ctx: p.ctx, traceparent: env.Trace} state.preparedHandoff = newPreparedHandoff(p, owner.native) p.handoff, owner.run = state.preparedHandoff, state r.sessions[input.RunID] = state diff --git a/apps/daemon/internal/dispatch/preparation_test.go b/apps/daemon/internal/dispatch/preparation_test.go index b01dbcca6..427470709 100644 --- a/apps/daemon/internal/dispatch/preparation_test.go +++ b/apps/daemon/internal/dispatch/preparation_test.go @@ -119,7 +119,7 @@ func preparationRequest() proto.ExecutionPreparePayload { func preparationRouter(t *testing.T, sender dispatch.Sender, timeout time.Duration, factory agent.PreparationFactory) *dispatch.Router { t.Helper() reg := agent.NewRegistry() - reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, Permissions: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}) + reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, Steering: proto.CapabilitySupported, DurableInputReceipts: proto.CapabilitySupported})}, harnessconfig.Configuration{}) reg.RegisterPreparation("prepared", true, factory) reg.RegisterExecutor("prepared", preparationExecutorFixture(factory)) r, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender, PreparationTimeout: timeout, LocalWorkspace: preparationWorkspace(t)}) diff --git a/apps/daemon/internal/dispatch/prepared_handoff.go b/apps/daemon/internal/dispatch/prepared_handoff.go index 31e55a248..382db5ab5 100644 --- a/apps/daemon/internal/dispatch/prepared_handoff.go +++ b/apps/daemon/internal/dispatch/prepared_handoff.go @@ -104,7 +104,6 @@ func (r *Router) claimPreparedReleaseLocked(state *sessionState, abort bool, fai handoff.release = release state.steeringClosed = true state.session = nil - r.clearInteractionRoutesLocked(state) } if abort && !release.aborted() { close(release.abort) @@ -328,10 +327,6 @@ func (r *Router) forwardPreparedOutput(state *sessionState) { if drainOnly { continue } - switch env.Type { - case proto.TypePermissionRequest, proto.TypePermissionCancel, proto.TypePromptForUserChoice: - r.indexPermissionFrame(state, env) - } if err := r.sendSessionOutput(pumpCtx, state, env); err != nil { r.mu.Lock() if r.closed { @@ -407,16 +402,6 @@ func (r *Router) forwardPreparedTerminal(state *sessionState, failure string, te return r.sendSessionOutput(pumpCtx, state, done) } -func (r *Router) awaitPreparedRelease(ctx context.Context, handoff *preparedHandoff, release *preparedRelease, attempt *preparedReleaseAttempt) error { - if err := r.awaitPreparedNativeRelease(ctx, release, attempt); err != nil { - return err - } - r.mu.Lock() - err := handoff.outputErr - r.mu.Unlock() - return err -} - func (r *Router) awaitPreparedNativeRelease(ctx context.Context, release *preparedRelease, attempt *preparedReleaseAttempt) error { select { case <-attempt.done: diff --git a/apps/daemon/internal/dispatch/prepared_handoff_mutation_test.go b/apps/daemon/internal/dispatch/prepared_handoff_mutation_test.go index 095ec5642..b00b647e9 100644 --- a/apps/daemon/internal/dispatch/prepared_handoff_mutation_test.go +++ b/apps/daemon/internal/dispatch/prepared_handoff_mutation_test.go @@ -77,7 +77,7 @@ func (s *blockingPreparedReceiptSender) matches(env proto.Envelope) bool { } func TestPreparedHandoffReleaseWaitsForMutationReceipt(t *testing.T) { - for _, operation := range []string{"function", "permission", "choice", "steering"} { + for _, operation := range []string{"function", "steering"} { t.Run(operation, func(t *testing.T) { sender := &blockingPreparedReceiptSender{ recSender: &recSender{}, deliveryID: operation + "-delivery", inputID: operation + "-input", @@ -97,14 +97,6 @@ func TestPreparedHandoffReleaseWaitsForMutationReceipt(t *testing.T) { switch operation { case "function": mutation = mustEnv(t, proto.TypeFunctionResult, "run", proto.FunctionResultPayload{CallID: "call", Success: true, Content: functionResultContent("answer"), DeliveryID: sender.deliveryID}) - case "permission": - session.out <- mustEnv(t, proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{RequestID: "permission"}) - waitFor(t, func() bool { return hasFrame(sender.recSender, proto.TypePermissionRequest, "run") }, "permission request") - mutation = mustEnv(t, proto.TypePermissionDecision, "permission", proto.PermissionDecisionPayload{DeliveryID: sender.deliveryID, Approved: true}) - case "choice": - session.out <- mustEnv(t, proto.TypePromptForUserChoice, "run", proto.PromptForUserChoicePayload{AskID: "ask"}) - waitFor(t, func() bool { return hasFrame(sender.recSender, proto.TypePromptForUserChoice, "run") }, "choice request") - mutation = mustEnv(t, proto.TypePromptForUserChoiceDecision, "ask", proto.PromptForUserChoiceDecisionPayload{DeliveryID: sender.deliveryID, QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}}) case "steering": mutation = mustEnv(t, proto.TypePromptSteer, "run", proto.PromptSteerPayload{InputID: sender.inputID, Input: proto.TextInput("continue")}) } @@ -125,28 +117,6 @@ func TestPreparedHandoffReleaseWaitsForMutationReceipt(t *testing.T) { t.Fatal(err) } assertDecisionAck(t, sender.recSender, "late-function", false, "not_ready") - case "permission": - late := mustEnv(t, proto.TypePermissionDecision, "permission", proto.PermissionDecisionPayload{DeliveryID: "late-permission", Approved: true}) - if err := r.Handle(t.Context(), late); err != nil { - t.Fatal(err) - } - assertDecisionAck(t, sender.recSender, "late-permission", true, "") - newRequest := mustEnv(t, proto.TypePermissionDecision, "new-permission", proto.PermissionDecisionPayload{DeliveryID: "new-permission", Approved: true}) - if err := r.Handle(t.Context(), newRequest); err != nil { - t.Fatal(err) - } - assertDecisionAck(t, sender.recSender, "new-permission", false, "not_pending") - case "choice": - late := mustEnv(t, proto.TypePromptForUserChoiceDecision, "ask", proto.PromptForUserChoiceDecisionPayload{DeliveryID: "late-choice", QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}}) - if err := r.Handle(t.Context(), late); err != nil { - t.Fatal(err) - } - assertDecisionAck(t, sender.recSender, "late-choice", true, "") - newRequest := mustEnv(t, proto.TypePromptForUserChoiceDecision, "new-choice", proto.PromptForUserChoiceDecisionPayload{DeliveryID: "new-choice", QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}}) - if err := r.Handle(t.Context(), newRequest); err != nil { - t.Fatal(err) - } - assertDecisionAck(t, sender.recSender, "new-choice", false, "not_pending") case "steering": late := mustEnv(t, proto.TypePromptSteer, "run", proto.PromptSteerPayload{InputID: "late-steering", Input: proto.TextInput("late")}) if err := r.Handle(t.Context(), late); err != nil { @@ -187,17 +157,6 @@ func TestPreparedHandoffReleaseWaitsForMutationReceipt(t *testing.T) { if session.functions.Load() != 1 { t.Fatalf("function native calls = %d, want 1", session.functions.Load()) } - case "permission": - if len(session.submissions()) != 1 { - t.Fatalf("permission native calls = %d, want 1", len(session.submissions())) - } - case "choice": - session.askMu.Lock() - calls := len(session.askCalls) - session.askMu.Unlock() - if calls != 1 { - t.Fatalf("choice native calls = %d, want 1", calls) - } case "steering": if session.steers.Load() != 1 { t.Fatalf("steering native calls = %d, want 1", session.steers.Load()) @@ -234,7 +193,7 @@ func assertReceiptBeforeDone(t *testing.T, sender *recSender, operation string) } func TestPreparedHandoffRouterShutdownWaitsForReceiptAttempt(t *testing.T) { - for _, operation := range []string{"function", "permission", "choice", "steering"} { + for _, operation := range []string{"function", "steering"} { t.Run(operation, func(t *testing.T) { sender := &blockingPreparedReceiptSender{recSender: &recSender{}, deliveryID: operation + "-delivery", inputID: operation + "-input", entered: make(chan struct{}), release: make(chan struct{}), exited: make(chan struct{})} defer close(sender.release) @@ -252,14 +211,6 @@ func TestPreparedHandoffRouterShutdownWaitsForReceiptAttempt(t *testing.T) { switch operation { case "function": mutation = mustEnv(t, proto.TypeFunctionResult, "run", proto.FunctionResultPayload{CallID: "call", Success: true, Content: functionResultContent("answer"), DeliveryID: sender.deliveryID}) - case "permission": - session.out <- mustEnv(t, proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{RequestID: "permission"}) - waitFor(t, func() bool { return hasFrame(sender.recSender, proto.TypePermissionRequest, "run") }, "permission request") - mutation = mustEnv(t, proto.TypePermissionDecision, "permission", proto.PermissionDecisionPayload{DeliveryID: sender.deliveryID, Approved: true}) - case "choice": - session.out <- mustEnv(t, proto.TypePromptForUserChoice, "run", proto.PromptForUserChoicePayload{AskID: "ask"}) - waitFor(t, func() bool { return hasFrame(sender.recSender, proto.TypePromptForUserChoice, "run") }, "choice request") - mutation = mustEnv(t, proto.TypePromptForUserChoiceDecision, "ask", proto.PromptForUserChoiceDecisionPayload{DeliveryID: sender.deliveryID, QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}}) case "steering": mutation = mustEnv(t, proto.TypePromptSteer, "run", proto.PromptSteerPayload{InputID: sender.inputID, Input: proto.TextInput("continue")}) } diff --git a/apps/daemon/internal/dispatch/prepared_handoff_test.go b/apps/daemon/internal/dispatch/prepared_handoff_test.go index f70e4db7b..37c77c149 100644 --- a/apps/daemon/internal/dispatch/prepared_handoff_test.go +++ b/apps/daemon/internal/dispatch/prepared_handoff_test.go @@ -161,8 +161,6 @@ func TestPreparedHandoffDuplicateStartDoesNotReexecuteDuringPublication(t *testi p := &controlledPreparation{closed: make(chan struct{})} p.start = func(_ context.Context, _ string, _ proto.MessageInput, out chan<- proto.Envelope) (agent.Session, error) { session.out = out - out <- mustEnv(t, proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{RequestID: "publication-permission"}) - out <- mustEnv(t, proto.TypePromptForUserChoice, "run", proto.PromptForUserChoicePayload{AskID: "publication-choice"}) return session, nil } r := preparationRouter(t, sender, time.Minute, func(context.Context, proto.PromptRequestPayload) (agent.Prepared, error) { return p, nil }) @@ -172,21 +170,11 @@ func TestPreparedHandoffDuplicateStartDoesNotReexecuteDuringPublication(t *testi case <-time.After(2 * time.Second): t.Fatal("started status did not reach publication boundary") } - waitFor(t, func() bool { - return hasFrame(sender.recSender, proto.TypePermissionRequest, "run") && hasFrame(sender.recSender, proto.TypePromptForUserChoice, "run") - }, "pre-publication interaction routes") - for _, mutation := range []proto.Envelope{ - mustEnv(t, proto.TypeFunctionResult, "run", proto.FunctionResultPayload{CallID: "call", Success: true, Content: functionResultContent("answer"), DeliveryID: "publication-function"}), - mustEnv(t, proto.TypePermissionDecision, "publication-permission", proto.PermissionDecisionPayload{DeliveryID: "publication-permission", Approved: true}), - mustEnv(t, proto.TypePromptForUserChoiceDecision, "publication-choice", proto.PromptForUserChoiceDecisionPayload{DeliveryID: "publication-choice", QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}}), - } { - if err := r.Handle(t.Context(), mutation); err != nil { - t.Fatal(err) - } + result := mustEnv(t, proto.TypeFunctionResult, "run", proto.FunctionResultPayload{CallID: "call", Success: true, Content: functionResultContent("answer"), DeliveryID: "publication-function"}) + if err := r.Handle(t.Context(), result); err != nil { + t.Fatal(err) } assertDecisionAck(t, sender.recSender, "publication-function", false, "not_ready") - assertDecisionAck(t, sender.recSender, "publication-permission", false, "not_ready") - assertDecisionAck(t, sender.recSender, "publication-choice", false, "not_ready") if err := r.Handle(t.Context(), mustEnv(t, proto.TypePromptSteer, "run", proto.PromptSteerPayload{InputID: "publication-steering", Input: proto.TextInput("continue")})); err != nil { t.Fatal(err) } @@ -202,10 +190,7 @@ func TestPreparedHandoffDuplicateStartDoesNotReexecuteDuringPublication(t *testi if len(frames) == 0 || frames[len(frames)-1].Type != proto.TypeWorkspaceReadResult || frames[len(frames)-1].DecodePayload(&readResult) != nil || readResult.ErrorCode != "resource_unavailable" { t.Fatalf("pre-publication workspace read = %+v", frames) } - session.askMu.Lock() - askCalls := len(session.askCalls) - session.askMu.Unlock() - if session.functions.Load() != 0 || session.steers.Load() != 0 || session.reads.Load() != 0 || len(session.submissions()) != 0 || askCalls != 0 { + if session.functions.Load() != 0 || session.steers.Load() != 0 || session.reads.Load() != 0 { t.Fatal("private Session accepted work before started publication") } duplicate := mustEnv(t, proto.TypeExecutionStart, "request", proto.ExecutionStartPayload{Handle: ready.Handle, ExecutorID: ready.ExecutorID, RunID: "run", Input: proto.TextInput("input")}) diff --git a/apps/daemon/internal/dispatch/router.go b/apps/daemon/internal/dispatch/router.go index ae283d321..5df1f4b32 100644 --- a/apps/daemon/internal/dispatch/router.go +++ b/apps/daemon/internal/dispatch/router.go @@ -1,8 +1,6 @@ // Package dispatch wires inbound WebSocket frames to the agent layer. -// It owns one Turn per active RunID, a per-Turn output goroutine that -// forwards the agent's events to the transport, and a -// permission_id → run_id index so permission_decision frames route -// back to the right session. +// It owns one Turn per active RunID and a per-Turn output goroutine that +// forwards the agent's events to the transport. // // Concurrency: Handle is safe for one goroutine (typically the read // loop). Each session runs its own goroutine. Internal state is @@ -37,10 +35,8 @@ type Router struct { admission sync.RWMutex suspension *proto.EnvironmentSuspendPayload mu sync.Mutex - sessions map[string]*sessionState // RunID → state - permIndex map[string]string // permID → RunID - askIndex map[string]string // askID → RunID - applied map[string]appliedInteractionDecision + sessions map[string]*sessionState // RunID → state + applied map[string]appliedFunctionResult // RunID and call ID → applied result shutdownAttempt *shutdownAttempt shutdownCh chan struct{} // closed by Shutdown shutdownWG dispatchWork // waits for all pump goroutines @@ -57,9 +53,7 @@ type Router struct { localWorkspace *localworkspace.Binding } -type appliedInteractionDecision struct { - requestID string - kind string +type appliedFunctionResult struct { fingerprint [32]byte recordedAt time.Time } @@ -76,8 +70,6 @@ type sessionState struct { session agent.Turn out chan proto.Envelope ctx context.Context - pendingIDs map[string]struct{} - pendingAsks map[string]struct{} traceparent string steering map[string]steeringReceipt steerBusy bool @@ -124,9 +116,7 @@ func New(cfg Config) (*Router, error) { sender: cfg.Sender, log: log, sessions: make(map[string]*sessionState), - permIndex: make(map[string]string), - askIndex: make(map[string]string), - applied: make(map[string]appliedInteractionDecision), + applied: make(map[string]appliedFunctionResult), shutdownCh: make(chan struct{}), idleTimeout: idleTimeout, executors: make(map[string]*executorState), @@ -151,7 +141,7 @@ func (r *Router) Handle(ctx context.Context, env proto.Envelope) error { r.mu.Unlock() return ErrRouterClosed } - if r.suspension != nil && env.Type != proto.TypeDeviceShutdown { + if r.suspension != nil { r.mu.Unlock() return ErrRouterQuiesced } @@ -180,12 +170,6 @@ func (r *Router) Handle(ctx context.Context, env proto.Envelope) error { return r.handleFunctionResult(ctx, env) case proto.TypePromptSteer: return r.handlePromptSteer(ctx, env) - case proto.TypePermissionDecision: - return r.handlePermissionDecision(ctx, env) - case proto.TypePromptForUserChoiceDecision: - return r.handlePromptForUserChoiceDecision(ctx, env) - case proto.TypeDeviceShutdown: - return r.handleDeviceShutdown(ctx, env) default: // Unknown types are logged and dropped — keeps the daemon // forward-compatible with server-side additions. @@ -231,11 +215,5 @@ func (r *Router) drain(ch <-chan proto.Envelope) { func (r *Router) cleanupSession(s *sessionState) { r.mu.Lock() delete(r.sessions, s.runID) - for permID := range s.pendingIDs { - delete(r.permIndex, permID) - } - for askID := range s.pendingAsks { - delete(r.askIndex, askID) - } r.mu.Unlock() } diff --git a/apps/daemon/internal/dispatch/router_test.go b/apps/daemon/internal/dispatch/router_test.go index 918f6a06c..ed2d90368 100644 --- a/apps/daemon/internal/dispatch/router_test.go +++ b/apps/daemon/internal/dispatch/router_test.go @@ -61,12 +61,7 @@ type fakeSession struct { runID string input proto.MessageInput cancelCalls int - submitCalls []permCall - askCalls []askCall - submitErr error - askErr error - cancelMu, submitMu sync.Mutex - askMu sync.Mutex + cancelMu sync.Mutex closeOutOnCancel bool out chan<- proto.Envelope closeOutOnCancelMu sync.Once @@ -74,16 +69,6 @@ type fakeSession struct { ctx context.Context } -type permCall struct { - id string - decision proto.PermissionDecisionPayload -} - -type askCall struct { - id string - decision proto.PromptForUserChoiceDecisionPayload -} - func (s *fakeSession) CancellationOutcome() proto.DonePayload { return proto.DonePayload{} } func (s *fakeSession) Cancel(context.Context) error { @@ -101,34 +86,12 @@ func (s *fakeSession) Cancel(context.Context) error { return nil } -func (s *fakeSession) SubmitPermission(_ context.Context, permID string, dec proto.PermissionDecisionPayload) error { - s.submitMu.Lock() - s.submitCalls = append(s.submitCalls, permCall{id: permID, decision: dec}) - s.submitMu.Unlock() - return s.submitErr -} - -func (s *fakeSession) SubmitPromptForUserChoice(_ context.Context, askID string, dec proto.PromptForUserChoiceDecisionPayload) error { - s.askMu.Lock() - s.askCalls = append(s.askCalls, askCall{id: askID, decision: dec}) - s.askMu.Unlock() - return s.askErr -} - func (s *fakeSession) cancels() int { s.cancelMu.Lock() defer s.cancelMu.Unlock() return s.cancelCalls } -func (s *fakeSession) submissions() []permCall { - s.submitMu.Lock() - defer s.submitMu.Unlock() - out := make([]permCall, len(s.submitCalls)) - copy(out, s.submitCalls) - return out -} - // --------------------------------------------------------------------- // helpers // --------------------------------------------------------------------- @@ -149,7 +112,7 @@ func newHarness(t *testing.T) *harness { reg: agent.NewRegistry(), gotSess: make(chan *fakeSession, 16), } - registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported, Permissions: proto.CapabilitySupported})}, sessionExecutor(func(_ context.Context, runID string, input proto.MessageInput, out chan<- proto.Envelope) (agent.Session, error) { + registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, sessionExecutor(func(_ context.Context, runID string, input proto.MessageInput, out chan<- proto.Envelope) (agent.Session, error) { sess := &fakeSession{runID: runID, input: input, out: out} h.gotSess <- sess return sess, nil @@ -292,192 +255,6 @@ func TestHandlePromptCancelUnknownRunIsNoop(t *testing.T) { } } -func TestPermissionRequestIsIndexedAndDecisionRoutes(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - - startRun(t, h.router, h.sender, "run_p", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) - sess := <-h.gotSess - - // Session emits a permission_request; pump should index it. - permEnv := mustEnv(t, proto.TypePermissionRequest, "run_p", proto.PermissionRequestPayload{ - RequestID: "perm_abcd1234", Tool: "Bash", Title: "rm -rf /", - }) - sess.out <- permEnv - // Wait until sender records — indexing happens before send. - waitFor(t, func() bool { return hasFrame(h.sender, proto.TypePermissionRequest, "run_p") }, "permission_request to be forwarded") - - dec := mustEnv(t, proto.TypePermissionDecision, "perm_abcd1234", proto.PermissionDecisionPayload{DeliveryID: "delivery-perm-1", Approved: true}) - if err := h.router.Handle(context.Background(), dec); err != nil { - t.Fatalf("permission_decision: %v", err) - } - calls := sess.submissions() - if len(calls) != 1 || calls[0].id != "perm_abcd1234" || !calls[0].decision.Approved { - t.Errorf("submissions = %+v, want one approved perm_abcd1234", calls) - } - assertDecisionAck(t, h.sender, "delivery-perm-1", true, "") - retry := mustEnv(t, proto.TypePermissionDecision, "perm_abcd1234", proto.PermissionDecisionPayload{DeliveryID: "delivery-perm-2", Approved: true}) - if err := h.router.Handle(context.Background(), retry); err != nil { - t.Fatalf("idempotent permission replay: %v", err) - } - if calls := sess.submissions(); len(calls) != 1 { - t.Fatalf("idempotent replay reached agent twice: %+v", calls) - } - assertDecisionAck(t, h.sender, "delivery-perm-2", true, "") - conflict := mustEnv(t, proto.TypePermissionDecision, "perm_abcd1234", proto.PermissionDecisionPayload{DeliveryID: "delivery-perm-3", Approved: false}) - if err := h.router.Handle(context.Background(), conflict); err != nil { - t.Fatalf("conflicting permission replay: %v", err) - } - assertDecisionAck(t, h.sender, "delivery-perm-3", false, "decision_conflict") - - close(sess.out) -} - -func TestPermissionDecisionUnknownPermIsNoop(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - - if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePermissionDecision, "perm_unknown", proto.PermissionDecisionPayload{DeliveryID: "delivery-unknown-perm"})); err != nil { - t.Errorf("decision for unknown perm = %v, want nil", err) - } - assertDecisionAck(t, h.sender, "delivery-unknown-perm", false, "not_pending") -} - -func TestPromptForUserChoiceDecisionRoutesToSession(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - - startRun(t, h.router, h.sender, "run_ask", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) - sess := <-h.gotSess - - // Envelope.ID is the run id (server-side dispatch fans on it); the - // ask id rides on the payload. Daemon's indexPermissionFrame reads - // payload.AskID to seed askIndex. - askEnv := mustEnv(t, proto.TypePromptForUserChoice, "run_ask", proto.PromptForUserChoicePayload{ - AskID: "ask_abcd1234", - Questions: []proto.PromptForUserChoiceQuestion{{Question: "?", Options: []proto.PromptForUserChoiceOption{{Label: "yes"}, {Label: "no"}}}}, - ToolUseID: "toolu_42", - }) - sess.out <- askEnv - waitFor(t, func() bool { return hasFrame(h.sender, proto.TypePromptForUserChoice, "run_ask") }, "prompt_for_user_choice forwarded") - - dec := mustEnv(t, proto.TypePromptForUserChoiceDecision, "ask_abcd1234", proto.PromptForUserChoiceDecisionPayload{ - DeliveryID: "delivery-ask-1", QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}, - }) - if err := h.router.Handle(context.Background(), dec); err != nil { - t.Fatalf("prompt_for_user_choice_decision: %v", err) - } - - sess.askMu.Lock() - calls := append([]askCall(nil), sess.askCalls...) - sess.askMu.Unlock() - if len(calls) != 1 || calls[0].id != "ask_abcd1234" { - t.Fatalf("askCalls = %+v, want one ask_abcd1234", calls) - } - if len(calls[0].decision.QuestionAnswers[0].Answers) != 1 || calls[0].decision.QuestionAnswers[0].Answers[0] != "yes" { - t.Errorf("answer payload mismatch: %+v", calls[0].decision) - } - assertDecisionAck(t, h.sender, "delivery-ask-1", true, "") - - // Cleanup contract: a successful decision drops the ask from both - // the router-level index and the session's pendingAsks set, so a - // stale retry short-circuits as "run gone". - if got := h.router.AskIndexLenForTest(); got != 0 { - t.Errorf("askIndex len = %d, want 0 after decision", got) - } - if got := h.router.PendingAsksLenForTest("run_ask"); got != 0 { - t.Errorf("pendingAsks len = %d, want 0 after decision", got) - } - - close(sess.out) -} - -// TestPromptForUserChoiceDecisionClearsIndexOnAgentUnknown locks in the -// other cleanup branch: when the session returns ErrUnknownAsk (timer -// already consumed the entry), the router still drops the index so a -// retry doesn't loop into Submit again. -func TestPromptForUserChoiceDecisionClearsIndexOnAgentUnknown(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - - startRun(t, h.router, h.sender, "run_ask_u", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) - sess := <-h.gotSess - sess.askErr = agent.ErrUnknownAsk - - askEnv := mustEnv(t, proto.TypePromptForUserChoice, "run_ask_u", proto.PromptForUserChoicePayload{ - AskID: "ask_xxxxxxxx", - Questions: []proto.PromptForUserChoiceQuestion{{Question: "?", Options: []proto.PromptForUserChoiceOption{{Label: "yes"}}}}, - ToolUseID: "toolu_y", - }) - sess.out <- askEnv - waitFor(t, func() bool { return hasFrame(h.sender, proto.TypePromptForUserChoice, "run_ask_u") }, "prompt_for_user_choice forwarded") - - dec := mustEnv(t, proto.TypePromptForUserChoiceDecision, "ask_xxxxxxxx", proto.PromptForUserChoiceDecisionPayload{ - DeliveryID: "delivery-ask-gone", QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}, - }) - if err := h.router.Handle(context.Background(), dec); err != nil { - t.Fatalf("prompt_for_user_choice_decision: %v", err) - } - - if got := h.router.AskIndexLenForTest(); got != 0 { - t.Errorf("askIndex len = %d, want 0 after ErrUnknownAsk", got) - } - if got := h.router.PendingAsksLenForTest("run_ask_u"); got != 0 { - t.Errorf("pendingAsks len = %d, want 0 after ErrUnknownAsk", got) - } - assertDecisionAck(t, h.sender, "delivery-ask-gone", false, "not_pending") - - close(sess.out) -} - -func TestPromptForUserChoiceDecisionKeepsIndexOnTransientAgentError(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - - startRun(t, h.router, h.sender, "run_ask_retry", proto.PromptRequestPayload{AgentKind: "fake_alpha"}) - sess := <-h.gotSess - sess.askErr = errors.New("temporary stdin failure") - - sess.out <- mustEnv(t, proto.TypePromptForUserChoice, "run_ask_retry", proto.PromptForUserChoicePayload{ - AskID: "ask_retry", Questions: []proto.PromptForUserChoiceQuestion{{ID: "q0", Question: "Retry?"}}, - }) - waitFor(t, func() bool { return hasFrame(h.sender, proto.TypePromptForUserChoice, "run_ask_retry") }, "prompt_for_user_choice forwarded") - - decision := mustEnv(t, proto.TypePromptForUserChoiceDecision, "ask_retry", proto.PromptForUserChoiceDecisionPayload{ - DeliveryID: "delivery-ask-retry", QuestionAnswers: []proto.PromptForUserChoiceQuestionAnswer{{QuestionID: "q0", Answers: []string{"yes"}}}, - }) - if err := h.router.Handle(context.Background(), decision); err != nil { - t.Fatalf("first decision: %v", err) - } - assertDecisionAck(t, h.sender, "delivery-ask-retry", false, "runtime_error") - if got := h.router.AskIndexLenForTest(); got != 1 { - t.Fatalf("askIndex len = %d, want 1 after transient error", got) - } - if got := h.router.PendingAsksLenForTest("run_ask_retry"); got != 1 { - t.Fatalf("pendingAsks len = %d, want 1 after transient error", got) - } - - sess.askErr = nil - if err := h.router.Handle(context.Background(), decision); err != nil { - t.Fatalf("retry decision: %v", err) - } - assertDecisionAck(t, h.sender, "delivery-ask-retry", true, "") - if got := h.router.AskIndexLenForTest(); got != 0 { - t.Fatalf("askIndex len = %d, want 0 after successful retry", got) - } - close(sess.out) -} - -func TestPromptForUserChoiceDecisionUnknownAskIsNoop(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - - if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypePromptForUserChoiceDecision, "ask_unknown", proto.PromptForUserChoiceDecisionPayload{DeliveryID: "delivery-unknown-ask"})); err != nil { - t.Errorf("decision for unknown ask = %v, want nil", err) - } - assertDecisionAck(t, h.sender, "delivery-unknown-ask", false, "not_pending") -} - func assertDecisionAck(t *testing.T, sender *recSender, deliveryID string, applied bool, errorCode string) { t.Helper() frames := sender.snapshot() @@ -499,27 +276,6 @@ func assertDecisionAck(t *testing.T, sender *recSender, deliveryID string, appli t.Fatalf("no decision ack for delivery %q in %+v", deliveryID, frames) } -func TestHandleDeviceShutdownCancelsAllSessions(t *testing.T) { - h := newHarness(t) - defer h.router.Shutdown(context.Background()) - - for _, rid := range []string{"r1", "r2", "r3"} { - startRun(t, h.router, h.sender, rid, proto.PromptRequestPayload{AgentKind: "fake_alpha"}) - } - sessions := make([]*fakeSession, 3) - for i := range sessions { - sessions[i] = <-h.gotSess - sessions[i].closeOutOnCancel = true - } - - if err := h.router.Handle(context.Background(), mustEnv(t, proto.TypeDeviceShutdown, "", proto.DeviceShutdownPayload{Reason: "reap"})); err != nil { - t.Fatalf("device_shutdown: %v", err) - } - for _, s := range sessions { - waitFor(t, func() bool { return s.cancels() == 1 }, "each session.Cancel to fire") - } -} - func TestHandleUnknownTypeIsNoop(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) diff --git a/apps/daemon/internal/dispatch/shutdown.go b/apps/daemon/internal/dispatch/shutdown.go index 5c350dccb..10ec3eb69 100644 --- a/apps/daemon/internal/dispatch/shutdown.go +++ b/apps/daemon/internal/dispatch/shutdown.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) type shutdownAttempt struct { @@ -116,33 +114,8 @@ func waitShutdown(ctx context.Context, attempt *shutdownAttempt) error { } } -func (r *Router) handleDeviceShutdown(ctx context.Context, env proto.Envelope) error { - var payload proto.DeviceShutdownPayload - _ = env.DecodePayload(&payload) // body optional - r.log.InfoContext(ctx, "device_shutdown received, cancelling runs", "reason", payload.Reason, "active_runs", r.ActiveRuns()) - r.mu.Lock() - if r.closed { - r.mu.Unlock() - return ErrRouterClosed - } - if r.runtimePreparation != nil { - r.runtimePreparation.cancel() - } - victims := r.sessionCancellationsLocked() - preparations := r.closePendingPreparationsLocked() - executors := r.closeIdleExecutorsLocked() - r.mu.Unlock() - r.closeIdleExecutors(executors) - for _, p := range preparations { - go func() { defer r.shutdownWG.Done(); r.closePreparationResource(p) }() - } - r.cancelSessions(ctx, victims) - return nil -} - type sessionCancellation struct { runID string - handoff *preparedHandoff release *preparedRelease attempt *preparedReleaseAttempt } @@ -153,15 +126,7 @@ func (r *Router) sessionCancellationsLocked() []sessionCancellation { victims := make([]sessionCancellation, 0, len(r.sessions)) for _, state := range r.sessions { release, attempt := r.claimPreparedReleaseLocked(state, true, "", true) - victims = append(victims, sessionCancellation{runID: state.runID, handoff: state.preparedHandoff, release: release, attempt: attempt}) + victims = append(victims, sessionCancellation{runID: state.runID, release: release, attempt: attempt}) } return victims } - -func (r *Router) cancelSessions(ctx context.Context, sessions []sessionCancellation) { - for _, session := range sessions { - if err := r.awaitPreparedRelease(ctx, session.handoff, session.release, session.attempt); err != nil { - r.log.Warn("prepared session release failed", "run_id", session.runID, "err", err) - } - } -} diff --git a/apps/daemon/internal/dispatch/suspend.go b/apps/daemon/internal/dispatch/suspend.go index a5e024746..97a8cc6fc 100644 --- a/apps/daemon/internal/dispatch/suspend.go +++ b/apps/daemon/internal/dispatch/suspend.go @@ -31,7 +31,7 @@ func (r *Router) Quiesce(ctx context.Context, request proto.EnvironmentSuspendPa r.mu.Unlock() return ErrRouterQuiesced } - if r.runtimePreparation != nil || len(r.sessions) != 0 || len(r.workspaceReads) != 0 || r.workspaceWrite != nil || r.workspaceExport != nil || len(r.permIndex) != 0 || len(r.askIndex) != 0 { + if r.runtimePreparation != nil || len(r.sessions) != 0 || len(r.workspaceReads) != 0 || r.workspaceWrite != nil || r.workspaceExport != nil { r.mu.Unlock() return ErrRouterBusy } diff --git a/apps/daemon/internal/dispatch/suspend_test.go b/apps/daemon/internal/dispatch/suspend_test.go index b713218ae..054a031ed 100644 --- a/apps/daemon/internal/dispatch/suspend_test.go +++ b/apps/daemon/internal/dispatch/suspend_test.go @@ -39,14 +39,12 @@ func suspensionRouter(t *testing.T, sender Sender) *Router { func TestQuiesceRejectsEveryUnsettledResource(t *testing.T) { cases := map[string]func(*Router){ - "active": func(r *Router) { r.sessions["run"] = &sessionState{} }, - "preparing": func(r *Router) { r.preparations["p"] = &preparationState{owns: true} }, - "receipt": func(r *Router) { r.preparations["p"] = &preparationState{busy: true} }, - "read": func(r *Router) { r.workspaceReads = map[string]struct{}{"read": {}} }, - "write": func(r *Router) { r.workspaceWrite = &workspaceUpload{} }, - "export": func(r *Router) { r.workspaceExport = &workspaceExport{} }, - "permission": func(r *Router) { r.permIndex["permission"] = "run" }, - "choice": func(r *Router) { r.askIndex["choice"] = "run" }, + "active": func(r *Router) { r.sessions["run"] = &sessionState{} }, + "preparing": func(r *Router) { r.preparations["p"] = &preparationState{owns: true} }, + "receipt": func(r *Router) { r.preparations["p"] = &preparationState{busy: true} }, + "read": func(r *Router) { r.workspaceReads = map[string]struct{}{"read": {}} }, + "write": func(r *Router) { r.workspaceWrite = &workspaceUpload{} }, + "export": func(r *Router) { r.workspaceExport = &workspaceExport{} }, } for name, setup := range cases { t.Run(name, func(t *testing.T) { diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index b8eddd89d..00be4d1c5 100644 --- a/contracts/agents-api/core-errors.md +++ b/contracts/agents-api/core-errors.md @@ -96,7 +96,7 @@ The [Session and Turn diagnostics reads](./session-diagnostics.md) return these | `execution_interrupted` | Core execution interrupted | | `delivery_unconfirmed` | `delivery_unknown`, `input_outcome_unknown`, `cancel_unconfirmed`, `cancel_outcome_unavailable`, `function_result_unconfirmed` | | `input_rejected` | `invalid_input`, `input_not_applied`, `message_input_unsupported`, and the exact steering outcomes `input_invalid_input`, `input_run_inactive`, `input_input_conflict`, `input_input_limit`, `input_unsupported`, `input_rejected`, `input_not_ready`, `input_busy` | -| `executor_protocol_error` | `invalid_executor_result`, `interaction_not_supported`, `execution_state_unavailable`, `execution_state_changed`, `function_call_invalid`, `function_result_invalid` | +| `executor_protocol_error` | `invalid_executor_result`, `execution_state_unavailable`, `execution_state_changed`, `function_call_invalid`, `function_result_invalid` | | `core_storage_failed` | `event_persistence_failed`, `artifact_capture_failed` | | `internal_error` | Unknown or malformed outcome; no raw value is returned | | `environment_connection_timeout` | Initial input connection deadline expired | diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 8ba69107e..52a68f354 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -69,7 +69,6 @@ For example, the Codex adapter keeps its app-server and thread, the Claude adapt | `DurableSteerer` | Real implementation on every Turn | Distinguish a complete write from the native application receipt; keep retry identity | | `Steerer` | Explicit implementation or Unsupported | Additional non-durable active-Turn input | | `FunctionResultSubmitter` | Explicit implementation or Unsupported | Match native call and result identity and acknowledge application | -| `PermissionResponder`, `UserChoiceResponder` | Explicit implementation or Unsupported | Respond to exact emitted identities; unknown or expired interactions stay distinct from Unsupported | | `WorkspaceReader`, `WorkspaceDirectoryLister`, `WorkspaceWriter` | Explicit on Turn, Executor and Prepared owners | Use the authorized workspace, confirm access, commit or close, or return the operation's Unsupported error | | `Prepared`, `PreparedCancellation` | Real implementation for an executable preparation | Keep resource and output ownership across Start, cancellation and unused cleanup | | Neutral messages, images, MCP, structured output and Subagent observations | Explicit capability decisions | Keep each operation's protocol semantics; reject unsupported input before submission | @@ -84,7 +83,7 @@ func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayl } ``` -The reason is a fixed safe string, never submitted content, a credential or raw native diagnostics. Unsupported guarantees no native side effect and is not a successful empty operation. Installation unavailability, unknown interaction IDs, native failures and uncertain outcomes keep their own errors and ownership. A nil `Turn` still means that no input was submitted and the output stays with the caller; never use it as an Unsupported marker. +The reason is a fixed safe string, never submitted content, a credential or raw native diagnostics. Unsupported guarantees no native side effect and is not a successful empty operation. Installation unavailability, unknown call IDs, native failures and uncertain outcomes keep their own errors and ownership. A nil `Turn` still means that no input was submitted and the output stays with the caller; never use it as an Unsupported marker. The wire request carries no working directory. The Runtime checks `local_environment.workspace_directory` against its binding and gives the Harness its bound workspace directory in `LocalEnvironment.WorkspaceRoot`; run the native Harness there. @@ -106,11 +105,11 @@ A Session owns one reusable Executor in its connected Runtime; a Turn owns one i **Binding.** The Runtime binds its Executor record to the Session, Environment, connection and immutable execution configuration. Resume identity and prior-Turn recovery flags are continuity assertions, not configuration changes. A supplied native identity must match the retained owner, and when existing history is required, recovery never starts a new root. A configuration conflict is an error, not a hot switch. A lost connection retires its owners and handles; old timers, output and cancellation cannot affect their replacements. -**Per-Turn state.** Each Turn gets a fresh wrapper, output channel and receipt state. Steering, function, permission and user-choice interfaces belong to that Turn. Native callbacks capture the originating Turn before asynchronous work, so a late event is never attributed to whichever Turn is active. Native processes, query or transport connections, fixed capability configuration and native session identity belong to the Executor. Do not reset completed `sync.Once` values or reuse an old Turn object. +**Per-Turn state.** Each Turn gets a fresh wrapper, output channel and receipt state. Steering and function interfaces belong to that Turn. Native callbacks capture the originating Turn before asynchronous work, so a late event is never attributed to whichever Turn is active. Native processes, query or transport connections, fixed capability configuration and native session identity belong to the Executor. Do not reset completed `sync.Once` values or reuse an old Turn object. **Start.** A nil Turn from `StartTurn` guarantees that no native input was submitted and the output channel was not retained; the Runtime then closes the channel. Once input may have been submitted, return a non-nil Turn even with an error: that Turn owns exactly-once output closure and stays tracked until settlement. Unknown input is never replayed. A definite `executor_unavailable` Start rejection allows one common recovery attempt, only after the previous Executor has been closed and no input was submitted; the Runtime rechecks the same physical peer and the current authorization. -**Cancellation and settlement.** `Turn.Cancel` targets only that Turn and does not close a healthy Executor. `AwaitSettlement` applies after both natural completion and cancellation. Success means output can no longer be written and the Turn's native events, input, functions, interactions and child work have settled. Native completion or cancellation confirmation is independent of resource retirement: closing a transport cannot supply a missing native terminal or operation receipt. +**Cancellation and settlement.** `Turn.Cancel` targets only that Turn and does not close a healthy Executor. `AwaitSettlement` applies after both natural completion and cancellation. Success means output can no longer be written and the Turn's native events, input, functions and child work have settled. Native completion or cancellation confirmation is independent of resource retirement: closing a transport cannot supply a missing native terminal or operation receipt. - `Reusable=true` also confirms that the native owner can accept the next Turn. `Reusable=false` requires a reason and a later confirmed Executor close. - An error means settlement is unconfirmed and frees neither ownership nor capacity. Caller deadlines stop the wait, not the tracked cleanup. Retry the same cleanup target serially; a failed cleanup blocks replacement and keeps its resource slot. @@ -119,13 +118,13 @@ A Session owns one reusable Executor in its connected Runtime; a Turn owns one i - Every `Session` declares `CancellationOutcome`. `Turn` and `PreparedCancellation` inherit it. The snapshot keeps observed native identity, Usage and output and remains readable after cancellation. Missing evidence stays unset; an empty `DonePayload` means nothing has been observed, not that cancellation succeeded or is unsupported. Reading the snapshot does not wait for settlement. - `Session.Cancel` requests cancellation; output closure signals teardown. Executable `PreparedCancellation.Cancel` waits for local cleanup and output writes to stop. Turn settlement still requires `AwaitSettlement` and any required `Executor.Close`; neither a successful cancellation request nor its snapshot replaces those waits. -**What the Runtime does around a Turn.** One output consumer starts before native Start, drains the bounded 64-frame channel and keeps the terminal observation until Start publication, Turn settlement and admitted operation receipts finish. Natural completion never calls Cancel. Input, function and interaction admission close before settlement; operations already admitted hold their barrier through native receipts and outbound acknowledgement. The Runtime sends cancellation to the Turn before waiting on that barrier, because a written input may need a native interrupt to produce its receipt. It joins native settlement, any required confirmed Executor close, output drain and all admitted operations before an applied acknowledgement or reuse, and only then forwards Done or an applied cancellation receipt. A failed Close can report failure while keeping the same Run and outstanding operations for retry; a closed caller wait cannot manufacture an applied input receipt. The Runtime commits native continuity and releases the old Run's admission before publishing Done, since the receiver may start another Turn at once; a late terminal-send failure belongs to the old Run and cannot invalidate a successor that already owns the Executor. Connection shutdown owns transport-loss cleanup. The settlement wait is ten seconds and the receipt send budget five seconds; a timeout is not proof of quiescence. +**What the Runtime does around a Turn.** One output consumer starts before native Start, drains the bounded 64-frame channel and keeps the terminal observation until Start publication, Turn settlement and admitted operation receipts finish. Natural completion never calls Cancel. Input and function admission close before settlement; operations already admitted hold their barrier through native receipts and outbound acknowledgement. The Runtime sends cancellation to the Turn before waiting on that barrier, because a written input may need a native interrupt to produce its receipt. It joins native settlement, any required confirmed Executor close, output drain and all admitted operations before an applied acknowledgement or reuse, and only then forwards Done or an applied cancellation receipt. A failed Close can report failure while keeping the same Run and outstanding operations for retry; a closed caller wait cannot manufacture an applied input receipt. The Runtime commits native continuity and releases the old Run's admission before publishing Done, since the receiver may start another Turn at once; a late terminal-send failure belongs to the old Run and cannot invalidate a successor that already owns the Executor. Connection shutdown owns transport-loss cleanup. The settlement wait is ten seconds and the receipt send budget five seconds; a timeout is not proof of quiescence. ## Events, inputs and optional capabilities Use [`internal/agentdaemon/proto`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/internal/agentdaemon/proto) for neutral requests, events and receipts. Each Turn emits only its own events with its Run ID, in order, and one terminal outcome. Native IDs and usage are observed, never invented; a missing measurement is unknown, not zero. -Initial input and steering use ordered `proto.MessageInput`. Keep user-message and content order. Text-only adapters reject images through `TextOnly()` instead of dropping them; image adapters translate each part natively and acknowledge an active batch only after all its messages are applied. A successful transport write is distinct from confirmed native application. User-choice answers use the emitted question ID and an array of values; the shared `PromptForUserChoiceDecisionPayload.AnswersFor` validates identity before consuming a pending interaction, so never map answers by header or position. Resume only the exact history bound to the Session; missing, ambiguous or foreign history fails before new model input. Device identity is not native session ownership. +Initial input and steering use ordered `proto.MessageInput`. Keep user-message and content order. Text-only adapters reject images through `TextOnly()` instead of dropping them; image adapters translate each part natively and acknowledge an active batch only after all its messages are applied. A successful transport write is distinct from confirmed native application. Resume only the exact history bound to the Session; missing, ambiguous or foreign history fails before new model input. Device identity is not native session ownership. ### Required and extension operations @@ -161,7 +160,7 @@ Registration is static and requires a build. Export one `agent.Declaration` from Every `proto.AgentKindCapabilities` field must be explicitly `proto.CapabilitySupported` or `proto.CapabilityUnsupported`, even for an unavailable Harness. `proto.CapabilityUnspecified` is invalid: zero values and omitted fields never mean Unsupported. An installation probe may set an individual field with `proto.CapabilityFromBool`; it must not populate unmentioned or future fields. Availability stays separate in `SupportedAgentKind.Available`. Registration validates the complete declaration before changing the registry, and the wire carries an explicit boolean for every field, so omitted and null fields are invalid. A new field requires a decision in every production declaration. Runtime consumers use `IsSupported()` and reject unsupported requests before native operations; an interface assertion verifies implementation, never support. Every declaration must match the behavior verified for that installation; the [Core–Runtime protocol](../../docs/runtime-protocol.md#capability-declarations) owns how declarations travel and are frozen. -The admission mapping is explicit. `Steering` controls non-durable `Steerer` input. `DurableInputReceipts` controls `DurableSteerer` input and also requires the Turn settlement contract; neither implies the other, and Core's public text profile requires both. `Permissions` qualifies permission and user-choice responses together and requires both native response paths. Workspace declarations describe the authorized resource owner, including the common Runtime workspace implementation. Runtime registration does not grant Core qualification; the service profile does. +The admission mapping is explicit. `Steering` controls non-durable `Steerer` input. `DurableInputReceipts` controls `DurableSteerer` input and also requires the Turn settlement contract; neither implies the other, and Core's public text profile requires both. Workspace declarations describe the authorized resource owner, including the common Runtime workspace implementation. Runtime registration does not grant Core qualification; the service profile does. The runnable test-only example [`testdata/onboarding/main.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/testdata/onboarding/main.go) registers a text-only synthetic Harness. It shows a Session-owned Executor, fresh Turns, durable steering, cancellation and history binding, and is never shipped. @@ -209,7 +208,7 @@ The declaration's ordered `protocols` list is the only source of accepted protoc Before starting, record the operation set, expected results, exclusions and stopping conditions. A qualification ends when its declared operations pass; it does not expand to match another Harness's feature list. 1. **Contract tests.** Call `agent/contracttest.TextLifecycle` from a test named `TestSharedTextLifecycle` with the adapter's prepared Executor and a deterministic native fixture; `claudesdk/executor_test.go` is the reference. It checks independent Turn streams, native owner and history continuity, durable write and application receipts, stale cancellation and healthy continuation after cancellation. `make check-runtime-contract` runs it together with the shared wire, gateway, transport and dispatcher tests, the declaration completeness check and each adapter's `TestUnsupportedExtensionsHaveNoNativeEffects`. Adapter tests also cover two ordinary Turns sharing one native process or connection and history, cancellation followed by another Turn, stale cancellation and late events, native exit, cleanup failure, input write and application receipts, unknown outcomes and fresh per-Turn usage, function, input and child-observation state. State whether a fixture is controlled or a real provider. -2. **Shared integration.** `TestThirdHarnessPublicOnboarding` runs the synthetic Harness through public Session and input admission, Worker device selection, the real WebSocket gateway, the daemon Registry and Router, neutral events and durable terminal projection. It uses a custom immutable `engine.Catalog` in the same `execution.Policy` given to the API handler and the dispatcher, and checks applied input receipts, saved native identity, continuation, cancellation, unsupported optional requests and missing mandatory Runtime support. The fixture has no workspace, MCP, public functions, permissions or user-choice handlers, and its registration stays local to the test. It proves the integration path, not native execution. +2. **Shared integration.** `TestThirdHarnessPublicOnboarding` runs the synthetic Harness through public Session and input admission, Worker device selection, the real WebSocket gateway, the daemon Registry and Router, neutral events and durable terminal projection. It uses a custom immutable `engine.Catalog` in the same `execution.Policy` given to the API handler and the dispatcher, and checks applied input receipts, saved native identity, continuation, cancellation, unsupported optional requests and missing mandatory Runtime support. The fixture has no workspace, MCP or public functions, and its registration stays local to the test. It proves the integration path, not native execution. 3. **Real acceptance.** Use the pinned official Python SDK and raw HTTP against Core, a real provider API, the native Harness and a dedicated database. Verify initial execution, a warm follow-up, cancellation and restart with continuation; record native owner identity and same-condition cold and warm timing. For workspace placements also verify Files and Artifacts, workspace identity, that no credentials appear in public responses and that foreign history is rejected. `services/core/tests/official_hosted_functions_native.py` holds the shared function assertions: success and error, native file output and public Artifact bytes, same-history continuation after restart, foreign result rejection and pending-call cancellation. Synthetic or failed runs never count. The opt-in tests below run the pinned-SDK fixtures in `services/core/tests` against a real daemon and model; each runs when `OAC_TEST_OFFICIAL_SDK_PYTHON`, `OAC_TEST_NATIVE_DAEMON_BIN`, `OAC_TEST_NATIVE_PROOF_DIR` and its private options file are set. The options file is a JSON object with exactly `model` and `model_provider` (the fields of `x_agents_core.model_provider`); the test sets it as the deployment default model provider, which the fixtures' `environment: none` Sessions freeze at creation. 4. **Regression.** Existing Harnesses keep working. Run targeted tests, then `make check`; run `make openapi` after API changes and `make sqlc-generate` after query changes. 5. **Review.** Follow the [blind review workflow](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#review). @@ -247,7 +246,7 @@ The daemon's `clirunner` starts every native child in its own Unix process group Owned output pipes stay readable after the leader exits. Consumers drain stdout and stderr before calling `Wait`, which joins the cached process result and closes the readers. `Done` reports leader reaping and group cleanup signals; it is not a native execution receipt or proof of persisted history. SDK adapters settle each Turn and drain its observations before publishing completion, and Executor close also closes the query and awaits the native child. Process groups are lifecycle supervision, not isolation or containment of descendants that leave the group. -Adapters run native tools unattended with the launching user's permissions: Codex with approval policy `never` and full access, Claude through the adapter's tool callback in native `default` permission mode with the SDK sandbox disabled, and MiniMax with bypassed permissions and its sandbox disabled. Do not add permission profiles, bubblewrap wrappers or native sandbox settings; there is one execution path for every Environment origin. Resource paths are operator configuration, not a permission boundary. +Adapters run native tools unattended with the launching user's permissions, and a Harness never asks a human. Codex runs with approval policy `never`, under which it settles MCP elicitation itself without reaching the client, and with full access; the adapter also disables its blocking `request_user_input` tool. Claude runs through the adapter's tool callback, which allows or denies without asking, in native `default` permission mode with the SDK sandbox disabled. MiniMax runs with bypassed permissions and its sandbox disabled; the adapter disables `askUser`, advertises no elicitation and answers `session/request_permission` with the ACP `cancelled` outcome, which MiniMax treats as a denial. No native permission or question request reaches Core: a human in the loop goes through a function tool, the Session reads [`requires_action`](./sessions-events.md#session-status) and the application submits the function result. Do not add permission profiles, bubblewrap wrappers or native sandbox settings; there is one execution path for every Environment origin. Resource paths are operator configuration, not a permission boundary. Network admission follows [Restricted network](./environments.md#restricted-network). diff --git a/contracts/agents-api/zh/core-errors.md b/contracts/agents-api/zh/core-errors.md index d90ac52b0..c7bc4301b 100644 --- a/contracts/agents-api/zh/core-errors.md +++ b/contracts/agents-api/zh/core-errors.md @@ -1,7 +1,7 @@ --- title: "Core 管理错误" source: contracts/agents-api/core-errors.md -source_hash: d5c4450c0c74927115c79dca70d29372a7316d5d2591c7e18af688d08257a98c +source_hash: 3d8e6a03d6a54c7dc86a27164749ffae963b5ef52cbede0e0da843c7da0216ba --- `/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。 @@ -98,7 +98,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封 | `execution_interrupted` | Core 执行被中断 | | `delivery_unconfirmed` | `delivery_unknown`、`input_outcome_unknown`、`cancel_unconfirmed`、`cancel_outcome_unavailable`、`function_result_unconfirmed` | | `input_rejected` | `invalid_input`、`input_not_applied`、`message_input_unsupported`,以及确切的 steering 结果 `input_invalid_input`、`input_run_inactive`、`input_input_conflict`、`input_input_limit`、`input_unsupported`、`input_rejected`、`input_not_ready`、`input_busy` | -| `executor_protocol_error` | `invalid_executor_result`、`interaction_not_supported`、`execution_state_unavailable`、`execution_state_changed`、`function_call_invalid`、`function_result_invalid` | +| `executor_protocol_error` | `invalid_executor_result`、`execution_state_unavailable`、`execution_state_changed`、`function_call_invalid`、`function_result_invalid` | | `core_storage_failed` | `event_persistence_failed`、`artifact_capture_failed` | | `internal_error` | 结果未知或格式错误;不返回原始值 | | `environment_connection_timeout` | 初始输入连接截止时间已过 | diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index 8f21103a4..0a952cf17 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "将原生 Harness 添加到 OpenAgentCore" source: contracts/agents-api/harness-onboarding.md -source_hash: f9d04f91520968c71565aada8592f116bf794b0e094f8897cb21ef0d10e17793 +source_hash: 29b19e3f79eda12fcf888bf0551ea36a37df4f9eda8245fb98163978a3695aee --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、Core 资格认定和验收。[Harness capabilities](harness-capabilities.md) 记录了当前每个 Harness 支持的功能。 @@ -71,7 +71,6 @@ Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以 | `DurableSteerer` | 每个 Turn 上真实实现 | 区分完整写入与原生应用回执;保留重试身份 | | `Steerer` | 明确实现或 Unsupported | 额外的非持久化活动 Turn 输入 | | `FunctionResultSubmitter` | 明确实现或 Unsupported | 匹配原生调用和结果身份,并确认应用 | -| `PermissionResponder`、`UserChoiceResponder` | 明确实现或 Unsupported | 响应精确发出的身份;未知或已过期的交互与 Unsupported 保持区分 | | `WorkspaceReader`、`WorkspaceDirectoryLister`、`WorkspaceWriter` | 在 Turn、Executor 和 Prepared 所有者上明确实现 | 使用授权工作区,确认访问,提交或关闭,或者返回该操作的 Unsupported 错误 | | `Prepared`、`PreparedCancellation` | 对可执行准备进行真实实现 | 在 Start、取消和未使用清理之间保持资源和输出的所有权 | | 中立消息、图像、MCP、结构化输出和 Subagent 观察 | 明确作出能力决策 | 保持每项操作的协议语义;在提交前拒绝不受支持的输入 | @@ -86,7 +85,7 @@ func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayl } ``` -原因必须是固定的安全字符串,绝不能是已提交内容、凭据或原始原生诊断信息。Unsupported 保证不会产生原生副作用,也不表示操作成功且为空。安装不可用、未知交互 ID、原生失败和不确定结果应保留各自的错误和所有权。nil `Turn` 仍表示没有提交任何输入,并且输出归调用方所有;绝不能将其用作 Unsupported 标记。 +原因必须是固定的安全字符串,绝不能是已提交内容、凭据或原始原生诊断信息。Unsupported 保证不会产生原生副作用,也不表示操作成功且为空。安装不可用、未知调用 ID、原生失败和不确定结果应保留各自的错误和所有权。nil `Turn` 仍表示没有提交任何输入,并且输出归调用方所有;绝不能将其用作 Unsupported 标记。 线协议请求不携带工作目录。Runtime 将 `local_environment.workspace_directory` 与其绑定进行核对,并通过 `LocalEnvironment.WorkspaceRoot` 向 Harness 提供其绑定的工作区目录;必须在该目录中运行原生 Harness。 @@ -108,11 +107,11 @@ Session 在其已连接的 Runtime 中拥有一个可复用的 Executor;Turn **绑定。** Runtime 将其 Executor 记录绑定到 Session、Environment、连接和不可变执行配置。恢复身份和先前 Turn 恢复标志是连续性断言,而不是配置更改。提供的原生身份必须与保留的所有者匹配;当需要现有历史时,恢复绝不能启动新的根。配置冲突属于错误,而不是热切换。连接丢失会让其所有者和句柄退役;旧计时器、输出和取消操作不能影响替代对象。 -**每 Turn 状态。** 每个 Turn 都会获得全新的包装器、输出通道和回执状态。引导、函数、权限和用户选择接口均属于该 Turn。原生回调必须在异步工作开始前捕获来源 Turn,因此迟到事件绝不会被归到当前活动的 Turn 上。原生进程、query 或传输连接、固定能力配置和原生 session 身份均属于 Executor。不要重置已完成的 `sync.Once` 值,也不要复用旧 Turn 对象。 +**每 Turn 状态。** 每个 Turn 都会获得全新的包装器、输出通道和回执状态。引导和函数接口均属于该 Turn。原生回调必须在异步工作开始前捕获来源 Turn,因此迟到事件绝不会被归到当前活动的 Turn 上。原生进程、query 或传输连接、固定能力配置和原生 session 身份均属于 Executor。不要重置已完成的 `sync.Once` 值,也不要复用旧 Turn 对象。 **开始。** `StartTurn` 返回 nil Turn,保证没有提交任何原生输入,也没有保留输出通道;随后由 Runtime 关闭该通道。一旦输入可能已经提交,即使同时返回错误,也必须返回非 nil Turn:该 Turn 拥有恰好一次的输出关闭权,并在结算前持续接受跟踪。未知输入绝不能重放。明确的 `executor_unavailable` Start 拒绝允许进行一次通用恢复尝试,但只能在此前 Executor 已关闭且未提交输入之后进行;Runtime 会重新检查同一物理对端和当前授权。 -**取消和结算。** `Turn.Cancel` 仅以目标 Turn 为对象,不会关闭健康的 Executor。`AwaitSettlement` 同时适用于自然完成和取消。成功意味着输出已无法再写入,并且该 Turn 的原生事件、输入、函数、交互和子任务均已结算。原生完成或取消确认独立于资源退役:关闭传输层无法提供缺失的原生终态或操作回执。 +**取消和结算。** `Turn.Cancel` 仅以目标 Turn 为对象,不会关闭健康的 Executor。`AwaitSettlement` 同时适用于自然完成和取消。成功意味着输出已无法再写入,并且该 Turn 的原生事件、输入、函数和子任务均已结算。原生完成或取消确认独立于资源退役:关闭传输层无法提供缺失的原生终态或操作回执。 - `Reusable=true` 还要确认原生所有者能够接受下一个 Turn。`Reusable=false` 要求提供原因,并在之后确认 Executor 已关闭。 - 错误表示结算尚未确认,既不释放所有权,也不释放容量。调用方截止时间只会停止等待,不会停止受跟踪的清理。必须串行重试同一个清理目标;清理失败会阻止替换并保留其资源槽位。 @@ -121,13 +120,13 @@ Session 在其已连接的 Runtime 中拥有一个可复用的 Executor;Turn - 每个 `Session` 都要声明 `CancellationOutcome`。`Turn` 和 `PreparedCancellation` 继承该声明。快照保留已观察到的原生身份、Usage 和输出,并在取消后仍可读取。缺失的证据保持未设置;空的 `DonePayload` 表示未观察到任何内容,而不是表示取消成功或不受支持。读取快照不会等待结算。 - `Session.Cancel` 请求取消;输出关闭表示拆卸开始。可执行准备中的 `PreparedCancellation.Cancel` 会等待本地清理和输出写入停止。Turn 结算仍需要 `AwaitSettlement` 和所需的任何 `Executor.Close`;取消请求成功或其快照都不能替代这些等待。 -**Runtime 在 Turn 前后执行的工作。** 一个输出消费者会在原生 Start 之前启动,耗尽有界的 64 帧通道,并将终态观察保留到 Start 发布、Turn 结算和已准入操作回执完成为止。正常完成绝不调用 Cancel。输入、函数和交互准入会在结算前关闭;已准入的操作会持有其屏障,直至原生回执和出站确认完成。Runtime 会在等待该屏障之前向 Turn 发送取消,因为已写入的输入可能需要原生中断才能生成回执。Runtime 会汇合原生结算、所需的已确认 Executor 关闭、输出耗尽和所有已准入操作,然后应用确认或执行复用,之后才会转发 Done 或已应用的取消回执。Close 失败可以报告失败,同时保留同一 Run 和未完成操作以供重试;已关闭的调用方等待无法凭空生成已应用输入回执。Runtime 会在发布 Done 前提交原生连续性状态并释放旧 Run 的准入,因为接收方可能立即启动另一个 Turn;迟到的终态发送失败属于旧 Run,不能使已拥有 Executor 的后继对象失效。连接关闭负责传输丢失清理。结算等待时间为十秒,回执发送预算为五秒;超时不能证明已达到静默状态。 +**Runtime 在 Turn 前后执行的工作。** 一个输出消费者会在原生 Start 之前启动,耗尽有界的 64 帧通道,并将终态观察保留到 Start 发布、Turn 结算和已准入操作回执完成为止。正常完成绝不调用 Cancel。输入和函数准入会在结算前关闭;已准入的操作会持有其屏障,直至原生回执和出站确认完成。Runtime 会在等待该屏障之前向 Turn 发送取消,因为已写入的输入可能需要原生中断才能生成回执。Runtime 会汇合原生结算、所需的已确认 Executor 关闭、输出耗尽和所有已准入操作,然后应用确认或执行复用,之后才会转发 Done 或已应用的取消回执。Close 失败可以报告失败,同时保留同一 Run 和未完成操作以供重试;已关闭的调用方等待无法凭空生成已应用输入回执。Runtime 会在发布 Done 前提交原生连续性状态并释放旧 Run 的准入,因为接收方可能立即启动另一个 Turn;迟到的终态发送失败属于旧 Run,不能使已拥有 Executor 的后继对象失效。连接关闭负责传输丢失清理。结算等待时间为十秒,回执发送预算为五秒;超时不能证明已达到静默状态。 ## 事件、输入和可选能力 {#events-inputs-and-optional-capabilities} 使用 [`internal/agentdaemon/proto`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/internal/agentdaemon/proto) 处理中立请求、事件和回执。每个 Turn 只按顺序发出带有其 Run ID 的自身事件,并产生一个终态结果。原生 ID 和 usage 必须来自观察,绝不能虚构;缺失的度量值表示未知,而不是零。 -初始输入和引导使用有序的 `proto.MessageInput`。必须保持用户消息顺序和内容顺序。仅支持文本的适配器通过 `TextOnly()` 拒绝图像,而不是丢弃图像;图像适配器在原生环境中转换每个部分,并且只有在其所有消息均已应用后才确认活动批次。成功传输写入与确认原生应用是不同的事件。用户选择答案使用发出的问题 ID 和值数组;共享的 `PromptForUserChoiceDecisionPayload.AnswersFor` 会在消费待处理交互之前验证身份,因此绝不能按标头或位置映射答案。只能恢复绑定到 Session 的精确历史;缺失、含糊或外部历史会在新的模型输入之前导致失败。设备身份不代表原生 session 所有权。 +初始输入和引导使用有序的 `proto.MessageInput`。必须保持用户消息顺序和内容顺序。仅支持文本的适配器通过 `TextOnly()` 拒绝图像,而不是丢弃图像;图像适配器在原生环境中转换每个部分,并且只有在其所有消息均已应用后才确认活动批次。成功传输写入与确认原生应用是不同的事件。只能恢复绑定到 Session 的精确历史;缺失、含糊或外部历史会在新的模型输入之前导致失败。设备身份不代表原生 session 所有权。 ### 必需操作和扩展操作 {#required-and-extension-operations} @@ -163,7 +162,7 @@ MCP、公共函数、延迟函数发现、结构化输出、图像输入、详 每个 `proto.AgentKindCapabilities` 字段都必须显式设为 `proto.CapabilitySupported` 或 `proto.CapabilityUnsupported`,即使 Harness 不可用也是如此。`proto.CapabilityUnspecified` 无效:零值和省略字段绝不表示 Unsupported。安装探测可以使用 `proto.CapabilityFromBool` 设置单个字段;但不得填充未提及字段或未来字段。可用性通过 `SupportedAgentKind.Available` 单独表示。注册会在更改 registry 之前验证完整声明;线协议会为每个字段携带显式布尔值,因此省略字段和 null 字段均无效。添加新字段时,每个生产声明都必须作出决定。Runtime 使用者应调用 `IsSupported()`,并在原生操作前拒绝不受支持的请求;接口断言用于验证实现,绝不表示支持。每个声明都必须与针对该安装验证的行为一致;[Core–Runtime protocol](../../../docs/zh/runtime-protocol.md#capability-declarations) 负责声明的传输方式和冻结方式。 -准入映射是显式的。`Steering` 控制非持久化 `Steerer` 输入。`DurableInputReceipts` 控制 `DurableSteerer` 输入,并且还要求 Turn 结算契约;二者互不隐含,而且 Core 的公共文本 profile 要求同时具备二者。`Permissions` 一起认定权限响应和用户选择响应的资格,并要求两条原生响应路径均存在。工作区声明描述授权资源所有者,包括通用 Runtime 工作区实现。Runtime 注册不会授予 Core 资格;服务 profile 才会授予。 +准入映射是显式的。`Steering` 控制非持久化 `Steerer` 输入。`DurableInputReceipts` 控制 `DurableSteerer` 输入,并且还要求 Turn 结算契约;二者互不隐含,而且 Core 的公共文本 profile 要求同时具备二者。工作区声明描述授权资源所有者,包括通用 Runtime 工作区实现。Runtime 注册不会授予 Core 资格;服务 profile 才会授予。 可运行的仅测试示例 [`testdata/onboarding/main.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/testdata/onboarding/main.go) 会注册一个仅支持文本的合成 Harness。它展示 Session 所有的 Executor、全新的 Turn、持久化引导、取消和历史绑定,并且绝不会发布。 @@ -211,7 +210,7 @@ profile 是纯逻辑:它使用现有的公共类型和协议类型,声明受 开始前,记录操作集、预期结果、排除项和停止条件。当其声明的操作通过时,资格认定即结束;它不会扩展为匹配另一个 Harness 的功能列表。 1. **契约测试。** 在名为 `TestSharedTextLifecycle` 的测试中,使用适配器准备好的 Executor 和确定性的原生夹具调用 `agent/contracttest.TextLifecycle`;`claudesdk/executor_test.go` 是参考实现。它检查独立的 Turn 流、原生所有者和历史连续性、持久化写入与应用回执、过期取消,以及取消后的健康继续执行。`make check-runtime-contract` 会将它与共享线协议、gateway、传输层和调度器测试、声明完整性检查以及每个适配器的 `TestUnsupportedExtensionsHaveNoNativeEffects` 一起运行。适配器测试还覆盖两个普通 Turn 共享一个原生进程或连接和历史、取消后执行另一个 Turn、过期取消和迟到事件、原生退出、清理失败、输入写入与应用回执、未知结果,以及每 Turn 新鲜的 usage、函数、输入和子项观察状态。必须说明夹具是受控夹具还是真实 Provider。 -2. **共享集成。** `TestThirdHarnessPublicOnboarding` 让合成 Harness 通过公共 Session 和输入准入、Worker 设备选择、真实 WebSocket gateway、daemon Registry 和 Router、中立事件以及持久化终态投影运行。它在与 API 处理程序和调度器相同的 `execution.Policy` 中使用自定义不可变 `engine.Catalog`,并检查已应用输入回执、已保存原生身份、继续执行、取消、不受支持的可选请求以及缺少强制 Runtime 支持。该夹具没有工作区、MCP、公共函数、权限或用户选择处理器,其注册仅保留在测试本地。它证明的是集成路径,而不是原生执行。 +2. **共享集成。** `TestThirdHarnessPublicOnboarding` 让合成 Harness 通过公共 Session 和输入准入、Worker 设备选择、真实 WebSocket gateway、daemon Registry 和 Router、中立事件以及持久化终态投影运行。它在与 API 处理程序和调度器相同的 `execution.Policy` 中使用自定义不可变 `engine.Catalog`,并检查已应用输入回执、已保存原生身份、继续执行、取消、不受支持的可选请求以及缺少强制 Runtime 支持。该夹具没有工作区、MCP 或公共函数,其注册仅保留在测试本地。它证明的是集成路径,而不是原生执行。 3. **真实验收。** 使用锁定的官方 Python SDK 和针对 Core 的原始 HTTP、真实 Provider API、原生 Harness 以及专用数据库。验证初始执行、热后续执行、取消以及带继续执行的重启;记录原生所有者身份以及相同条件下的冷启动和热运行时间。对于工作区放置方式,还要验证 Files 和 Artifacts、工作区身份、公开响应中未出现凭据,以及外部历史会被拒绝。`services/core/tests/official_hosted_functions_native.py` 保存共享函数断言:成功和错误、原生文件输出和公共 Artifact 字节、重启后的同历史继续执行、外部结果拒绝以及待处理调用取消。合成运行或失败运行绝不计入。下面的选择性测试会在 `services/core/tests` 中针对真实 daemon 和模型运行锁定 SDK 夹具;设置 `OAC_TEST_OFFICIAL_SDK_PYTHON`、`OAC_TEST_NATIVE_DAEMON_BIN`、`OAC_TEST_NATIVE_PROOF_DIR` 及其私有选项文件后,每项测试才会运行。选项文件是一个 JSON 对象,恰好包含 `model` 和 `model_provider`(即 `x_agents_core.model_provider` 的字段);测试会将其设置为部署默认模型 Provider,而夹具的 `environment: none` Session 会在创建时将其冻结。 4. **回归。** 现有 Harness 必须继续正常工作。先运行定向测试,然后运行 `make check`;API 更改后运行 `make openapi`,查询更改后运行 `make sqlc-generate`。 5. **审查。** 遵循 [blind review workflow](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#review)。 @@ -249,7 +248,7 @@ daemon 的 `clirunner` 让每个原生子进程在自己的 Unix 进程组中启 所属输出管道在主进程退出后仍可读取。消费者在调用 `Wait` 之前耗尽 stdout 和 stderr;`Wait` 会汇合缓存的进程结果并关闭读取器。`Done` 报告主进程回收和进程组清理信号;它不是原生执行回执,也不是历史已持久化的证据。SDK 适配器会结算每个 Turn,并在发布完成状态前耗尽其观察结果;Executor 关闭还会关闭 Query 并等待原生子进程。进程组用于生命周期监管,而不是隔离或遏制离开进程组的后代进程。 -适配器以启动用户的权限无人值守运行原生工具:Codex 使用批准策略 `never` 和完全访问权限;Claude 通过适配器的工具回调,以原生 `default` 权限模式运行,并禁用 SDK sandbox;MiniMax 绕过权限并禁用 sandbox。不要添加权限 profile、bubblewrap 包装器或原生 sandbox 设置;每种 Environment 来源都只有一条执行路径。资源路径属于操作员配置,而不是权限边界。 +适配器以启动用户的权限无人值守运行原生工具,Harness 从不询问人类。Codex 使用批准策略 `never` 和完全访问权限,在该策略下 Codex 自行处理 MCP elicitation,不会发给客户端;适配器还禁用其阻塞式 `request_user_input` 工具。Claude 通过适配器的工具回调运行,回调直接允许或拒绝、不会询问,使用原生 `default` 权限模式并禁用 SDK sandbox。MiniMax 绕过权限并禁用 sandbox;适配器禁用 `askUser`、不声明 elicitation,并以 ACP `cancelled` 结果答复 `session/request_permission`,MiniMax 将其视为拒绝。原生权限或问题请求都不会到达 Core:需要人工介入时通过 function 工具完成,Session 读取 [`requires_action`](./sessions-events.md#session-status),由应用提交 function 结果。不要添加权限 profile、bubblewrap 包装器或原生 sandbox 设置;每种 Environment 来源都只有一条执行路径。资源路径属于操作员配置,而不是权限边界。 网络准入遵循 [Restricted network](environments.md#restricted-network)。 diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 6b74785a0..bc1d4859a 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -20,7 +20,7 @@ A Runtime connects in this order: The wire version is [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go), independent of the Runtime build version that heartbeats report. Core accepts only an exact match, including the patch component. A mismatch returns HTTP 426 `incompatible_version` before any dispatch; the daemon treats it as permanent and stops reconnecting. Deploy matching peers together. -Each physical connection has fresh routing, admission handles and transfer state. A newer connection for the same device replaces the previous one: Core fences the owner lease and evicts the old Run and interaction routes, and the new connection inherits none of them. A valid credential and connection are never authority to choose another Session or Environment binding. +Each physical connection has fresh routing, admission handles and transfer state. A newer connection for the same device replaces the previous one: Core fences the owner lease and evicts the old Run routes, and the new connection inherits none of them. A valid credential and connection are never authority to choose another Session or Environment binding. ## Capability declarations @@ -51,7 +51,7 @@ A declaration describes what the Runtime can do. Core admits a public feature on | `message_images`, `function_result_images` | A message, or a function result, carries an image | | `mcp_http_tools`, `mcp_http_required`, `mcp_http_bearer_auth` | The Agent declares HTTP MCP servers; one is `required`; a Vault credential is selected for one | -`permissions` gates permission decisions inside the Runtime. Core has no admission rule for `usage` and `resume`. +Core has no admission rule for `usage` and `resume`. The `execution_prepare` configuration carries the opt-ins Core sets for each Run: @@ -82,13 +82,10 @@ Every data frame is one JSON [`Envelope`](https://github.com/MiniMax-AI/OpenAgen | Preparation request ID | `Envelope.id` for prepare, start, release and status; distinct from a Run | | Admission handle | Runtime-generated reservation, valid only on the connection that accepted it | | Run ID | One execution attempt; `Envelope.id` for output, cancellation, active input and functions | -| Interaction ID | `permission_request.payload.request_id` or `prompt_for_user_choice.payload.ask_id`; these request envelopes still carry the Run ID | | Delivery ID / input ID / call ID | Resolve attempt, active-input receipt and native function identity; never interchangeable | | Transfer ID / suspension ID | Connection-local transfer correlation / persisted suspension-attempt fencing | -Decision and permission-cancel envelopes use the interaction ID. Cancellation and function-result acknowledgements use the Run ID. Every application decision receipt also matches the delivery ID. A reply without the required correlation cannot establish acceptance. - -User-choice decisions carry `question_answers`: an explicit `question_id` and an `answers` array for each provided answer. The IDs must belong to the emitted questions and cannot repeat. Question order and display headers do not identify answers; an omitted question stays unanswered, and an empty array is an explicit non-answer. Cancellation carries `cancelled: true` without answers. Shared validation rejects other shapes before native submission. +Cancellation and function-result acknowledgements use the Run ID and match the delivery ID. A reply without the required correlation cannot establish acceptance. ## Message families @@ -98,8 +95,7 @@ The linked source files define the required fields, validators, limits and finit | --- | --- | --- | | `runtime_prepare` | `runtime_prepare_result` | [Initialization and capability transfer](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/runtime_prepare.go) | | `execution_prepare`, `execution_start`, `execution_release` | `preparation_status` | [Execution admission](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/preparation.go) | -| `prompt_cancel`, `device_shutdown` | `delta`, `thinking`, `output_message`, `tool_call`, `usage`, `error`, `done`, `heartbeat` | [Requests](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go), [events and capabilities](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | -| `permission_decision`, `prompt_for_user_choice_decision` | `permission_request`, `permission_cancel`, `prompt_for_user_choice`, `interaction_decision_ack` | [Requests](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go), [interactions](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | +| `prompt_cancel` | `delta`, `thinking`, `output_message`, `tool_call`, `usage`, `error`, `done`, `heartbeat`, `interaction_decision_ack` | [Requests](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go), [events and capabilities](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | | `prompt_steer` | `prompt_steer_ack` | [Active input receipts](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/steering.go) | | `function_result` | `function_call`, `interaction_decision_ack` | [Function calls](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/functions.go) | | `workspace_read`, `workspace_write`, `workspace_export` | Matching `*_result` | [Read](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_read.go), [write](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_write.go), [export](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_export.go) | @@ -158,7 +154,7 @@ No generic receipt exists for every envelope. A successful send does not prove t ## Failures, retries and cleanup -Transport and execution outcomes are separate. Core's only Run subscription entry point is `SubscribeDurable`; inspect `Subscription.Err()` when its event channel closes. Disconnection and subscriber overflow close it with an explicit observation error and fabricate no `error` or `done`. Core keeps the durable truth and reconciles from confirmed facts. The Runtime keeps cleanup ownership until native work, input receipts, interactions and child work have settled. +Transport and execution outcomes are separate. Core's only Run subscription entry point is `SubscribeDurable`; inspect `Subscription.Err()` when its event channel closes. Disconnection and subscriber overflow close it with an explicit observation error and fabricate no `error` or `done`. Core keeps the durable truth and reconciles from confirmed facts. The Runtime keeps cleanup ownership until native work, input receipts, function results and child work have settled. The public Turn status is a separate projection. [`execution/delivery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/execution/delivery.go) records an unsuccessful orchestration attempt as `failed`, including `delivery_unknown` after an unconfirmed send and `event_stream_incomplete` after a subscription failure; a closed subscription can replace the send reason with `event_stream_incomplete`. Both mean the native effect is unknown: a public `failed` status does not prove that the Harness failed, that no side effect occurred or that cleanup completed. Keep the observation reason and any native evidence distinct. diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index e721f6133..11a7807a5 100644 --- a/docs/zh/runtime-protocol.md +++ b/docs/zh/runtime-protocol.md @@ -1,7 +1,7 @@ --- title: "Core–Runtime 协议" source: docs/runtime-protocol.md -source_hash: 1d425cdf68ffd76fbf4ebb856fcf7af8a96eea93e28b1686292eeb0184376664 +source_hash: aec653711cfaff6123091c418ef1d7eb9dc8b9648cc4a70f9fe231895fd7ae9d --- 此协议在 Runtime daemon 获取机器凭据后连接 Core 与 daemon,定义 daemon 连接上消息的含义和顺序。wire 类型、限制和验证器仅在 [`internal/agentdaemon/proto`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/internal/agentdaemon/proto) 中定义一次;Core 的 [gateway](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/runtimegateway) 与参考 Runtime 的 [dispatcher](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/apps/daemon/internal/dispatch) 都使用它们,因此无需同步第二套 payload schema。签发凭据和打开连接的 HTTP 路由见[机器连接 API](../../contracts/agents-api/zh/machine-api.md)。 @@ -22,7 +22,7 @@ Runtime 按以下顺序连接: wire 版本为 [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go),独立于 heartbeat 报告的 Runtime 构建版本。Core 仅接受精确匹配,包括 patch 部分。不匹配时,在任何 dispatch 前返回 HTTP 426 `incompatible_version`;daemon 将其视为永久错误并停止重连。应一起部署版本匹配的两端。 -每条物理连接拥有新的路由、admission handle 和传输状态。同一设备的新连接替代旧连接:Core 隔离 owner lease,移除旧 Run 和 interaction 路由,新连接不继承这些状态。有效凭据和连接不授权选择其他 Session 或 Environment 绑定。 +每条物理连接拥有新的路由、admission handle 和传输状态。同一设备的新连接替代旧连接:Core 隔离 owner lease,移除旧 Run 路由,新连接不继承这些状态。有效凭据和连接不授权选择其他 Session 或 Environment 绑定。 ## 能力声明 {#capability-declarations} @@ -53,7 +53,7 @@ wire 上每个字段都是 JSON boolean,所有字段都必须出现,包括 ` | `message_images`, `function_result_images` | 消息或 function result 携带图像 | | `mcp_http_tools`, `mcp_http_required`, `mcp_http_bearer_auth` | Agent 声明 HTTP MCP server;其中一个为 `required`;其中一个选用了 Vault 凭据 | -`permissions` 控制 Runtime 内部权限决定。Core 对 `usage` 和 `resume` 没有准入规则。 +Core 对 `usage` 和 `resume` 没有准入规则。 `execution_prepare` 的配置携带 Core 为各 Run 设置的显式启用项: @@ -84,13 +84,10 @@ wire 上每个字段都是 JSON boolean,所有字段都必须出现,包括 ` | Preparation request ID | prepare、start、release 和 status 的 `Envelope.id`;与 Run 不同 | | Admission handle | Runtime 生成的预约,仅在接受它的连接上有效 | | Run ID | 一次执行尝试;输出、取消、活动输入和 function 的 `Envelope.id` | -| Interaction ID | `permission_request.payload.request_id` 或 `prompt_for_user_choice.payload.ask_id`;这些请求 envelope 仍携带 Run ID | | Delivery ID / input ID / call ID | 分别标识 resolve 尝试、活动输入回执与原生 function;不能互换 | | Transfer ID / suspension ID | 连接本地传输关联 / 持久化 suspension 尝试的 fencing | -Decision 和 permission-cancel envelope 使用 interaction ID。取消和 function-result 确认使用 Run ID。每个应用 decision 回执还要匹配 delivery ID。缺少必需关联的回复不能证明已接受。 - -User-choice decision 携带 `question_answers`:每个已提供回答都有明确的 `question_id` 和 `answers` 数组。ID 必须属于已发出的问题且不能重复。问题顺序和显示 header 不标识回答;省略的问题仍未回答,空数组表示明确未作答。取消携带 `cancelled: true`,不包含回答。共享验证在原生提交前拒绝其他形式。 +取消和 function-result 确认使用 Run ID,并匹配 delivery ID。缺少必需关联的回复不能证明已接受。 ## 消息族 {#message-families} @@ -100,8 +97,7 @@ User-choice decision 携带 `question_answers`:每个已提供回答都有明 | --- | --- | --- | | `runtime_prepare` | `runtime_prepare_result` | [初始化与能力传输](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/runtime_prepare.go) | | `execution_prepare`, `execution_start`, `execution_release` | `preparation_status` | [执行准入](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/preparation.go) | -| `prompt_cancel`, `device_shutdown` | `delta`, `thinking`, `output_message`, `tool_call`, `usage`, `error`, `done`, `heartbeat` | [请求](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go)、[事件与能力](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | -| `permission_decision`, `prompt_for_user_choice_decision` | `permission_request`, `permission_cancel`, `prompt_for_user_choice`, `interaction_decision_ack` | [请求](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go)、[交互](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | +| `prompt_cancel` | `delta`, `thinking`, `output_message`, `tool_call`, `usage`, `error`, `done`, `heartbeat`, `interaction_decision_ack` | [请求](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go)、[事件与能力](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | | `prompt_steer` | `prompt_steer_ack` | [活动输入回执](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/steering.go) | | `function_result` | `function_call`, `interaction_decision_ack` | [Function 调用](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/functions.go) | | `workspace_read`, `workspace_write`, `workspace_export` | 对应的 `*_result` | [读取](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_read.go)、[写入](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_write.go)、[导出](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_export.go) | @@ -160,7 +156,7 @@ Core 通过 `prompt_steer` 交付活动输入,设置 `durable_receipt: true` ## 故障、重试与清理 {#failures-retries-and-cleanup} -transport 与 execution 结果分开。Core 唯一 Run 订阅入口是 `SubscribeDurable`;事件 channel 关闭时检查 `Subscription.Err()`。断连与 subscriber overflow 以明确 observation error 关闭订阅,不虚构 `error` 或 `done`。Core 保留持久事实,并依据已确认事实协调。Runtime 保留清理所有权,直到原生工作、输入回执、交互和子任务工作都结算完成。 +transport 与 execution 结果分开。Core 唯一 Run 订阅入口是 `SubscribeDurable`;事件 channel 关闭时检查 `Subscription.Err()`。断连与 subscriber overflow 以明确 observation error 关闭订阅,不虚构 `error` 或 `done`。Core 保留持久事实,并依据已确认事实协调。Runtime 保留清理所有权,直到原生工作、输入回执、function 结果和子任务工作都结算完成。 公开 Turn 状态是独立投影。[`execution/delivery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/execution/delivery.go) 将不成功的编排尝试记录为 `failed`,包括未确认发送后的 `delivery_unknown` 和订阅失败后的 `event_stream_incomplete`;关闭的订阅可以用 `event_stream_incomplete` 替换发送原因。两者都表示原生效果未知:公开 `failed` 状态不证明 Harness 失败、没有产生副作用或清理已完成。应区分观测原因与原生证据。 diff --git a/internal/agentdaemon/proto/envelope.go b/internal/agentdaemon/proto/envelope.go index 11772622a..bc4df3128 100644 --- a/internal/agentdaemon/proto/envelope.go +++ b/internal/agentdaemon/proto/envelope.go @@ -14,8 +14,6 @@ // Envelope.ID correlation: // - prompt_cancel: ID = RunID. // - delta / tool_call / usage / error / done: ID = originating RunID. -// - permission_request: ID = RunID; payload.request_id is the interaction ID. -// - permission_decision / permission_cancel: ID = interaction ID. // - execution_prepare / execution_start / execution_release and // preparation_status: ID = preparation request ID, never RunID. // - runtime_prepare / runtime_prepare_result: ID = connection-local transfer ID. @@ -81,8 +79,7 @@ func NewEnvelopeWithTrace(typ string, id string, payload any, traceparent string } // DecodePayload unpacks Envelope.Payload into out. An empty Payload is -// a non-error so bodyless types (prompt_cancel, permission_cancel) -// decode cleanly. +// a non-error so a bodyless prompt_cancel decodes cleanly. func (e Envelope) DecodePayload(out any) error { if len(e.Payload) == 0 { return nil diff --git a/internal/agentdaemon/proto/inbound.go b/internal/agentdaemon/proto/inbound.go index 73a49f6a8..ed236518f 100644 --- a/internal/agentdaemon/proto/inbound.go +++ b/internal/agentdaemon/proto/inbound.go @@ -28,26 +28,9 @@ const ( // before the call, "after" runs after. TypeToolCall = "tool_call" - // TypePermissionRequest carries an agent's request for human - // approval. Envelope.ID is the RunID; payload.request_id identifies the decision. - TypePermissionRequest = "permission_request" - - // TypePermissionCancel signals the agent withdrew an earlier - // permission request (e.g. its internal timeout fired). Used by - // the gateway to unblock pending SubmitPermission calls. - TypePermissionCancel = "permission_cancel" - - // TypePromptForUserChoice asks the human to pick one (or more) - // answers from a closed list before the agent can continue. Used - // to intercept Claude Code's built-in AskUserQuestion tool so the - // daemon doesn't deadlock waiting for a tool_result no one will - // send. Envelope.ID is the RunID; payload.ask_id identifies the decision. - TypePromptForUserChoice = "prompt_for_user_choice" - // TypeInteractionDecisionAck confirms that the daemon-side agent - // accepted (or definitively rejected) a permission/user-input decision or cancellation. - // The server must not mark the durable interaction terminal before this - // frame arrives. + // accepted (or definitively rejected) a function result or cancellation. + // The server must not mark the delivery terminal before this frame arrives. TypeInteractionDecisionAck = "interaction_decision_ack" // TypeUsage reports a cumulative usage snapshot for the current execution. @@ -100,18 +83,6 @@ type ToolCallPayload struct { Result map[string]any `json:"result,omitempty"` } -// PermissionRequestPayload carries an agent's request for human -// approval. RequestID is the daemon-minted handle used to route a later -// decision. It lives in the payload because Envelope.ID is the run ID used -// by the server gateway to deliver the request to the active run subscriber. -type PermissionRequestPayload struct { - RequestID string `json:"request_id"` - Tool string `json:"tool"` - Title string `json:"title"` - Detail string `json:"detail,omitempty"` - Payload map[string]any `json:"payload,omitempty"` -} - // InteractionDecisionAckPayload is the daemon's application-level receipt // for a server decision. DeliveryID correlates one resolve attempt without // relying on the request id, which may outlive a reconnect or timeout race. @@ -124,44 +95,6 @@ type InteractionDecisionAckPayload struct { Outcome *DonePayload `json:"outcome,omitempty"` } -// PromptForUserChoiceOption is one button / checkbox the user can -// pick when answering a PromptForUserChoice. Label is the human- -// readable choice; Description is optional inline help. -type PromptForUserChoiceOption struct { - Label string `json:"label"` - Description string `json:"description,omitempty"` -} - -// PromptForUserChoiceQuestion is one question in a (possibly multi- -// question) AskUserQuestion call. Mirrors the Claude Code built-in -// schema verbatim so the daemon doesn't translate the shape twice. -type PromptForUserChoiceQuestion struct { - ID string `json:"id"` - Header string `json:"header,omitempty"` - Question string `json:"question"` - MultiSelect bool `json:"multi_select,omitempty"` - IsOther bool `json:"is_other,omitempty"` - IsSecret bool `json:"is_secret,omitempty"` - Options []PromptForUserChoiceOption `json:"options"` -} - -// PromptForUserChoicePayload carries the AskUserQuestion interception. -// -// AskID is the daemon-minted "ask_<8hex>" handle the server uses to -// route SubmitPromptForUserChoice back to the right session. It rides -// on the payload (not Envelope.ID) because Envelope.ID is reserved for -// the run id — that's the field server-side session.dispatch fans on -// to deliver the frame to the run's subscriber channel. ToolUseID is -// the originating Claude Code tool_use id; empty when the call came -// through the control_request channel (CCRequestID then identifies the -// daemon-side waiter instead but it doesn't ride on the wire). -type PromptForUserChoicePayload struct { - AskID string `json:"ask_id"` - Questions []PromptForUserChoiceQuestion `json:"questions"` - ToolUseID string `json:"tool_use_id,omitempty"` - AutoResolutionMs *uint64 `json:"auto_resolution_ms,omitempty"` -} - // TokenUsage is a complete cumulative measurement for the current execution. // Adapters publish observed snapshots promptly; historical resume totals belong // to the adapter baseline, not this execution. Unknown fields must not become zero. @@ -222,7 +155,6 @@ const ( type AgentKindCapabilities struct { SubagentObservations CapabilitySupport `json:"subagent_observations"` Streaming CapabilitySupport `json:"streaming"` - Permissions CapabilitySupport `json:"permissions"` Usage CapabilitySupport `json:"usage"` Resume CapabilitySupport `json:"resume"` NativeSessionRecovery CapabilitySupport `json:"native_session_recovery"` diff --git a/internal/agentdaemon/proto/outbound.go b/internal/agentdaemon/proto/outbound.go index 03e4acfbf..f5f2062a9 100644 --- a/internal/agentdaemon/proto/outbound.go +++ b/internal/agentdaemon/proto/outbound.go @@ -11,22 +11,6 @@ const ( // RunID. Idempotent — cancelling an unknown / already-finished // run is a no-op on the daemon side. TypePromptCancel = "prompt_cancel" - - // TypePermissionDecision delivers a human verdict back to the - // daemon. Envelope.ID = the perm_<8hex> id the daemon minted in - // the matching permission_request. - TypePermissionDecision = "permission_decision" - - // TypePromptForUserChoiceDecision delivers the human's answer back - // to the daemon. Envelope.ID = the ask_<8hex> id the daemon minted - // in the matching prompt_for_user_choice frame. - TypePromptForUserChoiceDecision = "prompt_for_user_choice_decision" - - // TypeDeviceShutdown asks the daemon to exit gracefully (SIGTERM - // child processes, flush state, close the socket). Ignored by - // long-lived local devices unless the operator explicitly - // requested it. - TypeDeviceShutdown = "device_shutdown" ) // PromptRequestPayload is the execution configuration that execution_prepare @@ -77,39 +61,6 @@ type PromptCancelPayload struct { DeliveryID string `json:"delivery_id,omitempty"` } -// PermissionDecisionPayload carries the human verdict. UpdatedInput -// lets the approver edit the tool input before letting the call -// proceed (Claude Code's allow-with-changes path). -type PermissionDecisionPayload struct { - DeliveryID string `json:"delivery_id"` - Approved bool `json:"approved"` - Message string `json:"message,omitempty"` - UpdatedInput map[string]any `json:"updated_input,omitempty"` -} - -// PromptForUserChoiceQuestionAnswer binds answer values to one emitted question ID. -// Headers and array positions never identify a question. -type PromptForUserChoiceQuestionAnswer struct { - QuestionID string `json:"question_id"` - Answers []string `json:"answers"` -} - -// PromptForUserChoiceDecisionPayload carries either explicitly identified answers -// or cancellation. Omitted questions remain unanswered; adapters never reassign -// answers by position or display text. Native choice validation stays in the adapter. -type PromptForUserChoiceDecisionPayload struct { - DeliveryID string `json:"delivery_id"` - QuestionAnswers []PromptForUserChoiceQuestionAnswer `json:"question_answers,omitempty"` - Cancelled bool `json:"cancelled,omitempty"` - Reason string `json:"reason,omitempty"` -} - -// DeviceShutdownPayload tells the daemon why we're closing it (for log -// lines / metrics on the daemon side). Optional. -type DeviceShutdownPayload struct { - Reason string `json:"reason,omitempty"` -} - // ExecutionControls requires both values when supplied; omitting the block preserves agent options. // Send only to a peer advertising execution_controls, independently of older option capabilities. type ExecutionControls struct { diff --git a/internal/agentdaemon/proto/prototest/capabilities.go b/internal/agentdaemon/proto/prototest/capabilities.go index f3337682f..82afd39b5 100644 --- a/internal/agentdaemon/proto/prototest/capabilities.go +++ b/internal/agentdaemon/proto/prototest/capabilities.go @@ -14,7 +14,6 @@ func Capabilities(overrides proto.AgentKindCapabilities) proto.AgentKindCapabili c := proto.AgentKindCapabilities{ SubagentObservations: proto.CapabilityUnsupported, Streaming: proto.CapabilityUnsupported, - Permissions: proto.CapabilityUnsupported, Usage: proto.CapabilityUnsupported, Resume: proto.CapabilityUnsupported, NativeSessionRecovery: proto.CapabilityUnsupported, diff --git a/internal/agentdaemon/proto/tool_call_test.go b/internal/agentdaemon/proto/tool_call_test.go new file mode 100644 index 000000000..d65d9d169 --- /dev/null +++ b/internal/agentdaemon/proto/tool_call_test.go @@ -0,0 +1,13 @@ +package proto + +import ( + "encoding/json" + "testing" +) + +func TestToolCallRejectsEngineSnapshot(t *testing.T) { + var call ToolCallPayload + if json.Unmarshal([]byte(`{"id":"tool","stage":"after","native_item":{"type":"commandExecution"}}`), &call) == nil { + t.Fatal("accepted engine-specific snapshot in common wire") + } +} diff --git a/internal/agentdaemon/proto/user_choice.go b/internal/agentdaemon/proto/user_choice.go deleted file mode 100644 index 3c59f701f..000000000 --- a/internal/agentdaemon/proto/user_choice.go +++ /dev/null @@ -1,67 +0,0 @@ -package proto - -import ( - "bytes" - "encoding/json" - "errors" - "strings" -) - -// UnmarshalJSON rejects fields outside the current decision contract, including -// unrecognized fields nested inside question_answers. Do not silently discard an -// answer from a peer using a different wire shape. -func (p *PromptForUserChoiceDecisionPayload) UnmarshalJSON(raw []byte) error { - if raw = bytes.TrimSpace(raw); len(raw) == 0 || raw[0] != '{' { - return errors.New("user-choice decision must be an object") - } - type decision PromptForUserChoiceDecisionPayload - var value decision - decoder := json.NewDecoder(bytes.NewReader(raw)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(&value); err != nil { - return errors.New("invalid user-choice decision") - } - decoded := PromptForUserChoiceDecisionPayload(value) - if err := decoded.Validate(); err != nil { - return err - } - *p = decoded - return nil -} - -// Validate checks answer identity independently of delivery correlation and -// native question constraints. An empty answer array is an explicit non-answer. -func (p PromptForUserChoiceDecisionPayload) Validate() error { - if p.Cancelled && len(p.QuestionAnswers) != 0 { - return errors.New("cancelled user-choice decision cannot contain answers") - } - seen := make(map[string]bool, len(p.QuestionAnswers)) - for _, answer := range p.QuestionAnswers { - if strings.TrimSpace(answer.QuestionID) == "" || seen[answer.QuestionID] || answer.Answers == nil { - return errors.New("user-choice answers require unique question IDs and answer arrays") - } - seen[answer.QuestionID] = true - } - return nil -} - -// AnswersFor binds a decision to the exact emitted question identities. It never -// derives identity from a header, answer text or array position. Call it before -// consuming the pending interaction or submitting a native response. -func (p PromptForUserChoiceDecisionPayload) AnswersFor(questionIDs []string) (map[string][]string, error) { - if err := p.Validate(); err != nil { - return nil, err - } - known := make(map[string]bool, len(questionIDs)) - for _, id := range questionIDs { - known[id] = true - } - answers := make(map[string][]string, len(p.QuestionAnswers)) - for _, answer := range p.QuestionAnswers { - if !known[answer.QuestionID] { - return nil, errors.New("user-choice answer refers to an unknown question") - } - answers[answer.QuestionID] = answer.Answers - } - return answers, nil -} diff --git a/internal/agentdaemon/proto/user_choice_test.go b/internal/agentdaemon/proto/user_choice_test.go deleted file mode 100644 index 74bfc55cf..000000000 --- a/internal/agentdaemon/proto/user_choice_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package proto - -import ( - "encoding/json" - "reflect" - "testing" -) - -func TestUserChoiceOnlyAcceptsCurrentAnswerShape(t *testing.T) { - for _, raw := range []string{ - `null`, - `{"answers":["yes"]}`, - `{"question_answers":[{"header":"Question","answer":"yes"}]}`, - `{"question_answers":[{"question_id":"q","answers":["yes"],"answer":"no"}]}`, - `{"question_answers":[{"answers":["yes"]}]}`, - `{"question_answers":[{"question_id":"q","answers":null}]}`, - `{"question_answers":[{"question_id":"q","answers":["yes"]},{"question_id":"q","answers":["no"]}]}`, - `{"cancelled":true,"question_answers":[{"question_id":"q","answers":["yes"]}]}`, - } { - var decision PromptForUserChoiceDecisionPayload - if json.Unmarshal([]byte(raw), &decision) == nil { - t.Fatalf("accepted invalid decision: %s", raw) - } - } - var decision PromptForUserChoiceDecisionPayload - if err := json.Unmarshal([]byte(`{"delivery_id":"d","question_answers":[{"question_id":"b","answers":["two,three","four"]},{"question_id":"a","answers":[]}]}`), &decision); err != nil { - t.Fatal(err) - } - answers, err := decision.AnswersFor([]string{"a", "b"}) - if err != nil || !reflect.DeepEqual(answers["b"], []string{"two,three", "four"}) || answers["a"] == nil { - t.Fatalf("answers=%v err=%v", answers, err) - } - if _, err := decision.AnswersFor([]string{"a"}); err == nil { - t.Fatal("accepted a foreign question ID") - } - if _, err := (PromptForUserChoiceDecisionPayload{Cancelled: true}).AnswersFor([]string{"a"}); err != nil { - t.Fatal(err) - } -} - -func TestToolCallRejectsEngineSnapshot(t *testing.T) { - var call ToolCallPayload - if json.Unmarshal([]byte(`{"id":"tool","stage":"after","native_item":{"type":"commandExecution"}}`), &call) == nil { - t.Fatal("accepted engine-specific snapshot in common wire") - } -} diff --git a/internal/agentdaemon/proto/version.go b/internal/agentdaemon/proto/version.go index 9ff1c71ec..101fd8dd3 100644 --- a/internal/agentdaemon/proto/version.go +++ b/internal/agentdaemon/proto/version.go @@ -2,7 +2,7 @@ package proto // Version identifies the complete Core–Runtime wire contract. Change it when // removing or changing a payload or its semantics; deploy both endpoints together. -const Version = "0.11.0" +const Version = "0.12.0" // VersionCompatible accepts only this contract. Patch drift, prerelease suffixes // and malformed versions do not select an implicit compatibility path. diff --git a/services/core/internal/api/turn_diagnostic_failure.go b/services/core/internal/api/turn_diagnostic_failure.go index 4449831de..1f2081511 100644 --- a/services/core/internal/api/turn_diagnostic_failure.go +++ b/services/core/internal/api/turn_diagnostic_failure.go @@ -49,7 +49,7 @@ func turnDiagnosticFailure(turn sessions.Turn) *DiagnosticFailure { code = "delivery_unconfirmed" case "invalid_input", "input_not_applied", "message_input_unsupported", "input_invalid_input", "input_run_inactive", "input_input_conflict", "input_input_limit", "input_unsupported", "input_rejected", "input_not_ready", "input_busy": code = "input_rejected" - case "invalid_executor_result", "interaction_not_supported", "execution_state_unavailable", "execution_state_changed", "function_call_invalid", "function_result_invalid": + case "invalid_executor_result", "execution_state_unavailable", "execution_state_changed", "function_call_invalid", "function_result_invalid": code = "executor_protocol_error" case "event_persistence_failed", "artifact_capture_failed": code = "core_storage_failed" diff --git a/services/core/internal/api/turn_diagnostic_failure_test.go b/services/core/internal/api/turn_diagnostic_failure_test.go index 0cffe47e1..8fe650ca2 100644 --- a/services/core/internal/api/turn_diagnostic_failure_test.go +++ b/services/core/internal/api/turn_diagnostic_failure_test.go @@ -14,7 +14,7 @@ func TestDiagnosticFailureWhitelist(t *testing.T) { "harness_error": {"engine_failed"}, "model_provider_required": {"model_provider_required"}, "runtime_unavailable": {"execution_device_unavailable", "execution_unavailable"}, "runtime_disconnected": {"device_disconnected", "event_stream_incomplete"}, "runtime_preparation_failed": {"preparation_start_failed", "preparation_interrupted"}, "execution_interrupted": {"execution_interrupted"}, "delivery_unconfirmed": {"delivery_unknown", "input_outcome_unknown", "cancel_unconfirmed", "cancel_outcome_unavailable", "function_result_unconfirmed"}, "input_rejected": {"invalid_input", "input_not_applied", "message_input_unsupported", "input_invalid_input", "input_run_inactive", "input_input_conflict", "input_input_limit", "input_unsupported", "input_rejected", "input_not_ready", "input_busy"}, - "executor_protocol_error": {"invalid_executor_result", "interaction_not_supported", "execution_state_unavailable", "execution_state_changed", "function_call_invalid", "function_result_invalid"}, "core_storage_failed": {"event_persistence_failed", "artifact_capture_failed"}, "internal_error": {"secret-canary", "input_secret-canary", "execution_state_secret-canary", ""}, + "executor_protocol_error": {"invalid_executor_result", "execution_state_unavailable", "execution_state_changed", "function_call_invalid", "function_result_invalid"}, "core_storage_failed": {"event_persistence_failed", "artifact_capture_failed"}, "internal_error": {"secret-canary", "input_secret-canary", "execution_state_secret-canary", ""}, } catalog, err := os.ReadFile("../../../../contracts/agents-api/core-errors.md") if err != nil { diff --git a/services/core/internal/execution/delivery.go b/services/core/internal/execution/delivery.go index 70b9c331f..35b4c828b 100644 --- a/services/core/internal/execution/delivery.go +++ b/services/core/internal/execution/delivery.go @@ -250,9 +250,6 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe return } } - case proto.TypePermissionRequest, proto.TypePromptForUserChoice: - result.ErrorCode = "interaction_not_supported" - return } case <-ticker.C: select { diff --git a/services/core/internal/runtimedevice/state.go b/services/core/internal/runtimedevice/state.go index 0d0cf69a0..73f917dc1 100644 --- a/services/core/internal/runtimedevice/state.go +++ b/services/core/internal/runtimedevice/state.go @@ -62,7 +62,6 @@ type HeartbeatStatus struct { type KindCapabilities struct { SubagentObservations bool `json:"subagent_observations,omitempty"` Streaming bool `json:"streaming,omitempty"` - Permissions bool `json:"permissions,omitempty"` Usage bool `json:"usage,omitempty"` Resume bool `json:"resume,omitempty"` NativeSessionRecovery bool `json:"native_session_recovery,omitempty"` diff --git a/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go b/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go index a4b7d2ea1..c0cfe329f 100644 --- a/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go +++ b/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go @@ -60,26 +60,35 @@ func TestLiveMCPBearerGatewayColdContinuation(t *testing.T) { mcpBearerStartDaemon(t, root, daemon, native, provider, fixture.caFile, server.URL, id, runner, capture) ctx, cancel := context.WithTimeout(t.Context(), 6*time.Minute) defer cancel() - peer, err := registry.WaitForDevice(ctx, id, 30*time.Second) - if err != nil { - t.Fatal("built daemon did not connect through the real gateway") - } - t.Cleanup(func() { peer.Close("owned MCP acceptance finished") }) - ready := time.Now().Add(30 * time.Second) - for { - info, found, known := peer.AgentKindStatus("codex") - if known && found && info.Available { - if !info.Capabilities.MCPHTTPTools || !info.Capabilities.MCPHTTPBearerAuth || !info.Capabilities.ToolObservations || !info.Capabilities.DurableTurns || !info.Capabilities.EnvironmentNone { - t.Fatal("built daemon did not advertise the required private execution capabilities") - } - proof["codex_descriptor"] = info - break + var peer *Session + t.Cleanup(func() { + if peer != nil { + peer.Close("owned MCP acceptance finished") + } + }) + connect := func() { + t.Helper() + var err error + if peer, err = registry.WaitForDevice(ctx, id, 30*time.Second); err != nil { + t.Fatal("built daemon did not connect through the real gateway") } - if time.Now().After(ready) { - t.Fatal("pinned Codex capability discovery did not complete") + ready := time.Now().Add(30 * time.Second) + for { + info, found, known := peer.AgentKindStatus("codex") + if known && found && info.Available { + if !info.Capabilities.MCPHTTPTools || !info.Capabilities.MCPHTTPBearerAuth || !info.Capabilities.ToolObservations || !info.Capabilities.DurableTurns || !info.Capabilities.EnvironmentNone { + t.Fatal("built daemon did not advertise the required private execution capabilities") + } + proof["codex_descriptor"] = info + return + } + if time.Now().After(ready) { + t.Fatal("pinned Codex capability discovery did not complete") + } + time.Sleep(50 * time.Millisecond) } - time.Sleep(50 * time.Millisecond) } + connect() allowed, anonymousTools := []string{"remember", "fail"}, []string{"ping"} servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "private_mcp", ServerURL: fixture.private.URL, AllowedTools: &allowed, BearerToken: &token}, {ConnectionOrigin: "service", ServerLabel: "anonymous_mcp", ServerURL: fixture.anonymous.URL, AllowedTools: &anonymousTools}} run := func(prompt, resume string, expected map[string]string) *mcpBearerTurn { @@ -119,8 +128,10 @@ func TestLiveMCPBearerGatewayColdContinuation(t *testing.T) { defer peer.Unsubscribe(runID) send(proto.TypeExecutionStart, prepareID, proto.ExecutionStartPayload{Handle: ready.Handle, ExecutorID: ready.ExecutorID, RunID: runID, Input: proto.TextInput(prompt)}) mcpBearerCollectTurn(t, ctx, sub, runID, turn, expected, token, provider) - // The Executor stays warm after Done; closing it makes the next Turn a cold continuation. - send(proto.TypeDeviceShutdown, "", proto.DeviceShutdownPayload{Reason: "cold continuation"}) + // The Executor stays warm after Done. Reconnecting gives the daemon a fresh + // Router and closes the Executor, so the next Turn is a cold continuation. + peer.Close("cold continuation") + connect() turn.NativeLaunches = mcpBearerReleased(t, root) turn.BearerEnvironmentReference = mcpBearerConfigReference(t, root, token) return turn @@ -170,8 +181,8 @@ func mcpBearerCollectTurn(t *testing.T, ctx context.Context, sub *Subscription, } turn.Events = append(turn.Events, event) switch event.Type { - case proto.TypeError, proto.TypePermissionRequest, proto.TypePromptForUserChoice: - t.Fatal("unexpected execution failure or interaction during private MCP acceptance") + case proto.TypeError: + t.Fatal("unexpected execution failure during private MCP acceptance") case proto.TypeToolCall: var call proto.ToolCallPayload if event.DecodePayload(&call) != nil || call.Observation == nil { diff --git a/services/core/internal/runtimegateway/registry.go b/services/core/internal/runtimegateway/registry.go index 74de0139d..b5984821e 100644 --- a/services/core/internal/runtimegateway/registry.go +++ b/services/core/internal/runtimegateway/registry.go @@ -1,7 +1,7 @@ // Package gateway is the server-side hub of the agent_daemon connector. // It owns the HTTP / WebSocket entry points the daemon dials in to, // per-device long-lived WebSocket sessions, and a process-local -// registry of deviceID/runID/permID → Session for routing. +// registry of deviceID/runID → Session for routing. // // The package deliberately does NOT depend on the connector // implementation — the connector imports it, not the other way around. @@ -18,31 +18,18 @@ import ( // asks for a device the gateway has no live session for. var ErrDeviceNotRegistered = errors.New("agentdaemon gateway: device not registered (offline / never connected)") -// ErrPermissionNotRegistered is returned when SubmitPermission arrives -// for a perm id we don't have a pending mapping for. -var ErrPermissionNotRegistered = errors.New("agentdaemon gateway: permission id not registered (expired / unknown)") - -// ErrPromptForUserChoiceNotRegistered is returned when -// SubmitPromptForUserChoice arrives for an ask id we don't have a -// pending mapping for. Same race semantics as the permission variant -// (cancelled / expired / never seen). -var ErrPromptForUserChoiceNotRegistered = errors.New("agentdaemon gateway: prompt_for_user_choice id not registered (expired / unknown)") - // ErrWaitForDeviceTimeout is returned by WaitForDevice when the // deadline expires before a daemon dials in. var ErrWaitForDeviceTimeout = errors.New("agentdaemon gateway: timed out waiting for device to register") // Registry is the process-wide map of live daemon sessions. It is // concurrency-safe; readers and writers live in different goroutines. -// Four O(1) indexes are maintained: byDevice (primary), byRun (per -// Subscribe), byPerm (per permission_request), byAsk (per -// prompt_for_user_choice). +// Two O(1) indexes are maintained: byDevice (primary) and byRun (per +// Subscribe). type Registry struct { mu sync.RWMutex byDevice map[string]*Session byRun map[string]*Session - byPerm map[string]*Session - byAsk map[string]*Session // waiters holds buffered(1) channels that WaitForDevice callers // are blocked on. Register drains the slice the moment a session @@ -58,15 +45,13 @@ func NewRegistry() *Registry { return &Registry{ byDevice: map[string]*Session{}, byRun: map[string]*Session{}, - byPerm: map[string]*Session{}, - byAsk: map[string]*Session{}, waiters: map[string][]chan *Session{}, } } // Register adds a freshly-upgraded session under its deviceID. The // latest dial-in wins: if a session was already registered for that -// device, the old one's run/perm indexes are evicted (the caller +// device, the old one's run index entries are evicted (the caller // closes the displaced *Session). func (r *Registry) Register(sess *Session) (previous *Session) { r.mu.Lock() @@ -80,16 +65,6 @@ func (r *Registry) Register(sess *Session) (previous *Session) { delete(r.byRun, runID) } } - for permID, s := range r.byPerm { - if s == previous { - delete(r.byPerm, permID) - } - } - for askID, s := range r.byAsk { - if s == previous { - delete(r.byAsk, askID) - } - } } r.byDevice[sess.DeviceID] = sess @@ -123,16 +98,6 @@ func (r *Registry) Deregister(sess *Session) { delete(r.byRun, runID) } } - for permID, s := range r.byPerm { - if s == sess { - delete(r.byPerm, permID) - } - } - for askID, s := range r.byAsk { - if s == sess { - delete(r.byAsk, askID) - } - } } // LookupDevice returns the registered session for a device, or @@ -175,74 +140,6 @@ func (r *Registry) DetachRun(runID string) { delete(r.byRun, runID) } -// AttachPermission records a permID -> session mapping so a later -// SubmitPermission can route the decision frame to the right device. -func (r *Registry) AttachPermission(permID string, sess *Session) { - if permID == "" || sess == nil { - return - } - r.mu.Lock() - defer r.mu.Unlock() - r.byPerm[permID] = sess -} - -// DetachPermission clears the permID -> session mapping. Idempotent. -func (r *Registry) DetachPermission(permID string) { - if permID == "" { - return - } - r.mu.Lock() - defer r.mu.Unlock() - delete(r.byPerm, permID) -} - -// LookupPermission returns the session that owns a pending permID, -// or ErrPermissionNotRegistered when the permission has expired / -// been cancelled. -func (r *Registry) LookupPermission(permID string) (*Session, error) { - r.mu.RLock() - defer r.mu.RUnlock() - if sess, ok := r.byPerm[permID]; ok { - return sess, nil - } - return nil, ErrPermissionNotRegistered -} - -// AttachPromptForUserChoice records askID → session so a later -// SubmitPromptForUserChoice can route the decision back to the right -// daemon. Mirrors AttachPermission. -func (r *Registry) AttachPromptForUserChoice(askID string, sess *Session) { - if askID == "" || sess == nil { - return - } - r.mu.Lock() - defer r.mu.Unlock() - r.byAsk[askID] = sess -} - -// DetachPromptForUserChoice clears the askID → session mapping. -// Idempotent. -func (r *Registry) DetachPromptForUserChoice(askID string) { - if askID == "" { - return - } - r.mu.Lock() - defer r.mu.Unlock() - delete(r.byAsk, askID) -} - -// LookupPromptForUserChoice returns the session that owns a pending -// askID, or ErrPromptForUserChoiceNotRegistered when the ask has -// expired or been cancelled. -func (r *Registry) LookupPromptForUserChoice(askID string) (*Session, error) { - r.mu.RLock() - defer r.mu.RUnlock() - if sess, ok := r.byAsk[askID]; ok { - return sess, nil - } - return nil, ErrPromptForUserChoiceNotRegistered -} - // Devices returns a snapshot of registered device ids in arbitrary order. func (r *Registry) Devices() []string { r.mu.RLock() diff --git a/services/core/internal/runtimegateway/session.go b/services/core/internal/runtimegateway/session.go index 7ae93c4d0..6e7f0f707 100644 --- a/services/core/internal/runtimegateway/session.go +++ b/services/core/internal/runtimegateway/session.go @@ -34,7 +34,7 @@ var ( WriteTimeout = 10 * time.Second // InteractionAckTimeout bounds the application-level round trip for a - // permission or user-input decision after it is written to the daemon. + // function result or cancellation after it is written to the daemon. InteractionAckTimeout = 15 * time.Second // ReadLimit caps a single inbound frame at 4 MiB. tool_call @@ -302,8 +302,8 @@ func (s *Session) Send(ctx context.Context, env proto.Envelope) error { // SendAndWaitInteractionAck sends a decision and waits until the daemon // confirms that its agent session applied it. Queueing or writing the -// WebSocket frame alone is not success: without this receipt the canonical -// database interaction must remain retryable. +// WebSocket frame alone is not success: without this receipt the durable +// delivery must remain retryable. func (s *Session) SendAndWaitInteractionAck(ctx context.Context, env proto.Envelope, deliveryID string) (proto.InteractionDecisionAckPayload, error) { deliveryID = strings.TrimSpace(deliveryID) if deliveryID == "" { @@ -336,7 +336,7 @@ func (s *Session) SendAndWaitInteractionAck(ctx context.Context, env proto.Envel case <-waitCtx.Done(): // If the ack raced the deadline, prefer the application receipt; // treating an already-applied decision as retryable can trigger a - // contradictory second human response. + // contradictory second response. select { case ack := <-waiter: return ack, nil @@ -504,7 +504,6 @@ func deviceKindsFromHeartbeat(p proto.HeartbeatPayload) []runtimedevice.Supporte Version: info.Version, Capabilities: runtimedevice.KindCapabilities{ Streaming: info.Capabilities.Streaming.IsSupported(), - Permissions: info.Capabilities.Permissions.IsSupported(), Usage: info.Capabilities.Usage.IsSupported(), Resume: info.Capabilities.Resume.IsSupported(), Steering: info.Capabilities.Steering.IsSupported(), @@ -563,29 +562,6 @@ func (s *Session) dispatch(env proto.Envelope) { case proto.TypeHeartbeat: s.handleHeartbeat(env) return - case proto.TypePermissionRequest: - var p proto.PermissionRequestPayload - requestID := "" - if err := env.DecodePayload(&p); err == nil { - requestID = strings.TrimSpace(p.RequestID) - } - if requestID == "" { - return - } - s.reg.AttachPermission(requestID, s) - case proto.TypePermissionCancel: - if env.ID != "" { - s.reg.DetachPermission(env.ID) - } - case proto.TypePromptForUserChoice: - // env.ID is the run id (so the fan path below delivers this - // frame to the run's subscriber). The ask id rides on the - // payload — pull it out so SubmitPromptForUserChoice can find - // the session by ask id via the byAsk index. - var p proto.PromptForUserChoicePayload - if err := env.DecodePayload(&p); err == nil && p.AskID != "" { - s.reg.AttachPromptForUserChoice(p.AskID, s) - } case proto.TypeInteractionDecisionAck: var ack proto.InteractionDecisionAckPayload if err := env.DecodePayload(&ack); err != nil { @@ -606,9 +582,7 @@ func (s *Session) dispatch(env proto.Envelope) { return } - // All run-correlated frames fan to the matching subscriber. Current - // permission and prompt-for-user-choice frames keep their interaction ID - // in the payload so Envelope.ID remains the run ID. + // All run-correlated frames fan to the matching subscriber. if env.ID == "" { return } diff --git a/services/core/internal/runtimegateway/session_test.go b/services/core/internal/runtimegateway/session_test.go index 9990ef097..71eaca263 100644 --- a/services/core/internal/runtimegateway/session_test.go +++ b/services/core/internal/runtimegateway/session_test.go @@ -162,21 +162,17 @@ func TestRegistry_RegisterReplacesAndEvictsRuns(t *testing.T) { old := NewSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) reg.Register(old) reg.AttachRun("run-1", old) - reg.AttachPermission("perm-1", old) new := NewSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) prev := reg.Register(new) if prev != old { t.Fatalf("expected old session as displaced previous, got %p", prev) } - // Old session's run/perm indexes must be cleared so a stale + // Old session's run index must be cleared so a stale // Cancel can't be routed to the wrong session. if got := reg.LookupRun("run-1"); got != nil { t.Fatalf("expected run-1 mapping cleared, got %p", got) } - if _, err := reg.LookupPermission("perm-1"); !errors.Is(err, ErrPermissionNotRegistered) { - t.Fatalf("expected perm-1 cleared, got %v", err) - } } func TestRegistry_DeregisterPreservesNewer(t *testing.T) { @@ -257,48 +253,6 @@ func TestSession_DoneFrameAutoUnsubscribes(t *testing.T) { } } -func TestSession_PermissionRequestIndexedInRegistry(t *testing.T) { - reg := NewRegistry() - conn := newFakeConn() - sess := NewSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) - sess.Start() - defer sess.Close("test done") - - sub, err := sess.SubscribeDurable("run-1") - ch := sub.Events - if err != nil { - t.Fatalf("subscribe: %v", err) - } - env, _ := proto.NewEnvelope(proto.TypePermissionRequest, "run-1", proto.PermissionRequestPayload{ - RequestID: "perm-abc", - Tool: "Bash", - Title: "rm -rf /tmp/scratch", - }) - raw, _ := jsonMarshal(env) - conn.Feed(raw) - - // Poll because the read loop is async. - deadline := time.Now().Add(2 * time.Second) - for { - if got, err := reg.LookupPermission("perm-abc"); err == nil && got == sess { - break - } - if time.Now().After(deadline) { - t.Fatal("perm-abc never indexed in registry") - } - time.Sleep(10 * time.Millisecond) - } - - select { - case got := <-ch: - if got.ID != "run-1" || got.Type != proto.TypePermissionRequest { - t.Fatalf("subscriber received %+v", got) - } - case <-time.After(2 * time.Second): - t.Fatal("run subscriber never received permission request") - } -} - func TestSession_CloseReportsUnknownWithoutExecutionEvents(t *testing.T) { sess := NewSession(newFakeConn(), "device", "tenant", proto.Version, NewRegistry(), nil) sub, err := sess.SubscribeDurable("run") @@ -390,10 +344,9 @@ func TestSession_HeartbeatPersistsSupportedAgentKinds(t *testing.T) { Available: true, Version: "1.2.3", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{ - Streaming: proto.CapabilitySupported, - Permissions: proto.CapabilitySupported, - Usage: proto.CapabilitySupported, - Resume: proto.CapabilitySupported, + Streaming: proto.CapabilitySupported, + Usage: proto.CapabilitySupported, + Resume: proto.CapabilitySupported, }), }, { @@ -418,7 +371,7 @@ func TestSession_HeartbeatPersistsSupportedAgentKinds(t *testing.T) { byKind[info.Kind] = info } claude := byKind["fake_alpha"] - if !claude.Available || claude.Version != "1.2.3" || !claude.Capabilities.Permissions || !claude.Capabilities.Usage || !claude.Capabilities.Resume { + if !claude.Available || claude.Version != "1.2.3" || !claude.Capabilities.Streaming || !claude.Capabilities.Usage || !claude.Capabilities.Resume { t.Fatalf("fake_alpha descriptor not converted: %#v", claude) } fake_beta := byKind["fake_beta"] @@ -453,32 +406,6 @@ func TestSession_HeartbeatDoesNotInferCapabilities(t *testing.T) { } } -func TestSession_PermissionRequiresPayloadIdentity(t *testing.T) { - reg := NewRegistry() - sess := NewSession(newFakeConn(), "device", "tenant", proto.Version, reg, nil) - defer sess.Close("test done") - sub, err := sess.SubscribeDurable("run") - if err != nil { - t.Fatal(err) - } - env, _ := proto.NewEnvelope(proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{Tool: "test"}) - sess.dispatch(env) - if _, err := reg.LookupPermission("run"); !errors.Is(err, ErrPermissionNotRegistered) { - t.Fatal("run ID was treated as an interaction ID") - } - if len(sub.Events) != 0 { - t.Fatal("invalid permission request was forwarded") - } - env, _ = proto.NewEnvelope(proto.TypePermissionRequest, "run", proto.PermissionRequestPayload{RequestID: "permission", Tool: "test"}) - sess.dispatch(env) - if got, err := reg.LookupPermission("permission"); err != nil || got != sess { - t.Fatalf("declared interaction identity was not registered: %v", err) - } - if got := <-sub.Events; got.ID != "run" { - t.Fatal("run correlation lost") - } -} - // jsonMarshal aliases encoding/json.Marshal so call sites read cleanly. func jsonMarshal(v any) ([]byte, error) { return json.Marshal(v) diff --git a/services/core/tests/integration/runtime_compute_lifecycle_test.go b/services/core/tests/integration/runtime_compute_lifecycle_test.go index 7d3286718..c6949a6de 100644 --- a/services/core/tests/integration/runtime_compute_lifecycle_test.go +++ b/services/core/tests/integration/runtime_compute_lifecycle_test.go @@ -216,9 +216,6 @@ func (p *fakeCheckpointProvider) connect(ctx context.Context, b sandbox.Bootstra } continue } - if env.Type == proto.TypeDeviceShutdown { - return - } if env.Type != proto.TypeEnvironmentQuiesce && env.Type != proto.TypeEnvironmentResume { p.mu.Lock() p.promptFrames.Add(1) From 0a0328cfc80b59e5f511e8406ed232272b5283ae Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 21:28:44 +0800 Subject: [PATCH 06/14] Bound the Link relay's resources, attachments, closures and pending decisions (#488) * Bound the Link relay's resources, attachments and closures The relay held serve peers, attachments and pending AttachmentClosed events without limit, so peers could grow Core's memory without bound. Each now takes a slot at admission, before the Authority is consulted, and a Hello or Open beyond capacity is refused with LimitExceeded and leaves no state: - A resource holds one of 4096 slots from the serve Hello that first names it while the relay holds anything for it: its serve peer, a Hello being decided, an attachment or an unwritten event. Its generation, serve peer and events now live in one record, so the generation map no longer grows with every resource ever served. - An attachment holds one of 16384 slots until it is closed and its AttachmentClosed events are written or discarded, so pending events are bounded by construction. - An attach link holds one of 4096 slots until it ends. RevokeResource now discards the events its revoked serve peer could not read, since its authority is withdrawn and it cannot reconnect; otherwise every destroyed sandbox would keep its slots. An Open of an attachment that closes while the Authority decides fails with LeaseExpired instead of recreating it without a slot. * Bound the Link relay's pending Hellos and Opens A serve Hello now takes one of two Hello slots of its resource until it is decided, so later Hellos for a held resource no longer wait on the Authority without limit. An Open takes a stream slot of its attach link before the Authority decides and keeps it until its decision ends, even when the peer resets the stream, and an attach link keeps its slot until every stream and renewal it carried has finished. An Open whose attachment closes while it is decided now fails with LeaseExpired even when another Open has created an attachment under the same ID since. --- docs/sandbox-link-protocol.md | 29 +- docs/zh/sandbox-link-protocol.md | 31 +- internal/sandboxlink/relay/export_test.go | 38 +++ internal/sandboxlink/relay/relay.go | 348 ++++++++++++++++------ internal/sandboxlink/relay/relay_test.go | 270 ++++++++++++++++- 5 files changed, 601 insertions(+), 115 deletions(-) create mode 100644 internal/sandboxlink/relay/export_test.go diff --git a/docs/sandbox-link-protocol.md b/docs/sandbox-link-protocol.md index 874594390..3d3d61c92 100644 --- a/docs/sandbox-link-protocol.md +++ b/docs/sandbox-link-protocol.md @@ -41,7 +41,15 @@ An attachment outlives its link. After reconnecting, the Runtime opens a stream ## Run a relay -`relay.New` takes an `Authority` and returns a `*relay.Relay`, which is an `http.Handler`. The relay endpoint is served behind the installation's HTTPS ingress, which terminates TLS, so the handler accepts the upgrade on the ingress's plain HTTP hop; peers enforce TLS when they dial. Each link carries at most 256 concurrent service streams. +`relay.New` takes an `Authority` and returns a `*relay.Relay`, which is an `http.Handler`. The relay endpoint is served behind the installation's HTTPS ingress, which terminates TLS, so the handler accepts the upgrade on the ingress's plain HTTP hop; peers enforce TLS when they dial. + +Each link carries at most 256 concurrent service streams, and each resource has at most 2 serve Hellos being decided. The relay also holds at most 4096 resources, 16384 attachments and 4096 attach links. A Hello or an Open takes the slots it needs before the Authority is consulted. When one is not free, it is refused with `LimitExceeded` and leaves nothing behind: + +- A serve Hello takes one of its resource's Hello slots until it is decided. +- A resource takes a slot at the serve Hello that first names it and keeps it while the relay holds anything for it: its serve peer, a Hello being decided, an attachment or an unwritten `AttachmentClosed` event. A reconnect of a held resource needs no new resource slot. A resource has at most one serve link, so the resource limit also bounds serve links and, with the Hello slots, the serve Hellos being decided. +- An Open takes a stream slot of its attach link. An opened stream keeps it until the stream ends; a refused Open gives it back once it is decided, even when the peer reset the stream first. +- An attachment takes a slot at the Open that creates it and keeps it until it is closed and its `AttachmentClosed` events are written or discarded. +- An attach link takes a slot at its Hello and keeps it until it ends and every stream and renewal it carried has finished. The owner of the relay implements `Authority` from its durable records, and the relay consults it for every Hello, Open and renewal. To revoke, withdraw the authority first, then call `RevokeAttachment` or `RevokeResource` so the relay closes what it holds. @@ -194,7 +202,7 @@ AttachmentClosed 1. The peer dials the relay's URL: `wss://`, or `ws://` only when the host is `localhost` or a loopback address. The URL carries no user, query or fragment, and never a credential. `sandboxlink.CheckRelayURL` applies this rule for both peers and the [bootstrap input](./sandbox-bootstrap.md) and refuses any other URL with `sandboxlink.ErrRelayURL`. Over `wss://`, TLS authenticates the relay. 2. The peer starts yamux and opens the control stream. 3. It sends a Hello as the first request of the control stream, with request ID 1: `ServeHello` from the Sandbox I/O service, `AttachHello` from a Runtime. Credentials travel only in the Hello. -4. The relay authenticates the peer with its Authority and answers `HelloAccepted`, or a failure after which the link ends. A Hello of another version is answered `VersionMismatch` without reading past its version. When a revocation lands while the Authority decides a serve Hello, the relay asks again, so a withdrawn credential never installs a serve peer. +4. The relay takes the [slot](#run-a-relay) the Hello needs, authenticates the peer with its Authority and answers `HelloAccepted`, or a failure after which the link ends. A Hello of another version is answered `VersionMismatch` without reading past its version. When a revocation lands while the Authority decides a serve Hello, the relay asks again, so a withdrawn credential never installs a serve peer. Later control requests continue the Hello's request IDs. The relay ends an attach link whose request ID does not increase with `ProtocolViolation`. `Open` and `Bind` are each the only request on their stream and use request ID 1. @@ -206,10 +214,11 @@ The relay and the serve peer bound each handshake step, the WebSocket upgrade, t The attach peer opens a stream and sends `Open`. The relay then: -1. Calls `Authority.AuthorizeOpen`. The Authority checks the grant, the Runtime, the current assignment and its epoch, the resource generation, the permitted service and access, and that the resource's serve authority is current. It returns the binding identity, the service, the lease, the exports for `ServiceFile` and the egress rules for `ServiceNetwork`. When a revocation lands while the Authority decides, the relay asks again. -2. Checks, in order: each link's stream limit (`LimitExceeded`); that the newest generation the relay has seen for the resource is not newer than the Open's (`StaleGeneration`); that a serve peer of the Open's generation is connected and offers the service (`ServiceUnavailable`) at the Open's version (`VersionMismatch`); that a nonzero `ExpectedServerInstanceID` equals the serve peer's (`InstanceChanged`); that the lease lies in the future (`LeaseExpired`); and that an attachment the relay already holds under this `AttachmentID` has the identical identity and Runtime (`AttachmentConflict`). -3. Opens a stream to the serve peer and sends `Bind`. The serve peer answers `Bound`, or a failure: `ServiceUnavailable` for a service it does not serve, `VersionMismatch`, `InstanceChanged` when `ExpectedServerInstanceID` is not its own, `LeaseExpired` for a recently closed attachment, or `ProtocolViolation`. The relay passes a failure on to the attach peer. When the `Bind` began to be sent but no answer arrives, the relay answers `ServiceUnavailable` with `EffectPossible`. -4. Answers `Opened` and splices the two streams. +1. Takes a stream [slot](#run-a-relay) of the link, and an attachment slot when it holds no attachment under the Open's `AttachmentID`, or answers `LimitExceeded` when one is not free. +2. Calls `Authority.AuthorizeOpen`. The Authority checks the grant, the Runtime, the current assignment and its epoch, the resource generation, the permitted service and access, and that the resource's serve authority is current. It returns the binding identity, the service, the lease, the exports for `ServiceFile` and the egress rules for `ServiceNetwork`. When a revocation lands while the Authority decides, the relay asks again. +3. Checks, in order: the serve link's stream limit (`LimitExceeded`); that the newest generation the relay holds for the resource is not newer than the Open's (`StaleGeneration`); that a serve peer of the Open's generation is connected and offers the service (`ServiceUnavailable`) at the Open's version (`VersionMismatch`); that a nonzero `ExpectedServerInstanceID` equals the serve peer's (`InstanceChanged`); that the lease lies in the future and that an attachment the relay held when the Open arrived has not closed since (`LeaseExpired`); and that an attachment the relay already holds under this `AttachmentID` has the identical identity and Runtime (`AttachmentConflict`). +4. Opens a stream to the serve peer and sends `Bind`. The serve peer answers `Bound`, or a failure: `ServiceUnavailable` for a service it does not serve, `VersionMismatch`, `InstanceChanged` when `ExpectedServerInstanceID` is not its own, `LeaseExpired` for a recently closed attachment, or `ProtocolViolation`. The relay passes a failure on to the attach peer. When the `Bind` began to be sent but no answer arrives, the relay answers `ServiceUnavailable` with `EffectPossible`. +5. Answers `Opened` and splices the two streams. An attachment's binding identity is its `AttachmentID`, `Resource`, `SessionID`, `AssignmentID` and `AssignmentEpoch`. Reopening an attachment, on the same link or a later one, requires the identical identity from the same Runtime and a current authorization. @@ -231,7 +240,7 @@ The relay keeps links, attachments and leases in memory. The Authority stays the A method returns a `*sandboxlink.Error` for a typed refusal; any other error is answered `ServiceUnavailable`. Credential revision and allowed access come from the Authority, never from what a peer asserts. -A recreated resource has a higher generation. A serve peer of the same or a higher generation replaces the resource's current serve peer, and a higher generation also closes every attachment of an older generation with `CloseStaleGeneration`. A serve peer or an Open of a generation older than the newest the relay has seen is refused with `StaleGeneration`. +A recreated resource has a higher generation. A serve peer of the same or a higher generation replaces the resource's current serve peer, and a higher generation also closes every attachment of an older generation with `CloseStaleGeneration`. A serve peer or an Open of a generation older than the newest the relay holds for the resource is refused with `StaleGeneration`. The relay keeps a resource's generation only while it [holds the resource](#run-a-relay); after that the Authority alone refuses older generations. ## Leases, closing and revocation @@ -240,7 +249,7 @@ A recreated resource has a higher generation. A serve peer of the same or a high - `CloseAttachment` closes the caller's attachment with `CloseRequested`. Closing an unknown attachment succeeds, and closing another Runtime's returns `PermissionDenied`. - `Relay.RevokeAttachment` closes one attachment. `Relay.RevokeResource` closes every attachment of a resource generation and older, writes the serve peer their `AttachmentClosed` events and then disconnects it. Both close with `CloseRevoked`. -Closing an attachment resets all its streams and sends `AttachmentClosed` to its attach peer, except after `CloseRequested`, and to the serve peer. The relay holds each serve peer's unwritten events in a set with no size limit and removes an event only once it is written, so a serve peer that is disconnected, or whose link drops before the event is written, receives it when it reconnects with the same generation. An Open that a close interrupts fails with `AttachmentConflict`, `LeaseExpired`, `PermissionDenied` or `StaleGeneration`, matching the reason, with `EffectPossible` when its `Bind` may have reached the serve peer. +Closing an attachment resets all its streams and sends `AttachmentClosed` to its attach peer, except after `CloseRequested`, and to the serve peer. The relay holds each peer's unwritten events in a set and removes an event once it is written, so a serve peer that is disconnected, or whose link drops before the event is written, receives it when it reconnects with the same generation. Events no peer can read any more are discarded: an attach peer's when its link ends, and a serve peer's when a newer generation connects or `RevokeResource` revokes its generation. An Open that a close interrupts fails with `AttachmentConflict`, `LeaseExpired`, `PermissionDenied` or `StaleGeneration`, matching the reason, with `EffectPossible` when its `Bind` may have reached the serve peer. Losing a link resets the streams it carries and keeps its attachments until their leases expire or they are closed. @@ -269,7 +278,7 @@ The relay copies each direction through a 32 KiB buffer and holds at most one 25 | 8 | `InstanceChanged` | The serve peer's `ServerInstanceID` is not `ExpectedServerInstanceID` | | 9 | `LeaseExpired` | The lease has passed, or the relay no longer holds the attachment | | 10 | `AttachmentConflict` | The `AttachmentID` is held with another identity or Runtime, or was closed during the Open | -| 11 | `LimitExceeded` | A link's stream limit or its limit of renewals being decided is reached | +| 11 | `LimitExceeded` | A Hello or an Open needs a [slot](#run-a-relay) that is not free, a serve link has its limit of streams, or an attach link has its limit of renewals being decided | | 12 | `ProtocolViolation` | A message is malformed, not allowed where it arrived, or carries a request ID that does not increase | `ServiceUnavailable` and `LimitExceeded` are transient: the same request may succeed later, and `Code.Retryable` reports them. Every other code is final: repeating the request with the same credential, attachment and generation fails again. @@ -278,4 +287,4 @@ An answer that is malformed, or that carries another request ID or operation tha ## Verification -`go test ./internal/sandboxlink/...` covers the golden frames, decode rejection, and the relay's authorization, generation, lease, revocation, renewal bound and reconnect behavior, including orderly end and abort propagation. `go test -run '^$' -fuzz FuzzDecode ./internal/sandboxlink` fuzzes the decoder. +`go test ./internal/sandboxlink/...` covers the golden frames, decode rejection, and the relay's authorization, generation, lease, revocation, capacity and renewal bounds, and reconnect behavior, including orderly end and abort propagation. `go test -run '^$' -fuzz FuzzDecode ./internal/sandboxlink` fuzzes the decoder. diff --git a/docs/zh/sandbox-link-protocol.md b/docs/zh/sandbox-link-protocol.md index 4056fec05..99ca95126 100644 --- a/docs/zh/sandbox-link-protocol.md +++ b/docs/zh/sandbox-link-protocol.md @@ -1,7 +1,7 @@ --- title: "沙箱 Link 协议" source: docs/sandbox-link-protocol.md -source_hash: ea2004916489420736d6bc73b31f1e26b409747406c20bf646729a2fe0478c41 +source_hash: f9c047631990775331946f78c7fad835a0663932ac6a2d4bd7c2514405f00b6f --- Link 协议通过 relay 连接沙箱 I/O 的两端。Sandbox I/O 服务运行在沙箱内并为其提供服务,是 serve peer。agent host 上的 Runtime 在沙箱外运行 Harness,并通过该服务使用沙箱,是 attach peer。每个 peer 各自向 relay 认证自己的 link。relay 授权 attach peer 打开的每个服务 stream,将其绑定到该资源当前的 serve peer,然后在两个 stream 之间复制字节而不读取内容。服务帧从不携带凭据或 grant。 @@ -43,7 +43,15 @@ attachment 的生命周期长于其 link。重连后,Runtime 使用相同的 b ## 运行 relay {#run-a-relay} -`relay.New` 接收 `Authority`,返回 `*relay.Relay`,它是一个 `http.Handler`。relay endpoint 位于安装实例的 HTTPS ingress 之后,由 ingress 终止 TLS,因此 handler 在 ingress 的明文 HTTP 一跳上接受 upgrade;peer 在拨号时强制 TLS。每条 link 最多承载 256 个并发服务 stream。 +`relay.New` 接收 `Authority`,返回 `*relay.Relay`,它是一个 `http.Handler`。relay endpoint 位于安装实例的 HTTPS ingress 之后,由 ingress 终止 TLS,因此 handler 在 ingress 的明文 HTTP 一跳上接受 upgrade;peer 在拨号时强制 TLS。 + +每条 link 最多承载 256 个并发服务 stream,每个资源最多有 2 个裁决中的 serve Hello。relay 还最多持有 4096 个资源、16384 个 attachment 和 4096 条 attach link。Hello 或 Open 在咨询 Authority 之前占用它所需的名额。某个名额没有空闲时,它会以 `LimitExceeded` 被拒绝,且不留下任何状态: + +- serve Hello 在裁决结束前占用其资源的一个 Hello 名额。 +- 资源在首次指明它的 serve Hello 处占用名额,并在 relay 仍为它持有任何东西时保留该名额:其 serve peer、裁决中的 Hello、attachment 或未写出的 `AttachmentClosed` 事件。已持有资源的重连不需要新的资源名额。一个资源最多有一条 serve link,因此资源上限也限制了 serve link 的数量,并与 Hello 名额一起限制了裁决中的 serve Hello 数量。 +- Open 占用其 attach link 的一个 stream 名额。已打开的 stream 保留该名额直到 stream 结束;被拒绝的 Open 在裁决结束后归还名额,即使 peer 已先重置该 stream。 +- attachment 在创建它的 Open 处占用名额,并保留到它被关闭且其 `AttachmentClosed` 事件已写出或丢弃为止。 +- attach link 在其 Hello 处占用名额,并保留到 link 结束且它承载过的每个 stream 和续期都已结束为止。 relay 的 owner 基于其持久记录实现 `Authority`,relay 对每个 Hello、Open 和续期都咨询它。撤销时,先撤回授权,再调用 `RevokeAttachment` 或 `RevokeResource`,让 relay 关闭其持有的对象。 @@ -196,7 +204,7 @@ AttachmentClosed 1. peer 拨号 relay 的 URL:使用 `wss://`;仅当主机为 `localhost` 或 loopback 地址时可用 `ws://`。URL 不含 user、查询或片段,也绝不含凭据。`sandboxlink.CheckRelayURL` 对两个 peer 和[引导输入](./sandbox-bootstrap.md)应用此规则,并以 `sandboxlink.ErrRelayURL` 拒绝其他 URL。使用 `wss://` 时,由 TLS 认证 relay。 2. peer 启动 yamux 并打开 control stream。 3. 它以请求 ID 1 发送 Hello,作为 control stream 的第一个请求:Sandbox I/O 服务发送 `ServeHello`,Runtime 发送 `AttachHello`。凭据只在 Hello 中传输。 -4. relay 通过其 Authority 认证 peer,并回复 `HelloAccepted`,或回复失败,随后结束 link。对其他版本的 Hello,relay 不读取版本之后的内容,直接回复 `VersionMismatch`。如果 Authority 裁决 serve Hello 期间发生撤销,relay 会再次询问,因此已撤回的凭据绝不会建立 serve peer。 +4. relay 占用该 Hello 所需的[名额](#run-a-relay),通过其 Authority 认证 peer,并回复 `HelloAccepted`,或回复失败,随后结束 link。对其他版本的 Hello,relay 不读取版本之后的内容,直接回复 `VersionMismatch`。如果 Authority 裁决 serve Hello 期间发生撤销,relay 会再次询问,因此已撤回的凭据绝不会建立 serve peer。 后续控制请求延续 Hello 的请求 ID。attach link 的请求 ID 未递增时,relay 以 `ProtocolViolation` 结束该 link。`Open` 和 `Bind` 各自是其 stream 上唯一的请求,使用请求 ID 1。 @@ -208,10 +216,11 @@ relay 和 serve peer 以 `sandboxlink.HandshakeTimeout`(10 秒)限制每个 attach peer 打开一个 stream 并发送 `Open`。relay 随后: -1. 调用 `Authority.AuthorizeOpen`。Authority 检查 grant、Runtime、当前 assignment 及其 epoch、资源 generation、允许的服务和访问权限,以及资源的 serve 授权是否当前有效。它返回 binding 身份、服务、lease、`ServiceFile` 的 export 和 `ServiceNetwork` 的 egress 规则。如果 Authority 裁决期间发生撤销,relay 会再次询问。 -2. 依次检查:每条 link 的 stream 上限(`LimitExceeded`);relay 见过的该资源最新 generation 不比 Open 中的更新(`StaleGeneration`);Open 所指 generation 的 serve peer 已连接并提供该服务(`ServiceUnavailable`),且支持 Open 的版本(`VersionMismatch`);非零的 `ExpectedServerInstanceID` 等于该 serve peer 的值(`InstanceChanged`);lease 尚未到期(`LeaseExpired`);relay 已以该 `AttachmentID` 持有的 attachment 具有完全相同的身份和 Runtime(`AttachmentConflict`)。 -3. 向 serve peer 打开 stream 并发送 `Bind`。serve peer 回复 `Bound`,或回复失败:对其不提供的服务回复 `ServiceUnavailable`,或回复 `VersionMismatch`,`ExpectedServerInstanceID` 不是自身值时回复 `InstanceChanged`,对最近关闭的 attachment 回复 `LeaseExpired`,或回复 `ProtocolViolation`。relay 将失败转交给 attach peer。`Bind` 已开始发送但未收到回复时,relay 回复带 `EffectPossible` 的 `ServiceUnavailable`。 -4. 回复 `Opened`,并将两个 stream 拼接起来。 +1. 占用该 link 的一个 stream [名额](#run-a-relay),并在 relay 未以 Open 的 `AttachmentID` 持有 attachment 时占用一个 attachment 名额;某个名额没有空闲时回复 `LimitExceeded`。 +2. 调用 `Authority.AuthorizeOpen`。Authority 检查 grant、Runtime、当前 assignment 及其 epoch、资源 generation、允许的服务和访问权限,以及资源的 serve 授权是否当前有效。它返回 binding 身份、服务、lease、`ServiceFile` 的 export 和 `ServiceNetwork` 的 egress 规则。如果 Authority 裁决期间发生撤销,relay 会再次询问。 +3. 依次检查:serve link 的 stream 上限(`LimitExceeded`);relay 为该资源持有的最新 generation 不比 Open 中的更新(`StaleGeneration`);Open 所指 generation 的 serve peer 已连接并提供该服务(`ServiceUnavailable`),且支持 Open 的版本(`VersionMismatch`);非零的 `ExpectedServerInstanceID` 等于该 serve peer 的值(`InstanceChanged`);lease 尚未到期,且 Open 到达时 relay 持有的 attachment 此后未被关闭(`LeaseExpired`);relay 已以该 `AttachmentID` 持有的 attachment 具有完全相同的身份和 Runtime(`AttachmentConflict`)。 +4. 向 serve peer 打开 stream 并发送 `Bind`。serve peer 回复 `Bound`,或回复失败:对其不提供的服务回复 `ServiceUnavailable`,或回复 `VersionMismatch`,`ExpectedServerInstanceID` 不是自身值时回复 `InstanceChanged`,对最近关闭的 attachment 回复 `LeaseExpired`,或回复 `ProtocolViolation`。relay 将失败转交给 attach peer。`Bind` 已开始发送但未收到回复时,relay 回复带 `EffectPossible` 的 `ServiceUnavailable`。 +5. 回复 `Opened`,并将两个 stream 拼接起来。 attachment 的 binding 身份由其 `AttachmentID`、`Resource`、`SessionID`、`AssignmentID` 和 `AssignmentEpoch` 组成。无论在同一 link 还是之后的 link 上重新打开 attachment,都要求同一 Runtime 提供完全相同的身份,并具有当前有效的授权。 @@ -233,7 +242,7 @@ relay 在内存中保存 link、attachment 和 lease。Authority 始终是持久 方法以 `*sandboxlink.Error` 表示类型化拒绝;其他任何错误都回复为 `ServiceUnavailable`。凭据 revision 和允许的访问来自 Authority,绝不来自 peer 的声明。 -重新创建的资源具有更高的 generation。相同或更高 generation 的 serve peer 替换资源当前的 serve peer;更高 generation 还会以 `CloseStaleGeneration` 关闭旧 generation 的所有 attachment。generation 比 relay 见过的最新 generation 更旧的 serve peer 或 Open 会以 `StaleGeneration` 被拒绝。 +重新创建的资源具有更高的 generation。相同或更高 generation 的 serve peer 替换资源当前的 serve peer;更高 generation 还会以 `CloseStaleGeneration` 关闭旧 generation 的所有 attachment。generation 比 relay 为该资源持有的最新 generation 更旧的 serve peer 或 Open 会以 `StaleGeneration` 被拒绝。relay 仅在[持有该资源](#run-a-relay)期间保留其 generation;此后由 Authority 单独拒绝更旧的 generation。 ## Lease、关闭与撤销 {#leases-closing-and-revocation} @@ -242,7 +251,7 @@ relay 在内存中保存 link、attachment 和 lease。Authority 始终是持久 - `CloseAttachment` 以 `CloseRequested` 关闭调用方的 attachment。关闭未知 attachment 会成功,关闭其他 Runtime 的 attachment 返回 `PermissionDenied`。 - `Relay.RevokeAttachment` 关闭一个 attachment。`Relay.RevokeResource` 关闭某个资源 generation 及更旧 generation 的所有 attachment,向 serve peer 写入相应的 `AttachmentClosed` 事件,然后断开它。两者都以 `CloseRevoked` 关闭。 -关闭 attachment 会重置其全部 stream,并向 serve peer 发送 `AttachmentClosed`;除 `CloseRequested` 外,也向其 attach peer 发送。relay 将每个 serve peer 未写出的事件保存在没有大小上限的集合中,仅在事件写出后才将其移除,因此已断开的 serve peer,或在事件写出前 link 断开的 serve peer,会在以相同 generation 重连时收到该事件。被关闭打断的 Open 根据原因以 `AttachmentConflict`、`LeaseExpired`、`PermissionDenied` 或 `StaleGeneration` 失败;其 `Bind` 可能已到达 serve peer 时带 `EffectPossible`。 +关闭 attachment 会重置其全部 stream,并向 serve peer 发送 `AttachmentClosed`;除 `CloseRequested` 外,也向其 attach peer 发送。relay 将每个 peer 未写出的事件保存在集合中,在事件写出后将其移除,因此已断开的 serve peer,或在事件写出前 link 断开的 serve peer,会在以相同 generation 重连时收到该事件。不再有 peer 能读取的事件会被丢弃:attach peer 的事件在其 link 结束时丢弃,serve peer 的事件在更新的 generation 连接或 `RevokeResource` 撤销其 generation 时丢弃。被关闭打断的 Open 根据原因以 `AttachmentConflict`、`LeaseExpired`、`PermissionDenied` 或 `StaleGeneration` 失败;其 `Bind` 可能已到达 serve peer 时带 `EffectPossible`。 丢失 link 会重置其承载的 stream,并保留其 attachment,直到 lease 到期或 attachment 被关闭。 @@ -271,7 +280,7 @@ relay 通过 32 KiB 缓冲区复制每个方向的数据,每个 stream 最多 | 8 | `InstanceChanged` | serve peer 的 `ServerInstanceID` 不是 `ExpectedServerInstanceID` | | 9 | `LeaseExpired` | lease 已过期,或 relay 不再持有该 attachment | | 10 | `AttachmentConflict` | 该 `AttachmentID` 以其他身份或 Runtime 被持有,或在 Open 期间被关闭 | -| 11 | `LimitExceeded` | 达到 link 的 stream 上限,或达到裁决中续期的数量上限 | +| 11 | `LimitExceeded` | Hello 或 Open 所需的[名额](#run-a-relay)没有空闲,serve link 已达 stream 上限,或 attach link 已达裁决中续期的数量上限 | | 12 | `ProtocolViolation` | 消息格式错误、出现在不允许的位置,或携带未递增的请求 ID | `ServiceUnavailable` 和 `LimitExceeded` 是临时失败:相同请求稍后可能成功,`Code.Retryable` 将它们报告为可重试。其他 code 都是最终失败:以相同凭据、attachment 和 generation 重复请求会再次失败。 @@ -280,4 +289,4 @@ relay 通过 32 KiB 缓冲区复制每个方向的数据,每个 stream 最多 ## 验证 {#verification} -`go test ./internal/sandboxlink/...` 覆盖 golden 帧、解码拒绝,以及 relay 的授权、generation、lease、撤销、续期上限和重连行为,包括有序结束与中止的传播。`go test -run '^$' -fuzz FuzzDecode ./internal/sandboxlink` 对解码器进行 fuzz 测试。 +`go test ./internal/sandboxlink/...` 覆盖 golden 帧、解码拒绝,以及 relay 的授权、generation、lease、撤销、容量与续期上限以及重连行为,包括有序结束与中止的传播。`go test -run '^$' -fuzz FuzzDecode ./internal/sandboxlink` 对解码器进行 fuzz 测试。 diff --git a/internal/sandboxlink/relay/export_test.go b/internal/sandboxlink/relay/export_test.go new file mode 100644 index 000000000..6b07e83ec --- /dev/null +++ b/internal/sandboxlink/relay/export_test.go @@ -0,0 +1,38 @@ +package relay + +import ( + "time" + + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxwire" +) + +// The per-link and per-resource bounds the tests fill. +const ( + MaxStreams = maxStreams + MaxServeHellos = maxServeHellos +) + +// SetLimits lowers rl's capacity for resources, attachments and attach links. +func SetLimits(rl *Relay, resources, attachments, attachLinks int) { + rl.mu.Lock() + defer rl.mu.Unlock() + rl.maxResources, rl.maxAttachments, rl.maxAttachLinks = resources, attachments, attachLinks +} + +// Held reports the resources, attachment slots and attach links rl holds. +func Held(rl *Relay) (resources, attachments, attachLinks int) { + rl.mu.Lock() + defer rl.mu.Unlock() + return len(rl.resources), rl.held, rl.attachLinks +} + +// Lease reports the lease of the attachment rl holds under id, or the zero +// time when it holds none. +func Lease(rl *Relay, id sandboxwire.ID) time.Time { + rl.mu.Lock() + defer rl.mu.Unlock() + if a := rl.attachments[id]; a != nil { + return a.lease + } + return time.Time{} +} diff --git a/internal/sandboxlink/relay/relay.go b/internal/sandboxlink/relay/relay.go index 51ce1fb6c..ec671954d 100644 --- a/internal/sandboxlink/relay/relay.go +++ b/internal/sandboxlink/relay/relay.go @@ -20,7 +20,8 @@ import ( ) const ( - // maxStreams bounds the concurrent service streams of each link. + // maxStreams bounds the concurrent service streams of each link. An attach + // link's stream counts from its Open, through the Authority's decision. maxStreams = 256 // spliceBuffer bounds the bytes a splice holds per direction, on top of // one yamux window per stream. @@ -32,6 +33,26 @@ const ( answerQueue = 64 ) +// The relay's capacity, for one Core serving one deployment, which runs at +// most 1024 executions at once (the maximum OAC_EXECUTION_CONCURRENCY). +// Measured idle, the relay spends about 64 KiB on a serve link, 48 KiB on an +// attach link and 1 KiB on an attachment, so at capacity they hold under +// 500 MiB. +const ( + // maxResources bounds the resources held, and with them serve links, + // one per resource. It leaves room for idle sandboxes beside busy ones. + maxResources = 4096 + // maxServeHellos bounds the serve Hellos being decided for one resource, + // so a redial or a newer generation's peer can arrive while one is. + maxServeHellos = 2 + // maxAttachments bounds attachments, open or with AttachmentClosed events + // to write: four per resource. + maxAttachments = 16384 + // maxAttachLinks bounds attach links. An agent host dials one per + // attachment it uses, a few per execution. + maxAttachLinks = 4096 +) + // Relay accepts Link peers on ServeHTTP. It keeps the current serve peer of // each resource, the attachments it has opened and their leases in memory; the // Authority stays the durable judge of every grant. @@ -42,25 +63,41 @@ type Relay struct { mu sync.Mutex epoch uint64 // counts revocations, so a racing Open re-authorizes - generations map[resourceKey]uint64 - serves map[resourceKey]*serveLink - // closures holds the AttachmentClosed events not yet written to the serve - // peer of each resource's newest generation, connected or not. - closures map[resourceKey]closures + resources map[resourceKey]*resource attachments map[sandboxwire.ID]*attachment + // held counts attachment slots: open attachments, and closed ones whose + // AttachmentClosed events are not yet written or discarded. + held int + attachLinks int + // The capacity of each bounded kind. New sets the constants above; tests + // lower them. + maxResources, maxAttachments, maxAttachLinks int +} + +// resource is what the relay holds for one resource. It takes one of +// maxResources from the serve Hello that creates it until it holds nothing: +// no serve peer, Hello being decided, attachment or event. +type resource struct { + generation uint64 // the newest generation seen + serve *serveLink // nil while the serve peer is away + // closures holds the AttachmentClosed events not yet written to the serve + // peer of generation, connected or not. + closures closures + hellos int // serve Hellos being decided, at most maxServeHellos + attachments int // open attachments } -// closures is a set of AttachmentClosed events to write, by attachment. -type closures map[sandboxwire.ID]sandboxlink.CloseReason +// closures is a set of AttachmentClosed events to write, by attachment ID. +// Each event keeps its attachment's slot until it is written or discarded. +type closures map[sandboxwire.ID]*attachment // New returns a relay that asks auth to authenticate peers and authorize their // requests. func New(auth sandboxlink.Authority) *Relay { ctx, cancel := context.WithCancel(context.Background()) return &Relay{auth: auth, ctx: ctx, cancel: cancel, - generations: map[resourceKey]uint64{}, - serves: map[resourceKey]*serveLink{}, - closures: map[resourceKey]closures{}, + maxResources: maxResources, maxAttachments: maxAttachments, maxAttachLinks: maxAttachLinks, + resources: map[resourceKey]*resource{}, attachments: map[sandboxwire.ID]*attachment{}, } } @@ -89,8 +126,8 @@ func (rl *Relay) RevokeAttachment(id sandboxwire.ID) { // RevokeResource closes every attachment of ref's generation and older and // disconnects that serve peer after writing it their AttachmentClosed events. -// Events it could not write stay for a reconnect of the same generation. The -// caller withdraws the authority first. +// The caller withdraws the authority first, so the revoked generation cannot +// reconnect, and the events it could not read are discarded. func (rl *Relay) RevokeResource(ref sandboxlink.ResourceRef) { rl.mu.Lock() defer rl.mu.Unlock() @@ -101,10 +138,19 @@ func (rl *Relay) RevokeResource(ref sandboxlink.ResourceRef) { } } key := keyOf(ref) - if sl := rl.serves[key]; sl != nil && sl.hello.Resource.Generation <= ref.Generation { - delete(rl.serves, key) + r := rl.resources[key] + if r == nil || r.generation > ref.Generation { + return + } + if sl := r.serve; sl != nil { + // The link keeps the set to write before it ends. + r.serve = nil sl.end() + } else { + rl.dropLocked(r.closures) } + r.closures = closures{} + rl.releaseLocked(key, r) } type resourceKey struct { @@ -117,16 +163,17 @@ func keyOf(r sandboxlink.ResourceRef) resourceKey { } // link is one authenticated connection. Its writer sends answers from a -// bounded queue and AttachmentClosed events from an unbounded set, so the -// relay never blocks on a peer while holding its lock and never drops an -// event: an event leaves the set only once it is written. +// bounded queue and AttachmentClosed events from a set, so the relay never +// blocks on a peer while holding its lock: an event leaves the set once it is +// written, or with the set when no link can write it any more. type link struct { sess *yamux.Session ctl *yamux.Stream out chan outgoing wake chan struct{} // the set has events to write - // Under Relay.mu: the events to write and the open service streams. A - // serve link shares its resource's set, which outlives the link. + // Under Relay.mu: the events to write, nil once the link no longer + // writes them, and the open service streams. A serve link shares its + // resource's set, which outlives the link. closures closures streams uint32 } @@ -195,32 +242,66 @@ func (rl *Relay) write(l *link) { func (rl *Relay) writeClosures(l *link) error { for { rl.mu.Lock() - var c sandboxlink.AttachmentClosed - for id, reason := range l.closures { - c = sandboxlink.AttachmentClosed{AttachmentID: id, Reason: reason} + var a *attachment + for _, a = range l.closures { break } rl.mu.Unlock() - if c.Reason == 0 { + if a == nil { return nil } - if err := sandboxlink.WriteMessage(l.ctl, 0, c); err != nil { + id := a.identity.AttachmentID + if err := sandboxlink.WriteMessage(l.ctl, 0, sandboxlink.AttachmentClosed{AttachmentID: id, Reason: a.reason}); err != nil { return err } rl.mu.Lock() - if l.closures[c.AttachmentID] == c.Reason { - delete(l.closures, c.AttachmentID) + if l.closures[id] == a { + delete(l.closures, id) + rl.unrefLocked(a) } rl.mu.Unlock() } } -// closedLocked adds an AttachmentClosed event to l's set and wakes its writer. -func (l *link) closedLocked(id sandboxwire.ID, reason sandboxlink.CloseReason) { - if l.closures == nil { - l.closures = closures{} +// queueLocked adds a's AttachmentClosed event to set, unless set is nil. An +// unwritten event of an earlier attachment with the same ID gives way to it. +func (rl *Relay) queueLocked(set closures, a *attachment) { + if set == nil { + return + } + id := a.identity.AttachmentID + if old := set[id]; old != nil { + rl.unrefLocked(old) + } + set[id] = a + a.refs++ +} + +// dropLocked discards the events of a set that no link will write. +func (rl *Relay) dropLocked(set closures) { + for _, a := range set { + rl.unrefLocked(a) + } +} + +// unrefLocked drops one of a's references, the open attachment or one of its +// events. The last frees its slot. +func (rl *Relay) unrefLocked(a *attachment) { + a.refs-- + if a.refs == 0 { + rl.held-- } - l.closures[id] = reason +} + +// releaseLocked forgets r, freeing its slot, once it holds nothing. +func (rl *Relay) releaseLocked(key resourceKey, r *resource) { + if r.serve == nil && r.hellos == 0 && r.attachments == 0 && len(r.closures) == 0 { + delete(rl.resources, key) + } +} + +// wakeWriter tells l's writer that its set has events. +func (l *link) wakeWriter() { select { case l.wake <- struct{}{}: default: @@ -262,7 +343,9 @@ func linger(sess *yamux.Session) { } // attachment is an attachment the relay has opened. It outlives the links -// that carry its streams until its lease expires or it is closed. +// that carry its streams until its lease expires or it is closed, and keeps +// one of maxAttachments until its AttachmentClosed events are written or +// discarded. type attachment struct { identity sandboxlink.Identity runtime sandboxwire.ID @@ -271,6 +354,8 @@ type attachment struct { owner *attachLink // the link that last opened a stream on it bound bool // a Bind may have reached the serve peer splices map[*splice]struct{} + reason sandboxlink.CloseReason // why it closed; zero while open + refs int // its slot's holders: the open attachment and its unwritten events } // splice is one service stream from its Open to its end. @@ -353,17 +438,43 @@ func (rl *Relay) serve(l *link, id uint64, hello sandboxlink.ServeHello) { }() <-l.sess.CloseChan() rl.mu.Lock() - if rl.serves[key] == sl { - delete(rl.serves, key) + defer rl.mu.Unlock() + if r := rl.resources[key]; r != nil && r.serve == sl { + // The resource keeps the set for a reconnect. + r.serve = nil + rl.releaseLocked(key, r) + } else { + // A replaced link has no set; a revoked one takes it along. + rl.dropLocked(sl.closures) } sl.closures = nil - rl.mu.Unlock() } -// admitServe authenticates a serve Hello and installs the link. A revocation -// that lands while the Authority decides forces a fresh decision, so a -// withdrawn credential never installs a peer. +// admitServe authenticates a serve Hello and installs the link. Until it is +// decided, the Hello takes one of its resource's maxServeHellos, and a +// resource slot when the relay does not hold the resource. A revocation that +// lands while the Authority decides forces a fresh decision, so a withdrawn +// credential never installs a peer. func (rl *Relay) admitServe(l *link, id uint64, hello sandboxlink.ServeHello) (*serveLink, error) { + key := keyOf(hello.Resource) + rl.mu.Lock() + r := rl.resources[key] + if r == nil && len(rl.resources) >= rl.maxResources || r != nil && r.hellos >= maxServeHellos { + rl.mu.Unlock() + return nil, sandboxlink.Fail(sandboxlink.LimitExceeded) + } + if r == nil { + r = &resource{closures: closures{}} + rl.resources[key] = r + } + r.hellos++ + rl.mu.Unlock() + defer func() { + rl.mu.Lock() + r.hellos-- + rl.releaseLocked(key, r) + rl.mu.Unlock() + }() for range authorizeAttempts { rl.mu.Lock() epoch := rl.epoch @@ -382,7 +493,7 @@ func (rl *Relay) admitServe(l *link, id uint64, hello sandboxlink.ServeHello) (* rl.mu.Unlock() continue } - sl, old, err := rl.installServeLocked(l, id, hello) + sl, old, err := rl.installServeLocked(r, l, id, hello) rl.mu.Unlock() if old != nil { old.sess.Close() @@ -392,41 +503,62 @@ func (rl *Relay) admitServe(l *link, id uint64, hello sandboxlink.ServeHello) (* return nil, sandboxlink.Fail(sandboxlink.ServiceUnavailable) } -// installServeLocked makes l the resource's serve peer and queues its -// HelloAccepted while the decision is still current; its writer then writes -// the resource's pending AttachmentClosed events. It returns the replaced -// link for the caller to close. -func (rl *Relay) installServeLocked(l *link, id uint64, hello sandboxlink.ServeHello) (sl, old *serveLink, err error) { - key, generation := keyOf(hello.Resource), hello.Resource.Generation - if rl.generations[key] > generation { +// installServeLocked makes l r's serve peer and queues its HelloAccepted +// while the decision is still current; its writer then writes r's pending +// AttachmentClosed events. It returns the replaced link for the caller to +// close. +func (rl *Relay) installServeLocked(r *resource, l *link, id uint64, hello sandboxlink.ServeHello) (sl, old *serveLink, err error) { + generation := hello.Resource.Generation + if r.generation > generation { return nil, nil, sandboxlink.Fail(sandboxlink.StaleGeneration) } - if rl.generations[key] < generation { - rl.generations[key] = generation - delete(rl.closures, key) + if r.generation < generation { + r.generation = generation + rl.dropLocked(r.closures) + r.closures = closures{} for _, a := range rl.attachments { if a.identity.Resource.SameResource(hello.Resource) { rl.closeLocked(a, sandboxlink.CloseStaleGeneration) } } } - if rl.closures[key] == nil { - rl.closures[key] = closures{} - } sl = &serveLink{link: l, hello: hello} - sl.closures = rl.closures[key] - old = rl.serves[key] - if old != nil { + sl.closures = r.closures + if old = r.serve; old != nil { old.closures = nil } - rl.serves[key] = sl + r.serve = sl sl.send(id, sandboxlink.HelloAccepted{}) return sl, old, nil } // attach serves an attach peer's control requests and service streams until -// the link ends. Its attachments stay open until their leases expire. +// the link ends. The link takes an attach link slot first and keeps it until +// every stream and renewal it carried has finished, so the decisions of a +// dropped link stay bounded. Its attachments stay open until their leases +// expire. func (rl *Relay) attach(l *link, id uint64, hello sandboxlink.AttachHello) { + rl.mu.Lock() + full := rl.attachLinks >= rl.maxAttachLinks + if !full { + rl.attachLinks++ + l.closures = closures{} + } + rl.mu.Unlock() + if full { + l.fail(id, sandboxlink.OpHello, sandboxlink.LimitExceeded) + return + } + var serving sync.WaitGroup + defer func() { + <-l.sess.CloseChan() + serving.Wait() + rl.mu.Lock() + rl.attachLinks-- + rl.dropLocked(l.closures) + l.closures = nil + rl.mu.Unlock() + }() ctx, cancel := rl.authorityContext() peer, err := rl.auth.AuthenticateAttach(ctx, hello) cancel() @@ -441,15 +573,15 @@ func (rl *Relay) attach(l *link, id uint64, hello sandboxlink.AttachHello) { var seq sandboxwire.RequestSequence seq.Admit(id) // the Hello takes the first ID al.send(id, sandboxlink.HelloAccepted{}) - go func() { + serving.Go(func() { for { st, err := l.sess.AcceptStream() if err != nil { return } - go rl.open(al, st) + serving.Go(func() { rl.open(al, st) }) } - }() + }) for { id, m, err := sandboxlink.ReadMessage(l.ctl) if err != nil { @@ -468,10 +600,10 @@ func (rl *Relay) attach(l *link, id uint64, hello sandboxlink.AttachHello) { al.send(id, sandboxlink.FailureFor(sandboxlink.OpRenewAttachment, sandboxlink.Fail(sandboxlink.LimitExceeded))) continue } - go func() { + serving.Go(func() { defer al.inflight.Add(-1) rl.renew(al, id, r) - }() + }) case sandboxlink.CloseAttachment: if !admitted { l.fail(id, sandboxlink.OpCloseAttachment, sandboxlink.ProtocolViolation) @@ -555,27 +687,27 @@ func (rl *Relay) closeRequested(al *attachLink, id uint64, c sandboxlink.CloseAt // peer and the serve peer why. A serve peer that is away when its attachment // closes hears of it when the same generation reconnects. func (rl *Relay) closeLocked(a *attachment, reason sandboxlink.CloseReason) { - id := a.identity.AttachmentID - delete(rl.attachments, id) + delete(rl.attachments, a.identity.AttachmentID) a.timer.Stop() for sp := range a.splices { sp.abortLocked(abortCodes[reason]) } + a.reason = reason if reason != sandboxlink.CloseRequested { - a.owner.closedLocked(id, reason) - } - key, generation := keyOf(a.identity.Resource), a.identity.Resource.Generation - if !a.bound || rl.generations[key] != generation { - return - } - if sl := rl.serves[key]; sl != nil { - sl.closedLocked(id, reason) - return - } - if rl.closures[key] == nil { - rl.closures[key] = closures{} + rl.queueLocked(a.owner.closures, a) + a.owner.wakeWriter() + } + key := keyOf(a.identity.Resource) + r := rl.resources[key] // the attachment holds it + if a.bound && r.generation == a.identity.Resource.Generation { + rl.queueLocked(r.closures, a) + if r.serve != nil { + r.serve.wakeWriter() + } } - rl.closures[key][id] = reason + r.attachments-- + rl.releaseLocked(key, r) + rl.unrefLocked(a) } // abortCodes answers an Open that an attachment's close interrupts. @@ -661,15 +793,41 @@ func (rl *Relay) open(al *attachLink, st *yamux.Stream) { rl.splice(sp) } -// admit authorizes o and registers its splice. A revocation that lands while -// the Authority decides forces a fresh decision. -func (rl *Relay) admit(al *attachLink, st *yamux.Stream, o sandboxlink.Open) (*splice, sandboxlink.Authorization, error) { +// admit authorizes o and registers its splice. Before the Authority decides, +// the Open takes a stream slot of its link, which an admitted stream keeps +// until finish and a refused one gives back once it is decided, whether or +// not the peer reset it. An Open of an attachment the relay does not hold +// also takes an attachment slot. A revocation that lands while the Authority +// decides forces a fresh decision. +func (rl *Relay) admit(al *attachLink, st *yamux.Stream, o sandboxlink.Open) (sp *splice, auth sandboxlink.Authorization, err error) { + rl.mu.Lock() + prior := rl.attachments[o.AttachmentID] + reserved := prior == nil + if al.streams >= maxStreams || reserved && rl.held >= rl.maxAttachments { + rl.mu.Unlock() + return nil, auth, sandboxlink.Fail(sandboxlink.LimitExceeded) + } + al.streams++ + if reserved { + rl.held++ + } + rl.mu.Unlock() + defer func() { + rl.mu.Lock() + if sp == nil { + al.streams-- + } + if reserved { + rl.held-- + } + rl.mu.Unlock() + }() for range authorizeAttempts { rl.mu.Lock() epoch := rl.epoch rl.mu.Unlock() ctx, cancel := rl.authorityContext() - auth, err := rl.auth.AuthorizeOpen(ctx, al.peer, o) + auth, err = rl.auth.AuthorizeOpen(ctx, al.peer, o) cancel() if err != nil { return nil, auth, err @@ -682,16 +840,25 @@ func (rl *Relay) admit(al *attachLink, st *yamux.Stream, o sandboxlink.Open) (*s rl.mu.Unlock() continue } - sp, err := rl.admitLocked(al, st, o, auth) + sp, err = rl.admitLocked(al, st, o, auth, prior, &reserved) rl.mu.Unlock() return sp, auth, err } return nil, sandboxlink.Authorization{}, sandboxlink.Fail(sandboxlink.ServiceUnavailable) } -func (rl *Relay) admitLocked(al *attachLink, st *yamux.Stream, o sandboxlink.Open, auth sandboxlink.Authorization) (*splice, error) { +// admitLocked checks o against what the relay holds and registers its +// splice. A new attachment takes the slot reserved for it. The attachment +// prior, which the relay held when the Open arrived, must still be the one +// under its ID: once it closed, the Open fails even if another attachment +// took the ID since. +func (rl *Relay) admitLocked(al *attachLink, st *yamux.Stream, o sandboxlink.Open, auth sandboxlink.Authorization, prior *attachment, reserved *bool) (*splice, error) { key, generation := keyOf(o.Resource), o.Resource.Generation - sl := rl.serves[key] + r := rl.resources[key] + var sl *serveLink + if r != nil { + sl = r.serve + } var offered *sandboxlink.ServiceVersion if sl != nil { for i, s := range sl.hello.Services { @@ -703,9 +870,9 @@ func (rl *Relay) admitLocked(al *attachLink, st *yamux.Stream, o sandboxlink.Ope a := rl.attachments[o.AttachmentID] var code sandboxlink.Code switch { - case al.streams >= maxStreams || (sl != nil && sl.streams >= maxStreams): + case sl != nil && sl.streams >= maxStreams: code = sandboxlink.LimitExceeded - case rl.generations[key] > generation: + case r != nil && r.generation > generation: code = sandboxlink.StaleGeneration case sl == nil || sl.hello.Resource.Generation != generation || offered == nil: code = sandboxlink.ServiceUnavailable @@ -713,7 +880,7 @@ func (rl *Relay) admitLocked(al *attachLink, st *yamux.Stream, o sandboxlink.Ope code = sandboxlink.VersionMismatch case !o.ExpectedServerInstanceID.IsZero() && o.ExpectedServerInstanceID != sl.hello.ServerInstanceID: code = sandboxlink.InstanceChanged - case !auth.LeaseExpiresAt.After(time.Now()): + case !auth.LeaseExpiresAt.After(time.Now()) || prior != nil && a != prior: code = sandboxlink.LeaseExpired case a != nil && (a.identity != o.Identity() || a.runtime != al.peer.RuntimeID): code = sandboxlink.AttachmentConflict @@ -722,15 +889,16 @@ func (rl *Relay) admitLocked(al *attachLink, st *yamux.Stream, o sandboxlink.Ope return nil, sandboxlink.Fail(code) } if a == nil { - a = &attachment{identity: o.Identity(), runtime: al.peer.RuntimeID, splices: map[*splice]struct{}{}} + a = &attachment{identity: o.Identity(), runtime: al.peer.RuntimeID, splices: map[*splice]struct{}{}, refs: 1} rl.attachments[o.AttachmentID] = a + r.attachments++ + *reserved = false } a.owner = al a.bound = true rl.setLeaseLocked(a, auth.LeaseExpiresAt) sp := &splice{att: a, serve: sl, al: al, attach: st} a.splices[sp] = struct{}{} - al.streams++ sl.streams++ return sp, nil } diff --git a/internal/sandboxlink/relay/relay_test.go b/internal/sandboxlink/relay/relay_test.go index f7bff701b..9db74bad5 100644 --- a/internal/sandboxlink/relay/relay_test.go +++ b/internal/sandboxlink/relay/relay_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink/relay" "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink/sandboxlinktest" "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxwire" ) @@ -50,12 +51,12 @@ func put[T any](ch chan T, v T) { } } -// hooked runs a test's hook after the Authority decides a serve Hello or a -// renewal, so a test can hold the decision. +// hooked runs a test's hook after the Authority decides a serve Hello, an +// Open or a renewal, so a test can hold the decision. type hooked struct { *sandboxlinktest.Authority - mu sync.Mutex - onServe, onRenew func() + mu sync.Mutex + onServe, onOpen, onRenew func() } func (h *hooked) hook(f *func()) { @@ -79,6 +80,12 @@ func (h *hooked) AuthenticateServe(ctx context.Context, hello sandboxlink.ServeH return peer, err } +func (h *hooked) AuthorizeOpen(ctx context.Context, peer sandboxlink.AttachPeer, o sandboxlink.Open) (sandboxlink.Authorization, error) { + auth, err := h.Authority.AuthorizeOpen(ctx, peer, o) + h.hook(&h.onOpen) + return auth, err +} + func (h *hooked) Renew(ctx context.Context, peer sandboxlink.AttachPeer, r sandboxlink.RenewAttachment) (sandboxlink.Authorization, error) { auth, err := h.Authority.Renew(ctx, peer, r) h.hook(&h.onRenew) @@ -193,6 +200,29 @@ func (f *fixture) startServe(generation uint64) *servePeer { return p } +// serveHello sends a serve Hello for ref with an unknown credential over a new +// link and returns the relay's answer. +func (f *fixture) serveHello(ref sandboxlink.ResourceRef) error { + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + conn, err := sandboxlink.DialWebSocket(ctx, f.srv.URL, f.srv.TLS) + if err != nil { + return err + } + sess, _ := sandboxlink.ClientSession(conn) + defer sess.Close() + ctl, err := sess.OpenStream(ctx) + if err == nil { + ctl.SetDeadline(time.Now().Add(wait)) + err = sandboxlink.WriteMessage(ctl, 1, sandboxlink.ServeHello{Version: sandboxlink.Version, Credential: []byte("unknown"), Resource: ref, + ServerInstanceID: sandboxwire.NewID(), Services: []sandboxlink.ServiceVersion{{Service: sandboxlink.ServiceFile, Version: 1}}}) + } + if err == nil { + _, err = sandboxlink.ReadReply(ctl, sandboxlink.OpHello, 1) + } + return err +} + // grant authorizes the fixture's Runtime for the resource's generation. func (f *fixture) grant(generation uint64) []byte { grant := []byte(fmt.Sprintf("grant %d", generation)) @@ -438,6 +468,238 @@ func TestClosuresReplayOnReconnect(t *testing.T) { } } +// A serve Hello for a resource the relay does not hold takes a slot before the +// Authority decides: beyond capacity even an unknown credential gets +// LimitExceeded. A refused Hello leaves no slot taken, and an admitted +// resource still reconnects at capacity. +func TestServesAreBounded(t *testing.T) { + f := newFixture(t) + relay.SetLimits(f.srv.Relay, 1, 16, 16) + other := resource(1) + other.ID = sandboxwire.NewID() + if err := f.serveHello(other); !errors.Is(err, sandboxlink.AuthenticationFailed) { + t.Fatalf("serve Hello with an unknown credential: %v, want AuthenticationFailed", err) + } + p := f.serve(1) + if err := f.serveHello(other); !errors.Is(err, sandboxlink.LimitExceeded) { + t.Fatalf("serve Hello beyond capacity: %v, want LimitExceeded", err) + } + if resources, _, _ := relay.Held(f.srv.Relay); resources != 1 { + t.Fatalf("relay holds %d resources, want 1", resources) + } + recv(t, p.conns).Close() + recv(t, p.connected) +} + +// Each Hello for a resource takes one of its MaxServeHellos while it is +// decided: past them, a Hello for a held resource gets LimitExceeded without +// reaching the Authority. +func TestServeHellosAreBounded(t *testing.T) { + f := newFixture(t) + f.serve(1) + entered, release := make(chan struct{}, relay.MaxServeHellos+1), make(chan struct{}) + f.auth.set(&f.auth.onServe, func() { + entered <- struct{}{} + <-release + }) + errs := make(chan error, relay.MaxServeHellos) + for range relay.MaxServeHellos { + go func() { errs <- f.serveHello(resource(1)) }() + recv(t, entered) + } + if err := f.serveHello(resource(1)); !errors.Is(err, sandboxlink.LimitExceeded) { + t.Fatalf("serve Hello beyond the bound: %v, want LimitExceeded", err) + } + close(release) + for range relay.MaxServeHellos { + if err := recv(t, errs); !errors.Is(err, sandboxlink.AuthenticationFailed) { + t.Fatalf("serve Hello within the bound: %v, want AuthenticationFailed", err) + } + } + if len(entered) != 0 { + t.Fatal("a serve Hello beyond the bound reached the Authority") + } +} + +// An Open takes one of its link's MaxStreams before the Authority decides and +// keeps it until the decision ends, even once the peer has reset the stream. +func TestOpenDecisionsAreBounded(t *testing.T) { + f := newFixture(t) + f.serve(1) + attachment := sandboxwire.NewID() + f.mustOpen(sandboxlink.ServiceFile, attachment, 1) // its stream takes one + grant := f.grant(1) + pending := relay.MaxStreams - 1 + entered, release := make(chan struct{}, pending+1), make(chan struct{}) + f.auth.set(&f.auth.onOpen, func() { + entered <- struct{}{} + <-release + }) + reset := make(chan error) + for range pending { + ctx, cancel := context.WithCancel(context.Background()) + go func() { + _, _, err := f.link.OpenService(ctx, sandboxlink.Open{Service: sandboxlink.ServiceFile, Version: 1, Resource: resource(1), + AttachmentID: attachment, SessionID: sessionID, AssignmentID: assignmentID, AssignmentEpoch: 1, AttachGrant: grant}) + reset <- err + }() + recv(t, entered) + cancel() // OpenService resets the stream + recv(t, reset) + } + if _, _, err := f.open(sandboxlink.ServiceFile, attachment, 1, grant); !errors.Is(err, sandboxlink.LimitExceeded) { + t.Fatalf("open beyond the stream bound: %v, want LimitExceeded", err) + } + if len(entered) != 0 { + t.Fatal("an Open beyond the bound reached the Authority") + } + f.auth.set(&f.auth.onOpen, nil) + close(release) + // The slots return as the decisions end; until then an Open is refused + // with LimitExceeded, which is retryable. + for { + _, _, err := f.open(sandboxlink.ServiceFile, attachment, 1, grant) + if err == nil { + break + } + if !errors.Is(err, sandboxlink.LimitExceeded) { + t.Fatalf("open after the decisions ended: %v", err) + } + } +} + +// An Open that finds an attachment fails with LeaseExpired when that +// attachment closes while the Open is decided, even if another Open has +// created a new attachment under the same ID since, and leaves the new +// attachment's lease alone. +func TestOpenOfAReplacedAttachment(t *testing.T) { + f := newFixture(t) + p := f.serve(1) + calls := make(chan chan struct{}) + f.auth.set(&f.auth.onOpen, func() { + release := make(chan struct{}) + calls <- release + <-release + }) + attachment := sandboxwire.NewID() + open := func(grant []byte) <-chan error { + errs := make(chan error, 1) + go func() { + _, _, err := f.open(sandboxlink.ServiceFile, attachment, 1, grant) + errs <- err + }() + return errs + } + grant := f.grant(1) + longer := []byte("longer grant") + f.auth.AddGrant(longer, sandboxlinktest.Grant{RuntimeID: f.runtime, Resource: resource(1), SessionID: sessionID, + AssignmentID: assignmentID, AssignmentEpoch: 1, Services: []sandboxlink.Service{sandboxlink.ServiceFile}, Lease: 2 * f.lease}) + + // Two Opens arrive while the relay holds no attachment; the first creates A. + first := open(grant) + releaseFirst := recv(t, calls) + second := open(grant) + releaseSecond := recv(t, calls) + close(releaseFirst) + if err := recv(t, first); err != nil { + t.Fatal(err) + } + // A third Open finds A, which closes while it is decided. + third := open(longer) + releaseThird := recv(t, calls) + if err := f.link.CloseAttachment(context.Background(), attachment); err != nil { + t.Fatal(err) + } + recv(t, p.closed) + // The second Open creates B under the same ID. The serve peer, which saw + // A close, refuses to bind it, but the relay holds B. + close(releaseSecond) + recv(t, second) + lease := relay.Lease(f.srv.Relay, attachment) + if lease.IsZero() { + t.Fatal("the second Open created no attachment") + } + close(releaseThird) + if err := recv(t, third); !errors.Is(err, sandboxlink.LeaseExpired) { + t.Fatalf("open of a replaced attachment: %v, want LeaseExpired", err) + } + if got := relay.Lease(f.srv.Relay, attachment); !got.Equal(lease) { + t.Fatalf("the new attachment's lease moved from %s to %s", lease, got) + } +} + +// An Open of an attachment the relay does not hold takes a slot before the +// Authority decides: beyond capacity even a forged grant gets LimitExceeded. +// A refused Open leaves no slot taken, and an open attachment still opens +// streams at capacity. +func TestAttachmentsAreBounded(t *testing.T) { + f := newFixture(t) + relay.SetLimits(f.srv.Relay, 16, 1, 16) + f.serve(1) + if _, _, err := f.open(sandboxlink.ServiceFile, sandboxwire.NewID(), 1, []byte("forged grant")); !errors.Is(err, sandboxlink.PermissionDenied) { + t.Fatalf("open with a forged grant: %v, want PermissionDenied", err) + } + attachment := sandboxwire.NewID() + f.mustOpen(sandboxlink.ServiceFile, attachment, 1) + if _, _, err := f.open(sandboxlink.ServiceFile, sandboxwire.NewID(), 1, []byte("forged grant")); !errors.Is(err, sandboxlink.LimitExceeded) { + t.Fatalf("open of a new attachment beyond capacity: %v, want LimitExceeded", err) + } + f.mustOpen(sandboxlink.ServiceFile, attachment, 1) + if _, attachments, _ := relay.Held(f.srv.Relay); attachments != 1 { + t.Fatalf("relay holds %d attachment slots, want 1", attachments) + } +} + +// A closed attachment keeps its slot until its AttachmentClosed event is +// written to the serve peer, which is away and then reconnects. +func TestClosuresKeepTheirSlots(t *testing.T) { + f := newFixture(t) + relay.SetLimits(f.srv.Relay, 16, 2, 16) + p := f.serve(1) + ids := []sandboxwire.ID{sandboxwire.NewID(), sandboxwire.NewID()} + for _, id := range ids { + f.mustOpen(sandboxlink.ServiceFile, id, 1) + } + held, release := make(chan struct{}), make(chan struct{}) + var once sync.Once + f.auth.set(&f.auth.onServe, func() { + once.Do(func() { close(held) }) + <-release + }) + recv(t, p.conns).Close() + recv(t, held) + for _, id := range ids { + if err := f.link.CloseAttachment(context.Background(), id); err != nil { + t.Fatal(err) + } + } + if _, _, err := f.open(sandboxlink.ServiceFile, sandboxwire.NewID(), 1, f.grant(1)); !errors.Is(err, sandboxlink.LimitExceeded) { + t.Fatalf("open while both closures are unwritten: %v, want LimitExceeded", err) + } + close(release) + for range ids { + if r := recv(t, p.closed); r != sandboxlink.CloseRequested { + t.Fatalf("serve peer saw close reason %d, want a requested close", r) + } + } + // The relay frees an event's slot before it writes the next, so one slot + // is free once both events have arrived. + f.mustOpen(sandboxlink.ServiceFile, sandboxwire.NewID(), 1) +} + +// An attach Hello takes a link slot before the Authority decides: beyond +// capacity even an unknown credential gets LimitExceeded. +func TestAttachLinksAreBounded(t *testing.T) { + f := newFixture(t) + relay.SetLimits(f.srv.Relay, 16, 16, 1) + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + _, err := sandboxlink.DialAttach(ctx, sandboxlink.AttachConfig{URL: f.srv.URL, TLS: f.srv.TLS, RuntimeID: sandboxwire.NewID(), Credential: []byte("unknown")}) + if !errors.Is(err, sandboxlink.LimitExceeded) { + t.Fatalf("attach Hello beyond capacity: %v, want LimitExceeded", err) + } +} + // A control call whose context ended before it was sent fails with no effect // and leaves the link up. func TestCancelledCallKeepsLink(t *testing.T) { From a02ff8e7fa6219c9eca90e2d4f947c4d9ad57f64 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 21:34:07 +0800 Subject: [PATCH 07/14] fix(ci): restore structured Feishu review cards (#492) --- .github/workflows/ci-review.yml | 6 +-- docs/maintainers.md | 2 +- docs/zh/maintainers.md | 4 +- scripts/ci_review.py | 96 ++++++++++++++++++++------------- scripts/ci_review_test.py | 56 +++++++++++++++---- 5 files changed, 112 insertions(+), 52 deletions(-) diff --git a/.github/workflows/ci-review.yml b/.github/workflows/ci-review.yml index 2f328e429..2bc1069bd 100644 --- a/.github/workflows/ci-review.yml +++ b/.github/workflows/ci-review.yml @@ -48,7 +48,7 @@ jobs: persist-credentials: false - name: Prepare the report schema id: schema - run: python3 scripts/ci_review.py schema >> "$GITHUB_OUTPUT" + run: python3 scripts/ci_review.py schema "${{ matrix.kind }}" >> "$GITHUB_OUTPUT" - name: Review with Claude Code id: llm continue-on-error: true @@ -78,7 +78,7 @@ jobs: ${{ matrix.instructions }} - 按 JSON schema 返回中文报告。status 为 ok(未发现问题)、issues(发现有证据的问题)或 incomplete(审查失败、范围未检查完整或检查结果尚未完成)。有问题且仍有未检查项时,用 issues 并在 summary 中说明缺项。summary 无问题时简短,有问题时保留依据和建议,遵守 schema 的长度限制。 + 按 JSON schema 返回中文报告。status 为 ok(未发现问题)、issues(发现有证据的问题)或 incomplete(审查失败、范围未检查完整或检查结果尚未完成)。有问题且仍有未检查项时,用 issues 并在对应字段中说明缺项。按 schema 把各项结论分别填入字段,不要重复标题;changes 用 1–3 条 Markdown 列表,其他字段无问题时一句话,有问题时保留依据和建议,遵守各字段长度限制。 围绕本次 diff 和受影响的文档展开,证据充分后输出结果,避免重复核对同一结论。 只读审查,不修改仓库,不触发新的 CI,不发送消息。两份报告会由后续 job 合并发送。 claude_args: >- @@ -89,7 +89,7 @@ jobs: env: REVIEW_OUTCOME: ${{ steps.llm.outcome }} REVIEW_RESULT: ${{ steps.llm.outputs.structured_output }} - run: python3 scripts/ci_review.py collect "$RUNNER_TEMP/review-${{ matrix.kind }}.json" + run: python3 scripts/ci_review.py collect "${{ matrix.kind }}" "$RUNNER_TEMP/review-${{ matrix.kind }}.json" - uses: actions/upload-artifact@v6 if: always() with: diff --git a/docs/maintainers.md b/docs/maintainers.md index 7f3ac6626..a410ad2d1 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -194,7 +194,7 @@ Use **Actions → core-check → Run workflow** for a manual full check. For a t The `CI review and Feishu notification` workflow runs after a PR merges into main. A matrix runs **Code review** and **Docs review** in independent LLM contexts with `fail-fast: false`. Both read the merged commit and PR diff. Code review checks implementation, repository rules and existing CI results; it does not start another test run. Docs review checks changed behavior against documentation even when no docs changed. When docs change, it also checks contradictions, duplicated facts, topic ownership under CONTRIBUTING, and English/Chinese agreement. Findings include file and line references, supporting evidence and a minimal correction. Reviews read the repository without editing it. -Each review returns a structured result (`ok`, `issues` or `incomplete`) and a Chinese summary, retained as an Actions artifact for seven days. **Combined Feishu notification** waits for both jobs and sends one Card 2.0 message containing both results through the existing `FEISHU_WEBHOOK_URL`; `FEISHU_WEBHOOK_SECRET` optionally signs it. Only this notification job receives the webhook secrets. Failed, missing or invalid reports appear as incomplete, alongside any available result from the other review. The notification job runs even when a review fails. Delivery requires Feishu's `code=0` response; a failed or ambiguous request is not automatically retried, avoiding duplicate messages. Each review has a 25-minute execution timeout. Review and delivery failures do not block merges, and closing an unmerged PR does not trigger this workflow. The workflow checks out only the merged commit with notification credentials. +Each review returns a structured result (`ok`, `issues` or `incomplete`) and Chinese sections, retained as an Actions artifact for seven days. **Combined Feishu notification** waits for both jobs and sends one Card 2.0 message containing both results through the existing `FEISHU_WEBHOOK_URL`; `FEISHU_WEBHOOK_SECRET` optionally signs it. The card uses fixed sections for PR details, behavior changes, code and repository rules, CI results, and documentation review, separated by dividers. Its header shows green for no findings, red for findings, and yellow for an incomplete review without findings. Only this notification job receives the webhook secrets. Failed, missing or invalid reports appear as incomplete, alongside any available result from the other review. The notification job runs even when a review fails. Delivery requires Feishu's `code=0` response; a failed or ambiguous request is not automatically retried, avoiding duplicate messages. Each review has a 25-minute execution timeout. Review and delivery failures do not block merges, and closing an unmerged PR does not trigger this workflow. The workflow checks out only the merged commit with notification credentials. Browser jobs own separate fixtures and servers; increasing workers against the shared mutable fixture is unsafe. Failed browser jobs retain reports/traces for seven days. Native failure phase summaries are retained for seven days and detailed output stays in the Actions logs; credentials and temporary installation trees are not uploaded. Successful native archives are uploaded only for explicit manual packaging or releases, without recompressing the compressed archive. Release distribution artifacts retain their existing recovery policy; failed publication can reuse the original build as described above. diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index 6fb303605..f6b2b17e3 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: e3435d01696a172a0a0bcd5acecf4167410389f2c249730c9333f5d24027248d +source_hash: aaadeb3a5e99b8d926e9f78b7fc41c7aba808e0d2af58969119e67954f9d7a58 --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请使用 [安装指南](getting-started/install.md)。安装器代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装器](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -198,7 +198,7 @@ Go 模块和工作区输入会选择后端、API(包括容器)、原生和 `CI review and Feishu notification` 工作流在 PR 合入 main 后运行。矩阵中的 **Code review** 和 **Docs review** 使用独立的 LLM 上下文,并设置 `fail-fast: false`。两者读取合并后的提交和 PR 差异。代码审查检查实现、仓库规则和已有 CI 结果,不触发新一轮测试。文档审查会核对行为变化与文档是否一致,即使没有修改文档;修改文档时,还会检查矛盾、重复维护的事实、CONTRIBUTING 规定的主题归属及中英文含义。每项问题包含文件和行号、依据及最小修改建议。审查只读取仓库,不修改文件。 -每项审查返回结构化结果(`ok`、`issues` 或 `incomplete`)及中文摘要,作为 Actions 构建产物保留七天。**Combined Feishu notification** 等待两项审查结束,通过已有的 `FEISHU_WEBHOOK_URL` 发送一张包含两份结果的 Card 2.0 卡片;可选的 `FEISHU_WEBHOOK_SECRET` 用于签名。只有通知 job 能读取 webhook 密钥。审查失败、报告缺失或格式无效时标为未完成,另一项已有的结果照常展示。某项审查失败时,通知 job 仍会运行。只有飞书返回 `code=0` 才确认送达;请求失败或送达状态不明时不自动重试,以免重复发消息。每项审查的执行超时为 25 分钟。审查和发送失败不会阻止合并;关闭未合并的 PR 不触发此流程。持有通知凭据时,工作流只检出合并后的提交。 +每项审查返回结构化结果(`ok`、`issues` 或 `incomplete`)及分项中文结论,作为 Actions 构建产物保留七天。**Combined Feishu notification** 等待两项审查结束,通过已有的 `FEISHU_WEBHOOK_URL` 发送一张包含两份结果的 Card 2.0 卡片;可选的 `FEISHU_WEBHOOK_SECRET` 用于签名。卡片固定分为 PR 信息、行为变化、代码与仓库规则、CI 结果和文档审查,各区之间使用分隔线。未发现问题时标题为绿色,发现问题时为红色,审查未完成且尚未发现问题时为黄色。只有通知 job 能读取 webhook 密钥。审查失败、报告缺失或格式无效时标为未完成,另一项已有的结果照常展示。某项审查失败时,通知 job 仍会运行。只有飞书返回 `code=0` 才确认送达;请求失败或送达状态不明时不自动重试,以免重复发消息。每项审查的执行超时为 25 分钟。审查和发送失败不会阻止合并;关闭未合并的 PR 不触发此流程。持有通知凭据时,工作流只检出合并后的提交。 浏览器作业各自拥有独立的固定数据和服务;对共享可变固定数据增加 worker 数不安全。失败的浏览器作业保留报告与 trace 七天。原生失败阶段摘要保留七天,详细输出留在 Actions 日志中;凭据和临时安装目录不上传。成功的原生归档仅用于显式手动打包或发布时上传,不重新压缩已压缩的归档。发布分发产物保留现有恢复策略;失败发布可以按前述方式复用原构建。 diff --git a/scripts/ci_review.py b/scripts/ci_review.py index a6b97b887..e7fb16645 100644 --- a/scripts/ci_review.py +++ b/scripts/ci_review.py @@ -15,46 +15,50 @@ STATUSES = {"ok": "未发现问题", "issues": "发现问题", "incomplete": "未完成"} -MAX_SUMMARY = 2000 -SCHEMA = { - "type": "object", - "properties": { - "status": {"type": "string", "enum": list(STATUSES)}, - "summary": {"type": "string", "minLength": 1, "maxLength": MAX_SUMMARY}, - }, - "required": ["status", "summary"], - "additionalProperties": False, +MAX_SECTION = 900 +REPORT_FIELDS = { + "code": {"changes": "实际行为变化,1–3 条 Markdown 列表", "review": "代码正确性与仓库规则审查结论", "ci": "已有 CI 结果;失败或未完成时给出具体检查项"}, + "docs": {"consistency": "文档与代码一致性", "organization": "文档矛盾、重复与归属;未改文档时写本次未修改文档"}, } -def incomplete(reason): - return {"status": "incomplete", "summary": reason} +def report_schema(kind): + properties = {"status": {"type": "string", "enum": list(STATUSES)}} + properties.update({name: {"type": "string", "minLength": 1, "maxLength": MAX_SECTION, "description": description} + for name, description in REPORT_FIELDS[kind].items()}) + return {"type": "object", "properties": properties, "required": list(properties), "additionalProperties": False} -def parse_report(raw): +def incomplete(reason, kind): + fields = dict.fromkeys(REPORT_FIELDS[kind], "未完成。") + fields[next(iter(fields))] = reason + return {"status": "incomplete", **fields} + + +def parse_report(raw, kind): try: report = json.loads(raw) except (ValueError, TypeError): - return incomplete("未收到有效报告,请查看审查日志。") - if (not isinstance(report, dict) or set(report) != set(SCHEMA["required"]) + return incomplete("未收到有效报告,请查看审查日志。", kind) + if (not isinstance(report, dict) or set(report) != {"status", *REPORT_FIELDS[kind]} or not isinstance(report["status"], str) or report["status"] not in STATUSES - or not isinstance(report["summary"], str) or not report["summary"].strip() - or len(report["summary"]) > MAX_SUMMARY): - return incomplete("报告格式不完整,请查看审查日志。") + or any(not isinstance(report[name], str) or not report[name].strip() + or len(report[name]) > MAX_SECTION for name in REPORT_FIELDS[kind])): + return incomplete("报告格式不完整,请查看审查日志。", kind) return report -def collect(outcome, raw): +def collect(outcome, raw, kind): if outcome != "success": - return incomplete("审查未成功结束,请查看审查日志。") - return parse_report(raw) + return incomplete("审查未成功结束,请查看审查日志。", kind) + return parse_report(raw, kind) def read_report(directory, kind): try: - return parse_report((directory / f"review-{kind}.json").read_text()) + return parse_report((directory / f"review-{kind}.json").read_text(), kind) except (OSError, UnicodeError): - return incomplete("审查报告缺失,请查看审查日志。") + return incomplete("审查报告缺失,请查看审查日志。", kind) def markdown_text(value): @@ -65,26 +69,44 @@ def build_card(event, reports, run_url): pr = event["pull_request"] statuses = {report["status"] for report in reports.values()} color = "red" if "issues" in statuses else "yellow" if "incomplete" in statuses else "green" - elements = [{"tag": "markdown", "content": ( - f"**{markdown_text(pr['title'][:200])}**\n" - f"{markdown_text(pr['user']['login'])} · [PR #{pr['number']}]({pr['html_url']})" - )}] - for kind, title in (("code", "代码审查与 CI"), ("docs", "文档审查")): - report = reports[kind] + heading = "❌ 审查发现问题" if "issues" in statuses else "⚠️ 审查未完成" if "incomplete" in statuses else "✅ 审查通过" + code, docs = reports["code"], reports["docs"] + + def text(value): # Feishu mentions use HTML-like tags; reports are ordinary Markdown. - summary = report["summary"].replace("<", "<").replace(">", ">") - elements.append({"tag": "markdown", "content": f"**{title}:{STATUSES[report['status']]}**\n{summary}"}) + return value.replace("<", "<").replace(">", ">") + + sections = [ + ("1. 哪个 PR", f"**github id:** {markdown_text(pr['user']['login'])}\n" + f"**标题:** {markdown_text(pr['title'][:200])}\n**链接:** [PR #{pr['number']}]({pr['html_url']})"), + ("2. 改了什么", text(code["changes"])), + ("3. 代码与仓库规则", text(code["review"])), + ("4. CI 结果", text(code["ci"])), + (f"5. 文档审查 · {STATUSES[docs['status']]}", + f"**文档与代码一致性**\n{text(docs['consistency'])}\n\n" + f"**文档矛盾、重复与归属**\n{text(docs['organization'])}"), + ] + elements = [] + for title, content in sections: + if elements: + elements.append({"tag": "hr"}) + elements.append({"tag": "markdown", "content": f"**{title}**\n\n{content}"}) elements.append({"tag": "markdown", "content": f"[查看审查日志]({run_url})"}) return { "msg_type": "interactive", "card": { "schema": "2.0", - "header": {"template": color, "title": {"tag": "plain_text", "content": f"CI 审查 · PR #{pr['number']}"}}, + "header": {"template": color, "title": {"tag": "plain_text", "content": f"{heading} · PR #{pr['number']}"}}, "body": {"elements": elements}, }, } +def card_markdown(card): + return "\n\n".join("---" if element["tag"] == "hr" else element["content"] + for element in card["card"]["body"]["elements"]) + "\n" + + def send_card(webhook, secret, card): if not webhook: raise ValueError("FEISHU_WEBHOOK_URL is not configured") @@ -110,14 +132,16 @@ def send_card(webhook, secret, card): def main(): parser = argparse.ArgumentParser(description=__doc__) commands = parser.add_subparsers(dest="command", required=True) - commands.add_parser("schema") - commands.add_parser("collect").add_argument("output", type=Path) + commands.add_parser("schema").add_argument("kind", choices=REPORT_FIELDS) + collector = commands.add_parser("collect") + collector.add_argument("kind", choices=REPORT_FIELDS) + collector.add_argument("output", type=Path) commands.add_parser("notify").add_argument("directory", type=Path) args = parser.parse_args() if args.command == "schema": - print("schema=" + json.dumps(SCHEMA, separators=(",", ":"))) + print("schema=" + json.dumps(report_schema(args.kind), separators=(",", ":"))) elif args.command == "collect": - report = collect(os.environ.get("REVIEW_OUTCOME"), os.environ.get("REVIEW_RESULT", "")) + report = collect(os.environ.get("REVIEW_OUTCOME"), os.environ.get("REVIEW_RESULT", ""), args.kind) args.output.write_text(json.dumps(report, ensure_ascii=False)) else: event = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text()) @@ -127,7 +151,7 @@ def main(): summary = os.environ.get("GITHUB_STEP_SUMMARY") if summary: with open(summary, "a") as output: - output.write("\n\n".join(element["content"] for element in card["card"]["body"]["elements"]) + "\n") + output.write(card_markdown(card)) send_card(os.environ.get("FEISHU_WEBHOOK_URL"), os.environ.get("FEISHU_WEBHOOK_SECRET"), card) print("代码与文档审查结果已合并发送至飞书群。") diff --git a/scripts/ci_review_test.py b/scripts/ci_review_test.py index 05161c0a8..c80fdfd99 100644 --- a/scripts/ci_review_test.py +++ b/scripts/ci_review_test.py @@ -16,38 +16,73 @@ class ReviewTests(unittest.TestCase): run_url = "https://github.com/org/repo/actions/runs/123" def setUp(self): - self.ok = {"status": "ok", "summary": "未发现问题"} + self.ok = {"status": "ok", "changes": "- 更新安装流程", "review": "未发现问题", "ci": "全部通过 ✅"} + self.docs = {"status": "ok", "consistency": "与代码一致", "organization": "无矛盾或重复"} def card(self, code=None, docs=None): - return review.build_card(self.event, {"code": code or self.ok, "docs": docs or self.ok}, self.run_url) + return review.build_card(self.event, {"code": code or self.ok, "docs": docs or self.docs}, self.run_url) @patch("ci_review.urllib.request.urlopen") def test_two_reports_make_one_delivery(self, post): post.return_value = io.BytesIO(b'{"code":0}') - card = self.card(docs={"status": "issues", "summary": "docs/install.md:12 与代码不符"}) + card = self.card(docs={**self.docs, "status": "issues", "consistency": "docs/install.md:12 与代码不符"}) review.send_card("https://example.invalid/webhook", "", card) post.assert_called_once() payload = json.loads(post.call_args.args[0].data) self.assertEqual(payload["card"]["schema"], "2.0") self.assertEqual(payload["card"]["header"]["template"], "red") - text = "\n".join(item["content"] for item in payload["card"]["body"]["elements"]) - self.assertIn("代码审查与 CI:未发现问题", text) - self.assertIn("文档审查:发现问题", text) + text = review.card_markdown(payload) + self.assertIn("3. 代码与仓库规则", text) + self.assertIn("文档审查 · 发现问题", text) self.assertIn("docs/install.md:12", text) self.assertIn(self.run_url, text) self.assertNotIn("sign", payload) + def test_fixed_sections_preserve_markdown_and_separators(self): + card = self.card() + elements = card["card"]["body"]["elements"] + self.assertEqual(sum(element["tag"] == "hr" for element in elements), 4) + text = review.card_markdown(card) + for heading in ("1. 哪个 PR", "2. 改了什么", "3. 代码与仓库规则", "4. CI 结果", "5. 文档审查"): + self.assertIn(heading, text) + self.assertIn("- 更新安装流程", text) + self.assertIn("全部通过 ✅", text) + self.assertIn("文档与代码一致性", text) + self.assertIn("文档矛盾、重复与归属", text) + self.assertIn("---", text) + self.assertEqual(card["card"]["header"]["template"], "green") + self.assertIn("✅ 审查通过", card["card"]["header"]["title"]["content"]) + + def test_pending_ci_does_not_label_the_code_review_incomplete(self): + card = self.card(code={**self.ok, "status": "incomplete", "ci": "backend 仍在运行"}) + text = review.card_markdown(card) + self.assertIn("**3. 代码与仓库规则**", text) + self.assertIn("未发现问题", text) + self.assertIn("backend 仍在运行", text) + self.assertEqual(card["card"]["header"]["template"], "yellow") + + def test_each_kind_requires_its_own_sections(self): + for kind, report in (("code", self.ok), ("docs", self.docs)): + self.assertEqual(review.parse_report(json.dumps(report), kind), report) + self.assertEqual(set(review.report_schema(kind)["required"]), set(report)) + for field in review.REPORT_FIELDS[kind]: + for value in (None, " ", "x" * (review.MAX_SECTION + 1)): + invalid = {**report, field: value} + self.assertEqual(review.parse_report(json.dumps(invalid), kind)["status"], "incomplete") + missing = {key: value for key, value in report.items() if key != field} + self.assertEqual(review.parse_report(json.dumps(missing), kind)["status"], "incomplete") + def test_failed_action_cannot_publish_a_success_report(self): for outcome in ("failure", "cancelled", "skipped", None): with self.subTest(outcome=outcome): - result = review.collect(outcome, json.dumps(self.ok)) + result = review.collect(outcome, json.dumps(self.ok), "code") self.assertEqual(result["status"], "incomplete") self.assertEqual(self.card(code=result)["card"]["header"]["template"], "yellow") def test_invalid_or_missing_reports_are_incomplete(self): for raw in ("", "not json", "null", "[]", '{"status":"ok"}', '{"status":[],"summary":"x"}', '{"status":"ok","summary":" "}', json.dumps({"status": "ok", "summary": "x" * 2001})): with self.subTest(raw=raw[:50]): - self.assertEqual(review.collect("success", raw)["status"], "incomplete") + self.assertEqual(review.collect("success", raw, "code")["status"], "incomplete") root = Path.home() / ".oac/tests" root.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(dir=root) as directory: @@ -85,8 +120,9 @@ def test_optional_signing(self, post, _clock): @patch("ci_review.urllib.request.urlopen") def test_bounded_unicode_reports_and_mentions(self, post): post.return_value = io.BytesIO(b'{"code":0}') - report = {"status": "issues", "summary": "所有人" + "问" * 1950} - review.send_card("https://example.invalid/webhook", "", self.card(report, report)) + reports = {kind: {"status": "issues", **dict.fromkeys(fields, "所有人" + "问" * (review.MAX_SECTION - 20))} + for kind, fields in review.REPORT_FIELDS.items()} + review.send_card("https://example.invalid/webhook", "", self.card(reports["code"], reports["docs"])) data = post.call_args.args[0].data self.assertLess(len(data), 20000) self.assertNotIn(b" Date: Wed, 7 Oct 2026 13:37:17 +0000 Subject: [PATCH 08/14] Fix agent tests after merging main --- .../agent/codex/execution_controls_test.go | 8 ------ .../internal/agent/configuration_test.go | 2 +- apps/daemon/internal/agent/harness.go | 2 +- apps/daemon/internal/agent/registry_test.go | 27 +++++++------------ 4 files changed, 12 insertions(+), 27 deletions(-) diff --git a/apps/daemon/internal/agent/codex/execution_controls_test.go b/apps/daemon/internal/agent/codex/execution_controls_test.go index b7c56dc98..865a9b817 100644 --- a/apps/daemon/internal/agent/codex/execution_controls_test.go +++ b/apps/daemon/internal/agent/codex/execution_controls_test.go @@ -9,14 +9,6 @@ import ( func TestExecutionControlsSelectNativeSettings(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "state"}) - if err != nil { - t.Fatal(err) - } - plan.Cleanup() - if len(plan.ExtraConfig) != 0 { - t.Fatal("native settings without ExecutionControls", plan.ExtraConfig) - } for _, search := range []string{"disabled", "cached", "live"} { for _, verbosity := range []string{"low", "medium", "high"} { plan, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "state", ExecutionControls: &proto.ExecutionControls{WebSearch: search, TextVerbosity: verbosity}}) diff --git a/apps/daemon/internal/agent/configuration_test.go b/apps/daemon/internal/agent/configuration_test.go index 9b5898e73..8400b7259 100644 --- a/apps/daemon/internal/agent/configuration_test.go +++ b/apps/daemon/internal/agent/configuration_test.go @@ -45,7 +45,7 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { t.Fatal("bound declaration was lost or mutated", err) } } - if calls != 2 { + if calls != 1 { t.Fatal("unexpected native calls", calls) } } diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index 4acd02298..d8cb34baa 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -76,7 +76,7 @@ func (r *Registry) Register(declaration Declaration, runtime Runtime) { if runtime.Info.Kind != declaration.Info.Kind { panic("agent.Registry.Register: discovery kind differs from declaration") } - if !runtime.Info.Available && (runtime.Executor != nil || runtime.View != nil) { + if !runtime.Info.Available && runtime.Executor != nil { panic("agent.Registry.Register: unavailable runtime has factories") } r.RegisterKind(runtime.Info, declaration.Configuration) diff --git a/apps/daemon/internal/agent/registry_test.go b/apps/daemon/internal/agent/registry_test.go index 87f8d63d1..3d484d6dc 100644 --- a/apps/daemon/internal/agent/registry_test.go +++ b/apps/daemon/internal/agent/registry_test.go @@ -36,23 +36,16 @@ func TestRegistryRegisterPanicsOnEmptyKind(t *testing.T) { func TestRegistryRegisterRejectsFactoriesForUnavailableRuntime(t *testing.T) { info := proto.SupportedAgentKind{Kind: "k", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})} executor := func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, nil } - for name, runtime := range map[string]agent.Runtime{ - "executor": {Info: info, Executor: executor}, - "view": {Info: info, View: &agent.View{}}, - } { - t.Run(name, func(t *testing.T) { - registry := agent.NewRegistry() - defer func() { - if recover() == nil { - t.Fatal("unavailable runtime registered factories") - } - if len(registry.SupportedAgentKinds()) != 0 { - t.Fatal("rejected runtime changed registry") - } - }() - registry.Register(agent.Declaration{Info: info}, runtime) - }) - } + registry := agent.NewRegistry() + defer func() { + if recover() == nil { + t.Fatal("unavailable runtime registered factories") + } + if len(registry.SupportedAgentKinds()) != 0 { + t.Fatal("rejected runtime changed registry") + } + }() + registry.Register(agent.Declaration{Info: info}, agent.Runtime{Info: info, Executor: executor}) } func TestRegistrySupportedAgentKindsReportsDescriptors(t *testing.T) { From 3c7147336d83f99060923aa87f9272ecdf0d6a19 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 21:48:13 +0800 Subject: [PATCH 09/14] Mirror the native matrix sentence in zh maintainers (#495) --- docs/zh/maintainers.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index f6b2b17e3..924c3b19f 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -184,9 +184,7 @@ gh workflow run core-release --repo MiniMax-AI/OpenAgentCore --ref main \ `.github/actionlint.yaml` 会选择 hygiene 和 lint。已知工作流变更会选择其使用方:CI review 和 actionlint 工作流运行 hygiene 和 lint;原生工作流变更会添加原生检查;API 验收工作流变更会添加启用容器验收的 API 检查;网站工作流变更会添加网站检查。共享 Node 操作会选择使用它的每个作业以及 lint。新工作流或未分类的工作流/操作会选择完整门禁,直至在计划器中声明其使用方。计划器测试和 CI 测量脚本运行 hygiene;更改计划器本身会运行完整门禁。 -Core 安装器在 Linux、macOS 和 Windows 原生 CI 中构建并测试。Compose 冒烟测试分别使用 Linux amd64 和 arm64 原生 runner。 - -Compose 模板和 Compose 测试发生变更时,会同时选择 `distribution` 固定数据和 `compose` 冒烟作业;Core、Web、共享 Go 软件包和镜像 Dockerfile 的变更也会选择冒烟作业。安装 Docker 后,可在本地运行 `python3 scripts/compose-smoke.py` 重复该测试。该脚本使用唯一的项目、自动分配的回环端口,并将在 `~/.oac/tests/` 下生成构件;退出时移除其容器和数据卷。CI 还会在冒烟步骤失败或中断后执行清理。诊断信息会显示容器状态,但不会打印 HTTP 响应正文或登录密钥。Core、Web 和 ingress 镜像都从当前检出构建;Web 提供占位页面而不是控制台构建。构建时的节点元数据来自 `deploy/compose/smoke-pins.json` 固定的发布版本;初始化容器禁用网络运行。 +Compose 模板和 Compose 测试发生变更时,会同时选择 `distribution` 固定数据和 `compose` 冒烟作业;Core、Web、共享 Go 软件包和镜像 Dockerfile 的变更也会选择冒烟作业。安装 Docker 后,可在本地运行 `python3 scripts/compose-smoke.py` 重复该测试。该脚本使用唯一的项目、自动分配的回环端口,并将在 `~/.oac/tests/` 下生成构件;退出时移除其容器和数据卷。CI 还会在冒烟步骤失败或中断后执行清理。诊断信息会显示容器状态,但不会打印 HTTP 响应正文或登录密钥。Core、Web 和 ingress 镜像都从当前检出构建;Web 提供占位页面而不是控制台构建。构建时的节点元数据来自 `deploy/compose/smoke-pins.json` 固定的发布版本;初始化容器禁用网络运行。冒烟矩阵使用 Linux amd64 和 arm64 原生 runner;原生矩阵在 Linux、macOS 和 Windows 上构建并测试共享的 Core 安装器。 Go 模块和工作区输入会选择后端、API(包括容器)、原生和分发检查。每个 Node 模块都拥有自己的清单和锁文件。网站依赖项会选择网站检查;Web 依赖项会选择 Web 和浏览器检查;示例依赖项会选择示例检查;共享 TypeScript 客户端依赖项会选择 Web、浏览器和示例检查;Claude 适配器依赖项会选择 Harness、原生和分发检查。共享包管理器配置会选择所有 Node 使用方。根 TypeScript 配置会选择 Web 和示例检查;适配器 TypeScript 配置会选择 Harness 和原生检查。每个所选集合都包含 hygiene。混合变更会累加其使用方,并且每个作业都读取同一计划,而不是维护各自的路径列表。例如,仅修改通知的 PR 会跳过数据库、浏览器和原生作业,而同时修改通知和 Core 的 PR 会添加后端和 API 检查。 From 1fcc0661ec1b9aeb7d192edf4881397625e0e5a9 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 21:50:12 +0800 Subject: [PATCH 10/14] refactor(api): generate public contracts from official OpenAPI (#490) * refactor(api): generate public contracts from pinned official OpenAPI * test(api): validate OpenAPI 3.1 responses and preserve package rejection * chore(api): remove unused Swagger parser dependency * fix(api): derive web search union serialization from the official schema * fix(api): preserve stored search items during public schema migration --- .github/workflows/api-acceptance.yml | 1 + AGENTS.md | 2 +- Makefile | 16 +- contracts/agents-api/core.openapi.yaml | 117 +- contracts/agents-api/go-bindings.json | 642 + contracts/agents-api/index.md | 14 +- contracts/agents-api/openapi.yaml | 24873 +++- contracts/agents-api/runtime.openapi.yaml | 4 + contracts/agents-api/upstream-fields.json | 2301 - contracts/agents-api/upstream-routes.json | 10 +- contracts/agents-api/upstream.json | 8 +- contracts/agents-api/upstream/LICENSE | 21 + contracts/agents-api/upstream/openapi.json | 107618 +++++++++++++++ contracts/agents-api/v1/agents.go | 82 - contracts/agents-api/v1/credentials.go | 99 - contracts/agents-api/v1/environment_events.go | 10 - contracts/agents-api/v1/environment_files.go | 25 - .../agents-api/v1/environment_templates.go | 52 - contracts/agents-api/v1/environments.go | 39 - contracts/agents-api/v1/events.go | 42 - contracts/agents-api/v1/function_actions.go | 10 - contracts/agents-api/v1/function_tools.go | 12 - contracts/agents-api/v1/inputs.go | 30 - contracts/agents-api/v1/items.go | 52 - contracts/agents-api/v1/items_test.go | 35 + contracts/agents-api/v1/mcp_tools.go | 34 - contracts/agents-api/v1/official.gen.go | 996 + contracts/agents-api/v1/required_actions.go | 15 - contracts/agents-api/v1/session_artifacts.go | 26 - contracts/agents-api/v1/session_deletion.go | 7 - .../agents-api/v1/session_environment.go | 17 - contracts/agents-api/v1/sessions.go | 130 - contracts/agents-api/v1/skills.go | 55 - contracts/agents-api/v1/source_files.go | 27 - contracts/agents-api/v1/subagent_items.go | 78 +- contracts/agents-api/v1/subagents.go | 28 - contracts/agents-api/v1/turns.go | 28 - .../agents-api/v1/upstream_contract_test.go | 319 +- contracts/agents-api/v1/usage.go | 15 - contracts/agents-api/v1/vaults.go | 30 - contracts/agents-api/zh/index.md | 17 +- docs/development.md | 2 +- docs/zh/development.md | 4 +- scripts/ci_plan.py | 2 +- scripts/extract-agents-api-upstream.py | 193 - scripts/generate-harness-catalog.test.py | 4 + scripts/generate-public-api.py | 276 + scripts/generate-public-api.test.py | 80 + scripts/name-allowlist.json | 5 - scripts/openapi-split/main.go | 146 +- scripts/openapi-split/main_test.go | 24 +- services/core/README.md | 2 +- services/core/internal/api/agents.go | 21 - services/core/internal/api/agents_delete.go | 10 - services/core/internal/api/agents_list.go | 12 - services/core/internal/api/agents_update.go | 12 - .../core/internal/api/contract_routes_test.go | 8 +- services/core/internal/api/credentials.go | 23 - .../core/internal/api/credentials_delete.go | 11 - .../core/internal/api/credentials_list.go | 15 - .../core/internal/api/credentials_update.go | 13 - .../core/internal/api/environment_files.go | 14 - .../internal/api/environment_files_create.go | 12 - .../internal/api/environment_templates.go | 55 - services/core/internal/api/environments.go | 10 - services/core/internal/api/handler.go | 35 - services/core/internal/api/inputs.go | 12 - services/core/internal/api/items.go | 13 - .../core/internal/api/session_artifacts.go | 46 - .../core/internal/api/session_deletion.go | 10 - .../core/internal/api/session_metadata.go | 12 - services/core/internal/api/skills.go | 43 - services/core/internal/api/skills_list.go | 21 - services/core/internal/api/skills_transfer.go | 35 - services/core/internal/api/source_files.go | 18 - .../core/internal/api/source_files_content.go | 8 - .../core/internal/api/source_files_list.go | 12 - .../core/internal/api/source_files_upload.go | 11 - services/core/internal/api/stream.go | 11 - services/core/internal/api/subagent_turns.go | 41 - services/core/internal/api/subagents.go | 38 - services/core/internal/api/turns.go | 24 - services/core/internal/api/vaults.go | 21 - services/core/internal/api/vaults_delete.go | 10 - services/core/internal/api/vaults_list.go | 14 - services/core/tests/official_client.py | 39 +- services/core/tests/official_schema.py | 41 + services/core/tests/official_schema_test.py | 80 + services/core/tests/requirements.txt | 1 - 89 files changed, 129546 insertions(+), 9941 deletions(-) create mode 100644 contracts/agents-api/go-bindings.json delete mode 100644 contracts/agents-api/upstream-fields.json create mode 100644 contracts/agents-api/upstream/LICENSE create mode 100644 contracts/agents-api/upstream/openapi.json delete mode 100644 contracts/agents-api/v1/environment_events.go delete mode 100644 contracts/agents-api/v1/environment_files.go delete mode 100644 contracts/agents-api/v1/environment_templates.go delete mode 100644 contracts/agents-api/v1/environments.go delete mode 100644 contracts/agents-api/v1/function_actions.go delete mode 100644 contracts/agents-api/v1/function_tools.go delete mode 100644 contracts/agents-api/v1/inputs.go delete mode 100644 contracts/agents-api/v1/mcp_tools.go create mode 100644 contracts/agents-api/v1/official.gen.go delete mode 100644 contracts/agents-api/v1/session_artifacts.go delete mode 100644 contracts/agents-api/v1/session_deletion.go delete mode 100644 contracts/agents-api/v1/session_environment.go delete mode 100644 contracts/agents-api/v1/skills.go delete mode 100644 contracts/agents-api/v1/source_files.go delete mode 100644 contracts/agents-api/v1/subagents.go delete mode 100644 contracts/agents-api/v1/turns.go delete mode 100644 contracts/agents-api/v1/usage.go delete mode 100644 contracts/agents-api/v1/vaults.go delete mode 100644 scripts/extract-agents-api-upstream.py create mode 100644 scripts/generate-public-api.py create mode 100644 scripts/generate-public-api.test.py create mode 100644 services/core/tests/official_schema.py create mode 100644 services/core/tests/official_schema_test.py diff --git a/.github/workflows/api-acceptance.yml b/.github/workflows/api-acceptance.yml index d4437a4c9..ee010ed18 100644 --- a/.github/workflows/api-acceptance.yml +++ b/.github/workflows/api-acceptance.yml @@ -62,6 +62,7 @@ jobs: OAC_TEST_SERVER_BIN: ${{ runner.temp }}/oac-core-build/oac-core OAC_TEST_OFFICIAL_SDK_PYTHON: python run: | + python services/core/tests/official_schema_test.py python services/core/tests/official_client.py go test ./services/core/tests/integration -run '^(TestFunctionStateOfficialClientReadsAndLiveEvents|TestSavedReferenceRetryOfficialClient|TestAgentUpdateOfficialClient|TestAgentDeletionOfficialClient|TestSessionAgentFilterOfficialClient|TestSessionDeletionOfficialClient|TestEnvironmentInitialFailureOfficialClient|TestSelfHostedInitialCreationOfficialClient|TestSelfHostedCancellationOfficialClient)$' -count=1 - uses: ./.github/actions/e2b-provider diff --git a/AGENTS.md b/AGENTS.md index 8ec48e767..d6e200722 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ Do not multiply entities without necessity. The long-term goal is minimal code, | Boundary | Protocol code | Protocol doc | | --- | --- | --- | -| Application–Core (`/v1`) | Types in `contracts/agents-api/v1/` and route annotations in `services/core/internal/api/`; `make openapi` generates `contracts/agents-api/openapi.yaml` | [Agents API guide](docs/api/public-agent-api.md) | +| Application–Core (`/v1`) | Official schema pinned by `contracts/agents-api/upstream.json` plus Go-owned `x_agents_core` extensions; `make openapi` generates public Go types and `contracts/agents-api/openapi.yaml` | [Agents API guide](docs/api/public-agent-api.md) | | Web and operators–Core (`/core/v1`) | Route annotations in `services/core/internal/api/`; `make openapi` generates `contracts/agents-api/core.openapi.yaml` | [Core administration API](contracts/agents-api/admin-api.md) | | Nodes and daemons–Core (`/api/v1` HTTP routes; the node and daemon wire protocols are separate rows) | Route annotations in `services/core/internal/api/`; `make openapi` generates `contracts/agents-api/runtime.openapi.yaml` | [Machine connection API](contracts/agents-api/machine-api.md) | | Core–Sandbox Provider | `services/core/internal/sandbox/sandbox_provider.go` | [Sandbox Provider guide](docs/sandbox-provider.md) | diff --git a/Makefile b/Makefile index 1958607db..4d91c1ca8 100644 --- a/Makefile +++ b/Makefile @@ -32,21 +32,29 @@ sqlc-generate: SWAG ?= go run github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION) -.PHONY: openapi +.PHONY: openapi check-openapi +OPENAPI_FLAGS ?= +check-openapi: + $(MAKE) openapi OPENAPI_FLAGS=--check + python3 scripts/generate-public-api.test.py + openapi: + python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) @set -e; root="$${OAC_DEV_HOME:-$$HOME/.oac}/build"; mkdir -p "$$root"; \ output=$$(mktemp -d "$$root/core-openapi.XXXXXX"); trap 'rm -rf "$$output"' EXIT; \ + python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --swag-roots "$$output/roots.go"; \ $(SWAG) init \ - -g cmd/server/main.go --dir ./services/core,./contracts/agents-api/v1 \ + -g cmd/server/main.go --dir "./services/core,./contracts/agents-api/v1,$$output" \ --output "$$output" \ --outputTypes yaml --parseInternal; \ python3 scripts/patch-agents-openapi.py "$$output/swagger.yaml"; \ - go run ./scripts/openapi-split "$$output/swagger.yaml" contracts/agents-api/openapi.yaml contracts/agents-api/core.openapi.yaml contracts/agents-api/runtime.openapi.yaml + go run ./scripts/openapi-split $(OPENAPI_FLAGS) "$$output/swagger.yaml" "$$output/extensions.json" contracts/agents-api/core.openapi.yaml contracts/agents-api/runtime.openapi.yaml; \ + python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --extensions "$$output/extensions.json" check-sqlc: python3 scripts/check-sqlc.py -check-go: +check-go: check-openapi go test ./apps/daemon/... ./internal/... ./contracts/agents-api/... ./scripts/openapi-split -count=1 .PHONY: check-runtime-contract diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index 9a74d1381..fb65920bd 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -1458,6 +1458,10 @@ definitions: service_tier: enum: - auto + - default + - flex + - priority + - fast type: string text: $ref: '#/definitions/v1.TextConfig' @@ -1466,13 +1470,13 @@ definitions: type: object type: array x_agents_core: - allOf: - - $ref: '#/definitions/v1.AgentsCore' - x-nullable: true + $ref: '#/definitions/v1.AgentsCore' required: - id + - instructions - model - multi_agent + - name - reasoning - service_tier - text @@ -1481,8 +1485,6 @@ definitions: v1.AgentDeleted: properties: deleted: - enum: - - true type: boolean id: type: string @@ -1546,8 +1548,8 @@ definitions: x-nullable: true type: enum: - - static_bearer - mcp_oauth + - static_bearer type: string required: - mcp_server_url @@ -1588,7 +1590,9 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.EnvironmentInstallation: @@ -1684,6 +1688,7 @@ definitions: - created_at - files - id + - name - network - object - packages @@ -1726,7 +1731,9 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.ExecutionHarnessConfigSelection: @@ -1794,7 +1801,9 @@ definitions: v1.Item: properties: action: - $ref: '#/definitions/v1.WebSearchAction' + allOf: + - $ref: '#/definitions/v1.WebSearchAction' + x-nullable: true agent_id: type: string arguments: {} @@ -1808,18 +1817,25 @@ definitions: type: array cwd: type: string + x-nullable: true duration_ms: type: integer - error: {} + x-nullable: true + error: + x-nullable: true exit_code: type: integer + x-nullable: true id: type: string + x-nullable: true model: type: string + x-nullable: true name: type: string - output: {} + output: + x-nullable: true phase: enum: - commentary @@ -1828,6 +1844,7 @@ definitions: x-nullable: true reasoning_effort: type: string + x-nullable: true recipient_agent_id: type: string recipient_agent_ids: @@ -1847,9 +1864,10 @@ definitions: enum: - in_progress - completed - - failed - incomplete + - failed type: string + x-nullable: true summary: items: $ref: '#/definitions/v1.SummaryText' @@ -1859,13 +1877,13 @@ definitions: type: enum: - message - - command_execution - - mcp_call + - reasoning - function_call - function_call_output - - web_search_call - - reasoning - agent_message + - mcp_call + - web_search_call + - command_execution - create_subagent_call - send_subagent_input_call - resume_subagent_call @@ -1889,8 +1907,8 @@ definitions: type: enum: - input_text - - output_text - input_image + - output_text - encrypted_content type: string required: @@ -1916,7 +1934,9 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.ModelConfigurationInput: @@ -1997,6 +2017,7 @@ definitions: x-nullable: true required: - enabled + - max_concurrent_subagents type: object v1.OAuthCredentialRefresh: properties: @@ -2014,6 +2035,8 @@ definitions: $ref: '#/definitions/v1.OAuthEndpointAuth' required: - client_id + - resource + - scope - token_endpoint - token_endpoint_auth type: object @@ -2038,9 +2061,21 @@ definitions: v1.Reasoning: properties: effort: + enum: + - none + - minimal + - low + - medium + - high + - xhigh + - max type: string x-nullable: true summary: + enum: + - concise + - detailed + - auto type: string x-nullable: true type: object @@ -2560,15 +2595,15 @@ definitions: updated_at: type: integer x_agents_core: - allOf: - - $ref: '#/definitions/v1.SavedAgentCore' - x-nullable: true + $ref: '#/definitions/v1.SavedAgentCore' required: - created_at - id + - instructions - metadata - model - multi_agent + - name - object - reasoning - service_tier @@ -2609,7 +2644,9 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.SavedAgentText: @@ -2686,12 +2723,14 @@ definitions: - agent - created_at - environment + - error - id - last_active_at - metadata - object - required_actions - status + - usage - vault_ids type: object v1.SessionArtifact: @@ -2759,7 +2798,9 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.SessionCore: @@ -2811,8 +2852,8 @@ definitions: type: enum: - none - - self_hosted - openai_hosted + - self_hosted type: string workspace_directory: type: string @@ -2868,7 +2909,9 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.Skill: @@ -2933,7 +2976,9 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.SkillVersion: @@ -3001,19 +3046,19 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.SourceFile: properties: bytes: - minimum: 0 type: integer created_at: type: integer expires_at: type: integer - x-nullable: true filename: type: string id: @@ -3024,15 +3069,23 @@ definitions: type: string purpose: enum: + - assistants + - assistants_output + - batch + - batch_output + - fine-tune + - fine-tune-results + - vision - user_data type: string status: enum: + - uploaded - processed + - error type: string status_details: type: string - x-nullable: true required: - bytes - created_at @@ -3065,19 +3118,17 @@ definitions: type: array first_id: type: string - x-nullable: true has_more: type: boolean last_id: type: string - x-nullable: true object: - enum: - - list type: string required: - data + - first_id - has_more + - last_id - object type: object v1.SummaryText: @@ -3179,11 +3230,16 @@ definitions: x-nullable: true required: - agent_id + - completed_at - created_at + - error - id - object - session_id + - started_at - status + - subagent_id + - usage type: object v1.TurnError: properties: @@ -3232,7 +3288,9 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.Vault: @@ -3256,6 +3314,7 @@ definitions: - created_at - id - metadata + - name - object type: object v1.VaultDeleted: @@ -3293,19 +3352,24 @@ definitions: type: string required: - data + - first_id - has_more + - last_id - object type: object v1.WebSearchAction: properties: pattern: type: string + x-nullable: true queries: items: type: string type: array + x-nullable: true query: type: string + x-nullable: true type: enum: - search @@ -3315,6 +3379,7 @@ definitions: type: string url: type: string + x-nullable: true required: - type type: object diff --git a/contracts/agents-api/go-bindings.json b/contracts/agents-api/go-bindings.json new file mode 100644 index 000000000..176a7d1c7 --- /dev/null +++ b/contracts/agents-api/go-bindings.json @@ -0,0 +1,642 @@ +{ + "APIError": { + "sources": ["#/components/schemas/Error"], + "fields": { + "code": {"type": "*string"} + }, + "order": ["message", "type", "code", "param"] + }, + "Agent": { + "sources": ["#/components/schemas/SessionAgentResource"], + "fields": { + "x_agents_core": {"type": "*AgentsCore"}, + "reasoning": {"type": "Reasoning"}, + "text": {"type": "TextConfig"} + }, + "order": ["x_agents_core", "id", "instructions", "model", "multi_agent", "name", "reasoning", "service_tier", "text", "tools"] + }, + "AgentContent": { + "sources": ["#/components/schemas/AgentContentResource"], + "order": ["type", "text", "encrypted_content"] + }, + "AgentDeleted": { + "sources": ["#/components/schemas/DeletedAgentResource"], + "order": ["id", "object", "deleted"] + }, + "AgentMessageItem": { + "sources": ["#/components/schemas/AgentMessageItemResource"], + "order": ["id", "turn_id", "type", "sender_agent_id", "recipient_agent_id", "content"] + }, + "CreateAgentRequest": { + "sources": ["#/components/schemas/CreateAgentParams"], + "fields": { + "x_agents_core": {"type": "*SavedAgentCoreInput"}, + "model": {"type": "*string"}, + "metadata": {"type": "map[string]*string"}, + "reasoning": {"type": "*Reasoning"}, + "text": {"type": "*SavedAgentTextInput"} + }, + "order": ["x_agents_core", "model", "name", "instructions", "metadata", "multi_agent", "reasoning", "service_tier", "text", "tools"] + }, + "CreateCredentialRequest": { + "sources": ["#/components/schemas/CreateVaultCredentialParams"], + "fields": { + "name": {"type": "*string"}, + "auth": {"type": "*CredentialAuthInput"} + }, + "order": ["name", "auth"] + }, + "CreateEventsRequest": { + "sources": ["#/components/schemas/CreateSessionEventsParams"], + "order": ["events"] + }, + "CreateSessionRequest": { + "sources": ["#/components/schemas/CreateAgentSessionParams"], + "fields": { + "x_agents_core": {"type": "*SessionExecutionInput"}, + "environment": {"type": "*Environment"}, + "input": {"type": "any"}, + "stream": {"type": "bool"} + }, + "order": ["x_agents_core", "agent", "agent_id", "environment", "input", "metadata", "stream", "vault_ids"] + }, + "CreateSubagentCallItem": { + "sources": ["#/components/schemas/CreateSubagentCallItemResource"], + "order": ["id", "turn_id", "type", "status", "agent_id", "content", "model", "reasoning_effort"] + }, + "CreateVaultRequest": { + "sources": ["#/components/schemas/CreateVaultParams"], + "fields": { + "metadata": {"type": "map[string]*string"} + }, + "order": ["name", "metadata"] + }, + "Credential": { + "sources": ["#/components/schemas/VaultCredentialResource"], + "order": ["id", "vault_id", "name", "object", "auth", "created_at", "updated_at"] + }, + "CredentialAuth": { + "sources": ["#/components/schemas/VaultCredentialAuthResource"], + "fields": { + "expires_at": {"omit": false}, + "refresh": {"type": "*OAuthCredentialRefresh", "omit": false} + }, + "order": ["type", "mcp_server_url", "expires_at", "refresh"] + }, + "CredentialAuthInput": { + "sources": ["#/components/schemas/CreateVaultCredentialAuthParam"], + "fields": { + "mcp_server_url": {"type": "*string"}, + "refresh": {"type": "*OAuthCredentialRefreshInput"} + }, + "order": ["type", "mcp_server_url", "token", "access_token", "expires_at", "refresh"] + }, + "CredentialAuthReplacement": { + "sources": ["#/components/schemas/RotateVaultCredentialAuthParam"], + "fields": { + "expires_at": {"type": "json.RawMessage"}, + "refresh": {"type": "*OAuthCredentialRefreshReplacement"} + }, + "order": ["type", "token", "access_token", "expires_at", "refresh"] + }, + "CredentialDeleted": { + "sources": ["#/components/schemas/DeletedVaultCredentialResource"], + "order": ["id", "deleted", "object"] + }, + "CredentialList": { + "sources": ["#/components/schemas/VaultCredentialListResource"], + "order": ["object", "data", "has_more", "first_id", "last_id"] + }, + "Environment": { + "sources": ["#/components/schemas/EnvironmentParam"], + "fields": { + "packages": {"type": "*EnvironmentPackages"}, + "files": {"type": "[]json.RawMessage"}, + "environment_template_id": {"type": "string"}, + "workspace_directory": {"type": "string"}, + "network": {"type": "*EnvironmentNetworkInput"} + }, + "order": ["plugins", "skills", "env", "setup_commands", "packages", "files", "environment_template_id", "type", "workspace_directory", "capability_directories", "network"] + }, + "EnvironmentConnectionAction": { + "sources": ["#/components/schemas/SessionRequiredActionResourceEnvironmentConnection"], + "order": ["environment_id", "type"] + }, + "EnvironmentFile": { + "sources": ["#/components/schemas/EnvironmentFileResource"], + "order": ["environment_id", "object", "path", "size_bytes"] + }, + "EnvironmentFileCreateRequest": { + "sources": ["#/components/schemas/HostedEnvironmentFileParam"], + "fields": { + "path": {"type": "*string"} + }, + "order": ["type", "data", "file_id", "path"] + }, + "EnvironmentFileList": { + "sources": ["#/components/schemas/EnvironmentFileListResource"], + "order": ["object", "data", "next", "has_more"] + }, + "EnvironmentInfo": { + "sources": ["#/components/schemas/PublicEnvironmentResource"], + "order": ["id", "object", "type", "status", "files", "plugins", "skills"] + }, + "EnvironmentNetwork": { + "sources": ["#/components/schemas/NetworkPolicyResource"], + "order": ["access", "allowed_domains"] + }, + "EnvironmentNetworkInput": { + "sources": ["#/components/schemas/NetworkPolicyParam"], + "order": ["access", "allowed_domains"] + }, + "EnvironmentPackages": { + "exclude": ["system"], + "sources": ["#/components/schemas/EnvironmentPackagesParam"], + "fields": { + "npm": {"omit": false}, + "python": {"omit": false} + }, + "order": ["npm", "python"] + }, + "EnvironmentPackagesInput": { + "exclude": ["system"], + "sources": ["#/components/schemas/EnvironmentPackagesParam"], + "order": ["npm", "python"] + }, + "EnvironmentPackagesResponse": { + "sources": ["#/components/schemas/EnvironmentPackagesResource"], + "order": ["npm", "python", "system"] + }, + "EnvironmentTemplate": { + "sources": ["#/components/schemas/EnvironmentTemplateResource"], + "order": ["id", "object", "name", "created_at", "updated_at", "capability_directories", "network", "packages", "files", "plugins", "skills"] + }, + "EnvironmentTemplateDeleted": { + "sources": ["#/components/schemas/DeletedEnvironmentTemplateResource"], + "order": ["id", "object", "deleted"] + }, + "EnvironmentTemplateList": { + "sources": ["#/components/schemas/EnvironmentTemplateListResource"], + "order": ["object", "data", "has_more", "first_id", "last_id"] + }, + "EnvironmentTemplateRequest": { + "sources": ["#/components/schemas/CreateEnvironmentTemplateParams"], + "fields": { + "network": {"type": "*EnvironmentNetworkInput"}, + "files": {"type": "[]json.RawMessage"}, + "packages": {"type": "*EnvironmentPackagesInput"} + }, + "order": ["name", "network", "capability_directories", "env", "files", "packages", "plugins", "skills", "setup_commands"] + }, + "ErrorResponse": { + "sources": ["#/components/schemas/ErrorResponse-2"], + "fields": {"error": {"type": "APIError"}}, + "order": ["error"] + }, + "FunctionCallAction": { + "sources": ["#/components/schemas/SessionRequiredActionResourceFunctionCall"], + "fields": { + "arguments": {"type": "any"} + }, + "order": ["arguments", "call_id", "name", "turn_id", "type"] + }, + "FunctionToolInput": { + "sources": ["#/components/schemas/AgentToolConfigParamFunction"], + "fields": { + "name": {"type": "*string"}, + "description": {"type": "*string"}, + "parameters": {"type": "json.RawMessage"}, + "defer_loading": {"type": "json.RawMessage"} + }, + "order": ["type", "name", "description", "parameters", "defer_loading"] + }, + "InlineAgent": { + "sources": ["#/components/schemas/SessionAgentConfigParam"], + "fields": { + "x_agents_core": {"type": "*AgentsCore"}, + "reasoning": {"type": "*Reasoning"}, + "text": {"type": "*SavedAgentTextInput"} + }, + "order": ["x_agents_core", "model", "instructions", "multi_agent", "reasoning", "service_tier", "text", "tools"] + }, + "InputContent": { + "sources": ["#/components/schemas/InputContentParam"], + "order": ["type", "text", "image_url"] + }, + "InputMessage": { + "sources": ["#/components/schemas/InputMessageParam"], + "fields": { + "type": {"type": "string"} + }, + "order": ["type", "role", "content"] + }, + "InputTokenDetails": { + "sources": ["#/components/schemas/InputTokensDetailsResource"], + "order": ["cached_tokens"] + }, + "Item": { + "sources": ["#/components/schemas/SessionTurnItemResource"], + "fields": { + "id": {"type": "string"}, + "status": {"type": "string", "omit": false}, + "role": {"type": "string"}, + "phase": {"type": "string"}, + "content": {"type": "[]ItemContent"}, + "command": {"type": "string"}, + "name": {"type": "string"}, + "call_id": {"type": "string"}, + "server_label": {"type": "string"}, + "arguments": {"type": "any"}, + "output": {"type": "any"}, + "error": {"type": "any"}, + "action": {"type": "*WebSearchAction"}, + "agent_id": {"type": "string"}, + "sender_agent_id": {"type": "string"}, + "recipient_agent_id": {"type": "string"} + }, + "order": ["id", "turn_id", "type", "status", "role", "phase", "content", "command", "cwd", "duration_ms", "exit_code", "name", "call_id", "server_label", "arguments", "output", "error", "action", "agent_id", "sender_agent_id", "recipient_agent_id", "recipient_agent_ids", "model", "reasoning_effort", "summary"] + }, + "ItemContent": { + "sources": ["#/components/schemas/MessageContentResource", "#/components/schemas/EncryptedContentResource"], + "fields": { + "image_url": {"type": "string"} + }, + "order": ["type", "text", "image_url", "encrypted_content"] + }, + "ItemList": { + "sources": ["#/components/schemas/SessionItemListResource"], + "order": ["object", "first_id", "last_id", "data", "has_more"] + }, + "MCPHTTPTransport": { + "sources": ["#/components/schemas/PersistedMcpTransportConfigParamHttp"], + "fields": { + "headers": {"type": "*map[string]string"} + }, + "order": ["type", "server_url", "headers"] + }, + "MCPTool": { + "sources": ["#/components/schemas/AgentToolResourceMcp"], + "fields": { + "transport": {"type": "MCPHTTPTransport"}, + "allowed_tools": {"type": "*[]string"} + }, + "order": ["type", "server_label", "transport", "allowed_tools", "connection_origin", "credential_id", "request_metadata", "required"] + }, + "MCPToolInput": { + "sources": ["#/components/schemas/AgentToolConfigParamMcp"], + "fields": { + "server_label": {"type": "*string"}, + "allowed_tools": {"type": "json.RawMessage", "omit": false}, + "connection_origin": {"omit": false}, + "credential_id": {"omit": false}, + "request_metadata": {"type": "json.RawMessage", "omit": false}, + "required": {"type": "json.RawMessage", "omit": false} + }, + "order": ["type", "server_label", "transport", "allowed_tools", "connection_origin", "credential_id", "request_metadata", "required"] + }, + "MultiAgentConfig": { + "sources": ["#/components/schemas/MultiAgentConfigResource"], + "fields": { + "max_concurrent_subagents": {"type": "*int"} + }, + "order": ["enabled", "max_concurrent_subagents"] + }, + "OAuthCredentialRefresh": { + "sources": ["#/components/schemas/McpOauthRefreshResource"], + "order": ["client_id", "token_endpoint", "token_endpoint_auth", "resource", "scope"] + }, + "OAuthCredentialRefreshInput": { + "sources": ["#/components/schemas/CreateMcpOauthRefreshParam"], + "fields": { + "client_id": {"type": "*string"}, + "refresh_token": {"type": "*string"}, + "token_endpoint": {"type": "*string"}, + "token_endpoint_auth": {"type": "*OAuthEndpointAuthInput"} + }, + "order": ["client_id", "refresh_token", "token_endpoint", "token_endpoint_auth", "resource", "scope"] + }, + "OAuthCredentialRefreshReplacement": { + "sources": ["#/components/schemas/RotateMcpOauthRefreshParam"], + "fields": { + "scope": {"type": "json.RawMessage"}, + "token_endpoint_auth": {"type": "*OAuthEndpointAuthReplacement"} + }, + "order": ["refresh_token", "scope", "token_endpoint_auth"] + }, + "OAuthEndpointAuth": { + "sources": ["#/components/schemas/McpOauthTokenEndpointAuthResource"], + "order": ["type"] + }, + "OAuthEndpointAuthInput": { + "sources": ["#/components/schemas/CreateMcpOauthTokenEndpointAuthParam"], + "order": ["type", "client_secret"] + }, + "OAuthEndpointAuthReplacement": { + "sources": ["#/components/schemas/RotateMcpOauthTokenEndpointAuthParam"], + "order": ["type", "client_secret"] + }, + "OutputTokenDetails": { + "sources": ["#/components/schemas/OutputTokensDetailsResource"], + "order": ["reasoning_tokens"] + }, + "Reasoning": { + "sources": ["#/components/schemas/ReasoningParam"], + "order": ["effort", "summary"] + }, + "ReasoningItem": { + "sources": ["#/components/schemas/ReasoningItemResource"], + "order": ["id", "turn_id", "type", "status", "summary"] + }, + "RequiredAction": { + "sources": ["#/components/schemas/SessionRequiredActionResource"], + "fields": { + "arguments": {"type": "any"}, + "call_id": {"type": "string"}, + "name": {"type": "string"}, + "turn_id": {"type": "string"}, + "environment_id": {"type": "string"} + }, + "order": ["type", "arguments", "call_id", "name", "turn_id", "environment_id"] + }, + "SavedAgent": { + "sources": ["#/components/schemas/AgentResource"], + "embed": {"SavedAgentConfiguration": ["x_agents_core", "model", "name", "instructions", "multi_agent", "reasoning", "service_tier", "text", "tools"]}, + "order": ["id", "object", "metadata", "created_at", "updated_at"] + }, + "SavedAgentConfiguration": { + "sources": ["#/components/schemas/AgentResource"], + "fields": { + "x_agents_core": {"type": "*SavedAgentCore"}, + "reasoning": {"type": "Reasoning"} + }, + "exclude": ["id", "object", "created_at", "updated_at", "metadata"], + "order": ["x_agents_core", "model", "name", "instructions", "multi_agent", "reasoning", "service_tier", "text", "tools"] + }, + "SavedAgentList": { + "sources": ["#/components/schemas/AgentListResource"], + "order": ["object", "data", "has_more", "first_id", "last_id"] + }, + "SavedAgentText": { + "sources": ["#/components/schemas/TextResource"], + "order": ["format", "verbosity"] + }, + "SavedAgentTextFormat": { + "sources": ["#/components/schemas/TextFormatResource"], + "fields": { + "schema": {"type": "json.RawMessage"} + }, + "order": ["type", "schema"] + }, + "SavedAgentTextInput": { + "sources": ["#/components/schemas/TextParam"], + "order": ["format", "verbosity"] + }, + "SendSubagentInputCallItem": { + "sources": ["#/components/schemas/SendSubagentInputCallItemResource"], + "order": ["id", "turn_id", "type", "status", "sender_agent_id", "recipient_agent_id", "content"] + }, + "Session": { + "sources": ["#/components/schemas/SessionResource"], + "fields": { + "x_agents_core": {"type": "*SessionCore"}, + "usage": {"type": "*TokenUsage"} + }, + "order": ["x_agents_core", "id", "agent", "created_at", "environment", "error", "last_active_at", "metadata", "object", "required_actions", "status", "usage", "vault_ids"] + }, + "SessionArtifact": { + "sources": ["#/components/schemas/SessionArtifactResource"], + "order": ["id", "created_at", "environment_id", "object", "path", "session_id", "size_bytes", "turn_id"] + }, + "SessionArtifactDeleted": { + "sources": ["#/components/schemas/DeletedSessionArtifactResource"], + "order": ["id", "object", "deleted"] + }, + "SessionArtifactList": { + "sources": ["#/components/schemas/SessionArtifactListResource"], + "order": ["object", "first_id", "last_id", "data", "has_more"] + }, + "SessionDeleted": { + "sources": ["#/components/schemas/DeletedSessionResource"], + "order": ["id", "deleted", "object"] + }, + "SessionEnvironment": { + "sources": ["#/components/schemas/EnvironmentResource"], + "fields": { + "id": {"type": "string"}, + "capability_directories": {"type": "*[]string"}, + "remote_url": {"type": "string"}, + "workspace_directory": {"type": "string"}, + "files": {"type": "*[]json.RawMessage"}, + "plugins": {"type": "*[]json.RawMessage"}, + "skills": {"type": "*[]json.RawMessage"} + }, + "order": ["type", "id", "capability_directories", "remote_url", "workspace_directory", "network", "packages", "files", "plugins", "skills"] + }, + "SessionEnvironmentState": { + "sources": ["#/components/schemas/SessionEnvironmentStateResource"], + "fields": { + "error": {"type": "*StreamError"} + }, + "order": ["id", "type", "status", "error"] + }, + "SessionEvent": { + "sources": ["#/components/schemas/SessionEvent"], + "fields": { + "subagent": {"type": "*Subagent"}, + "session_id": {"type": "string"}, + "turn_id": {"type": "string"}, + "session": {"type": "*Session"}, + "turn": {"type": "*Turn"}, + "item": {"type": "*Item"}, + "item_id": {"type": "string"}, + "output_index": {"type": "*int32"}, + "content_index": {"type": "*int"}, + "part": {"type": "*ItemContent"}, + "usage": {"type": "*TokenUsage"} + }, + "order": ["subagent", "type", "event_id", "session_id", "turn_id", "session", "turn", "item", "item_id", "output_index", "content_index", "part", "delta", "text", "error", "environment", "usage"] + }, + "SessionInput": { + "sources": ["#/components/schemas/SessionInputParam"], + "fields": { + "call_id": {"type": "string"}, + "turn_id": {"type": "string"}, + "error": {"type": "json.RawMessage"}, + "output": {"type": "any"} + }, + "order": ["type", "input", "call_id", "turn_id", "success", "error", "output"] + }, + "SessionList": { + "sources": ["#/components/schemas/SessionListResource"], + "order": ["object", "first_id", "last_id", "data", "has_more"] + }, + "Skill": { + "sources": ["#/components/schemas/SkillResource"], + "order": ["id", "object", "created_at", "name", "description", "default_version", "latest_version"] + }, + "SkillDeleted": { + "sources": ["#/components/schemas/DeletedSkillResource"], + "order": ["id", "object", "deleted"] + }, + "SkillList": { + "sources": ["#/components/schemas/SkillListResource"], + "order": ["object", "data", "first_id", "last_id", "has_more"] + }, + "SkillUpdateRequest": { + "sources": ["#/components/schemas/SetDefaultSkillVersionBody"], + "order": ["default_version"] + }, + "SkillVersion": { + "sources": ["#/components/schemas/SkillVersionResource"], + "order": ["id", "object", "created_at", "skill_id", "version", "name", "description"] + }, + "SkillVersionDeleted": { + "sources": ["#/components/schemas/DeletedSkillVersionResource"], + "order": ["id", "object", "version", "deleted"] + }, + "SkillVersionList": { + "sources": ["#/components/schemas/SkillVersionListResource"], + "order": ["object", "data", "first_id", "last_id", "has_more"] + }, + "SourceFile": { + "sources": ["#/components/schemas/OpenAIFile"], + "fields": { + "expires_at": {"omit": false}, + "status_details": {"omit": false} + }, + "order": ["id", "object", "bytes", "created_at", "filename", "purpose", "status", "expires_at", "status_details"] + }, + "SourceFileDeleted": { + "sources": ["#/components/schemas/DeleteFileResponse"], + "order": ["id", "object", "deleted"] + }, + "SourceFileList": { + "sources": ["#/components/schemas/ListFilesResponse"], + "fields": { + "first_id": {"type": "*string"}, + "last_id": {"type": "*string"} + }, + "order": ["object", "data", "has_more", "first_id", "last_id"] + }, + "StreamError": { + "sources": ["#/components/schemas/SessionErrorResource"], + "fields": { + "code": {"type": "string"}, + "param": {"omit": true} + }, + "order": ["code", "type", "message", "param"] + }, + "Subagent": { + "sources": ["#/components/schemas/SubagentResource"], + "order": ["id", "object", "session_id", "parent_agent_id", "opened_at", "closed_at", "name", "instructions", "status"] + }, + "SubagentControlCallItem": { + "sources": ["#/components/schemas/ResumeSubagentCallItemResource", "#/components/schemas/InterruptSubagentCallItemResource", "#/components/schemas/CloseSubagentCallItemResource"], + "order": ["id", "turn_id", "type", "status", "sender_agent_id", "recipient_agent_id"] + }, + "SubagentList": { + "sources": ["#/paths/~1agents~1sessions~1{session_id}~1subagents/get/responses/200/content/application~1json/schema"], + "order": ["object", "first_id", "last_id", "data", "has_more"] + }, + "SummaryText": { + "sources": ["#/components/schemas/SummaryTextResource"], + "order": ["type", "text"] + }, + "TextConfig": { + "sources": ["#/components/schemas/TextResource"], + "order": ["format", "verbosity"], + "fields": { + "format": {"type": "TextFormat"} + } + }, + "TextConfigInput": { + "sources": ["#/components/schemas/TextParam"], + "fields": { + "format": {"type": "*TextFormat"} + }, + "order": ["format", "verbosity"] + }, + "TextFormat": { + "sources": ["#/components/schemas/TextFormatResource"], + "fields": { + "schema": {"type": "json.RawMessage"} + }, + "order": ["type", "schema"] + }, + "TokenUsage": { + "sources": ["#/components/schemas/TokenUsageResource"], + "order": ["input_tokens", "input_tokens_details", "output_tokens", "output_tokens_details", "total_tokens"] + }, + "Turn": { + "sources": ["#/components/schemas/TurnResource"], + "fields": { + "error": {"type": "*TurnError"}, + "usage": {"type": "*TokenUsage"} + }, + "order": ["id", "agent_id", "subagent_id", "session_id", "object", "status", "created_at", "started_at", "completed_at", "error", "usage"] + }, + "TurnError": { + "sources": ["#/components/schemas/SessionTurnErrorResource"], + "order": ["code", "message"] + }, + "TurnList": { + "sources": ["#/components/schemas/SessionTurnListResource"], + "order": ["object", "first_id", "last_id", "data", "has_more"] + }, + "UpdateAgentRequest": { + "sources": ["#/components/schemas/UpdateAgentParams"], + "fields": { + "x_agents_core": {"type": "*SavedAgentCoreInput"}, + "metadata": {"type": "map[string]*string"}, + "reasoning": {"type": "*Reasoning"}, + "text": {"type": "*SavedAgentTextInput"} + }, + "order": ["x_agents_core", "model", "name", "instructions", "metadata", "multi_agent", "reasoning", "service_tier", "text", "tools"] + }, + "UpdateCredentialRequest": { + "sources": ["#/components/schemas/RotateVaultCredentialParams"], + "fields": { + "auth": {"type": "*CredentialAuthReplacement"} + }, + "order": ["auth"] + }, + "UpdateSessionRequest": { + "sources": ["#/components/schemas/UpdateAgentSessionParams"], + "fields": { + "metadata": {"omit": false} + }, + "order": ["metadata"] + }, + "Vault": { + "sources": ["#/components/schemas/VaultResource"], + "order": ["id", "object", "created_at", "name", "metadata"] + }, + "VaultDeleted": { + "sources": ["#/components/schemas/DeletedVaultResource"], + "order": ["id", "deleted", "object"] + }, + "VaultList": { + "sources": ["#/components/schemas/VaultListResource"], + "order": ["object", "data", "has_more", "first_id", "last_id"] + }, + "WaitForSubagentsCallItem": { + "sources": ["#/components/schemas/WaitForSubagentsCallItemResource"], + "order": ["id", "turn_id", "type", "status", "sender_agent_id", "recipient_agent_ids"] + }, + "WebSearchAction": { + "marshal_union": true, + "sources": ["#/components/schemas/WebSearchActionResource"], + "order": ["type", "query", "queries", "url", "pattern"] + }, + "reasoningResponse": { + "sources": ["#/components/schemas/ReasoningResource"], + "order": ["effort", "summary"] + }, + "sessionError": { + "sources": ["#/components/schemas/SessionErrorResource"], + "fields": { + "code": {"type": "string"} + }, + "order": ["code", "type", "message", "param"] + } +} diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index be6bed469..87dd67793 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -9,10 +9,18 @@ Core targets the complete OpenAI Agents API as pinned below ([public API rule](h | File | Contents | | --- | --- | | [upstream.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json) | The pin: [openai-python](https://github.com/openai/openai-python/tree/d7c41efee1b0802b79f3f88a678ef2052b06e9ce/src/openai/resources/beta/agents) 3.13.0 at commit `d7c41ef`, resources under `beta/agents`, Beta header `agents=v1` | -| [upstream-routes.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream-routes.json), [upstream-fields.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream-fields.json) | The 58 method and path pairs and their official fields: 42 operations under `beta/agents`, 5 Files and 11 Skills operations. `scripts/extract-agents-api-upstream.py` extracts them from the pinned SDK; run it with that SDK installed | -| [openapi.yaml](./openapi.yaml) | Core's public schema, generated by `make openapi` from the route annotations in `services/core/internal/api/` and the wire types in [`v1/`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/contracts/agents-api/v1) | +| [upstream/openapi.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream/openapi.json) | Unmodified official OpenAPI 3.1 at commit `046a2a0f325bf11f97966f2729219f27281ba71e`, published on 2026-09-10. Its 58 Agents, Vaults, Files and Skills operations match the pinned SDK route set. `upstream.json` records the SHA-256 checksum | +| [upstream-routes.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream-routes.json) | The normalized method/path inventory generated from the official schema | +| [openapi.yaml](./openapi.yaml) | The official public contract with Core's `x_agents_core` extension on Agent and Session request/response objects | +| [go-bindings.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/go-bindings.json) | Go names, field representations, encoding order and stored projections; it does not define official field membership, enums or constraints | -Contract tests hold Core to the pin: the router and `openapi.yaml` serve exactly the pinned routes (`services/core/internal/api/routing_test.go`, `v1/upstream_contract_test.go`), every query parameter and field is official, and Core-only fields sit only inside `x_agents_core` on Agents and Sessions. Swagger 2.0 cannot express string-or-array unions, so `openapi.yaml` leaves Session `input` and function-result `output` unconstrained; the pinned types and Core's validation define them. Operations and fields newer than the pin wait for a protocol upgrade. +Run `make openapi` to regenerate the public Go types, route inventory and all three OpenAPI documents. `scripts/generate-public-api.py` reads the checked-in, checksum-verified official source without network access. It selects Agents, Vaults, Files and Skills and follows their schema references, preserving union types, nullability, required fields and constraints. Core's extension types in `v1/` remain authored in Go and are added to the public schema during generation. The internal `/core/v1` and `/api/v1` documents come from handler annotations. `make check-openapi` checks freshness and the generator; it also runs through `make check-go`. + +The public contract is the official API plus Core extensions. Standard fields are generated into `v1/official.gen.go`; `go-bindings.json` controls their Go representation where existing storage or custom JSON encoding requires it. Selected discriminated unions also generate JSON serializers to retain required nullable fields for each variant. Other union serializers, request admission and state transitions remain implementation code. Contract tests verify that the public schema preserves the official definitions, extensions remain in `x_agents_core`, and all documents match registered routes. Official-client and raw HTTP tests verify behavior. Schema generation does not qualify an unimplemented feature; the gaps below still apply. Upstream upgrades update the OpenAPI and SDK pins together after comparison and compatibility tests. + +The official source and existing service have these recorded differences: Agents authentication errors can return a null `code`; empty Files pages return null `first_id` and `last_id`; File resources can return null `expires_at` and `status_details`. The source declares those fields non-null. The official-client response validator allows null only for these named fields and otherwise validates OpenAPI 3.1 response schemas. Files and Skills operations omit error responses in the source, so those error bodies use the upstream shared `ErrorResponse` schema. [Wire semantics](./wire-semantics.md) and raw HTTP tests qualify service behavior; the published schema retains the official definitions. + +Go input projections exclude `packages.system` to preserve its explicit rejection, recorded below. Generation does not enable an unsupported operation or change stored setup validation. Evidence for a status comes from the pinned official SDK and raw HTTP against the running service, as [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#compatibility-evidence) requires. diff --git a/contracts/agents-api/openapi.yaml b/contracts/agents-api/openapi.yaml index 64c942b2e..1885da9ed 100644 --- a/contracts/agents-api/openapi.yaml +++ b/contracts/agents-api/openapi.yaml @@ -1,5429 +1,19444 @@ -basePath: /v1 -definitions: - v1.APIError: - properties: - code: - type: string - x-nullable: true - message: - type: string - param: - type: string - x-nullable: true - type: - type: string - required: - - message - - type - type: object - v1.Agent: - properties: - id: - type: string - instructions: - type: string - x-nullable: true - model: - type: string - multi_agent: - $ref: '#/definitions/v1.MultiAgentConfig' - name: - type: string - x-nullable: true - reasoning: - $ref: '#/definitions/v1.Reasoning' - service_tier: - enum: - - auto - type: string - text: - $ref: '#/definitions/v1.TextConfig' - tools: - items: - type: object - type: array - x_agents_core: - allOf: - - $ref: '#/definitions/v1.AgentsCore' - x-nullable: true - required: - - id - - model - - multi_agent - - reasoning - - service_tier - - text - - tools - type: object - v1.AgentContent: - properties: - encrypted_content: - type: string - text: - type: string - type: - enum: - - output_text - - encrypted_content - type: string - required: - - type - type: object - v1.AgentDeleted: - properties: - deleted: - enum: - - true - type: boolean - id: - type: string - object: - enum: - - agent.deleted - type: string - required: - - deleted - - id - - object - type: object - v1.AgentsCore: - properties: - harness: - enum: - - claude_sdk - - codex - - mcode - type: string - harness_config: - type: object - type: object - v1.CreateAgentRequest: - properties: - instructions: - type: string - x-nullable: true - metadata: - additionalProperties: - type: string - type: object - x-nullable: true - model: - type: string - multi_agent: - type: object - x-nullable: true - name: - maxLength: 128 - type: string - x-nullable: true - reasoning: - allOf: - - $ref: '#/definitions/v1.Reasoning' - x-nullable: true - service_tier: - enum: - - auto - - default - - flex - - priority - - fast - type: string - x-nullable: true - text: - allOf: - - $ref: '#/definitions/v1.SavedAgentTextInput' - x-nullable: true - tools: - items: - type: object - type: array - x-nullable: true - x_agents_core: - allOf: - - $ref: '#/definitions/v1.SavedAgentCoreInput' - x-nullable: true - required: - - model - type: object - v1.CreateCredentialRequest: - properties: - auth: - $ref: '#/definitions/v1.CredentialAuthInput' - name: - type: string - required: - - auth - - name - type: object - v1.CreateEventsRequest: - properties: - events: - items: - $ref: '#/definitions/v1.SessionInput' - type: array - required: - - events - type: object - v1.CreateSessionRequest: - properties: - agent: - $ref: '#/definitions/v1.InlineAgent' - agent_id: - type: string - environment: - $ref: '#/definitions/v1.Environment' - input: - description: |- - Input accepts a string or an ordered array of user InputMessage objects. - Required for none and streamed creation outside self_hosted; otherwise optional. - x-nullable: true - metadata: - additionalProperties: - type: string - type: object - x-nullable: true - stream: - default: false - type: boolean - vault_ids: - items: - type: string - type: array - x_agents_core: - $ref: '#/definitions/v1.SessionExecutionInput' - required: - - environment - type: object - v1.CreateVaultRequest: - properties: - metadata: - additionalProperties: - type: string - type: object - x-nullable: true - name: - type: string - type: object - v1.Credential: - properties: - auth: - $ref: '#/definitions/v1.CredentialAuth' - created_at: - type: integer - id: - type: string - name: - type: string - object: - enum: - - vault.credential - type: string - updated_at: - type: integer - vault_id: - type: string - required: - - auth - - created_at - - id - - name - - object - - updated_at - - vault_id - type: object - v1.CredentialAuth: - properties: - expires_at: - type: string - x-nullable: true - mcp_server_url: - type: string - refresh: - allOf: - - $ref: '#/definitions/v1.OAuthCredentialRefresh' - x-nullable: true - type: - enum: - - static_bearer - - mcp_oauth - type: string - required: - - mcp_server_url - - type - type: object - v1.CredentialAuthInput: - properties: - access_token: - minLength: 1 - type: string - expires_at: - type: string - x-nullable: true - mcp_server_url: - type: string - refresh: - allOf: - - $ref: '#/definitions/v1.OAuthCredentialRefreshInput' - x-nullable: true - token: - minLength: 1 - type: string - type: - enum: - - static_bearer - - mcp_oauth - type: string - required: - - mcp_server_url - - type - type: object - v1.CredentialAuthReplacement: - properties: - access_token: - minLength: 1 - type: string - x-nullable: true - expires_at: - type: string - x-nullable: true - refresh: - allOf: - - $ref: '#/definitions/v1.OAuthCredentialRefreshReplacement' - x-nullable: true - token: - minLength: 1 - type: string - type: - enum: - - static_bearer - - mcp_oauth - type: string - required: - - type - type: object - v1.CredentialDeleted: - properties: - deleted: - type: boolean - id: - type: string - object: - enum: - - vault.credential.deleted - type: string - required: - - deleted - - id - - object - type: object - v1.CredentialList: - properties: - data: - items: - $ref: '#/definitions/v1.Credential' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.Environment: - properties: - capability_directories: - items: - type: string - type: array - x-nullable: true - env: - additionalProperties: - type: string - type: object - x-nullable: true - environment_template_id: - type: string - files: - items: - type: object - type: array - x-nullable: true - network: - allOf: - - $ref: '#/definitions/v1.EnvironmentNetworkInput' - x-nullable: true - packages: - allOf: - - $ref: '#/definitions/v1.EnvironmentPackages' - x-nullable: true - plugins: - items: - type: object - type: array - x-nullable: true - setup_commands: - items: - type: object - type: array - x-nullable: true - skills: - items: - type: object - type: array - x-nullable: true - type: - enum: - - none - - self_hosted - - openai_hosted - type: string - workspace_directory: - type: string - required: - - type - type: object - v1.EnvironmentFile: - properties: - environment_id: - type: string - object: - enum: - - agent.environment.file - type: string - path: - type: string - size_bytes: - minimum: 0 - type: integer - required: - - environment_id - - object - - path - - size_bytes - type: object - v1.EnvironmentFileCreateRequest: - properties: - data: - type: string - file_id: - type: string - path: - type: string - type: - enum: - - inline - - file_id - type: string - required: - - path - - type - type: object - v1.EnvironmentFileList: - properties: - data: - items: - $ref: '#/definitions/v1.EnvironmentFile' - type: array - has_more: - type: boolean - next: - type: string - x-nullable: true - object: - enum: - - page - type: string - required: - - data - - has_more - - object - type: object - v1.EnvironmentInfo: - properties: - files: - items: - type: object - type: array - id: - type: string - object: - enum: - - agent.environment - type: string - plugins: - items: - type: object - type: array - skills: - items: - type: object - type: array - status: - enum: - - pending - - connected - - disconnected - - expired - - failed - type: string - type: - enum: - - openai_hosted - - self_hosted - type: string - required: - - files - - id - - object - - plugins - - skills - - status - - type - type: object - v1.EnvironmentInstallation: - properties: - commands: - additionalProperties: - type: string - type: object - expires_at: - type: integer - message: - type: string - status: - enum: - - available - - unavailable - type: string - version: - type: string - type: object - v1.EnvironmentNetwork: - properties: - access: - enum: - - enabled - - disabled - - restricted - type: string - allowed_domains: - items: - type: string - type: array - required: - - access - - allowed_domains - type: object - v1.EnvironmentNetworkInput: - properties: - access: - enum: - - enabled - - disabled - - restricted - type: string - allowed_domains: - items: - type: string - type: array - x-nullable: true - required: - - access - type: object - v1.EnvironmentPackages: - properties: - npm: - items: - type: string - type: array - python: - items: - type: string - type: array - required: - - npm - - python - type: object - v1.EnvironmentPackagesInput: - properties: - npm: - items: - type: string - type: array - x-nullable: true - python: - items: - type: string - type: array - x-nullable: true - type: object - v1.EnvironmentPackagesResponse: - properties: - npm: - items: - type: string - type: array - python: - items: - type: string - type: array - system: - items: - type: string - type: array - required: - - npm - - python - - system - type: object - v1.EnvironmentTemplate: - properties: - capability_directories: - items: - type: string - type: array - created_at: - type: integer - files: - items: - type: object - type: array - id: - type: string - name: - type: string - x-nullable: true - network: - $ref: '#/definitions/v1.EnvironmentNetwork' - object: - enum: - - agent.environment.template - type: string - packages: - $ref: '#/definitions/v1.EnvironmentPackagesResponse' - plugins: - items: - type: object - type: array - skills: - items: - type: object - type: array - updated_at: - type: integer - required: - - capability_directories - - created_at - - files - - id - - network - - object - - packages - - plugins - - skills - - updated_at - type: object - v1.EnvironmentTemplateDeleted: - properties: - deleted: - type: boolean - id: - type: string - object: - enum: - - agent.environment.template.deleted - type: string - required: - - deleted - - id - - object - type: object - v1.EnvironmentTemplateList: - properties: - data: - items: - $ref: '#/definitions/v1.EnvironmentTemplate' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.EnvironmentTemplateRequest: - properties: - capability_directories: - items: - type: string - type: array - x-nullable: true - env: - additionalProperties: - type: string - type: object - x-nullable: true - files: - items: - type: object - type: array - x-nullable: true - name: - type: string - x-nullable: true - network: - allOf: - - $ref: '#/definitions/v1.EnvironmentNetworkInput' - x-nullable: true - packages: - allOf: - - $ref: '#/definitions/v1.EnvironmentPackagesInput' - x-nullable: true - plugins: - items: - type: object - type: array - x-nullable: true - setup_commands: - items: - type: object - type: array - x-nullable: true - skills: - items: - type: object - type: array - x-nullable: true - type: object - v1.ErrorResponse: - properties: - error: - $ref: '#/definitions/v1.APIError' - required: - - error - type: object - v1.InlineAgent: - properties: - instructions: - type: string - x-nullable: true - model: - type: string - multi_agent: - type: object - x-nullable: true - reasoning: - allOf: - - $ref: '#/definitions/v1.Reasoning' - x-nullable: true - service_tier: - enum: - - auto - - default - - flex - - priority - - fast - type: string - x-nullable: true - text: - allOf: - - $ref: '#/definitions/v1.SavedAgentTextInput' - x-nullable: true - tools: - items: - type: object - type: array - x-nullable: true - x_agents_core: - allOf: - - $ref: '#/definitions/v1.AgentsCore' - x-nullable: true - type: object - v1.InputContent: - properties: - image_url: - type: string - text: - type: string - type: - enum: - - input_text - - input_image - type: string - required: - - type - type: object - v1.InputMessage: - properties: - content: - items: - $ref: '#/definitions/v1.InputContent' - type: array - role: - enum: - - user - type: string - type: - enum: - - message - type: string - required: - - content - - role - type: object - v1.InputTokenDetails: - properties: - cached_tokens: - type: integer - required: - - cached_tokens - type: object - v1.Item: - properties: - action: - $ref: '#/definitions/v1.WebSearchAction' - agent_id: - type: string - arguments: {} - call_id: - type: string - command: - type: string - content: - items: - $ref: '#/definitions/v1.ItemContent' - type: array - cwd: - type: string - duration_ms: - type: integer - error: {} - exit_code: - type: integer - id: - type: string - model: - type: string - name: - type: string - output: {} - phase: - enum: - - commentary - - final_answer - type: string - x-nullable: true - reasoning_effort: - type: string - recipient_agent_id: - type: string - recipient_agent_ids: - items: - type: string - type: array - role: - enum: - - user - - assistant - type: string - sender_agent_id: - type: string - server_label: - type: string - status: - enum: - - in_progress - - completed - - failed - - incomplete - type: string - summary: - items: - $ref: '#/definitions/v1.SummaryText' - type: array - turn_id: - type: string - type: - enum: - - message - - command_execution - - mcp_call - - function_call - - function_call_output - - web_search_call - - reasoning - - agent_message - - create_subagent_call - - send_subagent_input_call - - resume_subagent_call - - wait_for_subagents_call - - interrupt_subagent_call - - close_subagent_call - type: string - required: - - id - - turn_id - - type - type: object - v1.ItemContent: - properties: - encrypted_content: - type: string - image_url: - type: string - text: - type: string - type: - enum: - - input_text - - output_text - - input_image - - encrypted_content - type: string - required: - - type - type: object - v1.ItemList: - properties: - data: - items: - $ref: '#/definitions/v1.Item' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.ModelProviderInput: - properties: - api_key: - type: string - base_url: - type: string - context_window: - type: integer - max_output_tokens: - type: integer - protocol: - enum: - - anthropic - - responses - - chat_completions - type: string - required: - - api_key - - base_url - - protocol - type: object - v1.ModelProviderView: - properties: - api_key_configured: - type: boolean - base_url: - type: string - context_window: - type: integer - max_output_tokens: - type: integer - protocol: - enum: - - anthropic - - responses - - chat_completions - type: string - required: - - api_key_configured - - base_url - - protocol - type: object - v1.MultiAgentConfig: - properties: - enabled: - type: boolean - max_concurrent_subagents: - type: integer - x-nullable: true - required: - - enabled - type: object - v1.OAuthCredentialRefresh: - properties: - client_id: - type: string - resource: - type: string - x-nullable: true - scope: - type: string - x-nullable: true - token_endpoint: - type: string - token_endpoint_auth: - $ref: '#/definitions/v1.OAuthEndpointAuth' - required: - - client_id - - token_endpoint - - token_endpoint_auth - type: object - v1.OAuthCredentialRefreshInput: - properties: - client_id: - type: string - refresh_token: - type: string - resource: - type: string - x-nullable: true - scope: - type: string - x-nullable: true - token_endpoint: - type: string - token_endpoint_auth: - $ref: '#/definitions/v1.OAuthEndpointAuthInput' - required: - - client_id - - refresh_token - - token_endpoint - - token_endpoint_auth - type: object - v1.OAuthCredentialRefreshReplacement: - properties: - refresh_token: - type: string - x-nullable: true - scope: - type: string - x-nullable: true - token_endpoint_auth: - allOf: - - $ref: '#/definitions/v1.OAuthEndpointAuthReplacement' - x-nullable: true - type: object - v1.OAuthEndpointAuth: - properties: - type: - enum: - - none - - client_secret_basic - - client_secret_post - type: string - required: - - type - type: object - v1.OAuthEndpointAuthInput: - properties: - client_secret: - type: string - type: - enum: - - none - - client_secret_basic - - client_secret_post - type: string - required: - - type - type: object - v1.OAuthEndpointAuthReplacement: - properties: - client_secret: - type: string - x-nullable: true - type: - enum: - - client_secret_basic - - client_secret_post - type: string - required: - - type - type: object - v1.OutputTokenDetails: - properties: - reasoning_tokens: - type: integer - required: - - reasoning_tokens - type: object - v1.Reasoning: - properties: - effort: - type: string - x-nullable: true - summary: - type: string - x-nullable: true - type: object - v1.RequiredAction: - properties: - arguments: {} - call_id: - type: string - environment_id: - type: string - name: - type: string - turn_id: - type: string - type: - enum: - - function_call - - environment_connection - type: string - required: - - type - type: object - v1.SavedAgent: - properties: - created_at: - type: integer - id: - type: string - instructions: - type: string - x-nullable: true - metadata: - additionalProperties: - type: string - type: object - model: - type: string - multi_agent: - $ref: '#/definitions/v1.MultiAgentConfig' - name: - type: string - x-nullable: true - object: - enum: - - agent - type: string - reasoning: - $ref: '#/definitions/v1.Reasoning' - service_tier: - enum: - - auto - - default - - flex - - priority - - fast - type: string - text: - $ref: '#/definitions/v1.SavedAgentText' - tools: - items: - type: object - type: array - updated_at: - type: integer - x_agents_core: - allOf: - - $ref: '#/definitions/v1.SavedAgentCore' - x-nullable: true - required: - - created_at - - id - - metadata - - model - - multi_agent - - object - - reasoning - - service_tier - - text - - tools - - updated_at - type: object - v1.SavedAgentCore: - properties: - harness: - enum: - - claude_sdk - - codex - - mcode - type: string - harness_config: - type: object - model_provider: - $ref: '#/definitions/v1.ModelProviderView' - type: object - v1.SavedAgentCoreInput: - properties: - harness: - enum: - - claude_sdk - - codex - - mcode - type: string - harness_config: - type: object - model_provider: - allOf: - - $ref: '#/definitions/v1.ModelProviderInput' - x-nullable: true - type: object - v1.SavedAgentList: - properties: - data: - items: - $ref: '#/definitions/v1.SavedAgent' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.SavedAgentText: - properties: - format: - $ref: '#/definitions/v1.SavedAgentTextFormat' - verbosity: - enum: - - low - - medium - - high - type: string - required: - - format - - verbosity - type: object - v1.SavedAgentTextFormat: - properties: - schema: - type: object - type: - enum: - - text - - json_schema - type: string - required: - - type - type: object - v1.SavedAgentTextInput: - properties: - format: - type: object - x-nullable: true - verbosity: - enum: - - low - - medium - - high - type: string - x-nullable: true - type: object - v1.Session: - properties: - agent: - $ref: '#/definitions/v1.Agent' - created_at: - type: integer - environment: - $ref: '#/definitions/v1.SessionEnvironment' - error: - type: string - x-nullable: true - id: - type: string - last_active_at: - type: integer - metadata: - additionalProperties: - type: string - type: object - object: - enum: - - agent.session - type: string - required_actions: - items: - $ref: '#/definitions/v1.RequiredAction' - type: array - status: - enum: - - idle - - in_progress - - requires_action - - failed - type: string - usage: - allOf: - - $ref: '#/definitions/v1.TokenUsage' - x-nullable: true - vault_ids: - items: - type: string - type: array - x_agents_core: - $ref: '#/definitions/v1.SessionCore' - required: - - agent - - created_at - - environment - - id - - last_active_at - - metadata - - object - - required_actions - - status - - vault_ids - type: object - v1.SessionArtifact: - properties: - created_at: - type: integer - environment_id: - type: string - id: - type: string - object: - enum: - - agent.session.artifact - type: string - path: - type: string - session_id: - type: string - size_bytes: - type: integer - turn_id: - type: string - required: - - created_at - - environment_id - - id - - object - - path - - session_id - - size_bytes - - turn_id - type: object - v1.SessionArtifactDeleted: - properties: - deleted: - type: boolean - id: - type: string - object: - enum: - - agent.session.artifact.deleted - type: string - required: - - deleted - - id - - object - type: object - v1.SessionArtifactList: - properties: - data: - items: - $ref: '#/definitions/v1.SessionArtifact' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.SessionCore: - properties: - installation: - $ref: '#/definitions/v1.EnvironmentInstallation' - type: object - v1.SessionDeleted: - properties: - deleted: - type: boolean - id: - type: string - object: - enum: - - agent.session.deleted - type: string - required: - - deleted - - id - - object - type: object - v1.SessionEnvironment: - properties: - capability_directories: - items: - type: string - type: array - files: - items: - type: object - type: array - id: - type: string - network: - $ref: '#/definitions/v1.EnvironmentNetwork' - packages: - $ref: '#/definitions/v1.EnvironmentPackagesResponse' - plugins: - items: - type: object - type: array - remote_url: - type: string - skills: - items: - type: object - type: array - type: - enum: - - none - - self_hosted - - openai_hosted - type: string - workspace_directory: - type: string - required: - - type - type: object - v1.SessionEnvironmentState: - properties: - error: - allOf: - - $ref: '#/definitions/v1.StreamError' - x-nullable: true - id: - type: string - status: - enum: - - pending - - ready - - connected - - disconnected - - failed - type: string - type: - type: string - required: - - id - - status - - type - type: object - v1.SessionEvent: - properties: - content_index: - type: integer - delta: - type: string - environment: - $ref: '#/definitions/v1.SessionEnvironmentState' - error: - $ref: '#/definitions/v1.StreamError' - event_id: - type: string - item: - $ref: '#/definitions/v1.Item' - item_id: - type: string - output_index: - type: integer - x-nullable: true - part: - $ref: '#/definitions/v1.ItemContent' - session: - $ref: '#/definitions/v1.Session' - session_id: - type: string - subagent: - $ref: '#/definitions/v1.Subagent' - text: - type: string - turn: - $ref: '#/definitions/v1.Turn' - turn_id: - type: string - type: - type: string - usage: - allOf: - - $ref: '#/definitions/v1.TokenUsage' - description: |- - Usage is present only on terminal Turn events, where it mirrors the Turn - snapshot and is null when unknown. Other events omit it. - x-nullable: true - required: - - event_id - - type - type: object - v1.SessionExecutionInput: - properties: - environment: - description: Environment supplies placement-independent preparation through - the Core extension. - type: object - harness_config: - type: object - model_provider: - $ref: '#/definitions/v1.ModelProviderInput' - type: object - v1.SessionInput: - properties: - call_id: - type: string - error: - type: string - x-nullable: true - input: - items: - $ref: '#/definitions/v1.InputMessage' - type: array - output: - x-nullable: true - success: - type: boolean - turn_id: - type: string - type: - enum: - - agent.session.input.message - - agent.session.input.cancel - - agent.session.input.tool_result - type: string - required: - - type - type: object - v1.SessionList: - properties: - data: - items: - $ref: '#/definitions/v1.Session' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.Skill: - properties: - created_at: - type: integer - default_version: - type: string - description: - type: string - id: - type: string - latest_version: - type: string - name: - type: string - object: - enum: - - skill - type: string - required: - - created_at - - default_version - - description - - id - - latest_version - - name - - object - type: object - v1.SkillDeleted: - properties: - deleted: - type: boolean - id: - type: string - object: - enum: - - skill.deleted - type: string - required: - - deleted - - id - - object - type: object - v1.SkillList: - properties: - data: - items: - $ref: '#/definitions/v1.Skill' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.SkillUpdateRequest: - properties: - default_version: - type: string - required: - - default_version - type: object - v1.SkillVersion: - properties: - created_at: - type: integer - description: - type: string - id: - type: string - name: - type: string - object: - enum: - - skill.version - type: string - skill_id: - type: string - version: - type: string - required: - - created_at - - description - - id - - name - - object - - skill_id - - version - type: object - v1.SkillVersionDeleted: - properties: - deleted: - type: boolean - id: - type: string - object: - enum: - - skill.version.deleted - type: string - version: - type: string - required: - - deleted - - id - - object - - version - type: object - v1.SkillVersionList: - properties: - data: - items: - $ref: '#/definitions/v1.SkillVersion' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.SourceFile: - properties: - bytes: - minimum: 0 - type: integer - created_at: - type: integer - expires_at: - type: integer - x-nullable: true - filename: - type: string - id: - type: string - object: - enum: - - file - type: string - purpose: - enum: - - user_data - type: string - status: - enum: - - processed - type: string - status_details: - type: string - x-nullable: true - required: - - bytes - - created_at - - filename - - id - - object - - purpose - - status - type: object - v1.SourceFileDeleted: - properties: - deleted: - type: boolean - id: - type: string - object: - enum: - - file - type: string - required: - - deleted - - id - - object - type: object - v1.SourceFileList: - properties: - data: - items: - $ref: '#/definitions/v1.SourceFile' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.StreamError: - properties: - code: - type: string - message: - type: string - param: - description: |- - Param is the pinned SessionError field. An error SessionEvent always - carries it, null when unset; Environment state errors and Core's own - stream_interrupted frame omit it. - type: string - x-nullable: true - type: - type: string - type: object - v1.Subagent: - properties: - closed_at: - type: integer - x-nullable: true - id: - type: string - instructions: - items: - $ref: '#/definitions/v1.AgentContent' - type: array - x-nullable: true - name: - type: string - x-nullable: true - object: - enum: - - agent.session.subagent - type: string - opened_at: - type: integer - parent_agent_id: - type: string - session_id: - type: string - status: - enum: - - active - - closed - type: string - required: - - id - - object - - opened_at - - parent_agent_id - - session_id - - status - type: object - v1.SubagentList: - properties: - data: - items: - $ref: '#/definitions/v1.Subagent' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.SummaryText: - properties: - text: - type: string - type: - enum: - - summary_text - type: string - required: - - text - - type - type: object - v1.TextConfig: - properties: - format: - $ref: '#/definitions/v1.TextFormat' - verbosity: - enum: - - low - - medium - - high - type: string - required: - - format - - verbosity - type: object - v1.TextFormat: - properties: - schema: - type: object - type: - enum: - - text - - json_schema - type: string - required: - - type - type: object - v1.TokenUsage: - properties: - input_tokens: - type: integer - input_tokens_details: - $ref: '#/definitions/v1.InputTokenDetails' - output_tokens: - type: integer - output_tokens_details: - $ref: '#/definitions/v1.OutputTokenDetails' - total_tokens: - type: integer - required: - - input_tokens - - input_tokens_details - - output_tokens - - output_tokens_details - - total_tokens - type: object - v1.Turn: - properties: - agent_id: - type: string - completed_at: - type: integer - x-nullable: true - created_at: - type: integer - error: - allOf: - - $ref: '#/definitions/v1.TurnError' - x-nullable: true - id: - type: string - object: - enum: - - agent.session.turn - type: string - session_id: - type: string - started_at: - type: integer - x-nullable: true - status: - enum: - - queued - - in_progress - - waiting - - completed - - failed - - cancelled - type: string - subagent_id: - type: string - x-nullable: true - usage: - allOf: - - $ref: '#/definitions/v1.TokenUsage' - x-nullable: true - required: - - agent_id - - created_at - - id - - object - - session_id - - status - type: object - v1.TurnError: - properties: - code: - enum: - - context_length_exceeded - - session_budget_exceeded - - usage_limit_exceeded - - rate_limit_exceeded - - server_overloaded - - cyber_policy - - connection_failed - - server_error - - authentication_error - - invalid_request - - resource_not_found - - sandbox_error - - executor_version_incompatible - - active_turn_not_steerable - - request_timeout - - internal_error - type: string - message: - type: string - required: - - code - - message - type: object - v1.TurnList: - properties: - data: - items: - $ref: '#/definitions/v1.Turn' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.UpdateAgentRequest: - properties: - instructions: - type: string - x-nullable: true - metadata: - additionalProperties: - type: string - type: object - x-nullable: true - model: - type: string - multi_agent: - type: object - x-nullable: true - name: - maxLength: 128 - type: string - x-nullable: true - reasoning: - allOf: - - $ref: '#/definitions/v1.Reasoning' - x-nullable: true - service_tier: - enum: - - auto - - default - - flex - - priority - - fast - type: string - x-nullable: true - text: - allOf: - - $ref: '#/definitions/v1.SavedAgentTextInput' - x-nullable: true - tools: - items: - type: object - type: array - x-nullable: true - x_agents_core: - allOf: - - $ref: '#/definitions/v1.SavedAgentCoreInput' - x-nullable: true - type: object - v1.UpdateCredentialRequest: - properties: - auth: - $ref: '#/definitions/v1.CredentialAuthReplacement' - required: - - auth - type: object - v1.UpdateSessionRequest: - properties: - metadata: - additionalProperties: - type: string - type: object - x-nullable: true - required: - - metadata - type: object - v1.Vault: - properties: - created_at: - type: integer - id: - type: string - metadata: - additionalProperties: - type: string - type: object - name: - type: string - x-nullable: true - object: - enum: - - vault - type: string - required: - - created_at - - id - - metadata - - object - type: object - v1.VaultDeleted: - properties: - deleted: - type: boolean - id: - type: string - object: - enum: - - vault.deleted - type: string - required: - - deleted - - id - - object - type: object - v1.VaultList: - properties: - data: - items: - $ref: '#/definitions/v1.Vault' - type: array - first_id: - type: string - x-nullable: true - has_more: - type: boolean - last_id: - type: string - x-nullable: true - object: - enum: - - list - type: string - required: - - data - - has_more - - object - type: object - v1.WebSearchAction: - properties: - pattern: - type: string - queries: - items: - type: string - type: array - query: - type: string - type: - enum: - - search - - open_page - - find_in_page - - other - type: string - url: - type: string - required: - - type - type: object -info: - contact: {} - description: Supported single-Agent execution resources from the pinned openai-python - beta/agents contract. Bearer keys bind an execution principal to one project; - optional OpenAI-Organization and OpenAI-Project headers must match that binding. - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html - title: OpenAgentCore Agents API - version: "1" -paths: - /agents: - get: - description: Lists only the authenticated tenant's saved Agents, independently - of Sessions. Limit 0 is treated as 1 and larger limits as 100, as observed - on the hosted service. The local default is 20; exact upstream default/cap - and empty cursor fields remain unverified. An unknown, malformed or foreign - after cursor returns not found. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Last Agent ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Page size; 0 is treated as 1 and values above 100 as 100 - in: query - minimum: 0 - name: limit - type: integer - - default: desc - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SavedAgentList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List reusable Agents - tags: - - Agents - post: - consumes: - - application/json - description: 'Persists configuration independently of execution. Names over - 128 characters and metadata outside 16 string pairs with 64-character keys - and 512-character values return invalid_request_error with the official param; - U+0000 in stored strings is rejected as a local storage limit. As on every - Agents API JSON route, a non-JSON Content-Type, invalid UTF-8, malformed JSON, - a repeated key at any depth or a non-object root returns invalid_request_error - with a null param and the official message before other checks; an empty or - null body is {}. Missing, unknown, wrongly typed or unsupported enum members - of the pinned configuration shapes (tools, text, reasoning, service_tier, - multi_agent) return invalid_request_error with the JSON path as param; duplicate - function names, repeated web_search or tool_search and non-object schema root - types return it with a null param. Supports model/name/instructions/metadata, - explicit reasoning and service tiers, multi_agent, text/json_schema, function/tool_search/programmatic_tool_calling/web_search - and HTTP MCP with nullable credential_id, service origin (omitted or null - on HTTP transport is saved as service) and boolean required defaulting to - false. Saving credential_id grants no access: Session admission checks attached - Vault ownership and destination. MCP allowed_tools preserves null versus empty; - saved HTTP transport includes empty headers. Model-derived reasoning defaults, - other MCP variants and public retry conformance remain incomplete. web_search - saves every pinned mode: omitted or null mode is saved as live and omitted - or null context_size as medium; allowed_domains preserves null versus empty - and a present location, including {}, includes all four keys with null for - omitted ones, as observed officially (req_db41d2f6261b4abfb69465eafe719ab5, - req_165d53b88445490b9146d8272c54134d). Session execution accepts only explicit - disabled web_search and disabled programmatic_tool_calling through qualified - Runtime controls; saved enabled forms reject at Session admission. Session - execution admits only its supported configuration subset.' - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Reusable Agent configuration - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.CreateAgentRequest' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/v1.SavedAgent' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Create a reusable Agent - tags: - - Agents - /agents/{agent_id}: - delete: - description: Deletes only the authenticated tenant's saved configuration. Existing - Session snapshots, history and recorded creation retry identities remain independent. - Missing and repeated deletion locally return404; exact hosted error and in-flight - creation/deletion semantics remain unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Agent ID - in: path - name: agent_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.AgentDeleted' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Delete a reusable Agent - tags: - - Agents - get: - description: Reads the saved resource owned by the authenticated tenant, independently - of execution Sessions. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Agent ID - in: path - name: agent_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SavedAgent' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve a reusable Agent - tags: - - Agents - post: - consumes: - - application/json - description: Preserves omitted fields and replaces supplied fields using shared - saved-configuration validation. Null name/instructions clear; null or empty - metadata clears all pairs. Name, metadata and configuration validation errors - return invalid_request_error with the official param, using the Agent create - rules before the Agent lookup. Existing Session snapshots are unchanged. Empty - updates advance updated_at without changing saved fields. Nested replacement/null - defaults, model-derived reasoning and exact hosted error behavior remain incompletely - verified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Agent ID - in: path - name: agent_id - required: true - type: string - - description: Supplied reusable Agent fields - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.UpdateAgentRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SavedAgent' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Update a reusable Agent - tags: - - Agents - /agents/environments/{environment_id}: - get: - description: Returns durable connection status and safe installed metadata for - supported self_hosted and basic openai_hosted profiles. Initial files expose - frozen safe metadata without content; Plugin/Skill entries expose only safe - configured installation metadata. Capability-directory discoveries are not - added to those arrays. Unsupported installation configurations remain implementation - gaps. This read does not prepare execution, start compute or require an enabled - execution worker. Session deletion removes the associated Environment from - public reads; project-shared read authorization is unchanged. Connection status - does not prove native readiness or process quiescence. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Environment ID - in: path - name: environment_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.EnvironmentInfo' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve an execution Environment - tags: - - Environments - /agents/environments/{environment_id}/files: - get: - description: Lists direct regular files in one authorized self_hosted or qualified - local workspace directory. Local paths use the public /workspace root and - must be in cleaned form. This partial implementation defaults to the workspace - root and limit 20; recursive scope and these defaults are not verified upstream - semantics. A missing path, a regular file or a symbolic link returns an empty - page; links are never followed. Daemons without a local workspace binding - use the Claude SDK adapter reader, which keeps 404 for a missing path and - 503 for a regular file or symbolic link. Well-formed unknown query keys are - ignored; malformed query encoding and a repeated supported key are rejected. - Sorts by case-sensitive path components, descending by default. Keep the same - path, order and limit when using page. Each page rereads the complete bounded - directory; changed file paths/sizes invalidate continuation locally with 400. - There is no snapshot guarantee. An openai_hosted Environment that has not - connected yet returns 400. Truncated or uncertain native results fail with - 503 without returning a partial page. This read never starts a Turn or admits - model input. Actual transport disconnect/reconnect events remain observable. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Environment ID - in: path - name: environment_id - required: true - type: string - - description: Absolute directory in cleaned form inside /workspace - in: query - name: path - type: string - - description: Maximum file count; local default 20 - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer - - default: desc - description: Case-sensitive path-component order; omit for descending, explicit - empty values are invalid - enum: - - asc - - desc - in: query - name: order - type: string - - description: Opaque continuation token; keep path, order and limit unchanged - in: query - name: page - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.EnvironmentFileList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List live Environment files - tags: - - Environments - post: - consumes: - - application/json - description: Uploads standard Base64 bytes to a file beneath /workspace in a - qualified local Environment and returns 201. Accepts inline bytes or a project-owned - source file_id through the same write path. Unknown body fields are rejected - with their name as param. Basic public hosted creation requires explicit managed - Runtime configuration; an openai_hosted Environment that has not connected - yet returns 400. Inline data is limited to 5 MiB decoded and a file_id copy - to 50 MiB. Missing parent directories are created with mode 0700 and the file - with mode 0600. An existing destination is never replaced; a directory, an - existing file or a path through a symlink or non-directory returns 400. Idle - writes exclude execution. Missing receipts return unavailable and retain a - durable mutation gate without automatic replay. Error/timing parity with upstream - remains unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Environment ID - in: path - name: environment_id - required: true - type: string - - description: Inline bytes or source file ID and absolute workspace path - in: body - name: request - required: true - schema: - $ref: '#/definitions/v1.EnvironmentFileCreateRequest' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/v1.EnvironmentFile' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "409": - description: Conflict - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Create an Environment file from inline bytes or a source file - tags: - - Environments - /agents/environments/templates: - get: - description: Lists tenant-owned safe template metadata in creation order with - ID tie-breaking. Defaults to limit 20 and descending order; limit 0 is treated - as 1 and larger limits as 100. Foreign, missing and malformed cursors return - the same not found error. Concurrent-page and exact hosted error behavior - remain unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Previous Template ID - in: query - name: after - type: string - - default: 20 - description: Page size; 0 is treated as 1 and values above 100 as 100 - in: query - minimum: 0 - name: limit - type: integer - - default: desc - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.EnvironmentTemplateList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List Environment Templates - tags: - - Environment Templates - post: - consumes: - - application/json - description: Saves tenant-owned hosted configuration. Supports nullable name, - enabled/disabled or exact-domain restricted network, initial inline/file_id - files, confidential env, ordered setup_commands, npm/Python packages inline/referenced - Skill ZIPs, Plugin ZIPs and workspace-contained capability directories. Omitted/null - network defaults to enabled. Restricted network requires 1–100 exact ASCII - hostnames; other host forms and populated unsupported installations are rejected - before persistence without echoing input. Network policy rejections return - invalid_request_error with a null param. System dependencies must be preinstalled - in the sandbox image or template, or on the host machine; packages.system - is rejected. No compute is allocated. Exact hosted error/retry semantics remain - unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Reusable configuration - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.EnvironmentTemplateRequest' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/v1.EnvironmentTemplate' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Create an Environment Template - tags: - - Environment Templates - /agents/environments/templates/{environment_template_id}: - delete: - description: Deletes the tenant-owned reusable configuration without changing - or deleting existing Sessions and their frozen configuration. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Template ID - in: path - name: environment_template_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.EnvironmentTemplateDeleted' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Delete an Environment Template - tags: - - Environment Templates - get: - description: Returns safe tenant-owned configuration metadata without allocating - compute. Missing and foreign resources return the same not-found response. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Template ID - in: path - name: environment_template_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.EnvironmentTemplate' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve an Environment Template - tags: - - Environment Templates - post: - consumes: - - application/json - description: Supplied fields replace atomically; omitted fields remain unchanged. - Null name clears and null network resets to the pinned enabled default. Existing - Session snapshots and creation retries remain unchanged. Initial files replace - as a list; null/empty clears. File data is encrypted separately and excluded - from response metadata. Skills replace as a list; null/empty clears. Skill - archives are encrypted separately and omitted from responses. Plugins and - capability directories replace as lists; null/empty clears. Plugin archives - are encrypted and omitted from responses. Capability directories are snapshotted - after setup. Environment MCP execution requires a qualified native transport - and runtime network policy. Empty updates advance updated_at without changing - saved fields or confidential contents. Network policy rejections return invalid_request_error - with a null param. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Template ID - in: path - name: environment_template_id - required: true - type: string - - description: Configuration replacements - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.EnvironmentTemplateRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.EnvironmentTemplate' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Update an Environment Template - tags: - - Environment Templates - /agents/sessions: - get: - description: Cursor and results are scoped to the authenticated execution tenant; - an unknown, malformed or foreign after cursor returns not found. Optional - agent_id matches the immutable root Agent ID, including inline Agents and - historical Sessions whose saved source was updated or deleted. Omission lists - all Agents. Returns the same Environment and pending-input activity projection - as Session retrieval, including self_hosted Sessions. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Root Agent ID whose Sessions to return - in: query - name: agent_id - type: string - - description: Last Session ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Page size; 0 is treated as 1 and values above 100 as 100 - in: query - minimum: 0 - name: limit - type: integer - - default: desc - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SessionList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List execution Sessions - tags: - - Sessions - post: - consumes: - - application/json - description: 'The optional Core model_provider bundle resolves from the Session - override, saved Agent defaults, then, for openai_hosted and none, the deployment - default of the resolved harness; self_hosted never uses the deployment default - and none accepts only it. openai_hosted and self_hosted Sessions that resolve - no bundle return 400 model_provider_required with param x_agents_core.model_provider - before any write. Core encrypts and freezes the resolved bundle; later Agent - or deployment default edits and same-key retries cannot change it. Keys are - never returned. Supports inline configuration or a tenant-owned saved agent_id - with per-Session field replacements. Execution supports model/instructions, - text verbosity, non-deferred function tools, adapter-qualified multi_agent - with persisted Subagent reads, implicit reasoning, service tier auto and environment - type none, subject to the configured engine. Codex additionally supports HTTP - MCP with service origin (omitted or null on HTTP transport is saved as service), - native allowed_tools and boolean required defaulting to false. Session vault_ids - attach only project-owned Vaults; credential_id selects an attached static - bearer or OAuth credential for the exact HTTPS URL, while null/omission selects - a unique match or remains anonymous. Session reads, lists and event snapshots - show that implicitly selected credential ID in a null or omitted credential_id, - also after the credential is deleted; anonymous selections stay null and the - stored caller intent is unchanged. After the input requirement and before - any write, a credential_id without vault_ids, one outside the attached Vaults - (one message for missing, foreign and unattached IDs) or one for another server_url - returns 400 invalid_request_error, and several implicit matches return 409 - conflict_error. Missing decryption configuration fails dispatch without anonymous - fallback. Required initialization uses native startup before the first native - Turn, including cold resume, and requires a separately advertised capability; - exact hosted creation timing and error parity remain unverified. Explicit - environment-origin HTTP MCP is supported on managed and self-hosted workspaces - through the same Runtime bindings; native OAuth login remains unsupported. - The self_hosted profile uses a qualified native harness, a clean absolute - workspace_directory and optional absolute local capability_directories prepared - by Runtime, with optional non-deferred function tools and HTTP MCP using explicit - environment origin, optionally authenticated by the attached Vault rules. - Service-origin HTTP remains restricted to service-side environment:none. Remote - MCP and remote Bearer authentication each require separately advertised combination - support; old peers cannot receive unsupported work. Omitted/null capability_directories - use the empty-list default; self_hosted requires configured execution plus - executor registry. Claude SDK currently requires medium verbosity and object-root - function schemas. It supports anonymous or attached static-bearer service-origin - HTTP MCP on none with boolean required and separately advertised MCP/bearer/required - runtime support. Required servers must be connected before the first native - input is released; pending or failed startup rejects execution. The shared - Vault selection and immutable binding rules apply; unsupported native labels/tool - names reject before persistence. An attached Vault with no matching credential - may remain anonymous; missing keys or failed credential lookup/decryption - never fall back to anonymous execution. Omitted stream defaults to false; - stream and agent_id cannot be null. Metadata may be null; non-string values - and limit violations return invalid_request_error with a metadata or metadata. - param. The inline agent uses the Agent create configuration validation with - agent.-prefixed params, reported before the input requirement and saved-Agent - lookup; saved configurations with conflicting tools or schema roots reject - admission with the same errors, and execution limits keep unsupported_or_invalid_configuration. - Hosted network policy rejections return invalid_request_error with a null - param. Initial input accepts a string or ordered user-message array. Codex - and Claude SDK on none and qualified managed or self_hosted workspace profiles - also accept inline PNG/JPEG image content; other image combinations and remote - URLs are unsupported. None initial input atomically starts a Turn; self_hosted - initial input is reserved while returning its Environment connection target, - with execution deferred to native readiness and Session failure on initial - timeout. Initial input is required for none and for streamed creation outside - self_hosted. Omitted/null input remains valid for non-streaming hosted and - self_hosted creation. With stream=true, returns live Session events starting - with the committed creation snapshot and closes right after the first agent.session.idle - recorded when a Turn ends or an input reservation stops being pending, or - any agent.session.failed, without sending later events. A creation that admitted - nothing closes after the snapshot; a settlement that records no event closes - after events up to the cursor read with a settled Session projection. Required - actions keep it open; disconnect does not cancel execution. The GET events - stream remains live-only. New Sessions retain their authenticated creator; - all creation retries require the same typed subject, including across key - rotation. Saved-Agent retries and inline requests using Vault attachments - or credential references retain caller intent independently of later resource - changes; new hosted inline requests also freeze caller intent before deployment - defaults resolve; unrelated non-hosted inline retries preserve resolved/default - equivalences, and their resolved hash leaves out any deployment default. Provider - keys enter retry hashes only as fingerprints keyed by the credential key. - Unknown historical creators reject retries; known creators without recorded - intent retain resolved-snapshot retry rules. These conflict policies are local - and not verified hosted parity. A same-key stream=true retry of an existing - creation returns 201 with no events and closes at once; retry with stream=false - or use the GET events stream to recover. Claude SDK on none, Core-managed - Docker openai_hosted and self_hosted supports qualified object-root json_schema - output with medium verbosity, single-Agent execution and ordinary functions. - Hosted execution reuses native workspace tools and Files/Artifacts; Skills, - Plugins, capability directories, HTTP MCP, Subagent and tool_search combinations - remain unqualified, including inherited template contents. Other non-text - initial input remains unsupported. Basic Codex and Claude SDK openai_hosted - creation requires an explicitly configured managed provider. The Claude workspace - profile supports non-deferred function tools with text or successful inline - PNG/JPEG results alongside native workspace tools; explicit environment-origin - HTTP MCP uses the common Runtime path. MiniMax accepts public environment-origin - HTTP MCP only with null or omitted allowed_tools and required=false; even - an empty non-null allowlist rejects. Idle Sessions provision automatically; - initial provisioning has no caller connection action. Network defaults to - enabled; disabled and restricted policies reject before compute allocation - because the current Runtime cannot enforce them. The x_agents_core.environment - extension accepts common preparation fields for either hosted or self-hosted - placement: environment_template_id, files, env, packages, setup_commands, - skills, plugins and capability_directories. Duplicate fields in environment - and the extension reject. Confidential env, npm/Python packages and ordered - setup commands use the same Environment-owned initialization lifecycle; compute - allocation does not own preparation. Unknown side effects are not replayed - after disconnect or restart. System dependencies must be preinstalled in the - sandbox image or template, or on the host machine; packages.system is rejected. - Initial inline and tenant-owned file_id files freeze encrypted bytes before - provisioning, then install through the common Core lifecycle before native - execution or live Files access. With a template reference, omitted/null files, - env, packages and setup_commands inherit. Non-null files and command lists - replace; env overlays by key; each package manager inherits on omission/null - and otherwise replaces its list. Empty lists clear their selected field. Tenant-owned - environment_template_id references inherit omitted/null network and allow - only narrowing overrides. Inline hosted network:null retains the enabled default; - updating a Template with network:null resets its saved policy to enabled. - Core freezes effective configuration; template updates/deletion do not alter - Session snapshots or same-intent creation retries. Inline or tenant-owned - skill_reference Skills share initialization. Templates preserve default/latest/explicit - selectors; Session creation freezes concrete metadata and encrypted content - atomically. Skill, Plugin and capability-directory list omission/null inherit; - a non-null list replaces, including empty-list clearing. Omitted/null Skill - version selectors resolve the default version. Source deletion/default updates - cannot change committed Session Skill contents. Deferred function discovery - uses type-only tool_search and per-function defer_loading in the qualified - single-agent Claude function profile on none or a managed/user-owned workspace, - including qualified inline image messages and text results. Explicit web_search - mode disabled and programmatic_tool_calling enabled false use frozen common - Runtime controls. Enabled forms, including those saved on an Agent, remain - unqualified and reject before any write unless the Session replaces tools. - Omitted programmatic configuration preserves native behavior, a documented - difference from the official default-on behavior. Other combinations remain - unqualified; see the operation coverage.' - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Creation retry key, up to 128 bytes - in: header - name: Idempotency-Key - type: string - - description: Session configuration - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.CreateSessionRequest' - produces: - - application/json - - text/event-stream - responses: - "201": - description: Created - schema: - $ref: '#/definitions/v1.Session' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "409": - description: Conflict - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Create an execution Session - tags: - - Sessions - /agents/sessions/{session_id}: - delete: - description: Removes a durably idle or failed Session and its history from the - public API. A Session whose root Turn is queued, in progress or waiting (including - required actions) or whose input reservation is pending returns 409 conflict_error - and is left unchanged; cancel it and wait until it is idle before deleting. - Subagent child Turns and pending Environment file writes are not checked and - do not block deletion. Repeating the deletion of the caller's own deleted - Session returns the same confirmation; missing and foreign Sessions return - 404. Internal records and native history are retained pending separate physical - cleanup; overlapping stream timing remains unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SessionDeleted' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "409": - description: Conflict - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Delete an execution Session - tags: - - Sessions - get: - description: Returns supported none, self_hosted and basic openai_hosted Session - environments. Self-hosted pending input can require a caller connection before - a Turn exists. Hosted initial provisioning remains idle until a Turn starts; - connection observations are not native execution readiness. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Session' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve an execution Session - tags: - - Sessions - post: - consumes: - - application/json - description: The metadata field is required in an update body. Send null or - {} to clear it, or supply an object to replace all pairs. Up to 16 string - pairs, with keys at most 64 characters and values at most 512 characters; - violations and non-string values return invalid_request_error with a metadata - or metadata. param. U+0000 is rejected as a local storage limit. Malformed, - missing and foreign Session IDs share the not-found response. Execution configuration - and activity are unchanged. Returns the same safe Environment and pending-input - activity projection as Session retrieval. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Session metadata - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.UpdateSessionRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Session' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Update execution Session metadata - tags: - - Sessions - /agents/sessions/{session_id}/artifacts: - get: - description: Lists published outputs independently of Environment availability. - Sorting uses publication time and ID. A later Turn publishes a path again - only when it is new, its bytes changed, or no Artifact remains for it. A malformed - environment_id matches nothing. An after value that is not an Artifact of - this Session, including a malformed one, returns 400 invalid_request_error - with the message "after is not a valid artifact ID". The local default page - size is 20; exact upstream defaults remain unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Producing Environment ID; an unknown or malformed ID returns - an empty page - in: query - name: environment_id - type: string - - description: Last immutable artifact ID - in: query - name: after - type: string - - default: 20 - description: Page size - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer - - default: desc - description: Publication order; omit for descending, explicit empty values - are invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SessionArtifactList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List immutable Session artifacts - tags: - - Artifacts - /agents/sessions/{session_id}/artifacts/{artifact_id}: - delete: - description: Deletes the published copy without modifying its original workspace - file. Already admitted content reads may finish; later reads reject. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Artifact ID - in: path - name: artifact_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SessionArtifactDeleted' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Delete a published artifact - tags: - - Artifacts - get: - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Artifact ID - in: path - name: artifact_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SessionArtifact' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve immutable artifact metadata - tags: - - Artifacts - /agents/sessions/{session_id}/artifacts/{artifact_id}/content: - get: - description: Streams stored bytes after tenant and Session authorization, including - after Environment expiration. Exact upstream headers and Range behavior remain - unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Artifact ID - in: path - name: artifact_id - required: true - type: string - produces: - - application/octet-stream - responses: - "200": - description: OK - schema: - type: file - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Download immutable artifact bytes - tags: - - Artifacts - /agents/sessions/{session_id}/events: - get: - description: |- - Live-only events, including command output fragments from capable Codex peers as agent.output.command_execution_output.delta with stable Item/output indexes. Native text conversion and output quotas apply; completion snapshots remain authoritative. Reconnect through Session, Turn and Items reads; missed events are not replayed. A lagging stream closes with an error when its bounded buffer is exceeded. When a hosted Environment fails to provision, the stream sends agent.session.environment.failed, an error event (environment_error/sandbox_error with the safe step and exit-status reason, never command output) and agent.session.failed, then ends. Session activity includes immutable pending-input connection actions before Turn creation; self_hosted environments use the same safe output as Session retrieval. - Active streams revalidate the original Project key every second before output; revocation, Project archival or authentication unavailability closes the stream. Authentication checks use a five-second timeout. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - produces: - - text/event-stream - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SessionEvent' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Stream live Session events - tags: - - Events - post: - consumes: - - application/json - description: An empty events array is a resource-authorized no-op; it creates - no execution retry identity, Turn, Item or input receipt. For environment - none, atomically accepts text messages, cancellation and function results. - Messages steer active work or start a queued Turn. Qualified Codex and Claude - SDK workspace profiles accept text and inline PNG/JPEG messages, independently - of managed or self_hosted ownership. Under the Session lock, matching retries - retain their original target; new active messages append to the current Turn, - while idle messages reserve work and wait up to the original five-minute connection/admission - deadline. Return 202 only after durable admission, without claiming native - application; active messages create no Turn or reservation. Cancellation-only - prepared-environment batches use existing durable cancellation admission and - return 202 without waiting for native exit; a new cancellation conflicts while - a pre-Turn reservation is pending. Homogeneous tool_result-only prepared-environment - batches reuse existing scoped result admission and application receipts without - creating a Turn or bypassing a pending reservation. Mixed prepared-environment - batches remain unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled - errors. New input on a Session whose hosted Environment failed to provision - returns the observed 409 conflict_error "the hosted environment failed to - provision"; input already waiting when it fails and expired Environments keep - the local 409 environment_unavailable. Input the Session cannot accept in - its current state, such as a result after cancellation or a batch while earlier - input is pending, and a result that differs from the call's saved result return - 409 with type and code conflict_error; reusing an Idempotency-Key with a different - batch returns the local 409 idempotency_conflict. Inside an owned Session, - a result for an unknown call or for a call of another Turn returns 400 invalid_request_error - and changes nothing; missing and foreign Sessions return 404. Losing execution - ownership returns 503. The response write deadline accommodates the admission - window for either prepared Environment, independently of new-hosted-admission - and executor URL settings. Disconnecting the waiting HTTP request does not - cancel retained work or restart its deadline. Retry keys identify the whole - ordered batch. Function output accepts text or ordered text/image parts subject - to engine support; Claude SDK accepts text results and, on none and qualified - workspace profiles, successful inline PNG/JPEG results, preserving ordered - content; error images and remote references reject before admission. Native - image resizing may change bytes. Runtime image-result support is checked only - for image-bearing delivery. Codex and Claude SDK on none and qualified managed - or self_hosted workspace profiles accept ordered inline PNG/JPEG image messages. - Other engines remain text-only; remote image URLs are unsupported. Image references - are retained unchanged without service-side downloads. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Retry key, up to 128 bytes - in: header - name: Idempotency-Key - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Ordered input events - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.CreateEventsRequest' - responses: - "202": - description: Accepted - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "409": - description: Conflict - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Submit Session input events - tags: - - Sessions - /agents/sessions/{session_id}/items: - get: - description: Returns supported message and tool Items in first-observation order. - Native engine fields are projected explicitly; unfinished Items on terminal - Turns are incomplete. Cursors are Items of the same tenant and Session. Any - other after value, including a malformed one, returns 400 invalid_request_error - with the message "Invalid session item ID in `after`". - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Last Item ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Page size; 0 is treated as 1 and values above 100 as 100 - in: query - minimum: 0 - name: limit - type: integer - - default: desc - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.ItemList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List persisted execution Items - tags: - - Items - /agents/sessions/{session_id}/subagents: - get: - description: Includes nested and closed Subagents. Cursors are Subagents of - the same tenant and Session. Any other after value, including a malformed - one, returns 400 invalid_request_error with the message "Invalid resource - ID in `after`". A limit outside 1–100 is rejected. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Last Subagent ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Page size - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer - - default: desc - description: Resource order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SubagentList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List Session Subagents - tags: - - Subagents - /agents/sessions/{session_id}/subagents/{subagent_id}: - get: - description: Returns this Session's persisted Subagent. Active includes idle - between Turns. Resuming preserves opened_at and clears closed_at. Unknown - or inaccessible parent scopes return not found. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Subagent ID - in: path - name: subagent_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Subagent' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve a Session Subagent - tags: - - Subagents - /agents/sessions/{session_id}/subagents/{subagent_id}/items: - get: - description: Returns only this Subagent's own Items across all its Turns, not - its descendants' Items. Cursors are Items of the same tenant, Session and - Subagent. Any other after value, including a malformed one, returns 400 invalid_request_error - with the message "Invalid session item ID in `after`". - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Subagent ID - in: path - name: subagent_id - required: true - type: string - - description: Last Item ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Page size; 0 is treated as 1 and values above 100 as 100 - in: query - minimum: 0 - name: limit - type: integer - - default: desc - description: Resource order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.ItemList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List a Subagent's Items - tags: - - Subagents - /agents/sessions/{session_id}/subagents/{subagent_id}/turns: - get: - description: Includes this Subagent's Turns after resume, with the Session's - Agent ID as agent_id. Cursors are Turns of the same tenant, Session and Subagent. - Any other after value, including a malformed one, returns 400 invalid_request_error - with the message "Invalid resource ID in `after`". Missing recorded usage - remains null. A limit outside 1–100 is rejected. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Subagent ID - in: path - name: subagent_id - required: true - type: string - - description: Last Turn ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Page size - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer - - default: desc - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.TurnList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List a Subagent's Turns - tags: - - Subagents - /agents/sessions/{session_id}/subagents/{subagent_id}/turns/{turn_id}: - get: - description: Returns a Turn owned by this Subagent. Its agent_id is the Session's - Agent ID and its subagent_id identifies the Subagent. Session Turn routes - do not return child Turns. Unknown or inaccessible parent scopes return not - found. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Subagent ID - in: path - name: subagent_id - required: true - type: string - - description: Turn ID - in: path - name: turn_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Turn' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve a Subagent Turn - tags: - - Subagents - /agents/sessions/{session_id}/subagents/{subagent_id}/turns/{turn_id}/items: - get: - description: Returns Items owned by this exact Subagent Turn. Cursors are Items - of the same tenant, Session, Subagent and Turn. Any other after value, including - a malformed one, returns 400 invalid_request_error with the message "Invalid - session item ID in `after`". - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Subagent ID - in: path - name: subagent_id - required: true - type: string - - description: Turn ID - in: path - name: turn_id - required: true - type: string - - description: Last Item ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Page size; 0 is treated as 1 and values above 100 as 100 - in: query - minimum: 0 - name: limit - type: integer - - default: desc - description: Resource order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.ItemList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List a Subagent Turn's Items - tags: - - Subagents - /agents/sessions/{session_id}/turns: - get: - description: Returns the Session's root Turns in creation order; Subagent Turns - are listed through the Subagent Turn routes. The cursor belongs to the same - Session and tenant; any other after value, including a malformed one or a - Subagent Turn ID, returns not found. Usage contains the latest recorded complete - token breakdown; missing measurements remain null. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Last Turn ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Page size - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer - - default: desc - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.TurnList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List execution Turns - tags: - - Turns - /agents/sessions/{session_id}/turns/{turn_id}: - get: - description: Returns a root Turn of this Session. A Subagent Turn ID returns - the same not found error as a missing Turn; read it through the Subagent Turn - routes. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Session ID - in: path - name: session_id - required: true - type: string - - description: Turn ID - in: path - name: turn_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Turn' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve an execution Turn - tags: - - Turns - /files: - get: - description: Lists project-owned Files without reading their bodies. The limit - defaults to 10000 and must be 1–10000. Equal creation times use ID ordering. - Purpose validation precedes cursor lookup; current storage contains only user_data. - An explicit empty purpose is treated as omitted. Repeated purpose values remain - rejected. Hosted positive filtering, default order and concurrent-page behavior - remain unverified. No Beta header is required. - parameters: - - description: Last File ID from the previous page - in: query - name: after - type: string - - default: 10000 - description: Maximum page size, 1–10000 - in: query - maximum: 10000 - minimum: 1 - name: limit - type: integer - - default: desc - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - - description: Only return Files with this purpose - in: query - name: purpose - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SourceFileList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List source files - tags: - - Files - post: - consumes: - - multipart/form-data - description: Accepts one multipart file and purpose=user_data in either order, - with a private 512 MiB content limit and 64 KiB envelope allowance. Commits - only after the entire request validates. The source is project-owned, independent - of Sessions and workspace copies. No Beta header is required. Other purposes, - expires_after, listing, resumable Uploads, quotas/rate-limit and complete - hosted error/status parity remain unsupported or unverified. - parameters: - - description: Source bytes - in: formData - name: file - required: true - type: file - - description: user_data - enum: - - user_data - in: formData - name: purpose - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SourceFile' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Upload a source file - tags: - - Files - /files/{file_id}: - delete: - description: Atomically deletes project-owned metadata and stored bytes. Already-admitted - reads or copies may finish. Workspace copies remain independent. Historical - WAL/backups are not erased. No Beta header is required; exact hosted concurrent - deletion/error semantics remain unverified. - parameters: - - description: Source file ID - in: path - name: file_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SourceFileDeleted' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Delete a source file - tags: - - Files - get: - description: Returns immutable project-owned user_data file metadata. No Beta - header is required. Other purposes, expiration and full hosted status/error - semantics remain unimplemented or unverified. - parameters: - - description: Source file ID - in: path - name: file_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SourceFile' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve source file metadata - tags: - - Files - /files/{file_id}/content: - get: - description: Resolves project-owned File metadata before enforcing download - policy. Public download of user_data Files returns 400; missing and foreign - Files return the same 404. Internal initial-file and workspace copies remain - available. No Beta header is required. - parameters: - - description: Source file ID - in: path - name: file_id - required: true - type: string - produces: - - application/json - responses: - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Download source file bytes - tags: - - Files - /skills: - get: - description: Lists tenant-owned metadata in timestamp order. Default page size - 20, maximum 100. Limit 0 returns an empty page whose has_more reports whether - any Skill follows the cursor; exact hosted defaults remain unverified. - parameters: - - description: Skill resource cursor - in: query - name: after - type: string - - default: 20 - description: Page size; 0 returns an empty page - in: query - maximum: 100 - minimum: 0 - name: limit - type: integer - - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SkillList' - security: - - BearerAuth: [] - summary: List Skills - tags: - - Skills - post: - consumes: - - multipart/form-data - description: Accepts one ZIP in files or a directory in files[]. Applies the - qualified portable Skill bundle profile. No Beta header is required; full - hosted upload limits and activation extensions are not qualified. - parameters: - - description: Skill ZIP or directory files - in: formData - name: files - required: true - type: file - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Skill' - security: - - BearerAuth: [] - summary: Upload a Skill - tags: - - Skills - /skills/{skill_id}: - delete: - description: Deletes tenant-owned source bundles. Existing Session installation - snapshots remain independent. - parameters: - - description: Skill ID - in: path - name: skill_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SkillDeleted' - security: - - BearerAuth: [] - summary: Delete a Skill and its versions - tags: - - Skills - get: - description: Returns tenant-owned metadata without decrypting contents or starting - Runtime. No Beta header is required. - parameters: - - description: Skill ID - in: path - name: skill_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Skill' - security: - - BearerAuth: [] - summary: Retrieve Skill metadata - tags: - - Skills - post: - consumes: - - application/json - description: Changes only the tenant-owned default pointer; immutable versions - and existing Session snapshots remain unchanged. - parameters: - - description: Skill ID - in: path - name: skill_id - required: true - type: string - - description: Default version - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.SkillUpdateRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Skill' - security: - - BearerAuth: [] - summary: Update the default Skill version - tags: - - Skills - /skills/{skill_id}/content: - get: - description: Downloads an authorized ZIP using the default pointer when no concrete - version is supplied. Exact upstream unversioned selection, content headers - and range semantics remain unverified. - parameters: - - description: Skill ID - in: path - name: skill_id - required: true - type: string - produces: - - application/octet-stream - responses: - "200": - description: OK - schema: - type: file - security: - - BearerAuth: [] - summary: Download Skill content - tags: - - Skills - /skills/{skill_id}/versions: - get: - description: Orders by version number; after identifies a version resource, - not a version number. An after value that does not begin with skillver, or - a version of another Skill, returns 400 invalid_value with param after; a - missing version returns not found. No contents are decrypted. Limit 0 returns - an empty page whose has_more reports whether any version follows the cursor. - parameters: - - description: Skill ID - in: path - name: skill_id - required: true - type: string - - description: Version resource cursor - in: query - name: after - type: string - - default: 20 - description: Page size; 0 returns an empty page - in: query - maximum: 100 - minimum: 0 - name: limit - type: integer - - description: Version order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SkillVersionList' - security: - - BearerAuth: [] - summary: List Skill versions - tags: - - Skills - post: - consumes: - - multipart/form-data - parameters: - - description: Skill ID - in: path - name: skill_id - required: true - type: string - - description: Skill ZIP or directory files - in: formData - name: files - required: true - type: file - - description: Set as default - in: formData - name: default - type: boolean - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SkillVersion' - security: - - BearerAuth: [] - summary: Upload an immutable Skill version - tags: - - Skills - /skills/{skill_id}/versions/{version}: - delete: - description: Deleting the only remaining version also deletes the Skill; existing - Session installation snapshots remain independent. The default version cannot - be deleted while other versions remain. Version numbers are never reused. - parameters: - - description: Skill ID - in: path - name: skill_id - required: true - type: string - - description: Concrete version number - in: path - name: version - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SkillVersionDeleted' - security: - - BearerAuth: [] - summary: Delete a Skill version - tags: - - Skills - get: - parameters: - - description: Skill ID - in: path - name: skill_id - required: true - type: string - - description: Concrete version number - in: path - name: version - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.SkillVersion' - security: - - BearerAuth: [] - summary: Retrieve Skill version metadata - tags: - - Skills - /skills/{skill_id}/versions/{version}/content: - get: - parameters: - - description: Skill ID - in: path - name: skill_id - required: true - type: string - - description: Concrete version number - in: path - name: version - required: true - type: string - produces: - - application/octet-stream - responses: - "200": - description: OK - schema: - type: file - security: - - BearerAuth: [] - summary: Download immutable Skill version content - tags: - - Skills - /vaults: - get: - description: Lists project-owned Vaults independently of execution. An unknown, - malformed or foreign after cursor returns not found. Includes active and archived - records by default. Status accepts a scalar, the SDK's status[] array or both, - filtering by their union; a repeated scalar is rejected. Limits default to - 20 and clamp to 1–100. Equal creation times use ID ordering; exact hosted - errors and concurrent-page behavior remain unverified. Archive/delete lifecycle - is not implemented. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Last Vault ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Requested page size, clamped to 1–100 - in: query - name: limit - type: integer - - default: desc - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - - description: Scalar status filter - enum: - - active - - archived - in: query - name: status - type: string - - collectionFormat: multi - description: Array status filter; combined with status as a union - in: query - items: - enum: - - active - - archived - type: string - name: status[] - type: array - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.VaultList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List Vaults - tags: - - Vaults - post: - consumes: - - application/json - description: Creates a project-owned Vault independently of execution. Omitted - name stays null; a supplied string is trimmed and must contain 1–256 UTF-8 - bytes. Explicit null name is invalid. Omitted/null metadata becomes an empty - object; non-string values return invalid_request_error with a metadata. - param. Metadata has a local 64 KiB encoded storage bound. U+0000 in stored - strings is rejected as a local storage limit. Credentials, Session binding - and hosted error/retry parity remain incomplete. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Vault name and metadata - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.CreateVaultRequest' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/v1.Vault' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Create a Vault - tags: - - Vaults - /vaults/{vault_id}: - delete: - description: Atomically removes the authenticated project's Vault and all its - stored Credentials without an encryption key, decryption or external requests. - Existing Session snapshots, history and recorded retries retain their frozen - identities; subsequent credential lookups fail without reselection or anonymous - fallback. Already-resolved tokens and running Sessions are not revoked or - cancelled. Missing/repeated deletion locally returns 404. Exact hosted archive, - post-delete visibility and concurrent/error semantics remain unverified; physical - erasure from native history, WAL or backups is not established. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Vault ID - in: path - name: vault_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.VaultDeleted' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Delete a Vault and all its Credentials - tags: - - Vaults - get: - description: Reads a Vault owned by the authenticated project without resolving - credentials, Sessions or execution devices. Missing and foreign IDs share - the same not-found response; exact hosted error semantics remain unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Vault ID - in: path - name: vault_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Vault' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve a Vault - tags: - - Vaults - /vaults/{vault_id}/credentials: - get: - description: Lists only metadata from the authenticated project's requested - Vault, without decryption or execution. An unknown, malformed or foreign after - cursor, including another Vault's Credential, returns not found. Includes - active and archived Credentials by default, independently of Vault status. - Status accepts a scalar, the SDK status[] array or both, filtering by their - union; a repeated scalar is rejected. Limits default to 20 and clamp to 1–100. - Equal creation times use ID ordering. Hosted errors, concurrent-page behavior - and archive/delete lifecycle remain unverified or unimplemented. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Vault ID - in: path - name: vault_id - required: true - type: string - - description: Last Credential ID from the previous page - in: query - name: after - type: string - - default: 20 - description: Requested page size, clamped to 1–100 - in: query - name: limit - type: integer - - default: desc - description: Creation order; omit for descending, explicit empty values are - invalid - enum: - - asc - - desc - in: query - name: order - type: string - - description: Scalar status filter - enum: - - active - - archived - in: query - name: status - type: string - - collectionFormat: multi - description: Array status filter; combined with status as a union - in: query - items: - enum: - - active - - archived - type: string - name: status[] - type: array - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.CredentialList' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: List safe Vault Credential metadata - tags: - - Credentials - post: - consumes: - - application/json - description: Stores static_bearer or mcp_oauth secrets as execution-owned authenticated - ciphertext without contacting any endpoint. Static bearer and OAuth access - tokens must be nonempty strings; their bytes are preserved. OAuth accepts - a required access token, nullable RFC3339 expiry and optional refresh configuration - with none, client_secret_basic or client_secret_post authentication. Required - name is trimmed to 1–256 UTF-8 bytes. Credential and token endpoints require - HTTPS without userinfo or fragments. Responses contain safe metadata only, - including explicit nullable OAuth expiry, refresh, resource and scope. Missing - encryption configuration returns local 503. External authorization and provider - revocation remain caller responsibilities; exact hosted error/default semantics - remain unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Vault ID - in: path - name: vault_id - required: true - type: string - - description: Write-only credential authentication union - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.CreateCredentialRequest' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/v1.Credential' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Create a Vault Credential - tags: - - Credentials - /vaults/{vault_id}/credentials/{credential_id}: - delete: - description: Removes one Credential and its encrypted token within the authenticated - project and owning Vault, without an encryption key or secret decryption. - Subsequent metadata reads, updates and dispatch lookups cannot use it. Existing - Session snapshots and history retain their frozen identities; already-resolved - tokens and running Sessions are not revoked or cancelled. This local policy - removes the row rather than defining archived lifecycle; missing/repeated - deletion returns 404. Exact hosted archive, post-delete visibility and retry/error - semantics remain unverified. Provider revocation and physical erasure from - native history, WAL or backups are separate concerns. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Vault ID - in: path - name: vault_id - required: true - type: string - - description: Credential ID - in: path - name: credential_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.CredentialDeleted' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Delete a Vault Credential - tags: - - Credentials - get: - description: Reads only non-secret metadata scoped to the authenticated project - and owning Vault. No token decryption, network request or execution is performed. - Unknown, foreign, wrong-Vault and malformed IDs use the same local not-found - response; hosted error parity remains unverified. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Vault ID - in: path - name: vault_id - required: true - type: string - - description: Credential ID - in: path - name: credential_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Credential' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Retrieve safe Vault Credential metadata - tags: - - Credentials - post: - consumes: - - application/json - description: Explicitly empty static bearer or OAuth access tokens and OAuth - patches without a mutable field are rejected before storage. Omitted OAuth - access tokens preserve the existing grant when expiry or refresh fields change. - Updates the existing static_bearer or mcp_oauth authentication method without - network requests. OAuth access_token omission/null retains the token; a new - token clears omitted expiry, explicit null clears expiry, and other omitted - fields remain unchanged. OAuth refresh patches cannot add configuration or - change client, endpoint, resource or authentication method; nullable token/client-secret - values retain stored secrets while explicit null scope clears scope. Whole-null - refresh and token_endpoint_auth retain existing configuration under local - policy. Identity, destination, creation time and Session bindings remain unchanged. - Responses expose safe metadata only. Already-dispatched work is not revoked; - provider revocation, storage-key rotation and exact hosted concurrent-update/error - semantics remain separate. - parameters: - - description: agents=v1 - in: header - name: OpenAI-Beta - required: true - type: string - - description: Vault ID - in: path - name: vault_id - required: true - type: string - - description: Credential ID - in: path - name: credential_id - required: true - type: string - - description: Write-only credential authentication replacement union - in: body - name: body - required: true - schema: - $ref: '#/definitions/v1.UpdateCredentialRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.Credential' - "400": - description: Bad Request - schema: - $ref: '#/definitions/v1.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/v1.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/v1.ErrorResponse' - "413": - description: Request Entity Too Large - schema: - $ref: '#/definitions/v1.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.ErrorResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/v1.ErrorResponse' - security: - - BearerAuth: [] - summary: Replace Vault Credential authentication secrets - tags: - - Credentials -schemes: -- http -- https -securityDefinitions: - BearerAuth: - in: header - name: Authorization - type: apiKey -swagger: "2.0" +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAgentCore public API", + "version": "v1", + "description": "The pinned OpenAI Agents, Files and Skills API with x_agents_core extensions. See the coverage ledger for implementation qualification." + }, + "servers": [ + { + "url": "/v1" + } + ], + "security": [ + { + "ProjectKey": [] + } + ], + "tags": [ + { + "name": "Assistants", + "description": "Build Assistants that can call models and use tools." + }, + { + "name": "Audio", + "description": "Turn audio into text or text into audio." + }, + { + "name": "Chat", + "description": "Given a list of messages comprising a conversation, the model will return a response." + }, + { + "name": "Conversations", + "description": "Manage conversations and conversation items." + }, + { + "name": "Completions", + "description": "Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position." + }, + { + "name": "Embeddings", + "description": "Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms." + }, + { + "name": "Evals", + "description": "Manage and run evals in the OpenAI platform." + }, + { + "name": "Fine-tuning", + "description": "Manage fine-tuning jobs to tailor a model to your specific training data." + }, + { + "name": "Graders", + "description": "Manage and run graders in the OpenAI platform." + }, + { + "name": "Batch", + "description": "Create large batches of API requests to run asynchronously." + }, + { + "name": "Files", + "description": "Files are used to upload documents that can be used with features like Assistants and Fine-tuning." + }, + { + "name": "Uploads", + "description": "Use Uploads to upload large files in multiple parts." + }, + { + "name": "Images", + "description": "Given a prompt and/or an input image, the model will generate a new image." + }, + { + "name": "Models", + "description": "List and describe the various models available in the API." + }, + { + "name": "Moderations", + "description": "Given text and/or image inputs, classifies if those inputs are potentially harmful." + }, + { + "name": "Audit Logs", + "description": "List user actions and configuration changes within this organization." + } + ], + "paths": { + "/files": { + "get": { + "operationId": "listFiles", + "tags": [ + "Files" + ], + "summary": "Returns a list of files.", + "parameters": [ + { + "in": "query", + "name": "purpose", + "required": false, + "schema": { + "type": "string" + }, + "description": "Only return files with the given purpose." + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 10,000, and the default is 10,000.\n", + "required": false, + "schema": { + "type": "integer", + "default": 10000 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order.\n", + "schema": { + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFilesResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List files", + "group": "files", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.files.list()\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const list = await openai.files.list();\n\n for await (const file of list) {\n console.log(file);\n }\n}\n\nmain();" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"file-abc123\",\n \"object\": \"file\",\n \"bytes\": 175,\n \"created_at\": 1613677385,\n \"expires_at\": 1677614202,\n \"filename\": \"salesOverview.pdf\",\n \"purpose\": \"assistants\",\n },\n {\n \"id\": \"file-abc456\",\n \"object\": \"file\",\n \"bytes\": 140,\n \"created_at\": 1613779121,\n \"expires_at\": 1677614202,\n \"filename\": \"puppy.jsonl\",\n \"purpose\": \"fine-tune\",\n }\n ],\n \"first_id\": \"file-abc123\",\n \"last_id\": \"file-abc456\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "operationId": "createFile", + "tags": [ + "Files" + ], + "summary": "Upload a file that can be used across various endpoints. Individual files\ncan be up to 512 MB, and each project can store up to 2.5 TB of files in\ntotal. There is no organization-wide storage limit. Uploads to this\nendpoint are rate-limited to 1,000 requests per minute per authenticated\nuser.\n\n- The Assistants API supports files up to 2 million tokens and of specific\n file types. See the [Assistants Tools guide](https://developers.openai.com/api/docs/guides/tools) for\n details.\n- The Fine-tuning API only supports `.jsonl` files. The input also has\n certain required formats for fine-tuning\n [chat](https://developers.openai.com/api/docs/guides/supervised-fine-tuning#formatting-your-data) or\n [completions](https://developers.openai.com/api/docs/guides/supervised-fine-tuning#formatting-your-data) models.\n- The Batch API only supports `.jsonl` files up to 200 MB in size. The input\n also has a specific required\n [format](https://developers.openai.com/api/docs/guides/batch#1-prepare-your-batch-file).\n- For Retrieval or `file_search` ingestion, upload files here first. If\n you need to attach multiple uploaded files to the same vector store, use\n [`/vector_stores/{vector_store_id}/file_batches`](https://developers.openai.com/api/reference/resources/vector_stores/subresources/file_batches/methods/create)\n instead of attaching them one by one. Vector store attachment has separate\n limits from file upload, including 2,000 attached files per minute per\n organization.\n\nPlease [contact us](https://help.openai.com/) if you need to increase these\nstorage limits.\n", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateFileRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OpenAIFile" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Upload file", + "group": "files", + "description": "Uploads a file for later use across OpenAI APIs. Uploads to this endpoint are rate-limited to 1,000 requests per minute per authenticated user. For Retrieval or `file_search` ingestion, upload files here first. If you need to attach multiple uploaded files to the same vector store, use vector store file batches instead of attaching them one by one.\n", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -F purpose=\"fine-tune\" \\\n -F file=\"@mydata.jsonl\"\n -F expires_after[anchor]=\"created_at\"\n -F expires_after[seconds]=2592000\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.files.create(\n file=open(\"mydata.jsonl\", \"rb\"),\n purpose=\"fine-tune\",\n expires_after={\n \"anchor\": \"created_at\",\n \"seconds\": 2592000\n }\n)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const file = await openai.files.create({\n file: fs.createReadStream(\"mydata.jsonl\"),\n purpose: \"fine-tune\",\n expires_after: {\n anchor: \"created_at\",\n seconds: 2592000\n }\n });\n\n console.log(file);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"file-abc123\",\n \"object\": \"file\",\n \"bytes\": 120000,\n \"created_at\": 1677610602,\n \"expires_at\": 1677614202,\n \"filename\": \"mydata.jsonl\",\n \"purpose\": \"fine-tune\",\n}\n" + } + } + } + }, + "/files/{file_id}": { + "delete": { + "operationId": "deleteFile", + "tags": [ + "Files" + ], + "summary": "Delete a file and remove it from all vector stores.", + "parameters": [ + { + "in": "path", + "name": "file_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the file to use for this request." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteFileResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete file", + "group": "files", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files/file-abc123 \\\n -X DELETE \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.files.delete(\"file-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const file = await openai.files.delete(\"file-abc123\");\n\n console.log(file);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"file-abc123\",\n \"object\": \"file\",\n \"deleted\": true\n}\n" + } + } + }, + "get": { + "operationId": "retrieveFile", + "tags": [ + "Files" + ], + "summary": "Returns information about a specific file.", + "parameters": [ + { + "in": "path", + "name": "file_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the file to use for this request." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OpenAIFile" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve file", + "group": "files", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files/file-abc123 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.files.retrieve(\"file-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const file = await openai.files.retrieve(\"file-abc123\");\n\n console.log(file);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"file-abc123\",\n \"object\": \"file\",\n \"bytes\": 120000,\n \"created_at\": 1677610602,\n \"expires_at\": 1677614202,\n \"filename\": \"mydata.jsonl\",\n \"purpose\": \"fine-tune\",\n}\n" + } + } + } + }, + "/files/{file_id}/content": { + "get": { + "operationId": "downloadFile", + "tags": [ + "Files" + ], + "summary": "Returns a response containing the contents of the specified file.", + "parameters": [ + { + "in": "path", + "name": "file_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the file to use for this request." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve file content", + "group": "files", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files/file-abc123/content \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" > file.jsonl\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncontent = client.files.content(\"file-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const response = await openai.files.content(\"file-abc123\");\n const content = await response.text();\n\n console.log(content);\n}\n\nmain();\n" + } + } + } + } + }, + "/skills": { + "post": { + "tags": [ + "Skills" + ], + "summary": "Create a new skill.", + "operationId": "CreateSkill", + "parameters": [], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateSkillBody" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSkillBody" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkillResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + }, + "get": { + "tags": [ + "Skills" + ], + "summary": "List all skills for the current project.", + "operationId": "ListSkills", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Number of items to retrieve", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order of results by timestamp. Use `asc` for ascending order or `desc` for descending order.", + "required": false, + "schema": { + "$ref": "#/components/schemas/OrderEnum" + } + }, + { + "name": "after", + "in": "query", + "description": "Identifier for the last item from the previous pagination request", + "required": false, + "schema": { + "description": "Identifier for the last item from the previous pagination request", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkillListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + } + }, + "/skills/{skill_id}": { + "delete": { + "tags": [ + "Skills" + ], + "summary": "Delete a skill by its ID.", + "operationId": "DeleteSkill", + "parameters": [ + { + "name": "skill_id", + "in": "path", + "description": "The identifier of the skill to delete.", + "required": true, + "schema": { + "example": "skill_123", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedSkillResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + }, + "get": { + "tags": [ + "Skills" + ], + "summary": "Get a skill by its ID.", + "operationId": "GetSkill", + "parameters": [ + { + "name": "skill_id", + "in": "path", + "description": "The identifier of the skill to retrieve.", + "required": true, + "schema": { + "example": "skill_123", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkillResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + }, + "post": { + "tags": [ + "Skills" + ], + "summary": "Update the default version pointer for a skill.", + "operationId": "UpdateSkillDefaultVersion", + "parameters": [ + { + "name": "skill_id", + "in": "path", + "description": "The identifier of the skill.", + "required": true, + "schema": { + "example": "skill_123", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetDefaultSkillVersionBody" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkillResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + } + }, + "/skills/{skill_id}/content": { + "get": { + "tags": [ + "Skills" + ], + "summary": "Download a skill zip bundle by its ID.", + "operationId": "GetSkillContent", + "parameters": [ + { + "name": "skill_id", + "in": "path", + "description": "The identifier of the skill to download.", + "required": true, + "schema": { + "example": "skill_123", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The skill zip bundle.", + "content": { + "application/zip": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + } + }, + "/skills/{skill_id}/versions": { + "post": { + "tags": [ + "Skills" + ], + "summary": "Create a new immutable skill version.", + "operationId": "CreateSkillVersion", + "parameters": [ + { + "name": "skill_id", + "in": "path", + "description": "The identifier of the skill to version.", + "required": true, + "schema": { + "example": "skill_123", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateSkillVersionBody" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSkillVersionBody" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkillVersionResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + }, + "get": { + "tags": [ + "Skills" + ], + "summary": "List skill versions for a skill.", + "operationId": "ListSkillVersions", + "parameters": [ + { + "name": "skill_id", + "in": "path", + "description": "The identifier of the skill.", + "required": true, + "schema": { + "example": "skill_123", + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of versions to retrieve.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order of results by version number.", + "required": false, + "schema": { + "$ref": "#/components/schemas/OrderEnum" + } + }, + { + "name": "after", + "in": "query", + "description": "The skill version ID to start after.", + "required": false, + "schema": { + "example": "skillver_123", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkillVersionListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + } + }, + "/skills/{skill_id}/versions/{version}": { + "get": { + "tags": [ + "Skills" + ], + "summary": "Get a specific skill version.", + "operationId": "GetSkillVersion", + "parameters": [ + { + "name": "skill_id", + "in": "path", + "description": "The identifier of the skill.", + "required": true, + "schema": { + "example": "skill_123", + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "description": "The version number to retrieve.", + "required": true, + "schema": { + "description": "The version number to retrieve.", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkillVersionResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + }, + "delete": { + "tags": [ + "Skills" + ], + "summary": "Delete a skill version.", + "operationId": "DeleteSkillVersion", + "parameters": [ + { + "name": "skill_id", + "in": "path", + "description": "The identifier of the skill.", + "required": true, + "schema": { + "example": "skill_123", + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "description": "The skill version number.", + "required": true, + "schema": { + "description": "The skill version number.", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedSkillVersionResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + } + }, + "/skills/{skill_id}/versions/{version}/content": { + "get": { + "tags": [ + "Skills" + ], + "summary": "Download a skill version zip bundle.", + "operationId": "GetSkillVersionContent", + "parameters": [ + { + "name": "skill_id", + "in": "path", + "description": "The identifier of the skill.", + "required": true, + "schema": { + "example": "skill_123", + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "description": "The skill version number.", + "required": true, + "schema": { + "description": "The skill version number.", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The skill zip bundle.", + "content": { + "application/zip": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + } + } + }, + "/agents/environments/{environment_id}": { + "get": { + "operationId": "retrieveAgentEnvironment", + "summary": "Retrieves an execution environment's connection status and safe installed metadata. See [environment lifecycle](https://developers.openai.com/api/docs/guides/agents-api/environments/lifecycle).", + "description": "Retrieves an execution environment's connection status and safe installed metadata.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "environment_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the environment." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested environment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicEnvironmentResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve an agent environment" + } + } + }, + "/agents/environments/{environment_id}/files": { + "get": { + "operationId": "listAgentEnvironmentFiles", + "summary": "Lists live files on a connected execution environment with optional directory filtering and opaque cursor pagination. See [environment files](https://developers.openai.com/api/docs/guides/agents-api/environments/files).", + "description": "Lists live files on a connected execution environment with optional directory filtering and opaque cursor pagination.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "environment_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the environment." + }, + { + "name": "path", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Restrict the listing to this absolute workspace directory." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 1, + "maximum": 100 + }, + "description": "The maximum number of files to return, between 1 and 100." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam" + }, + "description": "Sort by case-sensitive path components. Defaults to descending." + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The opaque token from the previous page. Keep the same path, order, and limit." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A page of live environment files.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvironmentFileListResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List agent environment files" + } + }, + "post": { + "operationId": "createAgentEnvironmentFile", + "summary": "Copies inline bytes or a Files API file into a connected execution environment. See [environment files](https://developers.openai.com/api/docs/guides/agents-api/environments/files).", + "description": "Copies inline bytes or a Files API file into a connected execution environment.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "environment_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the environment." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HostedEnvironmentFileParam" + } + } + } + }, + "responses": { + "201": { + "description": "The created live environment file.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvironmentFileResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create an agent environment file" + } + } + }, + "/agents/sessions/{session_id}/subagents": { + "get": { + "operationId": "listAgentSessionSubagents", + "summary": "Lists subagents in a session, including nested and closed subagents. See [subagent workflows](https://developers.openai.com/api/docs/guides/agents-api/multi-agent).", + "description": "Lists subagents in a session, including nested and closed subagents.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "The maximum number of resources to return, between 1 and 100. Defaults to 20." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "The order in which resources are returned. Defaults to `desc`." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested subagents.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "default": "list", + "x-stainless-const": true, + "description": "The object type, which is always `list`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SubagentResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The resources returned in this page, in the requested sort order." + }, + "first_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the first resource in `data`, or `null` if the page is empty." + }, + "last_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the last resource in `data`, or `null` if the page is empty. Pass this as `after` with the same order and filters." + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more resources to retrieve after this page." + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "additionalProperties": false, + "description": "A page of Agents API resources, with IDs for retrieving additional pages." + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List session subagents" + } + } + }, + "/agents/sessions/{session_id}/subagents/{subagent_id}": { + "get": { + "operationId": "retrieveAgentSessionSubagent", + "summary": "Retrieves a subagent belonging to this session. See [subagent workflows](https://developers.openai.com/api/docs/guides/agents-api/multi-agent).", + "description": "Retrieves a subagent belonging to this session.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "subagent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the subagent in this session." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested subagent.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubagentResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve a session subagent" + } + } + }, + "/agents/sessions/{session_id}/subagents/{subagent_id}/items": { + "get": { + "operationId": "listAgentSessionSubagentItems", + "summary": "Lists this subagent's own items across all of its turns. See [subagent workflows](https://developers.openai.com/api/docs/guides/agents-api/multi-agent).", + "description": "Lists this subagent's own items across all of its turns.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "subagent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the subagent in this session." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "The maximum number of resources to return, between 1 and 100. Defaults to 20." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "The order in which resources are returned. Defaults to `desc`." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested subagent history.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionItemListResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List subagent items" + } + } + }, + "/agents/sessions/{session_id}/subagents/{subagent_id}/turns": { + "get": { + "operationId": "listAgentSessionSubagentTurns", + "summary": "Lists all turns of this subagent, including turns after a resume. See [subagent workflows](https://developers.openai.com/api/docs/guides/agents-api/multi-agent).", + "description": "Lists all turns of this subagent, including turns after a resume.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "subagent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the subagent in this session." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "The maximum number of resources to return, between 1 and 100. Defaults to 20." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "The order in which resources are returned. Defaults to `desc`." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested subagent history.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionTurnListResource", + "description": "A page of turns from an agent session or subagent." + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List subagent turns" + } + } + }, + "/agents/sessions/{session_id}/subagents/{subagent_id}/turns/{turn_id}": { + "get": { + "operationId": "retrieveAgentSessionSubagentTurn", + "summary": "Retrieves a turn belonging to this subagent. See [subagent workflows](https://developers.openai.com/api/docs/guides/agents-api/multi-agent).", + "description": "Retrieves a turn belonging to this subagent.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "subagent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the subagent in this session." + }, + { + "name": "turn_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of a turn belonging to this subagent." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested subagent history.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TurnResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve a subagent turn" + } + } + }, + "/agents/sessions/{session_id}/subagents/{subagent_id}/turns/{turn_id}/items": { + "get": { + "operationId": "listAgentSessionSubagentTurnItems", + "summary": "Lists items belonging to one turn of this subagent. See [subagent workflows](https://developers.openai.com/api/docs/guides/agents-api/multi-agent).", + "description": "Lists items belonging to one turn of this subagent.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "subagent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the subagent in this session." + }, + { + "name": "turn_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of a turn belonging to this subagent." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "The maximum number of resources to return, between 1 and 100. Defaults to 20." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "The order in which resources are returned. Defaults to `desc`." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested subagent history.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionItemListResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List subagent turn items" + } + } + }, + "/agents": { + "get": { + "operationId": "listAgents", + "summary": "Lists reusable agents in the current project. See [agent configuration](https://developers.openai.com/api/docs/guides/agents-api/configuration).", + "description": "Lists reusable agents in the current project.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 1 + }, + "description": "The maximum number of resources to return." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "The order in which resources are returned. Defaults to `desc`." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A page of agents.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentListResource", + "description": "A page of reusable agents." + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List agents" + } + }, + "post": { + "operationId": "createAgent", + "summary": "Creates a reusable agent without storing credentials. See [agent configuration](https://developers.openai.com/api/docs/guides/agents-api/configuration).", + "description": "Creates a reusable agent without storing credentials.", + "tags": [ + "Agents" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAgentParams" + } + } + } + }, + "responses": { + "201": { + "description": "The created agent.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create an agent" + }, + "parameters": [ + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ] + } + }, + "/agents/{agent_id}": { + "get": { + "operationId": "retrieveAgent", + "summary": "Retrieves a reusable agent by ID. See [agent configuration](https://developers.openai.com/api/docs/guides/agents-api/configuration).", + "description": "Retrieves a reusable agent by ID.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the reusable agent." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested agent.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve an agent" + } + }, + "post": { + "operationId": "updateAgent", + "summary": "Updates a reusable agent. See [agent configuration](https://developers.openai.com/api/docs/guides/agents-api/configuration).", + "description": "Updates a reusable agent.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the reusable agent." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAgentParams" + } + } + } + }, + "responses": { + "200": { + "description": "The updated agent.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update an agent" + } + }, + "delete": { + "operationId": "deleteAgent", + "summary": "Deletes a reusable agent. See [agent configuration](https://developers.openai.com/api/docs/guides/agents-api/configuration).", + "description": "Deletes a reusable agent.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the reusable agent." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The deleted agent.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedAgentResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete an agent" + } + } + }, + "/agents/environments/templates": { + "get": { + "operationId": "listAgentEnvironmentTemplates", + "summary": "Lists reusable environment templates without returning confidential values. See [reusing a hosted setup](https://developers.openai.com/api/docs/guides/agents-api/tools#reuse-a-hosted-plugin-setup).", + "description": "Lists reusable environment templates without returning confidential values.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "The maximum number of resources to return, between 1 and 100. Defaults to 20." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "The order in which resources are returned. Defaults to `desc`." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A page of environment templates.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvironmentTemplateListResource", + "description": "A page of reusable environment templates." + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested environment definition was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List agent environment templates" + } + }, + "post": { + "operationId": "createAgentEnvironmentTemplate", + "summary": "Creates reusable environment configuration without returning confidential setup commands or environment values. See [reusing a hosted setup](https://developers.openai.com/api/docs/guides/agents-api/tools#reuse-a-hosted-plugin-setup).", + "description": "Creates reusable environment configuration without returning confidential setup commands or environment values.", + "tags": [ + "Agents" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEnvironmentTemplateParams" + } + } + } + }, + "responses": { + "201": { + "description": "The created environment template.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvironmentTemplateResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested environment definition was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create an agent environment template" + }, + "parameters": [ + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ] + } + }, + "/agents/environments/templates/{environment_template_id}": { + "get": { + "operationId": "retrieveAgentEnvironmentTemplate", + "summary": "Retrieves reusable environment configuration without returning confidential values. See [reusing a hosted setup](https://developers.openai.com/api/docs/guides/agents-api/tools#reuse-a-hosted-plugin-setup).", + "description": "Retrieves reusable environment configuration without returning confidential values.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "environment_template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the reusable environment template." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested environment template.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvironmentTemplateResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested environment definition was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve an agent environment template" + } + }, + "post": { + "operationId": "updateAgentEnvironmentTemplate", + "summary": "Updates reusable environment configuration without returning confidential values. See [reusing a hosted setup](https://developers.openai.com/api/docs/guides/agents-api/tools#reuse-a-hosted-plugin-setup).", + "description": "Updates reusable environment configuration without returning confidential values.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "environment_template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the reusable environment template." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEnvironmentTemplateParams" + } + } + } + }, + "responses": { + "200": { + "description": "The updated environment template.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvironmentTemplateResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested environment definition was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current environment state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update an agent environment template" + } + }, + "delete": { + "operationId": "deleteAgentEnvironmentTemplate", + "summary": "Deletes reusable environment configuration and all confidential template inputs. See [reusing a hosted setup](https://developers.openai.com/api/docs/guides/agents-api/tools#reuse-a-hosted-plugin-setup).", + "description": "Deletes reusable environment configuration and all confidential template inputs.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "environment_template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the reusable environment template." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The deleted environment template.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedEnvironmentTemplateResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested environment definition was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current environment state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete an agent environment template" + } + } + }, + "/agents/sessions": { + "get": { + "operationId": "listAgentSessions", + "summary": "Lists managed agent sessions using ID-based pagination and the requested sort order. See [managing sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions/manage).", + "description": "Lists managed agent sessions using ID-based pagination and the requested sort order.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 1 + }, + "description": "The maximum number of resources to return." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "Sort order by the `created_at` timestamp. Use `asc` for ascending order or `desc` for descending order. Defaults to `desc`." + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Only return sessions whose root agent has this ID. Omit to return sessions for all agents." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A page of sessions.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionListResource", + "description": "A paginated list of sessions." + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List agent sessions" + } + }, + "post": { + "operationId": "createAgentSession", + "summary": "Creates a managed agent session, optionally submits initial input, and returns the session or streams its events when stream is true. See [running sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions).", + "description": "Creates a managed agent session, optionally submits initial input, and returns the session or streams its events when stream is true.", + "tags": [ + "Agents" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAgentSessionParams" + } + } + } + }, + "responses": { + "201": { + "description": "The created session or its event stream.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResource" + } + }, + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/SessionEvent" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oai-streaming": { + "request_field": "stream", + "response_status_code": 201 + }, + "x-oaiMeta": { + "name": "Create an agent session" + }, + "parameters": [ + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ] + } + }, + "/agents/sessions/{session_id}": { + "get": { + "operationId": "retrieveAgentSession", + "summary": "Retrieves the current state of a managed agent session. See [managing sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions/manage).", + "description": "Retrieves the current state of a managed agent session.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve an agent session" + } + }, + "post": { + "operationId": "updateAgentSession", + "summary": "Updates session metadata. Omitted fields are unchanged. See [managing sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions/manage).", + "description": "Updates session metadata. Omitted fields are unchanged.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAgentSessionParams" + } + } + } + }, + "responses": { + "200": { + "description": "The updated session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update an agent session" + } + }, + "delete": { + "operationId": "deleteAgentSession", + "summary": "Removes a managed agent session from the public API and returns a deletion confirmation. Physical cleanup may continue asynchronously. See [managing sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions/manage).", + "description": "Removes a managed agent session from the public API and returns a deletion confirmation. Physical cleanup may continue asynchronously.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The deleted session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedSessionResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete an agent session" + } + } + }, + "/agents/sessions/{session_id}/artifacts": { + "get": { + "operationId": "listAgentSessionArtifacts", + "summary": "Lists immutable artifacts published by completed hosted session turns. See [session artifacts](https://developers.openai.com/api/docs/guides/agents-api/environments/files#openai-hosted-artifacts).", + "description": "Lists immutable artifacts published by completed hosted session turns.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam" + }, + "description": "Sort by creation time and ID. Defaults to descending." + }, + { + "name": "environment_id", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Restrict the listing to artifacts produced by this environment." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 1, + "maximum": 100 + }, + "description": "The maximum number of artifacts to return, between 1 and 100." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return artifacts after this immutable artifact ID." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A page of durable session artifacts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionArtifactListResource", + "description": "A page of durable artifacts published by a session." + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List agent session artifacts" + } + } + }, + "/agents/sessions/{session_id}/artifacts/{artifact_id}": { + "get": { + "operationId": "retrieveAgentSessionArtifact", + "summary": "Retrieves immutable metadata for one durable session artifact. See [session artifacts](https://developers.openai.com/api/docs/guides/agents-api/environments/files#openai-hosted-artifacts).", + "description": "Retrieves immutable metadata for one durable session artifact.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session that owns the artifact." + }, + { + "name": "artifact_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The immutable session artifact ID." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested session artifact.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionArtifactResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve an agent session artifact" + } + }, + "delete": { + "operationId": "deleteAgentSessionArtifact", + "summary": "Deletes an immutable session artifact without deleting its live environment file or original Files API object. See [session artifacts](https://developers.openai.com/api/docs/guides/agents-api/environments/files#openai-hosted-artifacts).", + "description": "Deletes an immutable session artifact without deleting its live environment file or original Files API object.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session that owns the artifact." + }, + { + "name": "artifact_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The immutable session artifact ID." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The deleted session artifact.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedSessionArtifactResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete an agent session artifact" + } + } + }, + "/agents/sessions/{session_id}/artifacts/{artifact_id}/content": { + "get": { + "operationId": "retrieveAgentSessionArtifactContent", + "summary": "Downloads immutable session artifact bytes after the execution environment expires. See [session artifacts](https://developers.openai.com/api/docs/guides/agents-api/environments/files#openai-hosted-artifacts).", + "description": "Downloads immutable session artifact bytes after the execution environment expires.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session that owns the artifact." + }, + { + "name": "artifact_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The immutable session artifact ID." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The immutable artifact contents.", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve agent session artifact content" + } + } + }, + "/agents/sessions/{session_id}/events": { + "get": { + "operationId": "listAgentSessionEvents", + "summary": "Streams live events for an agent session. See [session events](https://developers.openai.com/api/docs/guides/agents-api/sessions/events).", + "description": "Streams live events for an agent session.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A live stream of session events.", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/SessionEvent" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oai-streaming": { + "request_field": null, + "response_status_code": 200 + }, + "x-oaiMeta": { + "name": "Stream agent session events" + } + }, + "post": { + "operationId": "createAgentSessionEvents", + "summary": "Submits message, cancellation, or tool-result events to a managed agent session. See [session events](https://developers.openai.com/api/docs/guides/agents-api/sessions/events).", + "description": "Submits message, cancellation, or tool-result events to a managed agent session.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "description": "An optional client-generated key that makes retries of submitted messages idempotent." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSessionEventsParams" + } + } + } + }, + "responses": { + "202": { + "description": "The events were accepted." + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create agent session input events" + } + } + }, + "/agents/sessions/{session_id}/items": { + "get": { + "operationId": "listAgentSessionItems", + "summary": "Lists items produced by the session's root agent, including its interactions with subagents. Each subagent has its own item history. See [inspecting agent output](https://developers.openai.com/api/docs/guides/agents-api/observability).", + "description": "Lists items produced by the session's root agent, including its interactions with subagents. Each subagent has its own item history.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "The maximum number of resources to return, between 1 and 100. Defaults to 20." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "The order in which resources are returned. Defaults to `desc`." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A page of session items.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionItemListResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required Responses permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List agent session items" + } + } + }, + "/agents/sessions/{session_id}/turns": { + "get": { + "operationId": "listAgentSessionTurns", + "summary": "Lists turns by creation time and turn ID. The after cursor is exclusive in the selected order. See [session turns](https://developers.openai.com/api/docs/guides/agents-api/sessions/manage#inspect-session-turns).", + "description": "Lists turns by creation time and turn ID. The after cursor is exclusive in the selected order.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "The maximum number of resources to return, between 1 and 100. Defaults to 20." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "The order in which resources are returned. Defaults to `desc`." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A page of session turns.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionTurnListResource", + "description": "A page of turns from an agent session or subagent." + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List agent session turns" + } + } + }, + "/agents/sessions/{session_id}/turns/{turn_id}": { + "get": { + "operationId": "retrieveAgentSessionTurn", + "summary": "Retrieves a turn's current status, timestamps, usage, and error. Returns 404 if the turn does not belong to the session. See [session turns](https://developers.openai.com/api/docs/guides/agents-api/sessions/manage#inspect-session-turns).", + "description": "Retrieves a turn's current status, timestamps, usage, and error. Returns 404 if the turn does not belong to the session.", + "tags": [ + "Agents" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the session that owns the turn." + }, + { + "name": "turn_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the turn." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested turn.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TurnResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested session or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve an agent session turn" + } + } + }, + "/vaults": { + "get": { + "operationId": "listVaults", + "summary": "Lists vaults using ID-based pagination. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults).", + "description": "Lists vaults using ID-based pagination.", + "tags": [ + "Vaults" + ], + "parameters": [ + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "Sort order by the `created_at` timestamp. Use `asc` for ascending order or `desc` for descending order. Defaults to `desc`." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 0 + }, + "description": "The maximum number of resources to return. Defaults to 20. Values are clamped between 1 and 100." + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/VaultStatusFilterParam" + }, + "description": "Filter by one status or a list, such as `status=active` or `status[]=active&status[]=archived`. Both statuses are included by default." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A page of vaults.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaultListResource", + "description": "A page of vaults." + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested vault or credential was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current vault state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List vaults" + } + }, + "post": { + "operationId": "createVault", + "summary": "Creates a vault for the current project. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults).", + "description": "Creates a vault for the current project.", + "tags": [ + "Vaults" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateVaultParams" + } + } + } + }, + "responses": { + "201": { + "description": "The created vault.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaultResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested vault or credential was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current vault state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create a vault" + }, + "parameters": [ + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ] + } + }, + "/vaults/{vault_id}": { + "get": { + "operationId": "retrieveVault", + "summary": "Retrieves a vault by its ID. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults).", + "description": "Retrieves a vault by its ID.", + "tags": [ + "Vaults" + ], + "parameters": [ + { + "name": "vault_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested vault.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaultResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested vault or credential was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current vault state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve a vault" + } + }, + "delete": { + "operationId": "deleteVault", + "summary": "Deletes a vault and all its credentials. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults).", + "description": "Deletes a vault and all its credentials.", + "tags": [ + "Vaults" + ], + "parameters": [ + { + "name": "vault_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The deleted vault.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedVaultResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested vault or credential was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current vault state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete a vault" + } + } + }, + "/vaults/{vault_id}/credentials": { + "get": { + "operationId": "listVaultCredentials", + "summary": "Lists a vault's credentials using ID-based pagination without returning secret values. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults).", + "description": "Lists a vault's credentials using ID-based pagination without returning secret values.", + "tags": [ + "Vaults" + ], + "parameters": [ + { + "name": "vault_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ListOrderParam", + "default": "desc" + }, + "description": "Sort order by the `created_at` timestamp. Use `asc` for ascending order or `desc` for descending order. Defaults to `desc`." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 0 + }, + "description": "The maximum number of resources to return. Defaults to 20. Values are clamped between 1 and 100." + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/VaultStatusFilterParam" + }, + "description": "Filter by one status or a list, such as `status=active` or `status[]=active&status[]=archived`. Both statuses are included by default." + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "Return resources after this resource ID in the selected order." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "A page of vault credentials.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaultCredentialListResource", + "description": "A page of credentials in a vault." + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested vault or credential was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current vault state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List vault credentials" + } + }, + "post": { + "operationId": "createVaultCredential", + "summary": "Creates a vault credential. Secret values are write-only and are never returned. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults).", + "description": "Creates a vault credential. Secret values are write-only and are never returned.", + "tags": [ + "Vaults" + ], + "parameters": [ + { + "name": "vault_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateVaultCredentialParams" + } + } + } + }, + "responses": { + "201": { + "description": "The created vault credential without secret values.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaultCredentialResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested vault or credential was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current vault state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create a vault credential" + } + } + }, + "/vaults/{vault_id}/credentials/{credential_id}": { + "get": { + "operationId": "retrieveVaultCredential", + "summary": "Retrieves vault credential metadata without returning secret values. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults).", + "description": "Retrieves vault credential metadata without returning secret values.", + "tags": [ + "Vaults" + ], + "parameters": [ + { + "name": "vault_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault." + }, + { + "name": "credential_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault credential." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The requested vault credential without secret values.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaultCredentialResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested vault or credential was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current vault state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve a vault credential" + } + }, + "post": { + "operationId": "rotateVaultCredential", + "summary": "Rotates a vault credential's write-only secret and returns only credential metadata. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults).", + "description": "Rotates a vault credential's write-only secret and returns only credential metadata.", + "tags": [ + "Vaults" + ], + "parameters": [ + { + "name": "vault_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault." + }, + { + "name": "credential_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault credential." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RotateVaultCredentialParams" + } + } + } + }, + "responses": { + "200": { + "description": "The rotated vault credential without secret values.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaultCredentialResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested vault or credential was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current vault state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Rotate a vault credential" + } + }, + "delete": { + "operationId": "deleteVaultCredential", + "summary": "Deletes a vault credential. See [vaults](https://developers.openai.com/api/docs/guides/agents-api/tools/vaults).", + "description": "Deletes a vault credential.", + "tags": [ + "Vaults" + ], + "parameters": [ + { + "name": "vault_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault." + }, + { + "name": "credential_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "description": "The ID of the vault credential." + }, + { + "name": "OpenAI-Beta", + "in": "header", + "required": true, + "schema": { + "type": "string", + "const": "agents=v1" + } + } + ], + "responses": { + "200": { + "description": "The deleted vault credential.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedVaultCredentialResource" + } + } + } + }, + "400": { + "description": "The request was invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "401": { + "description": "Authentication or project context was missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "403": { + "description": "The API key lacks the required management permission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "404": { + "description": "The requested vault or credential was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "409": { + "description": "The request conflicted with the current vault state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "500": { + "description": "An internal error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + }, + "503": { + "description": "The service is temporarily unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse-2" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete a vault credential" + } + } + } + }, + "webhooks": { + "batch_cancelled": { + "post": { + "description": "Sent when a batch has been cancelled.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookBatchCancelled" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried.\n" + } + } + } + }, + "batch_completed": { + "post": { + "description": "Sent when a batch has completed processing.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookBatchCompleted" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried.\n" + } + } + } + }, + "batch_expired": { + "post": { + "description": "Sent when a batch has expired before completion.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookBatchExpired" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried.\n" + } + } + } + }, + "batch_failed": { + "post": { + "description": "Sent when a batch has failed.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookBatchFailed" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried.\n" + } + } + } + }, + "eval_run_canceled": { + "post": { + "description": "Sent when an eval run has been canceled.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEvalRunCanceled" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried. \n" + } + } + } + }, + "eval_run_failed": { + "post": { + "description": "Sent when an eval run has failed.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEvalRunFailed" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried. \n" + } + } + } + }, + "eval_run_succeeded": { + "post": { + "description": "Sent when an eval run has succeeded.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEvalRunSucceeded" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried. \n" + } + } + } + }, + "fine_tuning_job_cancelled": { + "post": { + "description": "Sent when a fine-tuning job has been cancelled.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookFineTuningJobCancelled" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried. \n" + } + } + } + }, + "fine_tuning_job_failed": { + "post": { + "description": "Sent when a fine-tuning job has failed.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookFineTuningJobFailed" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried. \n" + } + } + } + }, + "fine_tuning_job_succeeded": { + "post": { + "description": "Sent when a fine-tuning job has succeeded.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookFineTuningJobSucceeded" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried. \n" + } + } + } + }, + "live_call_incoming": { + "post": { + "deprecated": true, + "description": "Deprecated: use `live.transport.incoming`. Retained only for existing subscriptions.\nSent when an incoming API SIP session is available for Live acceptance.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookLiveCallIncoming" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200\nstatus codes will be retried.\n" + } + } + } + }, + "live_transport_incoming": { + "post": { + "description": "Sent when an incoming API SIP session is available for Live acceptance.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookLiveTransportIncoming" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200\nstatus codes will be retried.\n" + } + } + } + }, + "realtime_call_incoming": { + "post": { + "description": "Sent when an incoming API SIP session is available for Realtime acceptance.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookRealtimeCallIncoming" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200\nstatus codes will be retried.\n" + } + } + } + }, + "response_cancelled": { + "post": { + "description": "Sent when a background response has been cancelled.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookResponseCancelled" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried.\n" + } + } + } + }, + "response_completed": { + "post": { + "description": "Sent when a background response has completed successfully.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookResponseCompleted" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried. \n" + } + } + } + }, + "response_failed": { + "post": { + "description": "Sent when a background response has failed.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookResponseFailed" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried.\n" + } + } + } + }, + "response_incomplete": { + "post": { + "description": "Sent when a background response is incomplete.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookResponseIncomplete" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status code to acknowledge receipt of the event. Non-200 \nstatus codes will be retried.\n" + } + } + } + }, + "safety_alert_created": { + "post": { + "description": "Sent when an approved safety alert is available for an API project.\nRetrieve the alert with a project API key granted `api.safety.alerts.read`.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookSafetyAlertCreated" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status code to acknowledge receipt. A 410 Gone response also stops retries; other non-2xx responses are retried." + } + } + } + }, + "safety_org_alert_created": { + "post": { + "description": "Sent when an approved safety alert is available for an enterprise workspace.\nRetrieve the alert from `https://api.chatgpt.com/v1/safety/alerts/{id}` with\nan administrator API key for the workspace's backing organization granted\n`chatgpt.enterprise.safety_alerts.read`.\n", + "requestBody": { + "description": "The event payload sent by the API.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookSafetyOrgAlertCreated" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status code to acknowledge receipt. A 410 Gone response also stops retries; other non-2xx responses are retried." + } + } + } + } + }, + "components": { + "schemas": { + "CreateFileRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "file": { + "description": "The File object (not file name) to be uploaded.\n", + "type": "string", + "format": "binary" + }, + "purpose": { + "description": "The intended purpose of the uploaded file. One of:\n- `assistants`: Used in the Assistants API\n- `batch`: Used in the Batch API\n- `fine-tune`: Used for fine-tuning\n- `vision`: Images used for vision fine-tuning\n- `user_data`: Flexible file type for any purpose\n- `evals`: Used for eval data sets\n", + "type": "string", + "enum": [ + "assistants", + "batch", + "fine-tune", + "vision", + "user_data", + "evals" + ] + }, + "expires_after": { + "$ref": "#/components/schemas/FileExpirationAfter" + } + }, + "required": [ + "file", + "purpose" + ] + }, + "DeleteFileResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "enum": [ + "file" + ], + "x-stainless-const": true + }, + "deleted": { + "type": "boolean" + } + }, + "required": [ + "id", + "object", + "deleted" + ] + }, + "Error": { + "type": "object", + "properties": { + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + }, + "param": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string" + }, + "misalignment": { + "$ref": "#/components/schemas/MisalignmentErrorDetailsResource" + } + }, + "required": [ + "type", + "message", + "param", + "code" + ] + }, + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "$ref": "#/components/schemas/Error" + } + }, + "required": [ + "error" + ] + }, + "FileExpirationAfter": { + "type": "object", + "title": "File expiration policy", + "description": "The expiration policy for a file. By default, files with `purpose=batch` expire after 30 days and all other files are persisted until they are manually deleted.", + "properties": { + "anchor": { + "description": "Anchor timestamp after which the expiration policy applies. Supported anchors: `created_at`.", + "type": "string", + "enum": [ + "created_at" + ], + "x-stainless-const": true + }, + "seconds": { + "description": "The number of seconds after the anchor time that the file will expire. Must be between 3600 (1 hour) and 2592000 (30 days).", + "type": "integer", + "format": "int64", + "minimum": 3600, + "maximum": 2592000 + } + }, + "required": [ + "anchor", + "seconds" + ] + }, + "ListFilesResponse": { + "type": "object", + "properties": { + "object": { + "type": "string", + "example": "list" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OpenAIFile" + } + }, + "first_id": { + "type": "string", + "example": "file-abc123" + }, + "last_id": { + "type": "string", + "example": "file-abc456" + }, + "has_more": { + "type": "boolean", + "example": false + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ] + }, + "OpenAIFile": { + "title": "OpenAIFile", + "description": "The `File` object represents a document that has been uploaded to OpenAI.", + "properties": { + "id": { + "type": "string", + "description": "The file identifier, which can be referenced in the API endpoints." + }, + "bytes": { + "type": "integer", + "description": "The size of the file, in bytes." + }, + "created_at": { + "type": "integer", + "format": "unixtime", + "description": "The Unix timestamp (in seconds) for when the file was created." + }, + "expires_at": { + "type": "integer", + "format": "unixtime", + "description": "The Unix timestamp (in seconds) for when the file will expire." + }, + "filename": { + "type": "string", + "description": "The name of the file." + }, + "object": { + "type": "string", + "description": "The object type, which is always `file`.", + "enum": [ + "file" + ], + "x-stainless-const": true + }, + "purpose": { + "type": "string", + "description": "The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results`, `vision`, and `user_data`.", + "enum": [ + "assistants", + "assistants_output", + "batch", + "batch_output", + "fine-tune", + "fine-tune-results", + "vision", + "user_data" + ] + }, + "status": { + "type": "string", + "deprecated": true, + "description": "Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`.", + "enum": [ + "uploaded", + "processed", + "error" + ] + }, + "status_details": { + "type": "string", + "deprecated": true, + "description": "Deprecated. For details on why a fine-tuning training file failed validation, see the `error` field on `fine_tuning.job`." + } + }, + "required": [ + "id", + "object", + "bytes", + "created_at", + "filename", + "purpose", + "status" + ], + "x-oaiMeta": { + "name": "The file object", + "example": "{\n \"id\": \"file-abc123\",\n \"object\": \"file\",\n \"bytes\": 120000,\n \"created_at\": 1677610602,\n \"expires_at\": 1680202602,\n \"filename\": \"salesOverview.pdf\",\n \"purpose\": \"assistants\",\n}\n" + } + }, + "_MisalignmentErrorType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "enum": [ + "potentially_unintended_data_transfer", + "potentially_unintended_data_access", + "potentially_unintended_destructive_activity", + "other" + ] + } + ] + }, + "_MisalignmentSteer": { + "properties": { + "message": { + "type": "string", + "description": "The public continuation instruction." + } + }, + "type": "object", + "required": [ + "message" + ] + }, + "MisalignmentErrorDetailsResource": { + "properties": { + "error_type": { + "$ref": "#/components/schemas/_MisalignmentErrorType", + "description": "An optional classification; clients must accept additional values." + }, + "detailed_explanation": { + "type": "string", + "description": "The public explanation for this block." + }, + "steer": { + "$ref": "#/components/schemas/_MisalignmentSteer", + "description": "An optional public continuation instruction." + } + }, + "type": "object", + "required": [] + }, + "OrderEnum": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "SkillResource": { + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the skill." + }, + "object": { + "type": "string", + "enum": [ + "skill" + ], + "description": "The object type, which is `skill`.", + "default": "skill", + "x-stainless-const": true + }, + "name": { + "type": "string", + "description": "Name of the skill." + }, + "description": { + "type": "string", + "description": "Description of the skill." + }, + "created_at": { + "type": "integer", + "format": "unixtime", + "description": "Unix timestamp (seconds) for when the skill was created." + }, + "default_version": { + "type": "string", + "description": "Default version for the skill." + }, + "latest_version": { + "type": "string", + "description": "Latest version for the skill." + } + }, + "type": "object", + "required": [ + "id", + "object", + "name", + "description", + "created_at", + "default_version", + "latest_version" + ] + }, + "SkillListResource": { + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "description": "The type of object returned, must be `list`.", + "default": "list", + "x-stainless-const": true + }, + "data": { + "items": { + "$ref": "#/components/schemas/SkillResource" + }, + "type": "array", + "description": "A list of items" + }, + "first_id": { + "anyOf": [ + { + "type": "string", + "description": "The ID of the first item in the list." + }, + { + "type": "null" + } + ] + }, + "last_id": { + "anyOf": [ + { + "type": "string", + "description": "The ID of the last item in the list." + }, + { + "type": "null" + } + ] + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more items available." + } + }, + "type": "object", + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ] + }, + "CreateSkillBody": { + "properties": { + "files": { + "oneOf": [ + { + "items": { + "type": "string", + "format": "binary" + }, + "type": "array", + "maxItems": 500, + "description": "Skill files to upload (directory upload) or a single zip file." + }, + { + "type": "string", + "format": "binary", + "description": "Skill zip file to upload." + } + ] + } + }, + "type": "object", + "required": [ + "files" + ], + "title": "Create skill request", + "description": "Uploads a skill either as a directory (multipart `files[]`) or as a single zip file." + }, + "SetDefaultSkillVersionBody": { + "properties": { + "default_version": { + "type": "string", + "description": "The skill version number to set as default." + } + }, + "type": "object", + "required": [ + "default_version" + ], + "title": "Update skill request", + "description": "Updates the default version pointer for a skill." + }, + "DeletedSkillResource": { + "properties": { + "object": { + "type": "string", + "enum": [ + "skill.deleted" + ], + "default": "skill.deleted", + "x-stainless-const": true + }, + "deleted": { + "type": "boolean" + }, + "id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "object", + "deleted", + "id" + ] + }, + "SkillVersionResource": { + "properties": { + "object": { + "type": "string", + "enum": [ + "skill.version" + ], + "description": "The object type, which is `skill.version`.", + "default": "skill.version", + "x-stainless-const": true + }, + "id": { + "type": "string", + "description": "Unique identifier for the skill version." + }, + "skill_id": { + "type": "string", + "description": "Identifier of the skill for this version." + }, + "version": { + "type": "string", + "description": "Version number for this skill." + }, + "created_at": { + "type": "integer", + "format": "unixtime", + "description": "Unix timestamp (seconds) for when the version was created." + }, + "name": { + "type": "string", + "description": "Name of the skill version." + }, + "description": { + "type": "string", + "description": "Description of the skill version." + } + }, + "type": "object", + "required": [ + "object", + "id", + "skill_id", + "version", + "created_at", + "name", + "description" + ] + }, + "SkillVersionListResource": { + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "description": "The type of object returned, must be `list`.", + "default": "list", + "x-stainless-const": true + }, + "data": { + "items": { + "$ref": "#/components/schemas/SkillVersionResource" + }, + "type": "array", + "description": "A list of items" + }, + "first_id": { + "anyOf": [ + { + "type": "string", + "description": "The ID of the first item in the list." + }, + { + "type": "null" + } + ] + }, + "last_id": { + "anyOf": [ + { + "type": "string", + "description": "The ID of the last item in the list." + }, + { + "type": "null" + } + ] + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more items available." + } + }, + "type": "object", + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ] + }, + "CreateSkillVersionBody": { + "properties": { + "files": { + "oneOf": [ + { + "items": { + "type": "string", + "format": "binary" + }, + "type": "array", + "maxItems": 500, + "description": "Skill files to upload (directory upload) or a single zip file." + }, + { + "type": "string", + "format": "binary", + "description": "Skill zip file to upload." + } + ] + }, + "default": { + "type": "boolean", + "description": "Whether to set this version as the default." + } + }, + "type": "object", + "required": [ + "files" + ], + "title": "Create skill version request", + "description": "Uploads a new immutable version of a skill." + }, + "DeletedSkillVersionResource": { + "properties": { + "object": { + "type": "string", + "enum": [ + "skill.version.deleted" + ], + "default": "skill.version.deleted", + "x-stainless-const": true + }, + "deleted": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "version": { + "type": "string", + "description": "The deleted skill version." + } + }, + "type": "object", + "required": [ + "object", + "deleted", + "id", + "version" + ] + }, + "EnvironmentTypeResource": { + "type": "string", + "enum": [ + "openai_hosted", + "self_hosted" + ], + "description": "The kind of execution environment." + }, + "EnvironmentStatusResource": { + "type": "string", + "enum": [ + "pending", + "connected", + "disconnected", + "expired", + "failed" + ], + "description": "The public lifecycle status of an execution environment." + }, + "HostedEnvironmentFileResourceFileId": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "file_id" + ], + "default": "file_id", + "x-stainless-const": true, + "description": "The type of the object. Always `file_id`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The session-scoped ID of the file in the execution environment." + }, + "file_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the uploaded file." + }, + "path": { + "type": "string", + "minLength": 0, + "description": "The file's absolute path inside the environment." + }, + "size_bytes": { + "type": "integer", + "format": "int64", + "minimum": 0, + "description": "The decoded file size in bytes." + } + }, + "required": [ + "type", + "id", + "file_id", + "path", + "size_bytes" + ], + "additionalProperties": false, + "description": "A file copied from the OpenAI Files API." + }, + "HostedEnvironmentFileResourceInline": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ], + "default": "inline", + "x-stainless-const": true, + "description": "The type of the object. Always `inline`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The session-scoped ID of the file in the execution environment." + }, + "path": { + "type": "string", + "minLength": 0, + "description": "The file's absolute path inside the environment." + }, + "size_bytes": { + "type": "integer", + "format": "int64", + "minimum": 0, + "description": "The decoded file size in bytes." + } + }, + "required": [ + "type", + "id", + "path", + "size_bytes" + ], + "additionalProperties": false, + "description": "A file supplied inline when the session was created." + }, + "HostedEnvironmentFileResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/HostedEnvironmentFileResourceFileId" + }, + { + "$ref": "#/components/schemas/HostedEnvironmentFileResourceInline" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "file_id": "#/components/schemas/HostedEnvironmentFileResourceFileId", + "inline": "#/components/schemas/HostedEnvironmentFileResourceInline" + } + }, + "x-oai-discriminator-values": [ + "file_id", + "inline" + ], + "description": "Metadata for a file materialized in an OpenAI-hosted execution environment." + }, + "HostedSkillResourceSkillReference": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "skill_reference" + ], + "default": "skill_reference", + "x-stainless-const": true, + "description": "The type of the object. Always `skill_reference`." + }, + "skill_id": { + "type": "string", + "minLength": 0, + "description": "The referenced skill ID." + }, + "version": { + "type": "string", + "minLength": 0, + "description": "The concrete skill version installed for this session." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The installed skill name." + }, + "description": { + "type": "string", + "minLength": 0, + "description": "The installed skill description." + } + }, + "required": [ + "type", + "skill_id", + "version", + "name", + "description" + ], + "additionalProperties": false, + "description": "A skill installed from the Skills API." + }, + "HostedSkillResourceInline": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ], + "default": "inline", + "x-stainless-const": true, + "description": "The type of the object. Always `inline`." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The installed skill name." + }, + "description": { + "type": "string", + "minLength": 0, + "description": "The installed skill description." + } + }, + "required": [ + "type", + "name", + "description" + ], + "additionalProperties": false, + "description": "A skill installed from an inline ZIP archive." + }, + "HostedSkillResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/HostedSkillResourceSkillReference" + }, + { + "$ref": "#/components/schemas/HostedSkillResourceInline" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "skill_reference": "#/components/schemas/HostedSkillResourceSkillReference", + "inline": "#/components/schemas/HostedSkillResourceInline" + } + }, + "x-oai-discriminator-values": [ + "skill_reference", + "inline" + ], + "description": "A skill installed in an OpenAI-hosted environment." + }, + "HostedPluginResourceInline": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ], + "default": "inline", + "x-stainless-const": true, + "description": "The type of the object. Always `inline`." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The installed plugin name." + }, + "description": { + "type": "string", + "minLength": 0, + "description": "The installed plugin description." + } + }, + "required": [ + "type", + "name", + "description" + ], + "additionalProperties": false, + "description": "A plugin installed from an inline ZIP archive." + }, + "HostedPluginResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/HostedPluginResourceInline" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "inline": "#/components/schemas/HostedPluginResourceInline" + } + }, + "x-oai-discriminator-values": [ + "inline" + ], + "description": "A plugin installed in an OpenAI-hosted environment." + }, + "PublicEnvironmentResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the environment." + }, + "object": { + "type": "string", + "enum": [ + "agent.environment" + ], + "default": "agent.environment", + "x-stainless-const": true, + "description": "The object type. Always `agent.environment`." + }, + "type": { + "$ref": "#/components/schemas/EnvironmentTypeResource", + "description": "Whether the environment is hosted by OpenAI or by the application." + }, + "status": { + "$ref": "#/components/schemas/EnvironmentStatusResource", + "description": "The current environment connection status." + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostedEnvironmentFileResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Files installed in the environment, without their contents." + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostedSkillResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Skills installed in the environment, without their archive contents." + }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostedPluginResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Plugins installed in the environment, without their archive contents." + } + }, + "required": [ + "id", + "object", + "type", + "status", + "files", + "skills", + "plugins" + ], + "additionalProperties": false, + "description": "Safe metadata for a first-class execution environment." + }, + "ErrorBodyResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "minLength": 0, + "description": "The error type." + }, + "code": { + "type": "string", + "minLength": 0, + "description": "A machine-readable error code." + }, + "message": { + "type": "string", + "minLength": 0, + "description": "A human-readable error message." + }, + "param": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The request parameter that caused the error, or null for a request-wide error." + } + }, + "required": [ + "type", + "code", + "message", + "param" + ], + "additionalProperties": false, + "description": "Details about an API error." + }, + "ErrorResponse-2": { + "type": "object", + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorBodyResource", + "description": "The error returned by the API." + } + }, + "required": [ + "error" + ], + "additionalProperties": false, + "description": "An API error response." + }, + "ListOrderParam": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "x-enumDescriptions": [ + "Returns resources in ascending order.", + "Returns resources in descending order." + ], + "description": "The order in which paginated resources are returned." + }, + "EnvironmentFilePageObjectResource": { + "type": "string", + "enum": [ + "page" + ], + "default": "page", + "x-stainless-const": true, + "description": "The object type for a page of files in an execution environment." + }, + "EnvironmentFileResource": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "agent.environment.file" + ], + "default": "agent.environment.file", + "x-stainless-const": true, + "description": "The object type. Always `agent.environment.file`." + }, + "environment_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the environment containing this file." + }, + "path": { + "type": "string", + "minLength": 0, + "description": "The absolute file path inside the environment's workspace." + }, + "size_bytes": { + "type": "integer", + "format": "int64", + "minimum": 0, + "description": "The file size in bytes." + } + }, + "required": [ + "object", + "environment_id", + "path", + "size_bytes" + ], + "additionalProperties": false, + "description": "A live file in an execution environment." + }, + "EnvironmentFileListResource": { + "type": "object", + "properties": { + "object": { + "$ref": "#/components/schemas/EnvironmentFilePageObjectResource", + "description": "The object type. Always `page`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EnvironmentFileResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Files available on the current page." + }, + "next": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The opaque cursor to use when requesting the next page, if any." + }, + "has_more": { + "type": "boolean", + "description": "Whether more files follow this page." + } + }, + "required": [ + "object", + "data", + "next", + "has_more" + ], + "additionalProperties": false, + "description": "A paginated list of live execution environment files." + }, + "HostedEnvironmentFileParamFileId": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "file_id" + ], + "default": "file_id", + "x-stainless-const": true, + "description": "The type of the object. Always `file_id`." + }, + "file_id": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "The ID of the uploaded file." + }, + "path": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "The absolute destination path inside `/workspace`." + } + }, + "required": [ + "type", + "file_id", + "path" + ], + "additionalProperties": false, + "description": "A file previously uploaded through the OpenAI Files API." + }, + "HostedEnvironmentFileParamInline": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ], + "default": "inline", + "x-stainless-const": true, + "description": "The type of the object. Always `inline`." + }, + "data": { + "type": "string", + "minLength": 0, + "maxLength": 6990508, + "description": "The standard-base64-encoded file contents." + }, + "path": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "The absolute destination path inside `/workspace`." + } + }, + "required": [ + "type", + "data", + "path" + ], + "additionalProperties": false, + "description": "A file supplied directly as standard-base64 data." + }, + "HostedEnvironmentFileParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/HostedEnvironmentFileParamFileId" + }, + { + "$ref": "#/components/schemas/HostedEnvironmentFileParamInline" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "file_id": "#/components/schemas/HostedEnvironmentFileParamFileId", + "inline": "#/components/schemas/HostedEnvironmentFileParamInline" + } + }, + "x-oai-discriminator-values": [ + "file_id", + "inline" + ], + "description": "A file materialized in an OpenAI-hosted execution environment." + }, + "SubagentObjectResource": { + "type": "string", + "enum": [ + "agent.session.subagent" + ], + "default": "agent.session.subagent", + "x-stainless-const": true, + "description": "The object type for a subagent." + }, + "OutputTextResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "output_text" + ], + "default": "output_text", + "x-stainless-const": true, + "description": "The content type. Always `output_text`." + }, + "text": { + "type": "string", + "minLength": 0, + "description": "The text produced by the agent." + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false, + "description": "A text content part produced by the agent." + }, + "EncryptedContentResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "encrypted_content" + ], + "default": "encrypted_content", + "x-stainless-const": true, + "description": "The content type. Always `encrypted_content`." + }, + "encrypted_content": { + "type": "string", + "minLength": 0, + "description": "The encrypted content payload." + } + }, + "required": [ + "type", + "encrypted_content" + ], + "additionalProperties": false, + "description": "Encrypted content exchanged between agents." + }, + "AgentContentResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/OutputTextResource" + }, + { + "$ref": "#/components/schemas/EncryptedContentResource" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "output_text": "#/components/schemas/OutputTextResource", + "encrypted_content": "#/components/schemas/EncryptedContentResource" + } + }, + "x-oai-discriminator-values": [ + "output_text", + "encrypted_content" + ], + "description": "A plaintext or encrypted content part exchanged between agents." + }, + "SubagentStatusResource": { + "type": "string", + "enum": [ + "active", + "closed" + ], + "x-enumDescriptions": [ + "The subagent remains available, including while idle between turns.", + "The subagent is closed." + ], + "description": "The current status of a subagent." + }, + "SubagentResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the subagent." + }, + "object": { + "$ref": "#/components/schemas/SubagentObjectResource", + "description": "The object type. Always `agent.session.subagent`." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session that owns the subagent." + }, + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The runner-assigned nickname, or null when unavailable." + }, + "instructions": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/AgentContentResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Initial task content, or null when unavailable. Text may contain placeholders for images or audio when only a preview is available." + }, + "parent_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent that created this subagent." + }, + "status": { + "$ref": "#/components/schemas/SubagentStatusResource", + "description": "The current status of the subagent." + }, + "opened_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the subagent was first opened. Resuming does not change it." + }, + "closed_at": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "The Unix timestamp, in seconds, when the subagent was closed. Null while active, including after resume." + } + }, + "required": [ + "id", + "object", + "session_id", + "name", + "instructions", + "parent_agent_id", + "status", + "opened_at", + "closed_at" + ], + "additionalProperties": false, + "description": "A subagent created within a session." + }, + "SessionMessageRoleResource": { + "type": "string", + "enum": [ + "user", + "assistant" + ], + "description": "The author of a session message." + }, + "MessageContentResourceInputText": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "input_text" + ], + "default": "input_text", + "x-stainless-const": true, + "description": "The type of the object. Always `input_text`." + }, + "text": { + "type": "string", + "minLength": 0, + "description": "The text supplied by the user." + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false, + "description": "Text supplied by the user." + }, + "MessageContentResourceInputImage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "input_image" + ], + "default": "input_image", + "x-stainless-const": true, + "description": "The type of the object. Always `input_image`." + }, + "image_url": { + "type": "string", + "minLength": 0, + "description": "The URL of the image supplied by the user, which may be a base64-encoded data URL." + } + }, + "required": [ + "type", + "image_url" + ], + "additionalProperties": false, + "description": "An image supplied by the user." + }, + "MessageContentResourceOutputText": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "output_text" + ], + "default": "output_text", + "x-stainless-const": true, + "description": "The type of the object. Always `output_text`." + }, + "text": { + "type": "string", + "minLength": 0, + "description": "The text produced by the assistant." + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false, + "description": "Text produced by the assistant." + }, + "MessageContentResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/MessageContentResourceInputText" + }, + { + "$ref": "#/components/schemas/MessageContentResourceInputImage" + }, + { + "$ref": "#/components/schemas/MessageContentResourceOutputText" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "input_text": "#/components/schemas/MessageContentResourceInputText", + "input_image": "#/components/schemas/MessageContentResourceInputImage", + "output_text": "#/components/schemas/MessageContentResourceOutputText" + } + }, + "x-oai-discriminator-values": [ + "input_text", + "input_image", + "output_text" + ], + "description": "A content part in a session message." + }, + "OutputItemStatusResource": { + "type": "string", + "enum": [ + "in_progress", + "completed", + "incomplete" + ], + "x-enumDescriptions": [ + "The item is in progress.", + "The item is complete.", + "The item stopped before completing." + ], + "description": "The status of an agent output item." + }, + "MessagePhaseResource": { + "type": "string", + "enum": [ + "commentary", + "final_answer" + ], + "x-enumDescriptions": [ + "Commentary produced while the agent works.", + "The agent's final answer." + ], + "description": "The phase of an assistant message." + }, + "MessageItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "message" + ], + "default": "message", + "x-stainless-const": true, + "description": "The item type. Always `message`." + }, + "id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of this item, or null for legacy user messages whose ID was not recorded." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "role": { + "$ref": "#/components/schemas/SessionMessageRoleResource", + "description": "The role of the message author." + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageContentResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The content of the message. User messages contain input text or images; assistant messages contain output text." + }, + "status": { + "$ref": "#/components/schemas/OutputItemStatusResource", + "description": "The status of the message. User messages are always `completed`." + }, + "phase": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessagePhaseResource" + }, + { + "type": "null" + } + ], + "description": "The phase of an assistant message. Null for user messages." + } + }, + "required": [ + "type", + "id", + "turn_id", + "role", + "content", + "status", + "phase" + ], + "additionalProperties": false, + "description": "A user or assistant message recorded in a session." + }, + "SummaryTextResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "summary_text" + ], + "default": "summary_text", + "x-stainless-const": true, + "description": "The content type. Always `summary_text`." + }, + "text": { + "type": "string", + "minLength": 0, + "description": "The reasoning summary text." + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false, + "description": "A reasoning summary content part." + }, + "ReasoningItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "reasoning" + ], + "default": "reasoning", + "x-stainless-const": true, + "description": "The item type. Always `reasoning`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the reasoning item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "summary": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SummaryTextResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The reasoning summaries produced by the agent." + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/OutputItemStatusResource" + }, + { + "type": "null" + } + ], + "description": "The status of the reasoning item." + } + }, + "required": [ + "type", + "id", + "turn_id", + "summary", + "status" + ], + "additionalProperties": false, + "description": "A reasoning item produced by the agent." + }, + "FunctionCallStatusResource": { + "type": "string", + "enum": [ + "in_progress", + "completed", + "failed", + "incomplete" + ], + "x-enumDescriptions": [ + "The call is in progress.", + "The call completed successfully.", + "The call failed.", + "The call stopped before completing." + ], + "description": "The status of a tool call." + }, + "FunctionCallItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "function_call" + ], + "default": "function_call", + "x-stainless-const": true, + "description": "The item type. Always `function_call`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the function call item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "call_id": { + "type": "string", + "minLength": 0, + "description": "The ID used to submit the function result." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The name of the function to call." + }, + "arguments": { + "description": "The arguments to pass to the function." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the function call." + } + }, + "required": [ + "type", + "id", + "turn_id", + "call_id", + "name", + "arguments", + "status" + ], + "additionalProperties": false, + "description": "A function call produced by the agent." + }, + "InputContentResourceInputText": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "input_text" + ], + "default": "input_text", + "x-stainless-const": true, + "description": "The type of the object. Always `input_text`." + }, + "text": { + "type": "string", + "minLength": 0, + "description": "The text supplied to the agent." + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false, + "description": "Text input recorded in a session item." + }, + "InputContentResourceInputImage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "input_image" + ], + "default": "input_image", + "x-stainless-const": true, + "description": "The type of the object. Always `input_image`." + }, + "image_url": { + "type": "string", + "minLength": 0, + "description": "The URL of the image supplied to the agent, which may be a base64-encoded data URL." + } + }, + "required": [ + "type", + "image_url" + ], + "additionalProperties": false, + "description": "Image input recorded in a session item." + }, + "InputContentResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/InputContentResourceInputText" + }, + { + "$ref": "#/components/schemas/InputContentResourceInputImage" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "input_text": "#/components/schemas/InputContentResourceInputText", + "input_image": "#/components/schemas/InputContentResourceInputImage" + } + }, + "x-oai-discriminator-values": [ + "input_text", + "input_image" + ], + "description": "User-provided content recorded in a session item." + }, + "FunctionCallOutputResource": { + "oneOf": [ + { + "type": "string", + "minLength": 0 + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/InputContentResource" + }, + "minItems": 0, + "maxItems": 2000 + } + ], + "description": "The text or model-input content supplied as a function result." + }, + "FunctionCallOutputItemResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the function call output item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "type": { + "type": "string", + "enum": [ + "function_call_output" + ], + "default": "function_call_output", + "x-stainless-const": true, + "description": "The item type. Always `function_call_output`." + }, + "call_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the function call that produced this output." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the function call." + }, + "output": { + "anyOf": [ + { + "$ref": "#/components/schemas/FunctionCallOutputResource" + }, + { + "type": "null" + } + ], + "description": "The function result, if the call succeeded." + }, + "error": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The error message, if the call failed." + } + }, + "required": [ + "id", + "turn_id", + "type", + "call_id", + "status", + "output", + "error" + ], + "additionalProperties": false, + "description": "The result supplied for a function call." + }, + "AgentMessageItemResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the message." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "type": { + "type": "string", + "enum": [ + "agent_message" + ], + "default": "agent_message", + "x-stainless-const": true, + "description": "The item type. Always `agent_message`." + }, + "sender_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID or name of the sending agent." + }, + "recipient_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID or name of the receiving agent." + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentContentResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The content exchanged between the agents." + } + }, + "required": [ + "id", + "turn_id", + "type", + "sender_agent_id", + "recipient_agent_id", + "content" + ], + "additionalProperties": false, + "description": "A message exchanged between agent threads." + }, + "McpCallItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_call" + ], + "default": "mcp_call", + "x-stainless-const": true, + "description": "The item type. Always `mcp_call`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the MCP call item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "server_label": { + "type": "string", + "minLength": 0, + "description": "The label of the MCP server." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The name of the MCP tool." + }, + "arguments": { + "description": "The arguments passed to the MCP tool." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the MCP tool call." + }, + "output": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "description": "The output returned by the MCP tool, if any." + }, + "error": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "description": "The error returned by the MCP tool, if any." + } + }, + "required": [ + "type", + "id", + "turn_id", + "server_label", + "name", + "arguments", + "status", + "output", + "error" + ], + "additionalProperties": false, + "description": "A call to a tool on an MCP server." + }, + "WebSearchActionResourceSearch": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "search" + ], + "default": "search", + "x-stainless-const": true, + "description": "The type of the object. Always `search`." + }, + "query": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The search query, when a single query was used." + }, + "queries": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "The search queries, when multiple queries were used." + } + }, + "required": [ + "type", + "query", + "queries" + ], + "additionalProperties": false, + "description": "A search query or group of search queries." + }, + "WebSearchActionResourceOpenPage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "open_page" + ], + "default": "open_page", + "x-stainless-const": true, + "description": "The type of the object. Always `open_page`." + }, + "url": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The URL of the page that was opened." + } + }, + "required": [ + "type", + "url" + ], + "additionalProperties": false, + "description": "Opens a web page." + }, + "WebSearchActionResourceFindInPage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "find_in_page" + ], + "default": "find_in_page", + "x-stainless-const": true, + "description": "The type of the object. Always `find_in_page`." + }, + "url": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The URL of the page that was searched." + }, + "pattern": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The text pattern that was searched for." + } + }, + "required": [ + "type", + "url", + "pattern" + ], + "additionalProperties": false, + "description": "Finds text within a web page." + }, + "WebSearchActionResourceOther": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "other" + ], + "default": "other", + "x-stainless-const": true, + "description": "The type of the object. Always `other`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Another web search action." + }, + "WebSearchActionResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/WebSearchActionResourceSearch" + }, + { + "$ref": "#/components/schemas/WebSearchActionResourceOpenPage" + }, + { + "$ref": "#/components/schemas/WebSearchActionResourceFindInPage" + }, + { + "$ref": "#/components/schemas/WebSearchActionResourceOther" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "search": "#/components/schemas/WebSearchActionResourceSearch", + "open_page": "#/components/schemas/WebSearchActionResourceOpenPage", + "find_in_page": "#/components/schemas/WebSearchActionResourceFindInPage", + "other": "#/components/schemas/WebSearchActionResourceOther" + } + }, + "x-oai-discriminator-values": [ + "search", + "open_page", + "find_in_page", + "other" + ], + "description": "An action performed by the web search tool." + }, + "WebSearchCallItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "web_search_call" + ], + "default": "web_search_call", + "x-stainless-const": true, + "description": "The item type. Always `web_search_call`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the web search call." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "status": { + "$ref": "#/components/schemas/OutputItemStatusResource", + "description": "The status of the web search call." + }, + "action": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebSearchActionResource" + }, + { + "type": "null" + } + ], + "description": "The action performed by the web search tool." + } + }, + "required": [ + "type", + "id", + "turn_id", + "status", + "action" + ], + "additionalProperties": false, + "description": "A web search call produced by the agent." + }, + "CommandExecutionItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "command_execution" + ], + "default": "command_execution", + "x-stainless-const": true, + "description": "The item type. Always `command_execution`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the command execution item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "command": { + "type": "string", + "minLength": 0, + "description": "The command that was executed." + }, + "cwd": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The working directory used to execute the command." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the command execution." + }, + "output": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The command output, if available." + }, + "exit_code": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "The process exit code, if the command completed." + }, + "duration_ms": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "The command duration in milliseconds." + } + }, + "required": [ + "type", + "id", + "turn_id", + "command", + "cwd", + "status", + "output", + "exit_code", + "duration_ms" + ], + "additionalProperties": false, + "description": "A command execution produced by the agent." + }, + "InterruptSubagentCallItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "interrupt_subagent_call" + ], + "default": "interrupt_subagent_call", + "x-stainless-const": true, + "x-enumDescriptions": [ + "The current public item type." + ], + "description": "The item type. Always `interrupt_subagent_call`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the tool call item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the tool call." + }, + "sender_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent requesting the interrupt." + }, + "recipient_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent to interrupt." + } + }, + "required": [ + "type", + "id", + "turn_id", + "status", + "sender_agent_id", + "recipient_agent_id" + ], + "additionalProperties": false, + "description": "A request to interrupt a subagent's current turn. The subagent remains available." + }, + "CreateSubagentCallItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "create_subagent_call" + ], + "default": "create_subagent_call", + "x-stainless-const": true, + "x-enumDescriptions": [ + "The current public item type." + ], + "description": "The item type. Always `create_subagent_call`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the tool call item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the tool call." + }, + "agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent that requested the subagent." + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentContentResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The task given to the spawned agent." + }, + "model": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The model requested for the spawned agent." + }, + "reasoning_effort": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The reasoning effort requested for the spawned agent." + } + }, + "required": [ + "type", + "id", + "turn_id", + "status", + "agent_id", + "content", + "model", + "reasoning_effort" + ], + "additionalProperties": false, + "description": "A request to spawn a subagent." + }, + "SendSubagentInputCallItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "send_subagent_input_call" + ], + "default": "send_subagent_input_call", + "x-stainless-const": true, + "x-enumDescriptions": [ + "The current public item type." + ], + "description": "The item type. Always `send_subagent_input_call`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the tool call item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the tool call." + }, + "sender_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent sending the input." + }, + "recipient_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent receiving the input." + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentContentResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The input sent to the receiving agent." + } + }, + "required": [ + "type", + "id", + "turn_id", + "status", + "sender_agent_id", + "recipient_agent_id", + "content" + ], + "additionalProperties": false, + "description": "A request to send input to another agent." + }, + "ResumeSubagentCallItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "resume_subagent_call" + ], + "default": "resume_subagent_call", + "x-stainless-const": true, + "x-enumDescriptions": [ + "The current public item type." + ], + "description": "The item type. Always `resume_subagent_call`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the tool call item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the tool call." + }, + "sender_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent requesting the resume." + }, + "recipient_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent to resume." + } + }, + "required": [ + "type", + "id", + "turn_id", + "status", + "sender_agent_id", + "recipient_agent_id" + ], + "additionalProperties": false, + "description": "A request to resume a subagent." + }, + "WaitForSubagentsCallItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "wait_for_subagents_call" + ], + "default": "wait_for_subagents_call", + "x-stainless-const": true, + "x-enumDescriptions": [ + "The current public item type." + ], + "description": "The item type. Always `wait_for_subagents_call`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the tool call item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the tool call." + }, + "sender_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent waiting for results." + }, + "recipient_agent_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "The IDs of the agents to wait for." + } + }, + "required": [ + "type", + "id", + "turn_id", + "status", + "sender_agent_id", + "recipient_agent_ids" + ], + "additionalProperties": false, + "description": "A request to wait for one or more subagents." + }, + "CloseSubagentCallItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "close_subagent_call" + ], + "default": "close_subagent_call", + "x-stainless-const": true, + "x-enumDescriptions": [ + "The current public item type." + ], + "description": "The item type. Always `close_subagent_call`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the tool call item." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "status": { + "$ref": "#/components/schemas/FunctionCallStatusResource", + "description": "The status of the tool call." + }, + "sender_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent requesting the close." + }, + "recipient_agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent to close." + } + }, + "required": [ + "type", + "id", + "turn_id", + "status", + "sender_agent_id", + "recipient_agent_id" + ], + "additionalProperties": false, + "description": "A request to close a subagent." + }, + "SessionTurnItemResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/MessageItemResource" + }, + { + "$ref": "#/components/schemas/ReasoningItemResource" + }, + { + "$ref": "#/components/schemas/FunctionCallItemResource" + }, + { + "$ref": "#/components/schemas/FunctionCallOutputItemResource" + }, + { + "$ref": "#/components/schemas/AgentMessageItemResource" + }, + { + "$ref": "#/components/schemas/McpCallItemResource" + }, + { + "$ref": "#/components/schemas/WebSearchCallItemResource" + }, + { + "$ref": "#/components/schemas/CommandExecutionItemResource" + }, + { + "$ref": "#/components/schemas/CreateSubagentCallItemResource" + }, + { + "$ref": "#/components/schemas/SendSubagentInputCallItemResource" + }, + { + "$ref": "#/components/schemas/ResumeSubagentCallItemResource" + }, + { + "$ref": "#/components/schemas/WaitForSubagentsCallItemResource" + }, + { + "$ref": "#/components/schemas/InterruptSubagentCallItemResource" + }, + { + "$ref": "#/components/schemas/CloseSubagentCallItemResource" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "message": "#/components/schemas/MessageItemResource", + "reasoning": "#/components/schemas/ReasoningItemResource", + "function_call": "#/components/schemas/FunctionCallItemResource", + "function_call_output": "#/components/schemas/FunctionCallOutputItemResource", + "agent_message": "#/components/schemas/AgentMessageItemResource", + "mcp_call": "#/components/schemas/McpCallItemResource", + "web_search_call": "#/components/schemas/WebSearchCallItemResource", + "command_execution": "#/components/schemas/CommandExecutionItemResource", + "interrupt_subagent_call": "#/components/schemas/InterruptSubagentCallItemResource", + "create_subagent_call": "#/components/schemas/CreateSubagentCallItemResource", + "send_subagent_input_call": "#/components/schemas/SendSubagentInputCallItemResource", + "resume_subagent_call": "#/components/schemas/ResumeSubagentCallItemResource", + "wait_for_subagents_call": "#/components/schemas/WaitForSubagentsCallItemResource", + "close_subagent_call": "#/components/schemas/CloseSubagentCallItemResource" + } + }, + "x-oai-discriminator-values": [ + "message", + "reasoning", + "function_call", + "function_call_output", + "agent_message", + "mcp_call", + "web_search_call", + "command_execution", + "create_subagent_call", + "send_subagent_input_call", + "resume_subagent_call", + "wait_for_subagents_call", + "interrupt_subagent_call", + "close_subagent_call" + ], + "description": "An item associated with a session turn." + }, + "SessionItemListResource": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "default": "list", + "x-stainless-const": true, + "description": "The object type, which is always `list`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionTurnItemResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The resources returned in this page, in the requested sort order." + }, + "first_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the first resource in `data`, or `null` if the page is empty." + }, + "last_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the last resource in `data`, or `null` if the page is empty. Pass this as `after` with the same order and filters." + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more resources to retrieve after this page." + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "additionalProperties": false, + "description": "A page of messages, reasoning, and tool calls from a session's item history." + }, + "TurnObjectResource": { + "type": "string", + "enum": [ + "agent.session.turn" + ], + "default": "agent.session.turn", + "x-stainless-const": true, + "description": "The object type for a turn." + }, + "TurnStatusResource": { + "type": "string", + "enum": [ + "queued", + "in_progress", + "waiting", + "completed", + "failed", + "cancelled" + ], + "x-enumDescriptions": [ + "The turn is waiting to start.", + "The turn is in progress.", + "The turn is waiting for external input.", + "The turn completed successfully.", + "The turn failed.", + "The turn was cancelled." + ], + "description": "The current status of a turn." + }, + "SessionTurnErrorCodeResource": { + "type": "string", + "enum": [ + "context_length_exceeded", + "session_budget_exceeded", + "usage_limit_exceeded", + "rate_limit_exceeded", + "server_overloaded", + "cyber_policy", + "connection_failed", + "server_error", + "authentication_error", + "invalid_request", + "resource_not_found", + "sandbox_error", + "executor_version_incompatible", + "active_turn_not_steerable", + "request_timeout", + "internal_error" + ], + "x-enumDescriptions": [ + "The request exceeds the model's context window.", + "The session has reached its usage budget.", + "The organization has reached a usage, plan, or billing limit.", + "The request exceeds the available rate limit.", + "The model service is temporarily overloaded.", + "The request was rejected by a safety policy.", + "The request could not connect to the model service.", + "The model service encountered an unexpected error.", + "The API credentials are invalid or lack the required access.", + "The request contains invalid input or configuration.", + "The requested model or resource is unavailable.", + "The request could not complete in its execution environment.", + "The executor must be upgraded before it can run this turn.", + "The session cannot accept additional input while a request is running.", + "The request timed out before the model service responded.", + "An unexpected internal error prevented the session request from completing." + ], + "description": "Stable public categories for session request failures." + }, + "SessionTurnErrorResource": { + "type": "object", + "properties": { + "code": { + "$ref": "#/components/schemas/SessionTurnErrorCodeResource", + "description": "A stable, machine-readable failure category." + }, + "message": { + "type": "string", + "minLength": 0, + "description": "A customer-safe explanation of the failure." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": false, + "description": "A customer-safe error describing why a session request failed." + }, + "InputTokensDetailsResource": { + "type": "object", + "properties": { + "cached_tokens": { + "type": "integer", + "format": "int64", + "description": "The number of input tokens retrieved from the prompt cache." + } + }, + "required": [ + "cached_tokens" + ], + "additionalProperties": false, + "description": "A breakdown of input token usage for a session or turn." + }, + "OutputTokensDetailsResource": { + "type": "object", + "properties": { + "reasoning_tokens": { + "type": "integer", + "format": "int64", + "description": "The number of output tokens used for reasoning." + } + }, + "required": [ + "reasoning_tokens" + ], + "additionalProperties": false, + "description": "A breakdown of output token usage for a session or turn." + }, + "TokenUsageResource": { + "type": "object", + "properties": { + "input_tokens": { + "type": "integer", + "format": "int64", + "description": "The number of input tokens used by the agent." + }, + "input_tokens_details": { + "$ref": "#/components/schemas/InputTokensDetailsResource", + "description": "A breakdown of the agent's input token usage." + }, + "output_tokens": { + "type": "integer", + "format": "int64", + "description": "The number of output tokens generated by the agent." + }, + "output_tokens_details": { + "$ref": "#/components/schemas/OutputTokensDetailsResource", + "description": "A breakdown of the agent's output token usage." + }, + "total_tokens": { + "type": "integer", + "format": "int64", + "description": "The total number of input and output tokens used by the agent." + } + }, + "required": [ + "input_tokens", + "input_tokens_details", + "output_tokens", + "output_tokens_details", + "total_tokens" + ], + "additionalProperties": false, + "description": "Recorded token usage for a session or turn. Usage is best effort and may change." + }, + "TurnResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn." + }, + "object": { + "$ref": "#/components/schemas/TurnObjectResource", + "description": "The object type. Always `agent.session.turn`." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session that owns the turn." + }, + "agent_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent that ran the turn." + }, + "subagent_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the subagent that ran the turn, if applicable." + }, + "status": { + "$ref": "#/components/schemas/TurnStatusResource", + "description": "The current status of the turn." + }, + "created_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, used to order the turn by creation time. Subagent turns use their start time, falling back to completion time or the subagent opening time when the preceding timestamps are unavailable." + }, + "started_at": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "The Unix timestamp, in seconds, when the turn started." + }, + "completed_at": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "The Unix timestamp, in seconds, when the turn reached a terminal state." + }, + "error": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionTurnErrorResource" + }, + { + "type": "null" + } + ], + "description": "A customer-safe error. Non-null only for a failed turn." + }, + "usage": { + "anyOf": [ + { + "$ref": "#/components/schemas/TokenUsageResource" + }, + { + "type": "null" + } + ], + "description": "Best-effort token usage for the turn, or null if unknown. Recorded usage may change." + } + }, + "required": [ + "id", + "object", + "session_id", + "agent_id", + "subagent_id", + "status", + "created_at", + "started_at", + "completed_at", + "error", + "usage" + ], + "additionalProperties": false, + "description": "The canonical public representation of a session turn." + }, + "SessionTurnListResource": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "default": "list", + "x-stainless-const": true, + "description": "The object type, which is always `list`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TurnResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The resources returned in this page, in the requested sort order." + }, + "first_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the first resource in `data`, or `null` if the page is empty." + }, + "last_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the last resource in `data`, or `null` if the page is empty. Pass this as `after` with the same order and filters." + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more resources to retrieve after this page." + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "additionalProperties": false, + "description": "A page of Agents API resources, with IDs for retrieving additional pages." + }, + "ReasoningEffortResource": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "description": "The amount of reasoning effort used by an agent." + }, + "ReasoningSummaryResource": { + "type": "string", + "enum": [ + "concise", + "detailed", + "auto" + ], + "x-enumDescriptions": [ + "Returns a concise reasoning summary when supported.", + "Returns a detailed reasoning summary when supported.", + "Automatically selects the most detailed summary supported by the model." + ], + "description": "The reasoning summary format requested from an agent." + }, + "ReasoningResource": { + "type": "object", + "properties": { + "effort": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReasoningEffortResource" + }, + { + "type": "null" + } + ], + "description": "The requested reasoning effort, or `null` when the model selects its own default." + }, + "summary": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReasoningSummaryResource" + }, + { + "type": "null" + } + ], + "description": "The requested reasoning summary format, or `null` when summaries are disabled." + } + }, + "required": [ + "effort", + "summary" + ], + "additionalProperties": false, + "description": "The reasoning configuration used by an agent." + }, + "TextFormatResourceText": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ], + "default": "text", + "x-stainless-const": true, + "description": "The type of the object. Always `text`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Generates ordinary text without a structured-output constraint." + }, + "TextFormatResourceJsonSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "json_schema" + ], + "default": "json_schema", + "x-stainless-const": true, + "description": "The type of the object. Always `json_schema`." + }, + "schema": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 0 + }, + "minProperties": 0, + "description": "The JSON Schema that generated text must match." + } + }, + "required": [ + "type", + "schema" + ], + "additionalProperties": false, + "description": "Constrains generated text to a JSON Schema." + }, + "TextFormatResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextFormatResourceText" + }, + { + "$ref": "#/components/schemas/TextFormatResourceJsonSchema" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "text": "#/components/schemas/TextFormatResourceText", + "json_schema": "#/components/schemas/TextFormatResourceJsonSchema" + } + }, + "x-oai-discriminator-values": [ + "text", + "json_schema" + ], + "description": "The effective output format for generated text." + }, + "VerbosityResource": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "description": "The amount of text produced by an agent." + }, + "TextResource": { + "type": "object", + "properties": { + "format": { + "$ref": "#/components/schemas/TextFormatResource", + "description": "The effective output format. Defaults to ordinary text." + }, + "verbosity": { + "$ref": "#/components/schemas/VerbosityResource", + "description": "The amount of text produced by the agent. Defaults to `medium`." + } + }, + "required": [ + "format", + "verbosity" + ], + "additionalProperties": false, + "description": "The text configuration used by an agent." + }, + "ServiceTierResource": { + "type": "string", + "enum": [ + "auto", + "default", + "flex", + "priority", + "fast" + ], + "description": "The service-tier policy configured for an agent." + }, + "PersistedAgentToolResourceFunction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "function" + ], + "default": "function", + "x-stainless-const": true, + "description": "The type of the object. Always `function`." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The name of the function." + }, + "description": { + "type": "string", + "minLength": 0, + "description": "A description of what the function does." + }, + "parameters": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 0 + }, + "minProperties": 0, + "description": "A JSON Schema object describing the function's arguments." + }, + "defer_loading": { + "type": "boolean", + "description": "Whether the function is deferred and discovered through tool search." + } + }, + "required": [ + "type", + "name", + "description", + "parameters", + "defer_loading" + ], + "additionalProperties": false, + "description": "A function defined by the application." + }, + "PersistedAgentToolResourceToolSearch": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tool_search" + ], + "default": "tool_search", + "x-stainless-const": true, + "description": "The type of the object. Always `tool_search`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Discovers deferred function tools and loads them into the model context." + }, + "PersistedAgentToolResourceProgrammaticToolCalling": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "programmatic_tool_calling" + ], + "default": "programmatic_tool_calling", + "x-stainless-const": true, + "description": "The type of the object. Always `programmatic_tool_calling`." + }, + "enabled": { + "type": "boolean", + "description": "Whether tools can be called from model-generated code." + } + }, + "required": [ + "type", + "enabled" + ], + "additionalProperties": false, + "description": "Enables calling tools from model-generated code." + }, + "PersistedMcpTransportResourceHttp": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "http" + ], + "default": "http", + "x-stainless-const": true, + "description": "The type of the object. Always `http`." + }, + "server_url": { + "type": "string", + "minLength": 0, + "description": "The URL of the MCP server." + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 0 + }, + "propertyNames": { + "type": "string", + "minLength": 0 + }, + "minProperties": 0, + "description": "Non-secret HTTP headers sent to the MCP server." + } + }, + "required": [ + "type", + "server_url", + "headers" + ], + "additionalProperties": false, + "description": "Connects to an MCP server over HTTP." + }, + "PersistedMcpTransportResourceStdio": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stdio" + ], + "default": "stdio", + "x-stainless-const": true, + "description": "The type of the object. Always `stdio`." + }, + "command": { + "type": "string", + "minLength": 0, + "description": "The command used to start the MCP server." + }, + "args": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Arguments passed to the MCP server command." + }, + "cwd": { + "type": "string", + "minLength": 0, + "description": "The working directory used to start the MCP server." + }, + "env_vars": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Environment variable names inherited from the execution environment." + } + }, + "required": [ + "type", + "command", + "args", + "cwd", + "env_vars" + ], + "additionalProperties": false, + "description": "Starts an MCP server as a local process." + }, + "PersistedMcpTransportResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/PersistedMcpTransportResourceHttp" + }, + { + "$ref": "#/components/schemas/PersistedMcpTransportResourceStdio" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "http": "#/components/schemas/PersistedMcpTransportResourceHttp", + "stdio": "#/components/schemas/PersistedMcpTransportResourceStdio" + } + }, + "x-oai-discriminator-values": [ + "http", + "stdio" + ], + "description": "A credential-free transport used to connect to an MCP server." + }, + "McpConnectionOriginResource": { + "type": "string", + "enum": [ + "service", + "environment" + ], + "description": "Where outbound MCP HTTP connections originate." + }, + "PersistedAgentToolResourceMcp": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp" + ], + "default": "mcp", + "x-stainless-const": true, + "description": "The type of the object. Always `mcp`." + }, + "server_label": { + "type": "string", + "minLength": 0, + "description": "A label used to identify the MCP server in tool calls." + }, + "credential_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The vault credential selected for this MCP server, if any." + }, + "transport": { + "$ref": "#/components/schemas/PersistedMcpTransportResource", + "description": "The credential-free transport used to connect to the MCP server." + }, + "request_metadata": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 0 + }, + "minProperties": 0, + "description": "Metadata included with requests to this MCP server." + }, + "allowed_tools": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "The MCP tools the agent may call, or null when all server tools are allowed." + }, + "required": { + "type": "boolean", + "description": "Whether this MCP server must initialize before the first turn." + }, + "connection_origin": { + "$ref": "#/components/schemas/McpConnectionOriginResource", + "description": "Where outbound MCP HTTP connections originate." + } + }, + "required": [ + "type", + "server_label", + "credential_id", + "transport", + "request_metadata", + "allowed_tools", + "required", + "connection_origin" + ], + "additionalProperties": false, + "description": "Tools provided by a remote MCP server without stored credentials." + }, + "WebSearchModeResource": { + "type": "string", + "enum": [ + "disabled", + "cached", + "live" + ], + "description": "The source used for web search results." + }, + "WebSearchContextSizeResource": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "description": "The amount of web search context made available to the model." + }, + "WebSearchLocationResource": { + "type": "object", + "properties": { + "country": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The two-letter ISO country code, such as `US`." + }, + "region": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The region or state name." + }, + "city": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The city name." + }, + "timezone": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The IANA timezone, such as `America/Los_Angeles`." + } + }, + "required": [ + "country", + "region", + "city", + "timezone" + ], + "additionalProperties": false, + "description": "Approximate user location used to localize web search results." + }, + "PersistedAgentToolResourceWebSearch": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "web_search" + ], + "default": "web_search", + "x-stainless-const": true, + "description": "The type of the object. Always `web_search`." + }, + "mode": { + "$ref": "#/components/schemas/WebSearchModeResource", + "description": "The source used for web search results." + }, + "context_size": { + "$ref": "#/components/schemas/WebSearchContextSizeResource", + "description": "The amount of search context made available to the model. Defaults to `medium`." + }, + "allowed_domains": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Allowed search domains, or `null` when the search is unrestricted." + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebSearchLocationResource" + }, + { + "type": "null" + } + ], + "description": "Approximate location used to localize search results, if provided." + } + }, + "required": [ + "type", + "mode", + "context_size", + "allowed_domains", + "location" + ], + "additionalProperties": false, + "description": "Web search." + }, + "PersistedAgentToolResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/PersistedAgentToolResourceFunction" + }, + { + "$ref": "#/components/schemas/PersistedAgentToolResourceToolSearch" + }, + { + "$ref": "#/components/schemas/PersistedAgentToolResourceProgrammaticToolCalling" + }, + { + "$ref": "#/components/schemas/PersistedAgentToolResourceMcp" + }, + { + "$ref": "#/components/schemas/PersistedAgentToolResourceWebSearch" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "function": "#/components/schemas/PersistedAgentToolResourceFunction", + "tool_search": "#/components/schemas/PersistedAgentToolResourceToolSearch", + "programmatic_tool_calling": "#/components/schemas/PersistedAgentToolResourceProgrammaticToolCalling", + "mcp": "#/components/schemas/PersistedAgentToolResourceMcp", + "web_search": "#/components/schemas/PersistedAgentToolResourceWebSearch" + } + }, + "x-oai-discriminator-values": [ + "function", + "tool_search", + "programmatic_tool_calling", + "mcp", + "web_search" + ], + "description": "A credential-free tool available to a reusable agent." + }, + "MultiAgentConfigResource": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether subagent tools are enabled. Defaults to false." + }, + "max_concurrent_subagents": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 1, + "maximum": 4294967295, + "description": "Maximum number of subagents that may run concurrently, or null when disabled. Defaults to 6 when enabled." + } + }, + "required": [ + "enabled", + "max_concurrent_subagents" + ], + "additionalProperties": false, + "description": "The resolved configuration for creating and coordinating subagents." + }, + "AgentResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the reusable agent." + }, + "object": { + "type": "string", + "enum": [ + "agent" + ], + "default": "agent", + "x-stainless-const": true, + "description": "The object type. Always `agent`." + }, + "created_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the agent was created." + }, + "updated_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the agent was last updated." + }, + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "A human-readable name for the agent, or null if it is unnamed." + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 0 + }, + "propertyNames": { + "type": "string", + "minLength": 0 + }, + "minProperties": 0, + "description": "Custom string key-value pairs attached to the agent." + }, + "model": { + "type": "string", + "minLength": 0, + "description": "The requested model name used for inference." + }, + "reasoning": { + "$ref": "#/components/schemas/ReasoningResource", + "description": "The resolved reasoning configuration, including the model default for an omitted effort." + }, + "text": { + "$ref": "#/components/schemas/TextResource", + "description": "The resolved configuration for text generated by the agent." + }, + "service_tier": { + "$ref": "#/components/schemas/ServiceTierResource", + "description": "The resolved service-tier policy used for model requests." + }, + "instructions": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "Custom instructions appended to the agent's default base instructions." + }, + "tools": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersistedAgentToolResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Tools available to the agent." + }, + "multi_agent": { + "$ref": "#/components/schemas/MultiAgentConfigResource", + "description": "The resolved configuration for creating and coordinating subagents." + }, + "x_agents_core": { + "anyOf": [ + { + "$ref": "#/components/schemas/v1.SavedAgentCore" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "object", + "created_at", + "updated_at", + "name", + "metadata", + "model", + "reasoning", + "text", + "service_tier", + "instructions", + "tools", + "multi_agent" + ], + "additionalProperties": false, + "description": "A reusable agent scoped to the caller's project." + }, + "AgentListResource": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "default": "list", + "x-stainless-const": true, + "description": "The object type, which is always `list`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The resources returned in this page, in the requested sort order." + }, + "first_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the first resource in `data`, or `null` if the page is empty." + }, + "last_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the last resource in `data`, or `null` if the page is empty. Pass this as `after` with the same order and filters." + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more resources to retrieve after this page." + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "additionalProperties": false, + "description": "A page of Agents API resources, with IDs for retrieving additional pages." + }, + "ReasoningEffortParam": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "description": "The amount of reasoning effort the model should use." + }, + "ReasoningSummaryParam": { + "type": "string", + "enum": [ + "concise", + "detailed", + "auto" + ], + "x-enumDescriptions": [ + "Returns a concise reasoning summary when supported.", + "Returns a detailed reasoning summary when supported.", + "Automatically selects the most detailed summary supported by the model." + ], + "description": "The reasoning summary format requested from the model." + }, + "ReasoningParam": { + "type": "object", + "properties": { + "effort": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReasoningEffortParam" + }, + { + "type": "null" + } + ], + "description": "The amount of reasoning effort the model should use. Omission lets the model select it." + }, + "summary": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReasoningSummaryParam" + }, + { + "type": "null" + } + ], + "description": "Controls whether the response includes a reasoning summary." + } + }, + "additionalProperties": false, + "description": "Reasoning configuration for the agent." + }, + "TextFormatParamText": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ], + "default": "text", + "x-stainless-const": true, + "description": "The type of the object. Always `text`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Generates ordinary text without a structured-output constraint." + }, + "TextFormatParamJsonSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "json_schema" + ], + "default": "json_schema", + "x-stainless-const": true, + "description": "The type of the object. Always `json_schema`." + }, + "schema": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "The JSON Schema that generated text must match." + } + }, + "required": [ + "type", + "schema" + ], + "additionalProperties": false, + "description": "Constrains generated text to a JSON Schema." + }, + "TextFormatParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextFormatParamText" + }, + { + "$ref": "#/components/schemas/TextFormatParamJsonSchema" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "text": "#/components/schemas/TextFormatParamText", + "json_schema": "#/components/schemas/TextFormatParamJsonSchema" + } + }, + "x-oai-discriminator-values": [ + "text", + "json_schema" + ], + "description": "The output format for generated text." + }, + "VerbosityParam": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "x-enumDescriptions": [ + "Produces less text.", + "Uses the default amount of text.", + "Produces more text." + ], + "description": "The amount of text the model should produce." + }, + "TextParam": { + "type": "object", + "properties": { + "format": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextFormatParam" + }, + { + "type": "null" + } + ], + "description": "The output format. Omission uses ordinary text (`{\"type\": \"text\"}`)." + }, + "verbosity": { + "anyOf": [ + { + "$ref": "#/components/schemas/VerbosityParam" + }, + { + "type": "null" + } + ], + "description": "The amount of text the model should produce. Defaults to `medium`, matching Responses." + } + }, + "additionalProperties": false, + "description": "Configuration for text generated by the agent." + }, + "ServiceTierParam": { + "type": "string", + "enum": [ + "auto", + "default", + "flex", + "priority", + "fast" + ], + "x-enumDescriptions": [ + "Selects the service tier automatically.", + "Uses the default service tier.", + "Uses the flex service tier.", + "Uses the priority service tier.", + "Uses the fast service tier." + ], + "description": "The service tier used for model requests." + }, + "PersistedAgentToolConfigParamFunction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "function" + ], + "default": "function", + "x-stainless-const": true, + "description": "The type of the object. Always `function`." + }, + "name": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The name of the function." + }, + "description": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "A description of what the function does." + }, + "parameters": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "A JSON Schema object describing the function's arguments." + }, + "defer_loading": { + "type": "boolean", + "default": false, + "description": "Whether this function is deferred and discovered through tool search. Defaults to `false`." + } + }, + "required": [ + "type", + "name", + "description", + "parameters" + ], + "additionalProperties": false, + "description": "A function defined by the application." + }, + "PersistedAgentToolConfigParamToolSearch": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tool_search" + ], + "default": "tool_search", + "x-stainless-const": true, + "description": "The type of the object. Always `tool_search`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Discovers deferred function tools and loads them into the model context." + }, + "PersistedAgentToolConfigParamProgrammaticToolCalling": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "programmatic_tool_calling" + ], + "default": "programmatic_tool_calling", + "x-stainless-const": true, + "description": "The type of the object. Always `programmatic_tool_calling`." + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether tools can be called from model-generated code. Defaults to `true`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Enables calling tools from model-generated code." + }, + "PersistedMcpTransportConfigParamHttp": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "http" + ], + "default": "http", + "x-stainless-const": true, + "description": "The type of the object. Always `http`." + }, + "server_url": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The URL of the MCP server." + }, + "headers": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "Non-secret HTTP headers sent to the MCP server." + } + }, + "required": [ + "type", + "server_url" + ], + "additionalProperties": false, + "description": "Connects to an MCP server over HTTP." + }, + "PersistedMcpTransportConfigParamStdio": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stdio" + ], + "default": "stdio", + "x-stainless-const": true, + "description": "The type of the object. Always `stdio`." + }, + "command": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The command used to start the MCP server." + }, + "args": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Arguments passed to the MCP server command." + }, + "cwd": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The working directory used to start the MCP server." + }, + "env_vars": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Environment variable names to inherit from the selected execution environment." + } + }, + "required": [ + "type", + "command", + "cwd" + ], + "additionalProperties": false, + "description": "Starts an MCP server as a local process." + }, + "PersistedMcpTransportConfigParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/PersistedMcpTransportConfigParamHttp" + }, + { + "$ref": "#/components/schemas/PersistedMcpTransportConfigParamStdio" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "http": "#/components/schemas/PersistedMcpTransportConfigParamHttp", + "stdio": "#/components/schemas/PersistedMcpTransportConfigParamStdio" + } + }, + "x-oai-discriminator-values": [ + "http", + "stdio" + ], + "description": "A credential-free transport used to connect to an MCP server." + }, + "McpConnectionOriginParam": { + "type": "string", + "enum": [ + "service", + "environment" + ], + "x-enumDescriptions": [ + "Uses the Managed Agents service network.", + "Uses the session's execution environment." + ], + "description": "Where outbound MCP HTTP connections originate." + }, + "PersistedAgentToolConfigParamMcp": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp" + ], + "default": "mcp", + "x-stainless-const": true, + "description": "The type of the object. Always `mcp`." + }, + "server_label": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "A label used to identify the MCP server in tool calls." + }, + "credential_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The vault credential selected for this MCP server. Optional when exactly one attached credential matches the server URL." + }, + "transport": { + "$ref": "#/components/schemas/PersistedMcpTransportConfigParam", + "description": "The credential-free transport used to connect to the MCP server." + }, + "request_metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "Metadata included with requests to this MCP server." + }, + "allowed_tools": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "The MCP tools the agent may call. All server tools are allowed when omitted." + }, + "required": { + "type": "boolean", + "default": false, + "description": "Whether this MCP server must initialize before the first turn. Defaults to `false`." + }, + "connection_origin": { + "anyOf": [ + { + "$ref": "#/components/schemas/McpConnectionOriginParam" + }, + { + "type": "null" + } + ], + "description": "Selects where outbound MCP HTTP connections originate." + } + }, + "required": [ + "type", + "server_label", + "transport" + ], + "additionalProperties": false, + "description": "Tools provided by a remote MCP server without stored credentials." + }, + "WebSearchModeParam": { + "type": "string", + "enum": [ + "disabled", + "cached", + "live" + ], + "x-enumDescriptions": [ + "Disables web search.", + "Uses cached search results.", + "Searches the live web." + ], + "description": "The source used for web search results." + }, + "WebSearchContextSizeParam": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "description": "The amount of web search context made available to the model." + }, + "WebSearchLocationParam": { + "type": "object", + "properties": { + "country": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The two-letter ISO country code, such as `US`." + }, + "region": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The region or state name." + }, + "city": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The city name." + }, + "timezone": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The IANA timezone, such as `America/Los_Angeles`." + } + }, + "additionalProperties": false, + "description": "Approximate user location used to localize web search results." + }, + "PersistedAgentToolConfigParamWebSearch": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "web_search" + ], + "default": "web_search", + "x-stainless-const": true, + "description": "The type of the object. Always `web_search`." + }, + "mode": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebSearchModeParam" + }, + { + "type": "null" + } + ], + "description": "The source used for web search results. Defaults to `live`." + }, + "context_size": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebSearchContextSizeParam" + }, + { + "type": "null" + } + ], + "description": "The amount of search context made available to the model. Defaults to `medium`." + }, + "allowed_domains": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Domains the search may include." + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebSearchLocationParam" + }, + { + "type": "null" + } + ], + "description": "Approximate location used to localize search results." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Web search." + }, + "PersistedAgentToolConfigParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/PersistedAgentToolConfigParamFunction" + }, + { + "$ref": "#/components/schemas/PersistedAgentToolConfigParamToolSearch" + }, + { + "$ref": "#/components/schemas/PersistedAgentToolConfigParamProgrammaticToolCalling" + }, + { + "$ref": "#/components/schemas/PersistedAgentToolConfigParamMcp" + }, + { + "$ref": "#/components/schemas/PersistedAgentToolConfigParamWebSearch" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "function": "#/components/schemas/PersistedAgentToolConfigParamFunction", + "tool_search": "#/components/schemas/PersistedAgentToolConfigParamToolSearch", + "programmatic_tool_calling": "#/components/schemas/PersistedAgentToolConfigParamProgrammaticToolCalling", + "mcp": "#/components/schemas/PersistedAgentToolConfigParamMcp", + "web_search": "#/components/schemas/PersistedAgentToolConfigParamWebSearch" + } + }, + "x-oai-discriminator-values": [ + "function", + "tool_search", + "programmatic_tool_calling", + "mcp", + "web_search" + ], + "description": "A tool that can be stored on a reusable agent without session credentials." + }, + "MultiAgentConfigCurrentParam": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether subagent tools are enabled." + }, + "max_concurrent_subagents": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 4294967295, + "description": "Maximum number of subagents that may run concurrently. Defaults to 6." + } + }, + "required": [ + "enabled" + ], + "additionalProperties": false, + "description": "Explicit configuration for creating and coordinating subagents." + }, + "CreateAgentParams": { + "type": "object", + "properties": { + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 512 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "minProperties": 0, + "maxProperties": 16, + "description": "Up to 16 string key-value pairs, with keys up to 64 and values up to 512 characters. Omission or null defaults to an empty map." + }, + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 128, + "description": "A human-readable name for the agent. Omission or null leaves the agent unnamed." + }, + "model": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The model to use for the agent. The requested model name is preserved." + }, + "reasoning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReasoningParam" + }, + { + "type": "null" + } + ], + "description": "Configuration for model reasoning. Omission uses the model's default effort." + }, + "text": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextParam" + }, + { + "type": "null" + } + ], + "description": "Configuration for generated text. Defaults to the `text` format and medium verbosity." + }, + "service_tier": { + "anyOf": [ + { + "$ref": "#/components/schemas/ServiceTierParam" + }, + { + "type": "null" + } + ], + "description": "The service tier used for model requests. Defaults to `auto`." + }, + "instructions": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "Additional instructions appended to the agent's default base instructions. Omit or set to null to add no custom instructions." + }, + "tools": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/PersistedAgentToolConfigParam" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Tools available to the agent. Defaults to an empty list." + }, + "multi_agent": { + "anyOf": [ + { + "$ref": "#/components/schemas/MultiAgentConfigCurrentParam" + }, + { + "type": "null" + } + ], + "description": "Configuration for creating and coordinating subagents. Subagent tools are disabled by default." + }, + "x_agents_core": { + "anyOf": [ + { + "$ref": "#/components/schemas/v1.SavedAgentCoreInput" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "model" + ], + "additionalProperties": false, + "description": "Parameters for creating a reusable agent." + }, + "UpdateAgentParams": { + "type": "object", + "properties": { + "model": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The model to use for the agent. The requested model name is preserved." + }, + "reasoning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReasoningParam" + }, + { + "type": "null" + } + ], + "description": "Configuration for model reasoning. Omit to keep the current settings; pass `null` to reset to the model's default effort." + }, + "text": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextParam" + }, + { + "type": "null" + } + ], + "description": "Configuration for text generated by the agent." + }, + "service_tier": { + "anyOf": [ + { + "$ref": "#/components/schemas/ServiceTierParam" + }, + { + "type": "null" + } + ], + "description": "The service tier used for model requests." + }, + "instructions": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "Additional instructions appended to the agent's default base instructions. Omit to leave unchanged." + }, + "multi_agent": { + "anyOf": [ + { + "$ref": "#/components/schemas/MultiAgentConfigCurrentParam" + }, + { + "type": "null" + } + ], + "description": "Configuration for creating and coordinating subagents." + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 512 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "minProperties": 0, + "maxProperties": 16, + "description": "Replaces all metadata. Omit to leave unchanged, or pass null or {} to clear it. Up to 16 string key-value pairs, with keys up to 64 and values up to 512 characters." + }, + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 128, + "description": "A replacement name. Omit to leave unchanged, or pass null to clear it." + }, + "tools": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/PersistedAgentToolConfigParam" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Tools available to the agent." + }, + "x_agents_core": { + "anyOf": [ + { + "$ref": "#/components/schemas/v1.SavedAgentCoreInput" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "description": "Fields to replace on an existing reusable agent." + }, + "DeletedAgentResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the deleted agent." + }, + "object": { + "type": "string", + "enum": [ + "agent.deleted" + ], + "default": "agent.deleted", + "x-stainless-const": true, + "description": "The object type. Always `agent.deleted`." + }, + "deleted": { + "type": "boolean", + "description": "Whether the agent was deleted. Always `true`." + } + }, + "required": [ + "id", + "object", + "deleted" + ], + "additionalProperties": false, + "description": "A deleted reusable agent." + }, + "EnvironmentPackagesResource": { + "type": "object", + "properties": { + "python": { + "type": "array", + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Python packages installed in the environment." + }, + "system": { + "type": "array", + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "System packages installed in the environment." + }, + "npm": { + "type": "array", + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "npm packages installed globally in the environment." + } + }, + "required": [ + "python", + "system", + "npm" + ], + "additionalProperties": false, + "description": "Packages installed in an OpenAI-hosted environment." + }, + "NetworkAccessResource": { + "type": "string", + "enum": [ + "enabled", + "disabled", + "restricted" + ], + "x-enumDescriptions": [ + "Allows unrestricted network access.", + "Disables network access.", + "Allows access only to configured domains." + ], + "description": "The network access mode for an OpenAI-hosted environment." + }, + "NetworkPolicyResource": { + "type": "object", + "properties": { + "access": { + "$ref": "#/components/schemas/NetworkAccessResource", + "description": "The environment's network access mode." + }, + "allowed_domains": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Domains the environment may access when network access is restricted." + } + }, + "required": [ + "access", + "allowed_domains" + ], + "additionalProperties": false, + "description": "Network access for an OpenAI-hosted environment." + }, + "HostedTemplateSkillResourceSkillReference": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "skill_reference" + ], + "default": "skill_reference", + "x-stainless-const": true, + "description": "The type of the object. Always `skill_reference`." + }, + "skill_id": { + "type": "string", + "minLength": 0, + "description": "The referenced skill ID." + }, + "version": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The requested version selector, including `latest`." + } + }, + "required": [ + "type", + "skill_id", + "version" + ], + "additionalProperties": false, + "description": "A skill resolved afresh from the Skills API whenever a session starts." + }, + "HostedTemplateSkillResourceInline": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ], + "default": "inline", + "x-stainless-const": true, + "description": "The type of the object. Always `inline`." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The skill name declared in `SKILL.md`." + }, + "description": { + "type": "string", + "minLength": 0, + "description": "The skill description declared in `SKILL.md`." + } + }, + "required": [ + "type", + "name", + "description" + ], + "additionalProperties": false, + "description": "Safe metadata for an inline skill archive." + }, + "HostedTemplateSkillResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/HostedTemplateSkillResourceSkillReference" + }, + { + "$ref": "#/components/schemas/HostedTemplateSkillResourceInline" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "skill_reference": "#/components/schemas/HostedTemplateSkillResourceSkillReference", + "inline": "#/components/schemas/HostedTemplateSkillResourceInline" + } + }, + "x-oai-discriminator-values": [ + "skill_reference", + "inline" + ], + "description": "Safe metadata for a skill configured by an environment template." + }, + "HostedTemplateFileResourceFileId": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "file_id" + ], + "default": "file_id", + "x-stainless-const": true, + "description": "The type of the object. Always `file_id`." + }, + "file_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the uploaded file." + }, + "path": { + "type": "string", + "minLength": 0, + "description": "The file's absolute path inside the environment." + } + }, + "required": [ + "type", + "file_id", + "path" + ], + "additionalProperties": false, + "description": "A project-scoped Files API reference resolved separately for each session." + }, + "HostedTemplateFileResourceInline": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ], + "default": "inline", + "x-stainless-const": true, + "description": "The type of the object. Always `inline`." + }, + "path": { + "type": "string", + "minLength": 0, + "description": "The file's absolute path inside the environment." + }, + "size_bytes": { + "type": "integer", + "format": "int64", + "minimum": 0, + "description": "The decoded size of the inline file in bytes." + } + }, + "required": [ + "type", + "path", + "size_bytes" + ], + "additionalProperties": false, + "description": "Metadata for confidential inline file contents." + }, + "HostedTemplateFileResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/HostedTemplateFileResourceFileId" + }, + { + "$ref": "#/components/schemas/HostedTemplateFileResourceInline" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "file_id": "#/components/schemas/HostedTemplateFileResourceFileId", + "inline": "#/components/schemas/HostedTemplateFileResourceInline" + } + }, + "x-oai-discriminator-values": [ + "file_id", + "inline" + ], + "description": "Safe metadata for a file configured by an environment template." + }, + "EnvironmentTemplateResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the reusable environment template." + }, + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "An optional human-readable display name for the template." + }, + "object": { + "type": "string", + "enum": [ + "agent.environment.template" + ], + "default": "agent.environment.template", + "x-stainless-const": true, + "description": "The object type. Always `agent.environment.template`." + }, + "created_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the template was created." + }, + "updated_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the template was last updated." + }, + "packages": { + "$ref": "#/components/schemas/EnvironmentPackagesResource", + "description": "Packages installed in each fresh OpenAI-hosted environment." + }, + "network": { + "$ref": "#/components/schemas/NetworkPolicyResource", + "description": "Runtime network access for each OpenAI-hosted environment." + }, + "capability_directories": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Directories that expose capabilities to the agent." + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostedTemplateSkillResource" + }, + "minItems": 0, + "maxItems": 200, + "description": "Safe skill metadata, preserving unresolved version selectors." + }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostedPluginResource" + }, + "minItems": 0, + "maxItems": 32, + "description": "Safe plugin metadata, excluding inline archive contents." + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostedTemplateFileResource" + }, + "minItems": 0, + "maxItems": 50, + "description": "Safe file metadata, excluding contents and session-scoped file IDs." + } + }, + "required": [ + "id", + "name", + "object", + "created_at", + "updated_at", + "packages", + "network", + "capability_directories", + "skills", + "plugins", + "files" + ], + "additionalProperties": false, + "description": "Reusable configuration that provisions a fresh OpenAI-hosted environment for each session." + }, + "EnvironmentTemplateListResource": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "default": "list", + "x-stainless-const": true, + "description": "The object type, which is always `list`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EnvironmentTemplateResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The resources returned in this page, in the requested sort order." + }, + "first_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the first resource in `data`, or `null` if the page is empty." + }, + "last_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the last resource in `data`, or `null` if the page is empty. Pass this as `after` with the same order and filters." + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more resources to retrieve after this page." + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "additionalProperties": false, + "description": "A page of Agents API resources, with IDs for retrieving additional pages." + }, + "EnvironmentPackagesParam": { + "type": "object", + "properties": { + "python": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Python packages to install. Defaults to an empty list." + }, + "system": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "System packages to install. Defaults to an empty list." + }, + "npm": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "npm packages to install globally. Defaults to an empty list." + } + }, + "additionalProperties": false, + "description": "Packages to install in an OpenAI-hosted environment." + }, + "SetupCommandParam": { + "type": "object", + "properties": { + "command": { + "type": "string", + "minLength": 0, + "maxLength": 65536, + "description": "The shell command to execute." + }, + "cwd": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 4096, + "description": "The absolute working directory. Defaults to `/workspace`." + } + }, + "required": [ + "command" + ], + "additionalProperties": false, + "description": "A confidential setup command executed before the hosted agent starts." + }, + "NetworkAccessParam": { + "type": "string", + "enum": [ + "enabled", + "disabled", + "restricted" + ], + "x-enumDescriptions": [ + "Allows unrestricted network access, matching an omitted network policy.", + "Disables network access.", + "Allows access only to configured domains." + ], + "description": "The network access mode for an OpenAI-hosted environment." + }, + "NetworkPolicyParam": { + "type": "object", + "properties": { + "access": { + "$ref": "#/components/schemas/NetworkAccessParam", + "description": "The environment's network access mode." + }, + "allowed_domains": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Domains the environment may access when network access is restricted." + } + }, + "required": [ + "access" + ], + "additionalProperties": false, + "description": "Network access for an OpenAI-hosted environment." + }, + "HostedSkillParamSkillReference": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "skill_reference" + ], + "default": "skill_reference", + "x-stainless-const": true, + "description": "The type of the object. Always `skill_reference`." + }, + "skill_id": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "The ID of the skill created through `/v1/skills`." + }, + "version": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The skill version, a positive integer or `latest`; omission selects the default." + } + }, + "required": [ + "type", + "skill_id" + ], + "additionalProperties": false, + "description": "References a skill uploaded through the Skills API." + }, + "InlineCapabilitySourceParamBase64": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "base64" + ], + "default": "base64", + "x-stainless-const": true, + "description": "The type of the object. Always `base64`." + }, + "media_type": { + "type": "string", + "enum": [ + "application/zip" + ], + "default": "application/zip", + "x-stainless-const": true, + "x-enumDescriptions": [ + "A ZIP archive." + ], + "description": "The archive media type, always `application/zip`." + }, + "data": { + "type": "string", + "minLength": 1, + "maxLength": 70254592, + "description": "Standard-base64 encoded ZIP archive bytes." + } + }, + "required": [ + "type", + "media_type", + "data" + ], + "additionalProperties": false, + "description": "Provides ZIP bytes encoded with standard base64." + }, + "InlineCapabilitySourceParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/InlineCapabilitySourceParamBase64" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "base64": "#/components/schemas/InlineCapabilitySourceParamBase64" + } + }, + "x-oai-discriminator-values": [ + "base64" + ], + "description": "The encoded ZIP archive for an inline skill or plugin." + }, + "HostedSkillParamInline": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ], + "default": "inline", + "x-stainless-const": true, + "description": "The type of the object. Always `inline`." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "The skill name declared in `SKILL.md`." + }, + "description": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The skill description declared in `SKILL.md`." + }, + "source": { + "$ref": "#/components/schemas/InlineCapabilitySourceParam", + "description": "The inline ZIP archive." + } + }, + "required": [ + "type", + "name", + "description", + "source" + ], + "additionalProperties": false, + "description": "Supplies a skill ZIP directly in the session request." + }, + "HostedSkillParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/HostedSkillParamSkillReference" + }, + { + "$ref": "#/components/schemas/HostedSkillParamInline" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "skill_reference": "#/components/schemas/HostedSkillParamSkillReference", + "inline": "#/components/schemas/HostedSkillParamInline" + } + }, + "x-oai-discriminator-values": [ + "skill_reference", + "inline" + ], + "description": "A skill installed in an OpenAI-hosted environment." + }, + "HostedPluginParamInline": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "inline" + ], + "default": "inline", + "x-stainless-const": true, + "description": "The type of the object. Always `inline`." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "The plugin name declared in `.codex-plugin/plugin.json`." + }, + "description": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The plugin description declared in `.codex-plugin/plugin.json`." + }, + "source": { + "$ref": "#/components/schemas/InlineCapabilitySourceParam", + "description": "The inline ZIP archive." + } + }, + "required": [ + "type", + "name", + "description", + "source" + ], + "additionalProperties": false, + "description": "Supplies a plugin ZIP directly in the session request." + }, + "HostedPluginParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/HostedPluginParamInline" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "inline": "#/components/schemas/HostedPluginParamInline" + } + }, + "x-oai-discriminator-values": [ + "inline" + ], + "description": "A plugin installed in an OpenAI-hosted environment." + }, + "CreateEnvironmentTemplateParams": { + "type": "object", + "properties": { + "packages": { + "anyOf": [ + { + "$ref": "#/components/schemas/EnvironmentPackagesParam" + }, + { + "type": "null" + } + ], + "description": "Packages to install in the environment. Defaults to empty package lists." + }, + "setup_commands": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SetupCommandParam" + }, + "minItems": 0, + "maxItems": 16, + "description": "Ordered, confidential setup commands. Command bodies are never returned." + }, + "network": { + "anyOf": [ + { + "$ref": "#/components/schemas/NetworkPolicyParam" + }, + { + "type": "null" + } + ], + "description": "Network access policy for the environment. Defaults to enabled." + }, + "env": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "Environment variables made available to the agent." + }, + "capability_directories": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Directories that contain capabilities exposed to the agent. Defaults to an empty list." + }, + "skills": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/HostedSkillParam" + }, + "minItems": 0, + "maxItems": 200, + "description": "Skills referenced by ID or provided as inline ZIP archives. Defaults to an empty list." + }, + "plugins": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/HostedPluginParam" + }, + "minItems": 0, + "maxItems": 32, + "description": "Plugins provided as inline ZIP archives. Defaults to an empty list." + }, + "files": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/HostedEnvironmentFileParam" + }, + "minItems": 0, + "maxItems": 50, + "description": "Files available before the agent starts. Defaults to an empty list." + }, + "name": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 256, + "description": "An optional human-readable display name for the template." + } + }, + "additionalProperties": false, + "description": "Parameters for creating a reusable, project-scoped OpenAI-hosted environment template." + }, + "UpdateEnvironmentTemplateParams": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 256, + "description": "A replacement human-readable display name, or `null` to clear the name." + }, + "packages": { + "anyOf": [ + { + "$ref": "#/components/schemas/EnvironmentPackagesParam" + }, + { + "type": "null" + } + ], + "description": "Packages installed before the runtime network policy applies." + }, + "setup_commands": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SetupCommandParam" + }, + "minItems": 0, + "maxItems": 16, + "description": "Replacement confidential setup commands, never included in returned resources." + }, + "network": { + "anyOf": [ + { + "$ref": "#/components/schemas/NetworkPolicyParam" + }, + { + "type": "null" + } + ], + "description": "Network access available after setup completes." + }, + "env": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "Replacement confidential environment values." + }, + "capability_directories": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Directories that expose capabilities to the agent." + }, + "skills": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/HostedSkillParam" + }, + "minItems": 0, + "maxItems": 200, + "description": "Replacement skill configuration installed for each new session." + }, + "plugins": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/HostedPluginParam" + }, + "minItems": 0, + "maxItems": 32, + "description": "Replacement plugin configuration installed for each new session." + }, + "files": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/HostedEnvironmentFileParam" + }, + "minItems": 0, + "maxItems": 50, + "description": "Replacement file configuration materialized for each new session." + } + }, + "additionalProperties": false, + "description": "Fields to replace on an existing reusable OpenAI-hosted environment template." + }, + "DeletedEnvironmentTemplateResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the deleted environment template." + }, + "object": { + "type": "string", + "enum": [ + "agent.environment.template.deleted" + ], + "default": "agent.environment.template.deleted", + "x-stainless-const": true, + "description": "The object type. Always `agent.environment.template.deleted`." + }, + "deleted": { + "type": "boolean", + "description": "Whether the environment template was deleted. Always `true`." + } + }, + "required": [ + "id", + "object", + "deleted" + ], + "additionalProperties": false, + "description": "A deleted reusable environment template." + }, + "SessionStatusResource": { + "type": "string", + "enum": [ + "idle", + "in_progress", + "requires_action", + "failed" + ], + "x-enumDescriptions": [ + "The session has no turn in progress and is ready for input. A hosted environment may still be provisioning.", + "The session is processing a turn.", + "The session is waiting for one or more required actions.", + "The session failed." + ], + "description": "The current status of a session." + }, + "SessionRequiredActionResourceFunctionCall": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "function_call" + ], + "default": "function_call", + "x-stainless-const": true, + "description": "The type of the object. Always `function_call`." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that requested the function call." + }, + "call_id": { + "type": "string", + "minLength": 0, + "description": "The ID to include when submitting the function result." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The function name." + }, + "arguments": { + "description": "The arguments supplied by the model." + } + }, + "required": [ + "type", + "turn_id", + "call_id", + "name", + "arguments" + ], + "additionalProperties": false, + "description": "Run a function tool and submit its result." + }, + "SessionRequiredActionResourceEnvironmentConnection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "environment_connection" + ], + "default": "environment_connection", + "x-stainless-const": true, + "description": "The type of the object. Always `environment_connection`." + }, + "environment_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the environment to reconnect." + } + }, + "required": [ + "type", + "environment_id" + ], + "additionalProperties": false, + "description": "Reconnect a session environment." + }, + "SessionRequiredActionResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/SessionRequiredActionResourceFunctionCall" + }, + { + "$ref": "#/components/schemas/SessionRequiredActionResourceEnvironmentConnection" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "function_call": "#/components/schemas/SessionRequiredActionResourceFunctionCall", + "environment_connection": "#/components/schemas/SessionRequiredActionResourceEnvironmentConnection" + } + }, + "x-oai-discriminator-values": [ + "function_call", + "environment_connection" + ], + "description": "An action that must be completed before a session can continue." + }, + "AgentToolResourceFunction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "function" + ], + "default": "function", + "x-stainless-const": true, + "description": "The type of the object. Always `function`." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The name of the function." + }, + "description": { + "type": "string", + "minLength": 0, + "description": "A description of what the function does." + }, + "parameters": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 0 + }, + "minProperties": 0, + "description": "A JSON Schema object describing the function's arguments." + }, + "defer_loading": { + "type": "boolean", + "description": "Whether the function is deferred and discovered through tool search." + } + }, + "required": [ + "type", + "name", + "description", + "parameters", + "defer_loading" + ], + "additionalProperties": false, + "description": "A function defined by the application." + }, + "AgentToolResourceProgrammaticToolCalling": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "programmatic_tool_calling" + ], + "default": "programmatic_tool_calling", + "x-stainless-const": true, + "description": "The type of the object. Always `programmatic_tool_calling`." + }, + "enabled": { + "type": "boolean", + "description": "Whether tools can be called from model-generated code." + } + }, + "required": [ + "type", + "enabled" + ], + "additionalProperties": false, + "description": "Enables calling tools from model-generated code." + }, + "McpTransportResourceHttp": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "http" + ], + "default": "http", + "x-stainless-const": true, + "description": "The type of the object. Always `http`." + }, + "server_url": { + "type": "string", + "minLength": 0, + "description": "The URL of the MCP server." + } + }, + "required": [ + "type", + "server_url" + ], + "additionalProperties": false, + "description": "Connects to an MCP server over HTTP." + }, + "McpTransportResourceStdio": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stdio" + ], + "default": "stdio", + "x-stainless-const": true, + "description": "The type of the object. Always `stdio`." + }, + "command": { + "type": "string", + "minLength": 0, + "description": "The command used to start the MCP server." + }, + "args": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Arguments passed to the MCP server command." + }, + "cwd": { + "type": "string", + "minLength": 0, + "description": "The working directory used to start the MCP server." + }, + "env_vars": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Environment variable names inherited from the execution environment." + } + }, + "required": [ + "type", + "command", + "args", + "cwd", + "env_vars" + ], + "additionalProperties": false, + "description": "Starts an MCP server as a local process." + }, + "McpTransportResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/McpTransportResourceHttp" + }, + { + "$ref": "#/components/schemas/McpTransportResourceStdio" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "http": "#/components/schemas/McpTransportResourceHttp", + "stdio": "#/components/schemas/McpTransportResourceStdio" + } + }, + "x-oai-discriminator-values": [ + "http", + "stdio" + ], + "description": "The transport used to connect to an MCP server." + }, + "AgentToolResourceMcp": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp" + ], + "default": "mcp", + "x-stainless-const": true, + "description": "The type of the object. Always `mcp`." + }, + "server_label": { + "type": "string", + "minLength": 0, + "description": "A label used to identify the MCP server in tool calls." + }, + "credential_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The attached vault credential selected for this MCP server, if any. Optional when exactly one attached credential matches the server URL." + }, + "transport": { + "$ref": "#/components/schemas/McpTransportResource", + "description": "The transport used to connect to the MCP server." + }, + "request_metadata": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 0 + }, + "minProperties": 0, + "description": "Metadata included with requests to this MCP server." + }, + "allowed_tools": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "The MCP tools the agent may call." + }, + "required": { + "type": "boolean", + "description": "Whether this MCP server must initialize before the first turn." + }, + "connection_origin": { + "$ref": "#/components/schemas/McpConnectionOriginResource", + "description": "Where outbound MCP HTTP connections originate." + } + }, + "required": [ + "type", + "server_label", + "credential_id", + "transport", + "request_metadata", + "allowed_tools", + "required", + "connection_origin" + ], + "additionalProperties": false, + "description": "Tools provided by a remote MCP server." + }, + "AgentToolResourceWebSearch": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "web_search" + ], + "default": "web_search", + "x-stainless-const": true, + "description": "The type of the object. Always `web_search`." + }, + "mode": { + "$ref": "#/components/schemas/WebSearchModeResource", + "description": "The source used for web search results." + }, + "context_size": { + "$ref": "#/components/schemas/WebSearchContextSizeResource", + "description": "The amount of search context made available to the model. Defaults to `medium`." + }, + "allowed_domains": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Allowed search domains, or `null` when the search is unrestricted." + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebSearchLocationResource" + }, + { + "type": "null" + } + ], + "description": "Approximate location used to localize search results, if provided." + } + }, + "required": [ + "type", + "mode", + "context_size", + "allowed_domains", + "location" + ], + "additionalProperties": false, + "description": "Web search." + }, + "AgentToolResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/AgentToolResourceFunction" + }, + { + "$ref": "#/components/schemas/AgentToolResourceProgrammaticToolCalling" + }, + { + "$ref": "#/components/schemas/AgentToolResourceMcp" + }, + { + "$ref": "#/components/schemas/AgentToolResourceWebSearch" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "function": "#/components/schemas/AgentToolResourceFunction", + "programmatic_tool_calling": "#/components/schemas/AgentToolResourceProgrammaticToolCalling", + "mcp": "#/components/schemas/AgentToolResourceMcp", + "web_search": "#/components/schemas/AgentToolResourceWebSearch" + } + }, + "x-oai-discriminator-values": [ + "function", + "programmatic_tool_calling", + "mcp", + "web_search" + ], + "description": "A tool available to the agent." + }, + "SessionAgentResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the agent." + }, + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The reusable agent's name when the session was created, or null if no name was saved. Later changes to the agent's name do not affect this value." + }, + "model": { + "type": "string", + "minLength": 0, + "description": "The model used by the agent." + }, + "reasoning": { + "$ref": "#/components/schemas/ReasoningResource", + "description": "The agent's reasoning configuration." + }, + "text": { + "$ref": "#/components/schemas/TextResource", + "description": "Configuration for text generated by the agent." + }, + "service_tier": { + "$ref": "#/components/schemas/ServiceTierResource", + "description": "The effective service-tier policy for model requests. Defaults to `auto`." + }, + "instructions": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "Custom instructions appended to the agent's default base instructions." + }, + "tools": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentToolResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Tools available to the agent." + }, + "multi_agent": { + "$ref": "#/components/schemas/MultiAgentConfigResource", + "description": "Configuration for creating and coordinating subagents." + }, + "x_agents_core": { + "anyOf": [ + { + "$ref": "#/components/schemas/v1.AgentsCore" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "name", + "model", + "reasoning", + "text", + "service_tier", + "instructions", + "tools", + "multi_agent" + ], + "additionalProperties": false, + "description": "The effective agent configuration for a session." + }, + "EnvironmentResourceNone": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none" + ], + "default": "none", + "x-stainless-const": true, + "description": "The type of the object. Always `none`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "The session talks to CCA without selecting or provisioning an execution environment." + }, + "EnvironmentResourceOpenaiHosted": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "openai_hosted" + ], + "default": "openai_hosted", + "x-stainless-const": true, + "description": "The type of the object. Always `openai_hosted`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The public ID of the environment." + }, + "packages": { + "$ref": "#/components/schemas/EnvironmentPackagesResource", + "description": "Packages installed in the environment." + }, + "network": { + "$ref": "#/components/schemas/NetworkPolicyResource", + "description": "The effective network access policy for the environment." + }, + "capability_directories": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Directories that contain capabilities exposed to the agent." + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostedSkillResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Skills installed in the environment, excluding their archive contents." + }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostedPluginResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Plugins installed in the environment, excluding their archive contents." + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostedEnvironmentFileResource" + }, + "minItems": 0, + "maxItems": 50, + "description": "Files available in the environment, excluding their contents." + } + }, + "required": [ + "type", + "id", + "packages", + "network", + "capability_directories", + "skills", + "plugins", + "files" + ], + "additionalProperties": false, + "description": "An environment hosted by OpenAI." + }, + "EnvironmentResourceSelfHosted": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "self_hosted" + ], + "default": "self_hosted", + "x-stainless-const": true, + "description": "The type of the object. Always `self_hosted`." + }, + "remote_url": { + "type": "string", + "minLength": 0, + "description": "Pass this URL unchanged to `codex exec-server --remote` when connecting this environment." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The public ID of the environment." + }, + "workspace_directory": { + "type": "string", + "minLength": 0, + "description": "The absolute project directory inside the environment. Defaults to `/workspace`." + }, + "capability_directories": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "Directories that contain capabilities exposed to the agent." + } + }, + "required": [ + "type", + "remote_url", + "id", + "workspace_directory", + "capability_directories" + ], + "additionalProperties": false, + "description": "An environment hosted by the application." + }, + "EnvironmentResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/EnvironmentResourceNone" + }, + { + "$ref": "#/components/schemas/EnvironmentResourceOpenaiHosted" + }, + { + "$ref": "#/components/schemas/EnvironmentResourceSelfHosted" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "none": "#/components/schemas/EnvironmentResourceNone", + "openai_hosted": "#/components/schemas/EnvironmentResourceOpenaiHosted", + "self_hosted": "#/components/schemas/EnvironmentResourceSelfHosted" + } + }, + "x-oai-discriminator-values": [ + "none", + "openai_hosted", + "self_hosted" + ], + "description": "The execution environment for a session." + }, + "SessionResource": { + "type": "object", + "properties": { + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 0 + }, + "propertyNames": { + "type": "string", + "minLength": 0 + }, + "minProperties": 0, + "description": "Custom string key-value pairs attached to the session." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session." + }, + "object": { + "type": "string", + "enum": [ + "agent.session" + ], + "default": "agent.session", + "x-stainless-const": true, + "description": "The object type. Always `agent.session`." + }, + "created_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the session was created." + }, + "last_active_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the session was last active." + }, + "status": { + "$ref": "#/components/schemas/SessionStatusResource", + "description": "The current status of the session." + }, + "required_actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionRequiredActionResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "Actions that must be completed before the session can continue." + }, + "error": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The error that caused the session to fail, if any." + }, + "agent": { + "$ref": "#/components/schemas/SessionAgentResource", + "description": "The agent running in the session." + }, + "environment": { + "$ref": "#/components/schemas/EnvironmentResource", + "description": "The execution environment for the session." + }, + "vault_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 0 + }, + "minItems": 0, + "maxItems": 2000, + "description": "The IDs of vaults made available to the session." + }, + "usage": { + "anyOf": [ + { + "$ref": "#/components/schemas/TokenUsageResource" + }, + { + "type": "null" + } + ], + "description": "Best-effort token usage for the session, or null if unknown. Recorded usage may change." + }, + "x_agents_core": { + "anyOf": [ + { + "$ref": "#/components/schemas/v1.SessionCore" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "metadata", + "id", + "object", + "created_at", + "last_active_at", + "status", + "required_actions", + "error", + "agent", + "environment", + "vault_ids", + "usage" + ], + "additionalProperties": false, + "description": "A Managed Agents session." + }, + "SessionListResource": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "default": "list", + "x-stainless-const": true, + "description": "The object type, which is always `list`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The resources returned in this page, in the requested sort order." + }, + "first_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the first resource in `data`, or `null` if the page is empty." + }, + "last_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the last resource in `data`, or `null` if the page is empty. Pass this as `after` with the same order and filters." + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more resources to retrieve after this page." + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "additionalProperties": false, + "description": "A page of Agents API resources, with IDs for retrieving additional pages." + }, + "AgentToolConfigParamFunction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "function" + ], + "default": "function", + "x-stainless-const": true, + "description": "The type of the object. Always `function`." + }, + "name": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The name of the function." + }, + "description": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "A description of what the function does." + }, + "parameters": { + "type": "object", + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "A JSON Schema object describing the function's arguments." + }, + "defer_loading": { + "type": "boolean", + "default": false, + "description": "Whether this function is deferred and discovered through tool search. Defaults to `false`." + } + }, + "required": [ + "type", + "name", + "description", + "parameters" + ], + "additionalProperties": false, + "description": "A function defined by the application." + }, + "AgentToolConfigParamToolSearch": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tool_search" + ], + "default": "tool_search", + "x-stainless-const": true, + "description": "The type of the object. Always `tool_search`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Discovers deferred function tools and loads them into the model context." + }, + "AgentToolConfigParamProgrammaticToolCalling": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "programmatic_tool_calling" + ], + "default": "programmatic_tool_calling", + "x-stainless-const": true, + "description": "The type of the object. Always `programmatic_tool_calling`." + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether tools can be called from model-generated code. Defaults to `true`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Enables calling tools from model-generated code." + }, + "McpTransportConfigParamHttp": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "http" + ], + "default": "http", + "x-stainless-const": true, + "description": "The type of the object. Always `http`." + }, + "server_url": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The URL of the MCP server." + }, + "authorization": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The authorization value sent to the MCP server, if any." + }, + "headers": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "Additional HTTP headers sent to the MCP server." + } + }, + "required": [ + "type", + "server_url" + ], + "additionalProperties": false, + "description": "Connects to an MCP server over HTTP." + }, + "McpTransportConfigParamStdio": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stdio" + ], + "default": "stdio", + "x-stainless-const": true, + "description": "The type of the object. Always `stdio`." + }, + "command": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The command used to start the MCP server." + }, + "args": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Arguments passed to the MCP server command." + }, + "cwd": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The working directory used to start the MCP server." + }, + "env": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "Environment variables set for the MCP server process." + }, + "env_vars": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Environment variable names to inherit from the selected execution environment." + } + }, + "required": [ + "type", + "command", + "cwd" + ], + "additionalProperties": false, + "description": "Starts an MCP server as a local process." + }, + "McpTransportConfigParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/McpTransportConfigParamHttp" + }, + { + "$ref": "#/components/schemas/McpTransportConfigParamStdio" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "http": "#/components/schemas/McpTransportConfigParamHttp", + "stdio": "#/components/schemas/McpTransportConfigParamStdio" + } + }, + "x-oai-discriminator-values": [ + "http", + "stdio" + ], + "description": "The transport used to connect to an MCP server." + }, + "AgentToolConfigParamMcp": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp" + ], + "default": "mcp", + "x-stainless-const": true, + "description": "The type of the object. Always `mcp`." + }, + "server_label": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "A label used to identify the MCP server in tool calls." + }, + "credential_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The attached vault credential used to authenticate this MCP server. Optional when exactly one attached credential matches the server URL." + }, + "transport": { + "$ref": "#/components/schemas/McpTransportConfigParam", + "description": "The transport used to connect to the MCP server." + }, + "request_metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "Metadata included with requests to this MCP server." + }, + "allowed_tools": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "The MCP tools the agent may call. All server tools are allowed when omitted." + }, + "required": { + "type": "boolean", + "default": false, + "description": "Whether this MCP server must initialize before the first turn. Defaults to `false`." + }, + "connection_origin": { + "anyOf": [ + { + "$ref": "#/components/schemas/McpConnectionOriginParam" + }, + { + "type": "null" + } + ], + "description": "Selects where outbound MCP HTTP connections originate. Omitted or `service` uses the Managed Agents service network; `environment` uses the session's selected environment." + } + }, + "required": [ + "type", + "server_label", + "transport" + ], + "additionalProperties": false, + "description": "Tools provided by a remote MCP server." + }, + "AgentToolConfigParamWebSearch": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "web_search" + ], + "default": "web_search", + "x-stainless-const": true, + "description": "The type of the object. Always `web_search`." + }, + "mode": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebSearchModeParam" + }, + { + "type": "null" + } + ], + "description": "The source used for web search results. Defaults to `live`." + }, + "context_size": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebSearchContextSizeParam" + }, + { + "type": "null" + } + ], + "description": "The amount of search context made available to the model. Defaults to `medium`." + }, + "allowed_domains": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Domains the search may include." + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebSearchLocationParam" + }, + { + "type": "null" + } + ], + "description": "Approximate location used to localize search results." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Web search." + }, + "AgentToolConfigParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/AgentToolConfigParamFunction" + }, + { + "$ref": "#/components/schemas/AgentToolConfigParamToolSearch" + }, + { + "$ref": "#/components/schemas/AgentToolConfigParamProgrammaticToolCalling" + }, + { + "$ref": "#/components/schemas/AgentToolConfigParamMcp" + }, + { + "$ref": "#/components/schemas/AgentToolConfigParamWebSearch" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "function": "#/components/schemas/AgentToolConfigParamFunction", + "tool_search": "#/components/schemas/AgentToolConfigParamToolSearch", + "programmatic_tool_calling": "#/components/schemas/AgentToolConfigParamProgrammaticToolCalling", + "mcp": "#/components/schemas/AgentToolConfigParamMcp", + "web_search": "#/components/schemas/AgentToolConfigParamWebSearch" + } + }, + "x-oai-discriminator-values": [ + "function", + "tool_search", + "programmatic_tool_calling", + "mcp", + "web_search" + ], + "description": "A tool available to the agent." + }, + "SessionAgentConfigParam": { + "type": "object", + "properties": { + "model": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The model to use for the agent. The requested model name is preserved." + }, + "reasoning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ReasoningParam" + }, + { + "type": "null" + } + ], + "description": "Configuration for model reasoning. Omit to keep the current settings; pass `null` to reset to the model's default effort." + }, + "text": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextParam" + }, + { + "type": "null" + } + ], + "description": "Configuration for text generated by the agent." + }, + "service_tier": { + "anyOf": [ + { + "$ref": "#/components/schemas/ServiceTierParam" + }, + { + "type": "null" + } + ], + "description": "The service tier used for model requests." + }, + "instructions": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "Additional instructions appended to the agent's default base instructions. Omit to leave unchanged." + }, + "multi_agent": { + "anyOf": [ + { + "$ref": "#/components/schemas/MultiAgentConfigCurrentParam" + }, + { + "type": "null" + } + ], + "description": "Configuration for creating and coordinating subagents." + }, + "tools": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/AgentToolConfigParam" + }, + "minItems": 0, + "maxItems": 16384, + "description": "Tools available to the agent. Omit to inherit, or pass null to clear them." + }, + "x_agents_core": { + "anyOf": [ + { + "$ref": "#/components/schemas/v1.AgentsCore" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "description": "Agent configuration for a session. Omitted fields inherit from `agent_id` when supplied. Supplied objects and arrays replace the whole field; null resets nullable fields." + }, + "EnvironmentParamNone": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none" + ], + "default": "none", + "x-stainless-const": true, + "description": "The type of the object. Always `none`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Runs the agent without an execution environment." + }, + "EnvironmentParamOpenaiHosted": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "openai_hosted" + ], + "default": "openai_hosted", + "x-stainless-const": true, + "description": "The type of the object. Always `openai_hosted`." + }, + "packages": { + "anyOf": [ + { + "$ref": "#/components/schemas/EnvironmentPackagesParam" + }, + { + "type": "null" + } + ], + "description": "Packages to install in the environment. Defaults to empty package lists." + }, + "setup_commands": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SetupCommandParam" + }, + "minItems": 0, + "maxItems": 16, + "description": "Ordered, confidential setup commands. Command bodies are never returned." + }, + "network": { + "anyOf": [ + { + "$ref": "#/components/schemas/NetworkPolicyParam" + }, + { + "type": "null" + } + ], + "description": "Network access policy for the environment. Defaults to enabled." + }, + "env": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "Environment variables made available to the agent." + }, + "capability_directories": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Directories that contain capabilities exposed to the agent. Defaults to an empty list." + }, + "skills": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/HostedSkillParam" + }, + "minItems": 0, + "maxItems": 200, + "description": "Skills referenced by ID or provided as inline ZIP archives. Defaults to an empty list." + }, + "plugins": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/HostedPluginParam" + }, + "minItems": 0, + "maxItems": 32, + "description": "Plugins provided as inline ZIP archives. Defaults to an empty list." + }, + "files": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/HostedEnvironmentFileParam" + }, + "minItems": 0, + "maxItems": 50, + "description": "Files available before the agent starts. Defaults to an empty list." + }, + "environment_template_id": { + "type": "string", + "minLength": 0, + "maxLength": 64, + "description": "A reusable hosted template applied before inline session configuration. Omitted fields inherit the template; network overrides cannot broaden its policy." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "An OpenAI-hosted environment, optionally based on a reusable template." + }, + "EnvironmentParamSelfHosted": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "self_hosted" + ], + "default": "self_hosted", + "x-stainless-const": true, + "description": "The type of the object. Always `self_hosted`." + }, + "workspace_directory": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "Absolute project directory inside the self-hosted environment." + }, + "capability_directories": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "Directories that contain capabilities exposed to the agent. Defaults to an empty list." + } + }, + "required": [ + "type", + "workspace_directory" + ], + "additionalProperties": false, + "description": "An application-hosted environment configured inline." + }, + "EnvironmentParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/EnvironmentParamNone" + }, + { + "$ref": "#/components/schemas/EnvironmentParamOpenaiHosted" + }, + { + "$ref": "#/components/schemas/EnvironmentParamSelfHosted" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "none": "#/components/schemas/EnvironmentParamNone", + "openai_hosted": "#/components/schemas/EnvironmentParamOpenaiHosted", + "self_hosted": "#/components/schemas/EnvironmentParamSelfHosted" + } + }, + "x-oai-discriminator-values": [ + "none", + "openai_hosted", + "self_hosted" + ], + "description": "The execution environment and optional reusable template for a session." + }, + "InputContentParamInputText": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "input_text" + ], + "default": "input_text", + "x-stainless-const": true, + "description": "The type of the object. Always `input_text`." + }, + "text": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The text sent to the model." + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false, + "description": "Text input to the model." + }, + "InputContentParamInputImage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "input_image" + ], + "default": "input_image", + "x-stainless-const": true, + "description": "The type of the object. Always `input_image`." + }, + "image_url": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The URL of the image sent to the model." + } + }, + "required": [ + "type", + "image_url" + ], + "additionalProperties": false, + "description": "Image input to the model." + }, + "InputContentParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/InputContentParamInputText" + }, + { + "$ref": "#/components/schemas/InputContentParamInputImage" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "input_text": "#/components/schemas/InputContentParamInputText", + "input_image": "#/components/schemas/InputContentParamInputImage" + } + }, + "x-oai-discriminator-values": [ + "input_text", + "input_image" + ], + "description": "Content included in an input message." + }, + "InputMessageParam": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "message" + ], + "default": "message", + "x-stainless-const": true, + "description": "The type of the input item. Always `message`." + }, + "role": { + "type": "string", + "enum": [ + "user" + ], + "default": "user", + "x-stainless-const": true, + "description": "The role of the message author. Always `user`." + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InputContentParam" + }, + "minItems": 0, + "maxItems": 16384, + "description": "The content of the message." + } + }, + "required": [ + "role", + "content" + ], + "additionalProperties": false, + "description": "A user message submitted to a session." + }, + "CreateSessionInputParam": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1048576 + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/InputMessageParam" + }, + "minItems": 0, + "maxItems": 16384 + } + ], + "description": "Initial input submitted when creating a session." + }, + "CreateAgentSessionParams": { + "type": "object", + "properties": { + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 512 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "minProperties": 0, + "maxProperties": 16, + "description": "Up to 16 string key-value pairs, with keys up to 64 and values up to 512 characters. Omission or null defaults to an empty map." + }, + "agent": { + "$ref": "#/components/schemas/SessionAgentConfigParam", + "description": "Agent configuration. With `agent_id`, supplied fields override the saved agent for this session. Without `agent_id`, `model` is required." + }, + "agent_id": { + "type": "string", + "minLength": 0, + "maxLength": 64, + "description": "The ID of a saved reusable agent. Omit `agent` to use its configuration unchanged." + }, + "environment": { + "$ref": "#/components/schemas/EnvironmentParam", + "description": "An inline execution environment or a reference to an environment template." + }, + "vault_ids": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "minItems": 0, + "maxItems": 16384, + "description": "The IDs of vaults made available to the session." + }, + "input": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreateSessionInputParam" + }, + { + "type": "null" + } + ], + "description": "Initial input to submit when the session is created. A string is shorthand for a single user message. Required when `environment.type` is `none`, or when `stream` is `true` for an environment that is not `self_hosted`; optional for self-hosted and non-streaming execution environments." + }, + "stream": { + "type": "boolean", + "default": false, + "description": "Whether to stream session events as server-sent events. Defaults to `false`." + }, + "x_agents_core": { + "anyOf": [ + { + "$ref": "#/components/schemas/v1.SessionExecutionInput" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "environment" + ], + "additionalProperties": false, + "description": "Parameters for creating a Managed Agents session." + }, + "SessionErrorResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "minLength": 0, + "description": "The error type." + }, + "code": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The machine-readable error code, if any." + }, + "message": { + "type": "string", + "minLength": 0, + "description": "A customer-safe explanation of the error." + }, + "param": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The request parameter associated with the error, if any." + } + }, + "required": [ + "type", + "code", + "message", + "param" + ], + "additionalProperties": false, + "description": "An error payload with the same public fields as Responses API streaming errors." + }, + "SessionEventError": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "error" + ], + "default": "error", + "x-stainless-const": true, + "description": "The type of the object. Always `error`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "error": { + "$ref": "#/components/schemas/SessionErrorResource", + "description": "The error that occurred." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "error" + ], + "additionalProperties": false, + "description": "Emitted when a turn or session fails.", + "x-oaiMeta": { + "example": { + "type": "error", + "event_id": "event_123", + "session_id": "sess_123", + "error": { + "type": "server_error", + "code": null, + "message": "The session failed due to an internal server error.", + "param": null + } + } + } + }, + "SessionEnvironmentStatusResource": { + "type": "string", + "enum": [ + "pending", + "ready", + "connected", + "disconnected", + "failed" + ], + "x-enumDescriptions": [ + "The environment is being prepared.", + "The environment is ready to connect.", + "The environment is connected.", + "The environment is disconnected.", + "The environment failed to connect." + ], + "description": "The connection status of a session environment." + }, + "SessionEnvironmentErrorResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "minLength": 0, + "description": "The error type." + }, + "code": { + "type": "string", + "minLength": 0, + "description": "A machine-readable error code." + }, + "message": { + "type": "string", + "minLength": 0, + "description": "A human-readable error message." + } + }, + "required": [ + "type", + "code", + "message" + ], + "additionalProperties": false, + "description": "An error reported while preparing a session environment." + }, + "SessionEnvironmentStateResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The public ID of the environment." + }, + "type": { + "type": "string", + "minLength": 0, + "description": "The environment type." + }, + "status": { + "$ref": "#/components/schemas/SessionEnvironmentStatusResource", + "description": "The environment's connection status." + }, + "error": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionEnvironmentErrorResource" + }, + { + "type": "null" + } + ], + "description": "The error reported while preparing the environment, if any." + } + }, + "required": [ + "id", + "type", + "status", + "error" + ], + "additionalProperties": false, + "description": "The current state of a session environment." + }, + "SessionEventAgentSessionEnvironmentReady": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.environment.ready" + ], + "default": "agent.session.environment.ready", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.environment.ready`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "environment": { + "$ref": "#/components/schemas/SessionEnvironmentStateResource", + "description": "The current environment state." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "environment" + ], + "additionalProperties": false, + "description": "Emitted when a hosted session environment is ready to connect." + }, + "SessionEventAgentOutputCommandExecutionOutputDelta": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.output.command_execution_output.delta" + ], + "default": "agent.output.command_execution_output.delta", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.output.command_execution_output.delta`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "item_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the command execution item." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output." + }, + "delta": { + "type": "string", + "minLength": 0, + "description": "The output text that was appended." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "item_id", + "output_index", + "delta" + ], + "additionalProperties": false, + "description": "Emitted when command execution produces an output delta." + }, + "SessionEventAgentSessionCreated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.created" + ], + "default": "agent.session.created", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.created`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session": { + "$ref": "#/components/schemas/SessionResource", + "description": "The session that was created." + } + }, + "required": [ + "type", + "event_id", + "session" + ], + "additionalProperties": false, + "description": "Emitted when a session is created." + }, + "SessionEventAgentSessionTurnCreated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.created" + ], + "default": "agent.session.turn.created", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.created`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn associated with the event." + }, + "turn": { + "$ref": "#/components/schemas/TurnResource", + "description": "The turn at the time it was created." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "turn" + ], + "additionalProperties": false, + "description": "Emitted when a turn is created." + }, + "SessionEventAgentSessionTurnInProgress": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.in_progress" + ], + "default": "agent.session.turn.in_progress", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.in_progress`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn associated with the event." + }, + "turn": { + "$ref": "#/components/schemas/TurnResource", + "description": "The turn at the time it started running." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "turn" + ], + "additionalProperties": false, + "description": "Emitted when a turn starts running." + }, + "SessionEventAgentSessionTurnCompleted": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.completed" + ], + "default": "agent.session.turn.completed", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.completed`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn associated with the event." + }, + "turn": { + "$ref": "#/components/schemas/TurnResource", + "description": "The completed turn." + }, + "usage": { + "anyOf": [ + { + "$ref": "#/components/schemas/TokenUsageResource" + }, + { + "type": "null" + } + ], + "description": "Token usage by the root agent during the turn, when available." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "turn", + "usage" + ], + "additionalProperties": false, + "description": "Emitted when a turn completes." + }, + "SessionEventAgentSessionTurnFailed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.failed" + ], + "default": "agent.session.turn.failed", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.failed`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn associated with the event." + }, + "turn": { + "$ref": "#/components/schemas/TurnResource", + "description": "The failed turn." + }, + "usage": { + "anyOf": [ + { + "$ref": "#/components/schemas/TokenUsageResource" + }, + { + "type": "null" + } + ], + "description": "Token usage by the root agent during the turn, when available." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "turn", + "usage" + ], + "additionalProperties": false, + "description": "Emitted when a turn fails." + }, + "SessionEventAgentSessionTurnCancelled": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.cancelled" + ], + "default": "agent.session.turn.cancelled", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.cancelled`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn associated with the event." + }, + "turn": { + "$ref": "#/components/schemas/TurnResource", + "description": "The cancelled turn." + }, + "usage": { + "anyOf": [ + { + "$ref": "#/components/schemas/TokenUsageResource" + }, + { + "type": "null" + } + ], + "description": "Token usage by the root agent during the turn, when available." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "turn", + "usage" + ], + "additionalProperties": false, + "description": "Emitted when a turn is cancelled." + }, + "SessionEventAgentSessionTurnItemAdded": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.item.added" + ], + "default": "agent.session.turn.item.added", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.item.added`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "output_index": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output, when the item is agent output." + }, + "item": { + "$ref": "#/components/schemas/SessionTurnItemResource", + "description": "The item that was added." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "output_index", + "item" + ], + "additionalProperties": false, + "description": "Emitted when an item is added to a turn." + }, + "SessionEventAgentSessionIdle": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.idle" + ], + "default": "agent.session.idle", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.idle`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session": { + "$ref": "#/components/schemas/SessionResource", + "description": "The session that became idle." + } + }, + "required": [ + "type", + "event_id", + "session" + ], + "additionalProperties": false, + "description": "Emitted when a session becomes idle." + }, + "SessionEventAgentSessionInProgress": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.in_progress" + ], + "default": "agent.session.in_progress", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.in_progress`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session": { + "$ref": "#/components/schemas/SessionResource", + "description": "The session that started processing." + } + }, + "required": [ + "type", + "event_id", + "session" + ], + "additionalProperties": false, + "description": "Emitted when a session starts processing a turn." + }, + "SessionEventAgentSessionRequiresAction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.requires_action" + ], + "default": "agent.session.requires_action", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.requires_action`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session": { + "$ref": "#/components/schemas/SessionResource", + "description": "The session and its current required actions." + } + }, + "required": [ + "type", + "event_id", + "session" + ], + "additionalProperties": false, + "description": "Emitted when a session is waiting for one or more required actions." + }, + "SessionEventAgentSessionFailed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.failed" + ], + "default": "agent.session.failed", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.failed`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session": { + "$ref": "#/components/schemas/SessionResource", + "description": "The failed session." + } + }, + "required": [ + "type", + "event_id", + "session" + ], + "additionalProperties": false, + "description": "Emitted when a session fails." + }, + "SessionEventAgentSessionEnvironmentPending": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.environment.pending" + ], + "default": "agent.session.environment.pending", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.environment.pending`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "environment": { + "$ref": "#/components/schemas/SessionEnvironmentStateResource", + "description": "The current environment state." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "environment" + ], + "additionalProperties": false, + "description": "Emitted while a session environment is being prepared." + }, + "SessionEventAgentSessionEnvironmentConnected": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.environment.connected" + ], + "default": "agent.session.environment.connected", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.environment.connected`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "environment": { + "$ref": "#/components/schemas/SessionEnvironmentStateResource", + "description": "The current environment state." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "environment" + ], + "additionalProperties": false, + "description": "Emitted when a session environment connects." + }, + "SessionEventAgentSessionEnvironmentDisconnected": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.environment.disconnected" + ], + "default": "agent.session.environment.disconnected", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.environment.disconnected`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "environment": { + "$ref": "#/components/schemas/SessionEnvironmentStateResource", + "description": "The current environment state." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "environment" + ], + "additionalProperties": false, + "description": "Emitted when a session environment disconnects." + }, + "SessionEventAgentSessionEnvironmentFailed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.environment.failed" + ], + "default": "agent.session.environment.failed", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.environment.failed`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "environment": { + "$ref": "#/components/schemas/SessionEnvironmentStateResource", + "description": "The current environment state." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "environment" + ], + "additionalProperties": false, + "description": "Emitted when a session environment fails." + }, + "SessionEventAgentSessionSubagentCreated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.subagent.created" + ], + "default": "agent.session.subagent.created", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.subagent.created`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "subagent": { + "$ref": "#/components/schemas/SubagentResource", + "description": "The subagent that was created." + } + }, + "required": [ + "type", + "event_id", + "subagent" + ], + "additionalProperties": false, + "description": "Emitted when a subagent is created." + }, + "SessionEventAgentSessionSubagentActive": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.subagent.active" + ], + "default": "agent.session.subagent.active", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.subagent.active`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "subagent": { + "$ref": "#/components/schemas/SubagentResource", + "description": "The subagent that resumed." + } + }, + "required": [ + "type", + "event_id", + "subagent" + ], + "additionalProperties": false, + "description": "Emitted when a closed subagent successfully resumes." + }, + "SessionEventAgentSessionSubagentClosed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.subagent.closed" + ], + "default": "agent.session.subagent.closed", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.subagent.closed`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "subagent": { + "$ref": "#/components/schemas/SubagentResource", + "description": "The subagent that was closed." + } + }, + "required": [ + "type", + "event_id", + "subagent" + ], + "additionalProperties": false, + "description": "Emitted when a subagent is closed." + }, + "AssistantMessageItemResource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "message" + ], + "default": "message", + "x-stainless-const": true, + "description": "The item type. Always `message`." + }, + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the message." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the turn that contains this item." + }, + "role": { + "type": "string", + "enum": [ + "assistant" + ], + "default": "assistant", + "x-stainless-const": true, + "description": "The role of the message author. Always `assistant`." + }, + "status": { + "$ref": "#/components/schemas/OutputItemStatusResource", + "description": "The status of the message." + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OutputTextResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The content of the message." + }, + "phase": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessagePhaseResource" + }, + { + "type": "null" + } + ], + "description": "The phase of the assistant message." + } + }, + "required": [ + "type", + "id", + "turn_id", + "role", + "status", + "content", + "phase" + ], + "additionalProperties": false, + "description": "An assistant message produced by the agent." + }, + "AgentOutputItemResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/AssistantMessageItemResource" + }, + { + "$ref": "#/components/schemas/ReasoningItemResource" + }, + { + "$ref": "#/components/schemas/FunctionCallItemResource" + }, + { + "$ref": "#/components/schemas/McpCallItemResource" + }, + { + "$ref": "#/components/schemas/WebSearchCallItemResource" + }, + { + "$ref": "#/components/schemas/CommandExecutionItemResource" + }, + { + "$ref": "#/components/schemas/CreateSubagentCallItemResource" + }, + { + "$ref": "#/components/schemas/SendSubagentInputCallItemResource" + }, + { + "$ref": "#/components/schemas/ResumeSubagentCallItemResource" + }, + { + "$ref": "#/components/schemas/WaitForSubagentsCallItemResource" + }, + { + "$ref": "#/components/schemas/InterruptSubagentCallItemResource" + }, + { + "$ref": "#/components/schemas/CloseSubagentCallItemResource" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "message": "#/components/schemas/AssistantMessageItemResource", + "reasoning": "#/components/schemas/ReasoningItemResource", + "function_call": "#/components/schemas/FunctionCallItemResource", + "mcp_call": "#/components/schemas/McpCallItemResource", + "web_search_call": "#/components/schemas/WebSearchCallItemResource", + "command_execution": "#/components/schemas/CommandExecutionItemResource", + "interrupt_subagent_call": "#/components/schemas/InterruptSubagentCallItemResource", + "create_subagent_call": "#/components/schemas/CreateSubagentCallItemResource", + "send_subagent_input_call": "#/components/schemas/SendSubagentInputCallItemResource", + "resume_subagent_call": "#/components/schemas/ResumeSubagentCallItemResource", + "wait_for_subagents_call": "#/components/schemas/WaitForSubagentsCallItemResource", + "close_subagent_call": "#/components/schemas/CloseSubagentCallItemResource" + } + }, + "x-oai-discriminator-values": [ + "message", + "reasoning", + "function_call", + "mcp_call", + "web_search_call", + "command_execution", + "create_subagent_call", + "send_subagent_input_call", + "resume_subagent_call", + "wait_for_subagents_call", + "interrupt_subagent_call", + "close_subagent_call" + ], + "description": "An output item produced by an agent." + }, + "SessionEventAgentSessionTurnItemDone": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.item.done" + ], + "default": "agent.session.turn.item.done", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.item.done`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the output item in the turn output." + }, + "item": { + "$ref": "#/components/schemas/AgentOutputItemResource", + "description": "The completed output item." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "output_index", + "item" + ], + "additionalProperties": false, + "description": "Emitted when an output item is complete." + }, + "SessionEventAgentSessionTurnContentPartAdded": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.content_part.added" + ], + "default": "agent.session.turn.content_part.added", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.content_part.added`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "item_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the message item." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output." + }, + "content_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the content part in the message." + }, + "part": { + "$ref": "#/components/schemas/OutputTextResource", + "description": "The initial content part." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "item_id", + "output_index", + "content_index", + "part" + ], + "additionalProperties": false, + "description": "Emitted when an output text content part is added." + }, + "SessionEventAgentSessionTurnContentPartDone": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.content_part.done" + ], + "default": "agent.session.turn.content_part.done", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.content_part.done`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "item_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the message item." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output." + }, + "content_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the content part in the message." + }, + "part": { + "$ref": "#/components/schemas/OutputTextResource", + "description": "The completed content part." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "item_id", + "output_index", + "content_index", + "part" + ], + "additionalProperties": false, + "description": "Emitted when an output content part is complete." + }, + "SessionEventAgentSessionTurnOutputTextDelta": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.output_text.delta" + ], + "default": "agent.session.turn.output_text.delta", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.output_text.delta`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "item_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the message item." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output." + }, + "content_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the content part in the message." + }, + "delta": { + "type": "string", + "minLength": 0, + "description": "The text that was appended." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "item_id", + "output_index", + "content_index", + "delta" + ], + "additionalProperties": false, + "description": "Emitted when text is appended to an output text content part." + }, + "SessionEventAgentSessionTurnOutputTextDone": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.output_text.done" + ], + "default": "agent.session.turn.output_text.done", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.output_text.done`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "item_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the message item." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output." + }, + "content_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the content part in the message." + }, + "text": { + "type": "string", + "minLength": 0, + "description": "The complete output text." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "item_id", + "output_index", + "content_index", + "text" + ], + "additionalProperties": false, + "description": "Emitted when an output text content part is complete." + }, + "SessionEventAgentSessionTurnReasoningSummaryPartAdded": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.reasoning_summary_part.added" + ], + "default": "agent.session.turn.reasoning_summary_part.added", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.reasoning_summary_part.added`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "item_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the reasoning item." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output." + }, + "summary_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the summary content part." + }, + "part": { + "$ref": "#/components/schemas/SummaryTextResource", + "description": "The initial summary part." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "item_id", + "output_index", + "summary_index", + "part" + ], + "additionalProperties": false, + "description": "Emitted when a reasoning summary content part is added." + }, + "SessionEventAgentSessionTurnReasoningSummaryPartDone": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.reasoning_summary_part.done" + ], + "default": "agent.session.turn.reasoning_summary_part.done", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.reasoning_summary_part.done`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "item_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the reasoning item." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output." + }, + "summary_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the summary part." + }, + "part": { + "$ref": "#/components/schemas/SummaryTextResource", + "description": "The completed summary part." + }, + "status": { + "type": [ + "string", + "null" + ], + "enum": [ + "incomplete", + null + ], + "description": "Present as `incomplete` when summary generation was interrupted.", + "x-stainless-const": true + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "item_id", + "output_index", + "summary_index", + "part", + "status" + ], + "additionalProperties": false, + "description": "Emitted when a reasoning summary part is complete." + }, + "SessionEventAgentSessionTurnReasoningSummaryTextDelta": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.reasoning_summary_text.delta" + ], + "default": "agent.session.turn.reasoning_summary_text.delta", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.reasoning_summary_text.delta`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "item_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the reasoning item." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output." + }, + "summary_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the summary content part." + }, + "delta": { + "type": "string", + "minLength": 0, + "description": "The summary text that was appended." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "item_id", + "output_index", + "summary_index", + "delta" + ], + "additionalProperties": false, + "description": "Emitted when text is appended to a reasoning summary." + }, + "SessionEventAgentSessionTurnReasoningSummaryTextDone": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.turn.reasoning_summary_text.done" + ], + "default": "agent.session.turn.reasoning_summary_text.done", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.turn.reasoning_summary_text.done`." + }, + "event_id": { + "type": "string", + "minLength": 0, + "description": "The unique ID of the event." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session associated with the event." + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the turn associated with the event, when applicable." + }, + "item_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the reasoning item." + }, + "output_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the item in the turn output." + }, + "summary_index": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 4294967295, + "description": "The index of the summary content part." + }, + "text": { + "type": "string", + "minLength": 0, + "description": "The complete reasoning summary text." + } + }, + "required": [ + "type", + "event_id", + "session_id", + "turn_id", + "item_id", + "output_index", + "summary_index", + "text" + ], + "additionalProperties": false, + "description": "Emitted when a reasoning summary content part is complete." + }, + "SessionEvent": { + "oneOf": [ + { + "$ref": "#/components/schemas/SessionEventError" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionEnvironmentReady" + }, + { + "$ref": "#/components/schemas/SessionEventAgentOutputCommandExecutionOutputDelta" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionCreated" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnCreated" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnInProgress" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnCompleted" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnFailed" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnCancelled" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnItemAdded" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionIdle" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionInProgress" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionRequiresAction" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionFailed" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionEnvironmentPending" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionEnvironmentConnected" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionEnvironmentDisconnected" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionEnvironmentFailed" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionSubagentCreated" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionSubagentActive" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionSubagentClosed" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnItemDone" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnContentPartAdded" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnContentPartDone" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnOutputTextDelta" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnOutputTextDone" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnReasoningSummaryPartAdded" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnReasoningSummaryPartDone" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnReasoningSummaryTextDelta" + }, + { + "$ref": "#/components/schemas/SessionEventAgentSessionTurnReasoningSummaryTextDone" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "error": "#/components/schemas/SessionEventError", + "agent.session.environment.ready": "#/components/schemas/SessionEventAgentSessionEnvironmentReady", + "agent.output.command_execution_output.delta": "#/components/schemas/SessionEventAgentOutputCommandExecutionOutputDelta", + "agent.session.created": "#/components/schemas/SessionEventAgentSessionCreated", + "agent.session.turn.created": "#/components/schemas/SessionEventAgentSessionTurnCreated", + "agent.session.turn.in_progress": "#/components/schemas/SessionEventAgentSessionTurnInProgress", + "agent.session.turn.completed": "#/components/schemas/SessionEventAgentSessionTurnCompleted", + "agent.session.turn.failed": "#/components/schemas/SessionEventAgentSessionTurnFailed", + "agent.session.turn.cancelled": "#/components/schemas/SessionEventAgentSessionTurnCancelled", + "agent.session.turn.item.added": "#/components/schemas/SessionEventAgentSessionTurnItemAdded", + "agent.session.idle": "#/components/schemas/SessionEventAgentSessionIdle", + "agent.session.in_progress": "#/components/schemas/SessionEventAgentSessionInProgress", + "agent.session.requires_action": "#/components/schemas/SessionEventAgentSessionRequiresAction", + "agent.session.failed": "#/components/schemas/SessionEventAgentSessionFailed", + "agent.session.environment.pending": "#/components/schemas/SessionEventAgentSessionEnvironmentPending", + "agent.session.environment.connected": "#/components/schemas/SessionEventAgentSessionEnvironmentConnected", + "agent.session.environment.disconnected": "#/components/schemas/SessionEventAgentSessionEnvironmentDisconnected", + "agent.session.environment.failed": "#/components/schemas/SessionEventAgentSessionEnvironmentFailed", + "agent.session.subagent.created": "#/components/schemas/SessionEventAgentSessionSubagentCreated", + "agent.session.subagent.active": "#/components/schemas/SessionEventAgentSessionSubagentActive", + "agent.session.subagent.closed": "#/components/schemas/SessionEventAgentSessionSubagentClosed", + "agent.session.turn.item.done": "#/components/schemas/SessionEventAgentSessionTurnItemDone", + "agent.session.turn.content_part.added": "#/components/schemas/SessionEventAgentSessionTurnContentPartAdded", + "agent.session.turn.content_part.done": "#/components/schemas/SessionEventAgentSessionTurnContentPartDone", + "agent.session.turn.output_text.delta": "#/components/schemas/SessionEventAgentSessionTurnOutputTextDelta", + "agent.session.turn.output_text.done": "#/components/schemas/SessionEventAgentSessionTurnOutputTextDone", + "agent.session.turn.reasoning_summary_part.added": "#/components/schemas/SessionEventAgentSessionTurnReasoningSummaryPartAdded", + "agent.session.turn.reasoning_summary_part.done": "#/components/schemas/SessionEventAgentSessionTurnReasoningSummaryPartDone", + "agent.session.turn.reasoning_summary_text.delta": "#/components/schemas/SessionEventAgentSessionTurnReasoningSummaryTextDelta", + "agent.session.turn.reasoning_summary_text.done": "#/components/schemas/SessionEventAgentSessionTurnReasoningSummaryTextDone" + } + }, + "x-oai-discriminator-values": [ + "error", + "agent.session.environment.ready", + "agent.output.command_execution_output.delta", + "agent.session.created", + "agent.session.turn.created", + "agent.session.turn.in_progress", + "agent.session.turn.completed", + "agent.session.turn.failed", + "agent.session.turn.cancelled", + "agent.session.turn.item.added", + "agent.session.idle", + "agent.session.in_progress", + "agent.session.requires_action", + "agent.session.failed", + "agent.session.environment.pending", + "agent.session.environment.connected", + "agent.session.environment.disconnected", + "agent.session.environment.failed", + "agent.session.subagent.created", + "agent.session.subagent.active", + "agent.session.subagent.closed", + "agent.session.turn.item.done", + "agent.session.turn.content_part.added", + "agent.session.turn.content_part.done", + "agent.session.turn.output_text.delta", + "agent.session.turn.output_text.done", + "agent.session.turn.reasoning_summary_part.added", + "agent.session.turn.reasoning_summary_part.done", + "agent.session.turn.reasoning_summary_text.delta", + "agent.session.turn.reasoning_summary_text.done" + ], + "description": "An event emitted by a Managed Agents session." + }, + "UpdateAgentSessionParams": { + "type": "object", + "properties": { + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 512 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "minProperties": 0, + "maxProperties": 16, + "description": "Replaces all metadata. Omit to leave unchanged, or pass null or {} to clear it. Up to 16 string key-value pairs, with keys up to 64 and values up to 512 characters." + } + }, + "additionalProperties": false, + "description": "Fields to update on an existing session." + }, + "DeletedSessionResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the deleted session." + }, + "object": { + "type": "string", + "enum": [ + "agent.session.deleted" + ], + "default": "agent.session.deleted", + "x-stainless-const": true, + "description": "The object type. Always `agent.session.deleted`." + }, + "deleted": { + "type": "boolean", + "description": "Whether the session has been removed from the public API. Always `true`. Physical cleanup may still be in progress." + } + }, + "required": [ + "id", + "object", + "deleted" + ], + "additionalProperties": false, + "description": "A Managed Agents session removed from the public API. Physical cleanup may continue asynchronously." + }, + "SessionArtifactResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The immutable artifact ID." + }, + "object": { + "type": "string", + "enum": [ + "agent.session.artifact" + ], + "default": "agent.session.artifact", + "x-stainless-const": true, + "description": "The object type. Always `agent.session.artifact`." + }, + "session_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the session that owns the artifact." + }, + "environment_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the environment that produced the artifact." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the completed turn that published the artifact." + }, + "path": { + "type": "string", + "minLength": 0, + "description": "The original absolute file path in the execution environment." + }, + "size_bytes": { + "type": "integer", + "format": "int64", + "minimum": 0, + "description": "The immutable artifact size in bytes." + }, + "created_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the artifact was published." + } + }, + "required": [ + "id", + "object", + "session_id", + "environment_id", + "turn_id", + "path", + "size_bytes", + "created_at" + ], + "additionalProperties": false, + "description": "An immutable file published by a completed hosted session turn." + }, + "SessionArtifactListResource": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "default": "list", + "x-stainless-const": true, + "description": "The object type, which is always `list`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionArtifactResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The resources returned in this page, in the requested sort order." + }, + "first_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the first resource in `data`, or `null` if the page is empty." + }, + "last_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the last resource in `data`, or `null` if the page is empty. Pass this as `after` with the same order and filters." + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more resources to retrieve after this page." + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "additionalProperties": false, + "description": "A page of Agents API resources, with IDs for retrieving additional pages." + }, + "DeletedSessionArtifactResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the deleted session artifact." + }, + "object": { + "type": "string", + "enum": [ + "agent.session.artifact.deleted" + ], + "default": "agent.session.artifact.deleted", + "x-stainless-const": true, + "description": "The object type. Always `agent.session.artifact.deleted`." + }, + "deleted": { + "type": "boolean", + "description": "Whether the session artifact was deleted. Always `true`." + } + }, + "required": [ + "id", + "object", + "deleted" + ], + "additionalProperties": false, + "description": "Confirmation that an immutable session artifact was deleted." + }, + "SessionInputParamAgentSessionInputMessage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.input.message" + ], + "default": "agent.session.input.message", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.input.message`." + }, + "input": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InputMessageParam" + }, + "minItems": 0, + "maxItems": 16384, + "description": "The user messages to add to the session." + } + }, + "required": [ + "type", + "input" + ], + "additionalProperties": false, + "description": "Adds one or more user messages and starts a turn." + }, + "SessionInputParamAgentSessionInputCancel": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.input.cancel" + ], + "default": "agent.session.input.cancel", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.input.cancel`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Cancels the session's active turn." + }, + "FunctionCallOutputParam": { + "oneOf": [ + { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/InputContentParam" + }, + "minItems": 0, + "maxItems": 16384 + } + ], + "description": "A function result represented as text or supported model-input content." + }, + "SessionInputParamAgentSessionInputToolResult": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agent.session.input.tool_result" + ], + "default": "agent.session.input.tool_result", + "x-stainless-const": true, + "description": "The type of the object. Always `agent.session.input.tool_result`." + }, + "turn_id": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The ID of the turn that requested the function call." + }, + "call_id": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The ID of the function call." + }, + "success": { + "type": "boolean", + "description": "Whether the function call succeeded." + }, + "output": { + "anyOf": [ + { + "$ref": "#/components/schemas/FunctionCallOutputParam" + }, + { + "type": "null" + } + ], + "description": "The function result when the call succeeded." + }, + "error": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The error message when the call failed." + } + }, + "required": [ + "type", + "turn_id", + "call_id", + "success" + ], + "additionalProperties": false, + "description": "Submits the result of a function call." + }, + "SessionInputParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/SessionInputParamAgentSessionInputMessage" + }, + { + "$ref": "#/components/schemas/SessionInputParamAgentSessionInputCancel" + }, + { + "$ref": "#/components/schemas/SessionInputParamAgentSessionInputToolResult" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "agent.session.input.message": "#/components/schemas/SessionInputParamAgentSessionInputMessage", + "agent.session.input.cancel": "#/components/schemas/SessionInputParamAgentSessionInputCancel", + "agent.session.input.tool_result": "#/components/schemas/SessionInputParamAgentSessionInputToolResult" + } + }, + "x-oai-discriminator-values": [ + "agent.session.input.message", + "agent.session.input.cancel", + "agent.session.input.tool_result" + ], + "description": "Input submitted to an existing session." + }, + "CreateSessionEventsParams": { + "type": "object", + "properties": { + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionInputParam" + }, + "minItems": 0, + "maxItems": 16384, + "description": "The input events to submit to the session." + } + }, + "required": [ + "events" + ], + "additionalProperties": false, + "description": "Input events submitted to an existing session." + }, + "VaultStatusParam": { + "type": "string", + "enum": [ + "active", + "archived" + ], + "description": "Whether a vault or credential is active or archived." + }, + "VaultStatusFilterParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/VaultStatusParam" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/VaultStatusParam" + }, + "minItems": 0, + "maxItems": 16384 + } + ], + "description": "One or more lifecycle statuses to include when listing vaults or credentials." + }, + "VaultResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the vault." + }, + "object": { + "type": "string", + "enum": [ + "vault" + ], + "default": "vault", + "x-stainless-const": true, + "description": "The object type. Always `vault`." + }, + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The human-readable name of the vault, if set." + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 0 + }, + "propertyNames": { + "type": "string", + "minLength": 0 + }, + "minProperties": 0, + "description": "Key-value pairs associated with the vault, such as an application or team identifier." + }, + "created_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the vault was created." + } + }, + "required": [ + "id", + "object", + "name", + "metadata", + "created_at" + ], + "additionalProperties": false, + "description": "A collection of credentials that agent tools can use to authenticate to MCP servers." + }, + "VaultListResource": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "default": "list", + "x-stainless-const": true, + "description": "The object type, which is always `list`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VaultResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The resources returned in this page, in the requested sort order." + }, + "first_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the first resource in `data`, or `null` if the page is empty." + }, + "last_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the last resource in `data`, or `null` if the page is empty. Pass this as `after` with the same order and filters." + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more resources to retrieve after this page." + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "additionalProperties": false, + "description": "A page of Agents API resources, with IDs for retrieving additional pages." + }, + "CreateVaultParams": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 1048576, + "description": "The name is trimmed before storage. It must contain 1 to 256 UTF-8 bytes after trimming." + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 0, + "maxLength": 1048576 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minProperties": 0, + "maxProperties": 1024, + "description": "Key-value pairs to associate with the vault, such as an application or team identifier." + } + }, + "additionalProperties": false, + "description": "Parameters for creating a vault to store credentials used by agent tools." + }, + "DeletedVaultResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the deleted vault." + }, + "object": { + "type": "string", + "enum": [ + "vault.deleted" + ], + "default": "vault.deleted", + "x-stainless-const": true, + "description": "The object type. Always `vault.deleted`." + }, + "deleted": { + "type": "boolean", + "description": "Whether the resource was deleted. Always `true`." + } + }, + "required": [ + "id", + "object", + "deleted" + ], + "additionalProperties": false, + "description": "Confirmation that a vault was deleted." + }, + "McpOauthTokenEndpointAuthResourceNone": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none" + ], + "default": "none", + "x-stainless-const": true, + "description": "The type of the object. Always `none`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Sends the client ID without a client secret." + }, + "McpOauthTokenEndpointAuthResourceClientSecretBasic": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "client_secret_basic" + ], + "default": "client_secret_basic", + "x-stainless-const": true, + "description": "The type of the object. Always `client_secret_basic`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Sends the client ID and secret using HTTP Basic authentication." + }, + "McpOauthTokenEndpointAuthResourceClientSecretPost": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "client_secret_post" + ], + "default": "client_secret_post", + "x-stainless-const": true, + "description": "The type of the object. Always `client_secret_post`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Sends the client ID and secret in the token request body." + }, + "McpOauthTokenEndpointAuthResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/McpOauthTokenEndpointAuthResourceNone" + }, + { + "$ref": "#/components/schemas/McpOauthTokenEndpointAuthResourceClientSecretBasic" + }, + { + "$ref": "#/components/schemas/McpOauthTokenEndpointAuthResourceClientSecretPost" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "none": "#/components/schemas/McpOauthTokenEndpointAuthResourceNone", + "client_secret_basic": "#/components/schemas/McpOauthTokenEndpointAuthResourceClientSecretBasic", + "client_secret_post": "#/components/schemas/McpOauthTokenEndpointAuthResourceClientSecretPost" + } + }, + "x-oai-discriminator-values": [ + "none", + "client_secret_basic", + "client_secret_post" + ], + "description": "The client authentication method used for OAuth token refresh." + }, + "McpOauthRefreshResource": { + "type": "object", + "properties": { + "token_endpoint": { + "type": "string", + "minLength": 0, + "description": "The HTTPS OAuth token endpoint used for refresh." + }, + "client_id": { + "type": "string", + "minLength": 0, + "description": "The OAuth client ID used when requesting a new access token." + }, + "resource": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The resource URI sent to the OAuth token endpoint during refresh, if configured." + }, + "scope": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "Space-separated OAuth scopes requested during refresh, if configured." + }, + "token_endpoint_auth": { + "$ref": "#/components/schemas/McpOauthTokenEndpointAuthResource", + "description": "How the OAuth client authenticates to the token endpoint, excluding its client secret." + } + }, + "required": [ + "token_endpoint", + "client_id", + "resource", + "scope", + "token_endpoint_auth" + ], + "additionalProperties": false, + "description": "Configuration used to refresh an MCP OAuth access token, excluding secret values." + }, + "VaultCredentialAuthResourceMcpOauth": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_oauth" + ], + "default": "mcp_oauth", + "x-stainless-const": true, + "description": "The type of the object. Always `mcp_oauth`." + }, + "mcp_server_url": { + "type": "string", + "minLength": 0, + "description": "The HTTPS MCP server URL authorized by this credential." + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "When the OAuth access token expires, as an RFC 3339 timestamp, if known." + }, + "refresh": { + "anyOf": [ + { + "$ref": "#/components/schemas/McpOauthRefreshResource" + }, + { + "type": "null" + } + ], + "description": "Public refresh metadata without refresh tokens or OAuth client secrets." + } + }, + "required": [ + "type", + "mcp_server_url", + "expires_at", + "refresh" + ], + "additionalProperties": false, + "description": "Public metadata for an OAuth credential; tokens and client secrets are never returned." + }, + "VaultCredentialAuthResourceStaticBearer": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "static_bearer" + ], + "default": "static_bearer", + "x-stainless-const": true, + "description": "The type of the object. Always `static_bearer`." + }, + "mcp_server_url": { + "type": "string", + "minLength": 0, + "description": "The HTTPS MCP server URL authorized by this credential." + } + }, + "required": [ + "type", + "mcp_server_url" + ], + "additionalProperties": false, + "description": "Metadata for a bearer-token credential, without automatic OAuth refresh." + }, + "VaultCredentialAuthResource": { + "oneOf": [ + { + "$ref": "#/components/schemas/VaultCredentialAuthResourceMcpOauth" + }, + { + "$ref": "#/components/schemas/VaultCredentialAuthResourceStaticBearer" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "mcp_oauth": "#/components/schemas/VaultCredentialAuthResourceMcpOauth", + "static_bearer": "#/components/schemas/VaultCredentialAuthResourceStaticBearer" + } + }, + "x-oai-discriminator-values": [ + "mcp_oauth", + "static_bearer" + ], + "description": "The MCP server and authentication configuration of a vault credential, excluding secrets." + }, + "VaultCredentialResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the credential." + }, + "object": { + "type": "string", + "enum": [ + "vault.credential" + ], + "default": "vault.credential", + "x-stainless-const": true, + "description": "The object type. Always `vault.credential`." + }, + "vault_id": { + "type": "string", + "minLength": 0, + "description": "The ID of the vault containing this credential." + }, + "name": { + "type": "string", + "minLength": 0, + "description": "The human-readable name of the credential." + }, + "auth": { + "$ref": "#/components/schemas/VaultCredentialAuthResource", + "description": "The authentication method and non-secret configuration for the MCP server." + }, + "created_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the credential was created." + }, + "updated_at": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp, in seconds, when the credential was last updated." + } + }, + "required": [ + "id", + "object", + "vault_id", + "name", + "auth", + "created_at", + "updated_at" + ], + "additionalProperties": false, + "description": "Metadata for a stored MCP server credential. Secret values are never returned." + }, + "VaultCredentialListResource": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ], + "default": "list", + "x-stainless-const": true, + "description": "The object type, which is always `list`." + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VaultCredentialResource" + }, + "minItems": 0, + "maxItems": 2000, + "description": "The resources returned in this page, in the requested sort order." + }, + "first_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the first resource in `data`, or `null` if the page is empty." + }, + "last_id": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "description": "The ID of the last resource in `data`, or `null` if the page is empty. Pass this as `after` with the same order and filters." + }, + "has_more": { + "type": "boolean", + "description": "Whether there are more resources to retrieve after this page." + } + }, + "required": [ + "object", + "data", + "first_id", + "last_id", + "has_more" + ], + "additionalProperties": false, + "description": "A page of Agents API resources, with IDs for retrieving additional pages." + }, + "CreateMcpOauthTokenEndpointAuthParamNone": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none" + ], + "default": "none", + "x-stainless-const": true, + "description": "The type of the object. Always `none`." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Sends the client ID without a client secret." + }, + "CreateMcpOauthTokenEndpointAuthParamClientSecretBasic": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "client_secret_basic" + ], + "default": "client_secret_basic", + "x-stainless-const": true, + "description": "The type of the object. Always `client_secret_basic`." + }, + "client_secret": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The OAuth client secret to store. Never returned in credential resources." + } + }, + "required": [ + "type", + "client_secret" + ], + "additionalProperties": false, + "description": "Sends the client ID and secret using HTTP Basic authentication." + }, + "CreateMcpOauthTokenEndpointAuthParamClientSecretPost": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "client_secret_post" + ], + "default": "client_secret_post", + "x-stainless-const": true, + "description": "The type of the object. Always `client_secret_post`." + }, + "client_secret": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The OAuth client secret to store. Never returned in credential resources." + } + }, + "required": [ + "type", + "client_secret" + ], + "additionalProperties": false, + "description": "Sends the client ID and secret in the token request body." + }, + "CreateMcpOauthTokenEndpointAuthParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMcpOauthTokenEndpointAuthParamNone" + }, + { + "$ref": "#/components/schemas/CreateMcpOauthTokenEndpointAuthParamClientSecretBasic" + }, + { + "$ref": "#/components/schemas/CreateMcpOauthTokenEndpointAuthParamClientSecretPost" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "none": "#/components/schemas/CreateMcpOauthTokenEndpointAuthParamNone", + "client_secret_basic": "#/components/schemas/CreateMcpOauthTokenEndpointAuthParamClientSecretBasic", + "client_secret_post": "#/components/schemas/CreateMcpOauthTokenEndpointAuthParamClientSecretPost" + } + }, + "x-oai-discriminator-values": [ + "none", + "client_secret_basic", + "client_secret_post" + ], + "description": "Client authentication credentials for OAuth token refresh." + }, + "CreateMcpOauthRefreshParam": { + "type": "object", + "properties": { + "token_endpoint": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The HTTPS OAuth token endpoint used to exchange the refresh token for a new access token." + }, + "client_id": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The OAuth client ID used when requesting a new access token." + }, + "resource": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The resource URI to send to the OAuth token endpoint during refresh, if required." + }, + "scope": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "Space-separated OAuth scopes to request during refresh, if required." + }, + "refresh_token": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The refresh token to store. This secret is never returned in credential resources." + }, + "token_endpoint_auth": { + "$ref": "#/components/schemas/CreateMcpOauthTokenEndpointAuthParam", + "description": "How the OAuth client authenticates to the token endpoint." + } + }, + "required": [ + "token_endpoint", + "client_id", + "refresh_token", + "token_endpoint_auth" + ], + "additionalProperties": false, + "description": "Configuration for refreshing the access token of an MCP OAuth credential." + }, + "CreateVaultCredentialAuthParamMcpOauth": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_oauth" + ], + "default": "mcp_oauth", + "x-stainless-const": true, + "description": "The type of the object. Always `mcp_oauth`." + }, + "mcp_server_url": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The HTTPS MCP server URL authorized by this credential." + }, + "access_token": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "A write-only OAuth access token; never returned by credential resources." + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "When the OAuth access token expires, as an RFC 3339 timestamp, if known." + }, + "refresh": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreateMcpOauthRefreshParam" + }, + { + "type": "null" + } + ], + "description": "Optional refresh configuration for an HTTPS OAuth token endpoint." + } + }, + "required": [ + "type", + "mcp_server_url", + "access_token" + ], + "additionalProperties": false, + "description": "An OAuth credential for an HTTPS MCP destination." + }, + "CreateVaultCredentialAuthParamStaticBearer": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "static_bearer" + ], + "default": "static_bearer", + "x-stainless-const": true, + "description": "The type of the object. Always `static_bearer`." + }, + "mcp_server_url": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The HTTPS MCP server URL authorized by this credential." + }, + "token": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The bearer token to store. This secret is never returned in credential resources." + } + }, + "required": [ + "type", + "mcp_server_url", + "token" + ], + "additionalProperties": false, + "description": "A bearer token for an MCP server, without automatic OAuth refresh." + }, + "CreateVaultCredentialAuthParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateVaultCredentialAuthParamMcpOauth" + }, + { + "$ref": "#/components/schemas/CreateVaultCredentialAuthParamStaticBearer" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "mcp_oauth": "#/components/schemas/CreateVaultCredentialAuthParamMcpOauth", + "static_bearer": "#/components/schemas/CreateVaultCredentialAuthParamStaticBearer" + } + }, + "x-oai-discriminator-values": [ + "mcp_oauth", + "static_bearer" + ], + "description": "Authentication credentials for an MCP server used by agent tools." + }, + "CreateVaultCredentialParams": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 1048576, + "description": "The name is trimmed before storage. It must contain 1 to 256 UTF-8 bytes after trimming." + }, + "auth": { + "$ref": "#/components/schemas/CreateVaultCredentialAuthParam", + "description": "The authentication method and secret values to store for the MCP server." + } + }, + "required": [ + "auth", + "name" + ], + "additionalProperties": false, + "description": "Parameters for storing a credential that authorizes access to an MCP server." + }, + "RotateMcpOauthTokenEndpointAuthParamClientSecretBasic": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "client_secret_basic" + ], + "default": "client_secret_basic", + "x-stainless-const": true, + "description": "The type of the object. Always `client_secret_basic`." + }, + "client_secret": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The replacement OAuth client secret. Omit or pass `null` to keep the stored secret. This secret is never returned in resources." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Updates credentials sent using HTTP Basic authentication." + }, + "RotateMcpOauthTokenEndpointAuthParamClientSecretPost": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "client_secret_post" + ], + "default": "client_secret_post", + "x-stainless-const": true, + "description": "The type of the object. Always `client_secret_post`." + }, + "client_secret": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The replacement OAuth client secret. Omit or pass `null` to keep the stored secret. This secret is never returned in resources." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Updates credentials sent in the token request body." + }, + "RotateMcpOauthTokenEndpointAuthParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/RotateMcpOauthTokenEndpointAuthParamClientSecretBasic" + }, + { + "$ref": "#/components/schemas/RotateMcpOauthTokenEndpointAuthParamClientSecretPost" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "client_secret_basic": "#/components/schemas/RotateMcpOauthTokenEndpointAuthParamClientSecretBasic", + "client_secret_post": "#/components/schemas/RotateMcpOauthTokenEndpointAuthParamClientSecretPost" + } + }, + "x-oai-discriminator-values": [ + "client_secret_basic", + "client_secret_post" + ], + "description": "Client-secret updates that preserve the credential's OAuth authentication method." + }, + "RotateMcpOauthRefreshParam": { + "type": "object", + "properties": { + "refresh_token": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The replacement refresh token. Omit or pass `null` to keep the stored token. This secret is never returned in resources." + }, + "scope": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "Replacement space-separated OAuth scopes for refresh requests. Omit to keep the scopes, or pass `null` to stop sending a scope parameter." + }, + "token_endpoint_auth": { + "anyOf": [ + { + "$ref": "#/components/schemas/RotateMcpOauthTokenEndpointAuthParam" + }, + { + "type": "null" + } + ], + "description": "Client-secret updates for the existing token endpoint authentication method." + } + }, + "additionalProperties": false, + "description": "Updates to an MCP credential's existing OAuth refresh configuration." + }, + "RotateVaultCredentialAuthParamMcpOauth": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_oauth" + ], + "default": "mcp_oauth", + "x-stainless-const": true, + "description": "The type of the object. Always `mcp_oauth`." + }, + "access_token": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "A write-only replacement OAuth access token." + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 1048576, + "description": "The replacement expiry as an RFC 3339 timestamp, or `null` to clear it. Omitting this field preserves the expiry unless a new access token is supplied, in which case the expiry is cleared." + }, + "refresh": { + "anyOf": [ + { + "$ref": "#/components/schemas/RotateMcpOauthRefreshParam" + }, + { + "type": "null" + } + ], + "description": "Optional write-only refresh-token and client-secret updates." + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Rotate an OAuth credential for an HTTPS MCP destination." + }, + "RotateVaultCredentialAuthParamStaticBearer": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "static_bearer" + ], + "default": "static_bearer", + "x-stainless-const": true, + "description": "The type of the object. Always `static_bearer`." + }, + "token": { + "type": "string", + "minLength": 0, + "maxLength": 1048576, + "description": "The replacement bearer token. This secret is never returned in credential resources." + } + }, + "required": [ + "type", + "token" + ], + "additionalProperties": false, + "description": "Replace the bearer token for the credential's MCP server." + }, + "RotateVaultCredentialAuthParam": { + "oneOf": [ + { + "$ref": "#/components/schemas/RotateVaultCredentialAuthParamMcpOauth" + }, + { + "$ref": "#/components/schemas/RotateVaultCredentialAuthParamStaticBearer" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "mcp_oauth": "#/components/schemas/RotateVaultCredentialAuthParamMcpOauth", + "static_bearer": "#/components/schemas/RotateVaultCredentialAuthParamStaticBearer" + } + }, + "x-oai-discriminator-values": [ + "mcp_oauth", + "static_bearer" + ], + "description": "Updates to a vault credential without changing its authentication method or MCP server." + }, + "RotateVaultCredentialParams": { + "type": "object", + "properties": { + "auth": { + "$ref": "#/components/schemas/RotateVaultCredentialAuthParam", + "description": "Replacement values for the credential's existing authentication method." + } + }, + "required": [ + "auth" + ], + "additionalProperties": false, + "description": "Secret, expiry, and OAuth refresh scope updates for an existing vault credential." + }, + "DeletedVaultCredentialResource": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 0, + "description": "The ID of the deleted credential." + }, + "object": { + "type": "string", + "enum": [ + "vault.credential.deleted" + ], + "default": "vault.credential.deleted", + "x-stainless-const": true, + "description": "The object type. Always `vault.credential.deleted`." + }, + "deleted": { + "type": "boolean", + "description": "Whether the resource was deleted. Always `true`." + } + }, + "required": [ + "id", + "object", + "deleted" + ], + "additionalProperties": false, + "description": "Confirmation that a vault credential was deleted." + }, + "v1.AgentsCore": { + "properties": { + "harness": { + "enum": [ + "claude_sdk", + "codex", + "mcode" + ], + "type": "string" + }, + "harness_config": { + "type": "object" + } + }, + "type": "object" + }, + "v1.EnvironmentInstallation": { + "properties": { + "commands": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "expires_at": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "status": { + "enum": [ + "available", + "unavailable" + ], + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object" + }, + "v1.ModelProviderInput": { + "properties": { + "api_key": { + "type": "string" + }, + "base_url": { + "type": "string" + }, + "context_window": { + "type": "integer" + }, + "max_output_tokens": { + "type": "integer" + }, + "protocol": { + "enum": [ + "anthropic", + "responses", + "chat_completions" + ], + "type": "string" + } + }, + "required": [ + "api_key", + "base_url", + "protocol" + ], + "type": "object" + }, + "v1.ModelProviderView": { + "properties": { + "api_key_configured": { + "type": "boolean" + }, + "base_url": { + "type": "string" + }, + "context_window": { + "type": "integer" + }, + "max_output_tokens": { + "type": "integer" + }, + "protocol": { + "enum": [ + "anthropic", + "responses", + "chat_completions" + ], + "type": "string" + } + }, + "required": [ + "api_key_configured", + "base_url", + "protocol" + ], + "type": "object" + }, + "v1.SavedAgentCore": { + "properties": { + "harness": { + "enum": [ + "claude_sdk", + "codex", + "mcode" + ], + "type": "string" + }, + "harness_config": { + "type": "object" + }, + "model_provider": { + "$ref": "#/components/schemas/v1.ModelProviderView" + } + }, + "type": "object" + }, + "v1.SavedAgentCoreInput": { + "properties": { + "harness": { + "enum": [ + "claude_sdk", + "codex", + "mcode" + ], + "type": "string" + }, + "harness_config": { + "type": "object" + }, + "model_provider": { + "anyOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/v1.ModelProviderInput" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "v1.SessionCore": { + "properties": { + "installation": { + "$ref": "#/components/schemas/v1.EnvironmentInstallation" + } + }, + "type": "object" + }, + "v1.SessionExecutionInput": { + "properties": { + "environment": { + "description": "Environment supplies placement-independent preparation through the Core extension.", + "type": "object" + }, + "harness_config": { + "type": "object" + }, + "model_provider": { + "$ref": "#/components/schemas/v1.ModelProviderInput" + } + }, + "type": "object" + } + }, + "responses": { + "TooManyRequests": { + "description": "The request was rejected because a rate limit was exceeded.", + "headers": { + "Retry-After": { + "description": "The minimum number of seconds to wait before retrying. This header is returned when the server has computed a retry delay and may be omitted for 429 responses that require user action.", + "schema": { + "type": "integer", + "minimum": 1 + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "securitySchemes": { + "ProjectKey": { + "type": "http", + "scheme": "bearer", + "description": "OpenAgentCore Project API key." + } + } + }, + "x-oaiMeta": { + "navigationGroups": [ + { + "id": "responses", + "title": "Responses API" + }, + { + "id": "webhooks", + "title": "Webhooks" + }, + { + "id": "endpoints", + "title": "Platform APIs" + }, + { + "id": "vector_stores", + "title": "Vector stores" + }, + { + "id": "chatkit", + "title": "ChatKit", + "beta": true + }, + { + "id": "containers", + "title": "Containers" + }, + { + "id": "live", + "title": "Live (alpha)" + }, + { + "id": "realtime", + "title": "Realtime" + }, + { + "id": "chat", + "title": "Chat Completions" + }, + { + "id": "assistants", + "title": "Assistants", + "deprecated": true + }, + { + "id": "administration", + "title": "Administration" + }, + { + "id": "legacy", + "title": "Legacy" + } + ], + "groups": [ + { + "id": "responses-streaming", + "title": "Streaming events", + "description": "When you [create a Response](https://developers.openai.com/api/reference/resources/responses/methods/create) with\n`stream` set to `true`, the server will emit server-sent events to the\nclient as the Response is generated. This section contains the events that\nare emitted by the server.\n\n[Learn more about streaming responses](https://developers.openai.com/api/docs/guides/streaming-responses).\n", + "navigationGroup": "responses", + "sections": [ + { + "type": "object", + "key": "ResponseCreatedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFailedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseIncompleteEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseOutputItemAddedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseOutputItemDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseContentPartAddedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseContentPartDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseTextDeltaEvent", + "path": "response/output_text/delta" + }, + { + "type": "object", + "key": "ResponseTextDoneEvent", + "path": "response/output_text/done" + }, + { + "type": "object", + "key": "ResponseRefusalDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseRefusalDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFunctionCallArgumentsDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFunctionCallArgumentsDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFileSearchCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFileSearchCallSearchingEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFileSearchCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseWebSearchCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseWebSearchCallSearchingEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseWebSearchCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningSummaryPartAddedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningSummaryPartDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningSummaryTextDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningSummaryTextDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningTextDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningTextDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseImageGenCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseImageGenCallGeneratingEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseImageGenCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseImageGenCallPartialImageEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallArgumentsDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallArgumentsDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallFailedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPListToolsCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPListToolsFailedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPListToolsInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallInterpretingEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallCodeDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallCodeDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseOutputTextAnnotationAddedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseQueuedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCustomToolCallInputDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCustomToolCallInputDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseErrorEvent", + "path": "" + } + ] + }, + { + "id": "responses-websocket-client-events", + "title": "Client events", + "description": "Events sent by the client over a Responses API WebSocket connection.\n", + "navigationGroup": "responses", + "sections": [ + { + "type": "object", + "key": "ResponsesClientEventResponseCreate", + "path": "" + }, + { + "type": "object", + "key": "ResponseSteerEvent", + "path": "" + } + ] + }, + { + "id": "responses-websocket-server-events", + "title": "Server events (WebSocket only)", + "description": "Events emitted only over a Responses API WebSocket connection.\n", + "navigationGroup": "responses", + "sections": [ + { + "type": "object", + "key": "ResponseSteerAcceptedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseSteerPendingEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseSteerFailedEvent", + "path": "" + } + ] + }, + { + "id": "responses-websocket-shared-events", + "title": "Server events", + "description": "These events use the same payloads over WebSocket and\n[HTTP streaming](https://developers.openai.com/api/reference/resources/responses/streaming-events).\n", + "navigationGroup": "responses", + "sections": [ + { + "type": "object", + "key": "ResponseCreatedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFailedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseIncompleteEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseOutputItemAddedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseOutputItemDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseContentPartAddedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseContentPartDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseTextDeltaEvent", + "path": "response/output_text/delta" + }, + { + "type": "object", + "key": "ResponseTextDoneEvent", + "path": "response/output_text/done" + }, + { + "type": "object", + "key": "ResponseRefusalDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseRefusalDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFunctionCallArgumentsDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFunctionCallArgumentsDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFileSearchCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFileSearchCallSearchingEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseFileSearchCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseWebSearchCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseWebSearchCallSearchingEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseWebSearchCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningSummaryPartAddedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningSummaryPartDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningSummaryTextDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningSummaryTextDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningTextDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseReasoningTextDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseImageGenCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseImageGenCallGeneratingEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseImageGenCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseImageGenCallPartialImageEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallArgumentsDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallArgumentsDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallFailedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPListToolsCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPListToolsFailedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseMCPListToolsInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallInProgressEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallInterpretingEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallCodeDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCodeInterpreterCallCodeDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseOutputTextAnnotationAddedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseQueuedEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCustomToolCallInputDeltaEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseCustomToolCallInputDoneEvent", + "path": "" + }, + { + "type": "object", + "key": "ResponseErrorEvent", + "path": "" + } + ] + }, + { + "id": "safety-alerts", + "title": "Safety Alerts", + "description": "Retrieve approved safety alerts with an API key. Project keys require\n`api.safety.alerts.read` and can read alerts from their project.\n", + "navigationGroup": "endpoints", + "sections": [ + { + "type": "endpoint", + "key": "Getprojectsafetyalert", + "path": "retrieve" + }, + { + "type": "object", + "key": "SafetyAlertResource", + "path": "object" + } + ] + }, + { + "id": "webhook-events", + "title": "Webhook Events", + "description": "Webhooks are HTTP requests sent by OpenAI to a URL you specify when certain\nevents happen during the course of API usage.\n\n[Learn more about webhooks](https://developers.openai.com/api/docs/guides/webhooks).\n", + "navigationGroup": "webhooks", + "sections": [ + { + "type": "object", + "key": "WebhookResponseCompleted", + "path": "" + }, + { + "type": "object", + "key": "WebhookResponseCancelled", + "path": "" + }, + { + "type": "object", + "key": "WebhookResponseFailed", + "path": "" + }, + { + "type": "object", + "key": "WebhookResponseIncomplete", + "path": "" + }, + { + "type": "object", + "key": "WebhookBatchCompleted", + "path": "" + }, + { + "type": "object", + "key": "WebhookBatchCancelled", + "path": "" + }, + { + "type": "object", + "key": "WebhookBatchExpired", + "path": "" + }, + { + "type": "object", + "key": "WebhookBatchFailed", + "path": "" + }, + { + "type": "object", + "key": "WebhookFineTuningJobSucceeded", + "path": "" + }, + { + "type": "object", + "key": "WebhookFineTuningJobFailed", + "path": "" + }, + { + "type": "object", + "key": "WebhookFineTuningJobCancelled", + "path": "" + }, + { + "type": "object", + "key": "WebhookEvalRunSucceeded", + "path": "" + }, + { + "type": "object", + "key": "WebhookEvalRunFailed", + "path": "" + }, + { + "type": "object", + "key": "WebhookEvalRunCanceled", + "path": "" + }, + { + "type": "object", + "key": "WebhookRealtimeCallIncoming", + "path": "" + }, + { + "type": "object", + "key": "WebhookLiveCallIncoming", + "path": "" + }, + { + "type": "object", + "key": "WebhookLiveTransportIncoming", + "path": "" + }, + { + "type": "object", + "key": "WebhookSafetyAlertCreated", + "path": "" + }, + { + "type": "object", + "key": "WebhookSafetyOrgAlertCreated", + "path": "" + } + ] + }, + { + "id": "images-streaming", + "title": "Image Streaming", + "description": "Stream image generation and editing in real time with server-sent events.\n[Learn more about image streaming](https://developers.openai.com/api/docs/guides/image-generation).\n", + "navigationGroup": "endpoints", + "sections": [ + { + "type": "object", + "key": "ImageGenPartialImageEvent", + "path": "" + }, + { + "type": "object", + "key": "ImageGenCompletedEvent", + "path": "" + }, + { + "type": "object", + "key": "ImageEditPartialImageEvent", + "path": "" + }, + { + "type": "object", + "key": "ImageEditCompletedEvent", + "path": "" + } + ] + }, + { + "id": "realtime-client-events", + "title": "Client events", + "description": "These are events that the OpenAI Realtime WebSocket server will accept from the client.\n", + "navigationGroup": "realtime", + "sections": [ + { + "type": "object", + "key": "RealtimeClientEventSessionUpdate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventInputAudioBufferAppend", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventInputAudioBufferCommit", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventInputAudioBufferClear", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventConversationItemCreate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventConversationItemRetrieve", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventConversationItemTruncate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventConversationItemDelete", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventResponseCreate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventResponseCancel", + "path": "" + }, + { + "type": "object", + "key": "RealtimeClientEventOutputAudioBufferClear", + "path": "" + } + ] + }, + { + "id": "realtime-server-events", + "title": "Server events", + "description": "These are events emitted from the OpenAI Realtime WebSocket server to the client.\n", + "navigationGroup": "realtime", + "sections": [ + { + "type": "object", + "key": "RealtimeServerEventError", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventSessionCreated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventSessionUpdated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventConversationItemAdded", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventConversationItemDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventConversationItemRetrieved", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventConversationItemInputAudioTranscriptionCompleted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventConversationItemInputAudioTranscriptionDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventConversationItemInputAudioTranscriptionSegment", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventConversationItemInputAudioTranscriptionFailed", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventConversationItemTruncated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventConversationItemDeleted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventInputAudioBufferCommitted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventInputAudioBufferDtmfEventReceived", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventInputAudioBufferCleared", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventInputAudioBufferSpeechStarted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventInputAudioBufferSpeechStopped", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventInputAudioBufferTimeoutTriggered", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventOutputAudioBufferStarted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventOutputAudioBufferStopped", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventOutputAudioBufferCleared", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseCreated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseOutputItemAdded", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseOutputItemDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseContentPartAdded", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseContentPartDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseTextDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseTextDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseAudioTranscriptDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseAudioTranscriptDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseAudioDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseAudioDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseFunctionCallArgumentsDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseFunctionCallArgumentsDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseMCPCallArgumentsDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseMCPCallArgumentsDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseMCPCallInProgress", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseMCPCallCompleted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventResponseMCPCallFailed", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventMCPListToolsInProgress", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventMCPListToolsCompleted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventMCPListToolsFailed", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventRateLimitsUpdated", + "path": "" + } + ] + }, + { + "id": "realtime-translation-client-events", + "title": "Translation client events", + "description": "These are events that the OpenAI Realtime Translation WebSocket server will accept from the client.\n", + "navigationGroup": "realtime", + "sections": [ + { + "type": "object", + "key": "RealtimeTranslationClientEventSessionUpdate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeTranslationClientEventInputAudioBufferAppend", + "path": "" + }, + { + "type": "object", + "key": "RealtimeTranslationClientEventSessionClose", + "path": "" + } + ] + }, + { + "id": "realtime-translation-server-events", + "title": "Translation server events", + "description": "These are events emitted from the OpenAI Realtime Translation WebSocket server to the client.\n", + "navigationGroup": "realtime", + "sections": [ + { + "type": "object", + "key": "RealtimeServerEventError", + "path": "" + }, + { + "type": "object", + "key": "RealtimeTranslationServerEventSessionCreated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeTranslationServerEventSessionUpdated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeTranslationServerEventSessionClosed", + "path": "" + }, + { + "type": "object", + "key": "RealtimeTranslationServerEventSessionInputTranscriptDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeTranslationServerEventSessionOutputTranscriptDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeTranslationServerEventSessionOutputAudioDelta", + "path": "" + } + ] + }, + { + "id": "chat-streaming", + "title": "Streaming", + "description": "Stream Chat Completions in real time. Receive chunks of completions\nreturned from the model using server-sent events.\n[Learn more](https://developers.openai.com/api/docs/guides/streaming-responses).\n", + "navigationGroup": "chat", + "sections": [ + { + "type": "object", + "key": "CreateChatCompletionStreamResponse", + "path": "streaming" + } + ] + }, + { + "id": "assistants-streaming", + "title": "Streaming", + "beta": true, + "description": "Stream the result of executing a Run or resuming a Run after submitting tool outputs.\nYou can stream events from the [Create Thread and Run](https://developers.openai.com/api/docs/assistants/migration),\n[Create Run](https://developers.openai.com/api/docs/assistants/migration), and [Submit Tool Outputs](https://developers.openai.com/api/docs/assistants/migration)\nendpoints by passing `\"stream\": true`. The response will be a [Server-Sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) stream.\nOur Node and Python SDKs provide helpful utilities to make streaming easy. Reference the\n[Assistants API quickstart](https://developers.openai.com/api/docs/assistants/migration) to learn more.\n", + "navigationGroup": "assistants", + "sections": [ + { + "type": "object", + "key": "AssistantStreamEvent", + "path": "events" + } + ] + }, + { + "id": "realtime-beta-client-events", + "title": "Realtime Beta client events", + "description": "These are events that the OpenAI Realtime WebSocket server will accept from the client.\n", + "navigationGroup": "legacy", + "sections": [ + { + "type": "object", + "key": "RealtimeBetaClientEventSessionUpdate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventInputAudioBufferAppend", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventInputAudioBufferCommit", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventInputAudioBufferClear", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventConversationItemCreate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventConversationItemRetrieve", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventConversationItemTruncate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventConversationItemDelete", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventResponseCreate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventResponseCancel", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventTranscriptionSessionUpdate", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaClientEventOutputAudioBufferClear", + "path": "" + } + ] + }, + { + "id": "realtime-beta-server-events", + "title": "Realtime Beta server events", + "description": "These are events emitted from the OpenAI Realtime WebSocket server to the client.\n", + "navigationGroup": "legacy", + "sections": [ + { + "type": "object", + "key": "RealtimeBetaServerEventError", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventSessionCreated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventSessionUpdated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventTranscriptionSessionCreated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventTranscriptionSessionUpdated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventConversationItemCreated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventConversationItemRetrieved", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventConversationItemInputAudioTranscriptionCompleted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventConversationItemInputAudioTranscriptionDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventConversationItemInputAudioTranscriptionSegment", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventConversationItemInputAudioTranscriptionFailed", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventConversationItemTruncated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventConversationItemDeleted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventInputAudioBufferCommitted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventInputAudioBufferCleared", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventInputAudioBufferSpeechStarted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventInputAudioBufferSpeechStopped", + "path": "" + }, + { + "type": "object", + "key": "RealtimeServerEventInputAudioBufferTimeoutTriggered", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseCreated", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseOutputItemAdded", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseOutputItemDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseContentPartAdded", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseContentPartDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseTextDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseTextDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseAudioTranscriptDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseAudioTranscriptDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseAudioDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseAudioDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseFunctionCallArgumentsDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseFunctionCallArgumentsDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseMCPCallArgumentsDelta", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseMCPCallArgumentsDone", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseMCPCallInProgress", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseMCPCallCompleted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventResponseMCPCallFailed", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventMCPListToolsInProgress", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventMCPListToolsCompleted", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventMCPListToolsFailed", + "path": "" + }, + { + "type": "object", + "key": "RealtimeBetaServerEventRateLimitsUpdated", + "path": "" + } + ] + }, + { + "id": "live-client-events", + "title": "Client events", + "description": "Initialize a primary WebSocket with session.start and wait for session.started before sending other events. WebRTC creation starts the session for you. Start with the [Live prompting guide](https://developers.openai.com/api/docs/guides/live-prompting) to design frontend instructions and delegation policy; see [backend prompting](https://developers.openai.com/api/docs/guides/live-delegation#start-with-your-existing-backend-prompt) for tools and business rules.", + "navigationGroup": "live", + "sections": [ + { + "type": "object", + "key": "LiveSessionStartEvent", + "path": "" + }, + { + "type": "object", + "key": "LiveForkSessionStartEvent", + "path": "" + }, + { + "type": "object", + "key": "LiveSessionUpdateParam", + "path": "" + }, + { + "type": "object", + "key": "LiveInputAudioAppendEvent", + "path": "" + }, + { + "type": "object", + "key": "LiveInputAudioMuteParam", + "path": "" + }, + { + "type": "object", + "key": "LiveInputAudioUnmuteParam", + "path": "" + }, + { + "type": "object", + "key": "LiveInstructionsAppendParam", + "path": "" + }, + { + "type": "object", + "key": "LiveThinkingAppendParam", + "path": "" + }, + { + "type": "object", + "key": "LiveCommentaryAppendParam", + "path": "" + }, + { + "type": "object", + "key": "LiveResponseItemCreateParam", + "path": "" + }, + { + "type": "object", + "key": "LiveResponseCreateParam", + "path": "" + }, + { + "type": "object", + "key": "LiveSessionCloseParam", + "path": "" + } + ] + }, + { + "id": "live-server-events", + "title": "Server events", + "description": "Live server events. Responses delegation lifecycle events arrive inside response.event, not as top-level response events. Start with the [Live prompting guide](https://developers.openai.com/api/docs/guides/live-prompting) to design frontend instructions and delegation policy; see [backend prompting](https://developers.openai.com/api/docs/guides/live-delegation#start-with-your-existing-backend-prompt) for tools and business rules.", + "navigationGroup": "live", + "sections": [ + { + "type": "object", + "key": "LiveSessionStarted", + "path": "" + }, + { + "type": "object", + "key": "LiveSessionUpdated", + "path": "" + }, + { + "type": "object", + "key": "LiveInputAudioMuted", + "path": "" + }, + { + "type": "object", + "key": "LiveInputAudioUnmuted", + "path": "" + }, + { + "type": "object", + "key": "LiveInstructionsAppended", + "path": "" + }, + { + "type": "object", + "key": "LiveThinkingAppended", + "path": "" + }, + { + "type": "object", + "key": "LiveCommentaryAppended", + "path": "" + }, + { + "type": "object", + "key": "LiveOutputAudioDelta", + "path": "" + }, + { + "type": "object", + "key": "LiveInputTranscriptDelta", + "path": "" + }, + { + "type": "object", + "key": "LiveOutputTranscriptDelta", + "path": "" + }, + { + "type": "object", + "key": "LiveDelegationCreated", + "path": "" + }, + { + "type": "object", + "key": "LiveResponseEvent", + "path": "" + }, + { + "type": "object", + "key": "LiveSessionUsageUpdated", + "path": "" + }, + { + "type": "object", + "key": "LiveSessionClosed", + "path": "" + }, + { + "type": "object", + "key": "LiveErrorEvent", + "path": "" + }, + { + "type": "object", + "key": "LiveInfoEvent", + "path": "" + } + ] + } + ] + } +} diff --git a/contracts/agents-api/runtime.openapi.yaml b/contracts/agents-api/runtime.openapi.yaml index fc109bc6f..8e1ac37a7 100644 --- a/contracts/agents-api/runtime.openapi.yaml +++ b/contracts/agents-api/runtime.openapi.yaml @@ -151,13 +151,17 @@ definitions: x-nullable: true message: type: string + misalignment: + type: object param: type: string x-nullable: true type: type: string required: + - code - message + - param - type type: object v1.ErrorResponse: diff --git a/contracts/agents-api/upstream-fields.json b/contracts/agents-api/upstream-fields.json deleted file mode 100644 index f1668c995..000000000 --- a/contracts/agents-api/upstream-fields.json +++ /dev/null @@ -1,2301 +0,0 @@ -{ - "sdk_version": "3.13.0", - "commit": "d7c41efee1b0802b79f3f88a678ef2052b06e9ce", - "generator": "scripts/extract-agents-api-upstream.py", - "operations": { - "DELETE /agents/environments/templates/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.environments.environment_template_deleted.EnvironmentTemplateDeleted" - ] - }, - "DELETE /agents/sessions/{}": { - "request": [], - "query": [], - "response": [ - "beta.agent_session_deleted.AgentSessionDeleted" - ] - }, - "DELETE /agents/sessions/{}/artifacts/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.sessions.session_artifact_deleted.SessionArtifactDeleted" - ] - }, - "DELETE /agents/{}": { - "request": [], - "query": [], - "response": [ - "beta.agent_deleted.AgentDeleted" - ] - }, - "DELETE /files/{}": { - "request": [], - "query": [], - "response": [ - "file_deleted.FileDeleted" - ] - }, - "DELETE /skills/{}": { - "request": [], - "query": [], - "response": [ - "deleted_skill.DeletedSkill" - ] - }, - "DELETE /skills/{}/versions/{}": { - "request": [], - "query": [], - "response": [ - "skills.deleted_skill_version.DeletedSkillVersion" - ] - }, - "DELETE /vaults/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.vault_deleted.VaultDeleted" - ] - }, - "DELETE /vaults/{}/credentials/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.vaults.credential_deleted.CredentialDeleted" - ] - }, - "GET /agents": { - "request": [], - "query": [ - "beta.agent_list_params.AgentListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agent.Agent]" - ] - }, - "GET /agents/environments/templates": { - "request": [], - "query": [ - "beta.agents.environments.template_list_params.TemplateListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agents.environments.environment_template.EnvironmentTemplate]" - ] - }, - "GET /agents/environments/templates/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.environments.environment_template.EnvironmentTemplate" - ] - }, - "GET /agents/environments/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.environment_info.EnvironmentInfo" - ] - }, - "GET /agents/environments/{}/files": { - "request": [], - "query": [ - "beta.agents.environments.file_list_params.FileListParams" - ], - "response": [ - "pagination.SyncTokenPage[beta.agents.environments.environment_file.EnvironmentFile]" - ] - }, - "GET /agents/sessions": { - "request": [], - "query": [ - "beta.agents.session_list_params.SessionListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agent_session.AgentSession]" - ] - }, - "GET /agents/sessions/{}": { - "request": [], - "query": [], - "response": [ - "beta.agent_session.AgentSession" - ] - }, - "GET /agents/sessions/{}/artifacts": { - "request": [], - "query": [ - "beta.agents.sessions.artifact_list_params.ArtifactListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agents.sessions.session_artifact.SessionArtifact]" - ] - }, - "GET /agents/sessions/{}/artifacts/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.sessions.session_artifact.SessionArtifact" - ] - }, - "GET /agents/sessions/{}/artifacts/{}/content": { - "request": [], - "query": [], - "response": [] - }, - "GET /agents/sessions/{}/events": { - "request": [], - "query": [], - "response": [ - "beta.agent_output_command_execution_output_delta_event.AgentOutputCommandExecutionOutputDeltaEvent", - "beta.agent_session_created_event.AgentSessionCreatedEvent", - "beta.agent_session_environment_connected_event.AgentSessionEnvironmentConnectedEvent", - "beta.agent_session_environment_disconnected_event.AgentSessionEnvironmentDisconnectedEvent", - "beta.agent_session_environment_failed_event.AgentSessionEnvironmentFailedEvent", - "beta.agent_session_environment_pending_event.AgentSessionEnvironmentPendingEvent", - "beta.agent_session_environment_ready_event.AgentSessionEnvironmentReadyEvent", - "beta.agent_session_error_event.AgentSessionErrorEvent", - "beta.agent_session_failed_event.AgentSessionFailedEvent", - "beta.agent_session_idle_event.AgentSessionIdleEvent", - "beta.agent_session_in_progress_event.AgentSessionInProgressEvent", - "beta.agent_session_requires_action_event.AgentSessionRequiresActionEvent", - "beta.agent_session_subagent_active_event.AgentSessionSubagentActiveEvent", - "beta.agent_session_subagent_closed_event.AgentSessionSubagentClosedEvent", - "beta.agent_session_subagent_created_event.AgentSessionSubagentCreatedEvent", - "beta.agent_session_turn_cancelled_event.AgentSessionTurnCancelledEvent", - "beta.agent_session_turn_completed_event.AgentSessionTurnCompletedEvent", - "beta.agent_session_turn_content_part_added_event.AgentSessionTurnContentPartAddedEvent", - "beta.agent_session_turn_content_part_done_event.AgentSessionTurnContentPartDoneEvent", - "beta.agent_session_turn_created_event.AgentSessionTurnCreatedEvent", - "beta.agent_session_turn_failed_event.AgentSessionTurnFailedEvent", - "beta.agent_session_turn_in_progress_event.AgentSessionTurnInProgressEvent", - "beta.agent_session_turn_item_added_event.AgentSessionTurnItemAddedEvent", - "beta.agent_session_turn_item_done_event.AgentSessionTurnItemDoneEvent", - "beta.agent_session_turn_output_text_delta_event.AgentSessionTurnOutputTextDeltaEvent", - "beta.agent_session_turn_output_text_done_event.AgentSessionTurnOutputTextDoneEvent", - "beta.agent_session_turn_reasoning_summary_part_added_event.AgentSessionTurnReasoningSummaryPartAddedEvent", - "beta.agent_session_turn_reasoning_summary_part_done_event.AgentSessionTurnReasoningSummaryPartDoneEvent", - "beta.agent_session_turn_reasoning_summary_text_delta_event.AgentSessionTurnReasoningSummaryTextDeltaEvent", - "beta.agent_session_turn_reasoning_summary_text_done_event.AgentSessionTurnReasoningSummaryTextDoneEvent" - ] - }, - "GET /agents/sessions/{}/items": { - "request": [], - "query": [ - "beta.agents.sessions.item_list_params.ItemListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agent_close_subagent_call_item.AgentCloseSubagentCallItem | beta.agent_command_execution_item.AgentCommandExecutionItem | beta.agent_create_subagent_call_item.AgentCreateSubagentCallItem | beta.agent_function_call_item.AgentFunctionCallItem | beta.agent_interrupt_subagent_call_item.AgentInterruptSubagentCallItem | beta.agent_mcp_call_item.AgentMcpCallItem | beta.agent_reasoning_item.AgentReasoningItem | beta.agent_resume_subagent_call_item.AgentResumeSubagentCallItem | beta.agent_send_subagent_input_call_item.AgentSendSubagentInputCallItem | beta.agent_session_item.AgentMessageItemResource | beta.agent_session_item.FunctionCallOutputItemResource | beta.agent_session_message.AgentSessionMessage | beta.agent_wait_for_subagents_call_item.AgentWaitForSubagentsCallItem | beta.agent_web_search_call_item.AgentWebSearchCallItem]" - ] - }, - "GET /agents/sessions/{}/subagents": { - "request": [], - "query": [ - "beta.agents.sessions.subagent_list_params.SubagentListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.subagent.Subagent]" - ] - }, - "GET /agents/sessions/{}/subagents/{}": { - "request": [], - "query": [], - "response": [ - "beta.subagent.Subagent" - ] - }, - "GET /agents/sessions/{}/subagents/{}/items": { - "request": [], - "query": [ - "beta.agents.sessions.subagents.item_list_params.ItemListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agent_close_subagent_call_item.AgentCloseSubagentCallItem | beta.agent_command_execution_item.AgentCommandExecutionItem | beta.agent_create_subagent_call_item.AgentCreateSubagentCallItem | beta.agent_function_call_item.AgentFunctionCallItem | beta.agent_interrupt_subagent_call_item.AgentInterruptSubagentCallItem | beta.agent_mcp_call_item.AgentMcpCallItem | beta.agent_reasoning_item.AgentReasoningItem | beta.agent_resume_subagent_call_item.AgentResumeSubagentCallItem | beta.agent_send_subagent_input_call_item.AgentSendSubagentInputCallItem | beta.agent_session_item.AgentMessageItemResource | beta.agent_session_item.FunctionCallOutputItemResource | beta.agent_session_message.AgentSessionMessage | beta.agent_wait_for_subagents_call_item.AgentWaitForSubagentsCallItem | beta.agent_web_search_call_item.AgentWebSearchCallItem]" - ] - }, - "GET /agents/sessions/{}/subagents/{}/turns": { - "request": [], - "query": [ - "beta.agents.sessions.subagents.turn_list_params.TurnListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agents.sessions.turn.Turn]" - ] - }, - "GET /agents/sessions/{}/subagents/{}/turns/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.sessions.turn.Turn" - ] - }, - "GET /agents/sessions/{}/subagents/{}/turns/{}/items": { - "request": [], - "query": [ - "beta.agents.sessions.subagents.turns.item_list_params.ItemListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agent_close_subagent_call_item.AgentCloseSubagentCallItem | beta.agent_command_execution_item.AgentCommandExecutionItem | beta.agent_create_subagent_call_item.AgentCreateSubagentCallItem | beta.agent_function_call_item.AgentFunctionCallItem | beta.agent_interrupt_subagent_call_item.AgentInterruptSubagentCallItem | beta.agent_mcp_call_item.AgentMcpCallItem | beta.agent_reasoning_item.AgentReasoningItem | beta.agent_resume_subagent_call_item.AgentResumeSubagentCallItem | beta.agent_send_subagent_input_call_item.AgentSendSubagentInputCallItem | beta.agent_session_item.AgentMessageItemResource | beta.agent_session_item.FunctionCallOutputItemResource | beta.agent_session_message.AgentSessionMessage | beta.agent_wait_for_subagents_call_item.AgentWaitForSubagentsCallItem | beta.agent_web_search_call_item.AgentWebSearchCallItem]" - ] - }, - "GET /agents/sessions/{}/turns": { - "request": [], - "query": [ - "beta.agents.sessions.turn_list_params.TurnListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agents.sessions.turn.Turn]" - ] - }, - "GET /agents/sessions/{}/turns/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.sessions.turn.Turn" - ] - }, - "GET /agents/{}": { - "request": [], - "query": [], - "response": [ - "beta.agent.Agent" - ] - }, - "GET /files": { - "request": [], - "query": [ - "file_list_params.FileListParams" - ], - "response": [ - "pagination.SyncCursorPage[file_object.FileObject]" - ] - }, - "GET /files/{}": { - "request": [], - "query": [], - "response": [ - "file_object.FileObject" - ] - }, - "GET /files/{}/content": { - "request": [], - "query": [], - "response": [] - }, - "GET /skills": { - "request": [], - "query": [ - "skill_list_params.SkillListParams" - ], - "response": [ - "pagination.SyncCursorPage[skill.Skill]" - ] - }, - "GET /skills/{}": { - "request": [], - "query": [], - "response": [ - "skill.Skill" - ] - }, - "GET /skills/{}/content": { - "request": [], - "query": [], - "response": [] - }, - "GET /skills/{}/versions": { - "request": [], - "query": [ - "skills.version_list_params.VersionListParams" - ], - "response": [ - "pagination.SyncCursorPage[skills.skill_version.SkillVersion]" - ] - }, - "GET /skills/{}/versions/{}": { - "request": [], - "query": [], - "response": [ - "skills.skill_version.SkillVersion" - ] - }, - "GET /skills/{}/versions/{}/content": { - "request": [], - "query": [], - "response": [] - }, - "GET /vaults": { - "request": [], - "query": [ - "beta.agents.vault_list_params.VaultListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agents.vault.Vault]" - ] - }, - "GET /vaults/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.vault.Vault" - ] - }, - "GET /vaults/{}/credentials": { - "request": [], - "query": [ - "beta.agents.vaults.credential_list_params.CredentialListParams" - ], - "response": [ - "pagination.SyncCursorPage[beta.agents.vaults.credential.Credential]" - ] - }, - "GET /vaults/{}/credentials/{}": { - "request": [], - "query": [], - "response": [ - "beta.agents.vaults.credential.Credential" - ] - }, - "POST /agents": { - "request": [ - "beta.agent_create_params.AgentCreateParams" - ], - "query": [], - "response": [ - "beta.agent.Agent" - ] - }, - "POST /agents/environments/templates": { - "request": [ - "beta.agents.environments.template_create_params.TemplateCreateParams" - ], - "query": [], - "response": [ - "beta.agents.environments.environment_template.EnvironmentTemplate" - ] - }, - "POST /agents/environments/templates/{}": { - "request": [ - "beta.agents.environments.template_update_params.TemplateUpdateParams" - ], - "query": [], - "response": [ - "beta.agents.environments.environment_template.EnvironmentTemplate" - ] - }, - "POST /agents/environments/{}/files": { - "request": [ - "beta.agents.environments.file_create_params.HostedEnvironmentFileParamFileID", - "beta.agents.environments.file_create_params.HostedEnvironmentFileParamInline" - ], - "query": [], - "response": [ - "beta.agents.environments.environment_file.EnvironmentFile" - ] - }, - "POST /agents/sessions": { - "request": [ - "beta.agents.session_create_params.SessionCreateParamsNonStreaming", - "beta.agents.session_create_params.SessionCreateParamsStreaming" - ], - "query": [], - "response": [ - "beta.agent_session.AgentSession" - ] - }, - "POST /agents/sessions/{}": { - "request": [ - "beta.agents.session_update_params.SessionUpdateParams" - ], - "query": [], - "response": [ - "beta.agent_session.AgentSession" - ] - }, - "POST /agents/sessions/{}/events": { - "request": [ - "beta.agents.sessions.event_create_params.EventCreateParams" - ], - "query": [], - "response": [] - }, - "POST /agents/{}": { - "request": [ - "beta.agent_update_params.AgentUpdateParams" - ], - "query": [], - "response": [ - "beta.agent.Agent" - ] - }, - "POST /files": { - "request": [ - "file_create_params.FileCreateParams" - ], - "query": [], - "response": [ - "file_object.FileObject" - ] - }, - "POST /skills": { - "request": [ - "skill_create_params.SkillCreateParams" - ], - "query": [], - "response": [ - "skill.Skill" - ] - }, - "POST /skills/{}": { - "request": [ - "skill_update_params.SkillUpdateParams" - ], - "query": [], - "response": [ - "skill.Skill" - ] - }, - "POST /skills/{}/versions": { - "request": [ - "skills.version_create_params.VersionCreateParams" - ], - "query": [], - "response": [ - "skills.skill_version.SkillVersion" - ] - }, - "POST /vaults": { - "request": [ - "beta.agents.vault_create_params.VaultCreateParams" - ], - "query": [], - "response": [ - "beta.agents.vault.Vault" - ] - }, - "POST /vaults/{}/credentials": { - "request": [ - "beta.agents.vaults.credential_create_params.CredentialCreateParams" - ], - "query": [], - "response": [ - "beta.agents.vaults.credential.Credential" - ] - }, - "POST /vaults/{}/credentials/{}": { - "request": [ - "beta.agents.vaults.credential_update_params.CredentialUpdateParams" - ], - "query": [], - "response": [ - "beta.agents.vaults.credential.Credential" - ] - } - }, - "types": { - "beta.agent.Agent": { - "created_at": [], - "id": [], - "instructions": [], - "metadata": [], - "model": [], - "multi_agent": [ - "beta.multi_agent_config.MultiAgentConfig" - ], - "name": [], - "object": [], - "reasoning": [ - "beta.agent_reasoning.AgentReasoning" - ], - "service_tier": [], - "text": [ - "beta.agent_text.AgentText" - ], - "tools": [ - "beta.persisted_agent_tool.PersistedAgentToolResourceFunction", - "beta.persisted_agent_tool.PersistedAgentToolResourceMcp", - "beta.persisted_agent_tool.PersistedAgentToolResourceProgrammaticToolCalling", - "beta.persisted_agent_tool.PersistedAgentToolResourceToolSearch", - "beta.persisted_agent_tool.PersistedAgentToolResourceWebSearch" - ], - "updated_at": [] - }, - "beta.agent_close_subagent_call_item.AgentCloseSubagentCallItem": { - "id": [], - "recipient_agent_id": [], - "sender_agent_id": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_command_execution_item.AgentCommandExecutionItem": { - "command": [], - "cwd": [], - "duration_ms": [], - "exit_code": [], - "id": [], - "output": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_content.EncryptedContentResource": { - "encrypted_content": [], - "type": [] - }, - "beta.agent_create_params.AgentCreateParams": { - "instructions": [], - "metadata": [], - "model": [], - "multi_agent": [ - "beta.multi_agent_config_param.MultiAgentConfigParam" - ], - "name": [], - "reasoning": [ - "beta.agent_reasoning_param.AgentReasoningParam" - ], - "service_tier": [], - "text": [ - "beta.agent_text_param.AgentTextParam" - ], - "tools": [ - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamFunction", - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamMcp", - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamProgrammaticToolCalling", - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamToolSearch", - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamWebSearch" - ] - }, - "beta.agent_create_subagent_call_item.AgentCreateSubagentCallItem": { - "agent_id": [], - "content": [ - "beta.agent_content.EncryptedContentResource", - "beta.output_text.OutputText" - ], - "id": [], - "model": [], - "reasoning_effort": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_deleted.AgentDeleted": { - "deleted": [], - "id": [], - "object": [] - }, - "beta.agent_function_call_item.AgentFunctionCallItem": { - "arguments": [], - "call_id": [], - "id": [], - "name": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_interrupt_subagent_call_item.AgentInterruptSubagentCallItem": { - "id": [], - "recipient_agent_id": [], - "sender_agent_id": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_list_params.AgentListParams": { - "after": [], - "limit": [], - "order": [] - }, - "beta.agent_mcp_call_item.AgentMcpCallItem": { - "arguments": [], - "error": [], - "id": [], - "name": [], - "output": [], - "server_label": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_output_command_execution_output_delta_event.AgentOutputCommandExecutionOutputDeltaEvent": { - "delta": [], - "event_id": [], - "item_id": [], - "output_index": [], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_reasoning.AgentReasoning": { - "effort": [], - "summary": [] - }, - "beta.agent_reasoning_item.AgentReasoningItem": { - "id": [], - "status": [], - "summary": [ - "beta.summary_text.SummaryText" - ], - "turn_id": [], - "type": [] - }, - "beta.agent_reasoning_param.AgentReasoningParam": { - "effort": [], - "summary": [] - }, - "beta.agent_resume_subagent_call_item.AgentResumeSubagentCallItem": { - "id": [], - "recipient_agent_id": [], - "sender_agent_id": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_send_subagent_input_call_item.AgentSendSubagentInputCallItem": { - "content": [ - "beta.agent_content.EncryptedContentResource", - "beta.output_text.OutputText" - ], - "id": [], - "recipient_agent_id": [], - "sender_agent_id": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session.Agent": { - "id": [], - "instructions": [], - "model": [], - "multi_agent": [ - "beta.multi_agent_config.MultiAgentConfig" - ], - "name": [], - "reasoning": [ - "beta.agent_reasoning.AgentReasoning" - ], - "service_tier": [], - "text": [ - "beta.agent_text.AgentText" - ], - "tools": [ - "beta.agent_tool.AgentToolResourceFunction", - "beta.agent_tool.AgentToolResourceMcp", - "beta.agent_tool.AgentToolResourceProgrammaticToolCalling", - "beta.agent_tool.AgentToolResourceWebSearch" - ] - }, - "beta.agent_session.AgentSession": { - "agent": [ - "beta.agent_session.Agent" - ], - "created_at": [], - "environment": [ - "beta.environment.EnvironmentResourceNone", - "beta.environment.EnvironmentResourceOpenAIHosted", - "beta.environment.EnvironmentResourceSelfHosted" - ], - "error": [], - "id": [], - "last_active_at": [], - "metadata": [], - "object": [], - "required_actions": [ - "beta.agent_session.RequiredActionSessionRequiredActionResourceEnvironmentConnection", - "beta.agent_session.RequiredActionSessionRequiredActionResourceFunctionCall" - ], - "status": [], - "usage": [ - "beta.token_usage.TokenUsage" - ], - "vault_ids": [] - }, - "beta.agent_session.RequiredActionSessionRequiredActionResourceEnvironmentConnection": { - "environment_id": [], - "type": [] - }, - "beta.agent_session.RequiredActionSessionRequiredActionResourceFunctionCall": { - "arguments": [], - "call_id": [], - "name": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_assistant_message.AgentSessionAssistantMessage": { - "content": [ - "beta.output_text.OutputText" - ], - "id": [], - "phase": [], - "role": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_created_event.AgentSessionCreatedEvent": { - "event_id": [], - "session": [ - "beta.agent_session.AgentSession" - ], - "type": [] - }, - "beta.agent_session_deleted.AgentSessionDeleted": { - "deleted": [], - "id": [], - "object": [] - }, - "beta.agent_session_environment_connected_event.AgentSessionEnvironmentConnectedEvent": { - "environment": [ - "beta.agent_session_environment_state.AgentSessionEnvironmentState" - ], - "event_id": [], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_environment_disconnected_event.AgentSessionEnvironmentDisconnectedEvent": { - "environment": [ - "beta.agent_session_environment_state.AgentSessionEnvironmentState" - ], - "event_id": [], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_environment_failed_event.AgentSessionEnvironmentFailedEvent": { - "environment": [ - "beta.agent_session_environment_state.AgentSessionEnvironmentState" - ], - "event_id": [], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_environment_pending_event.AgentSessionEnvironmentPendingEvent": { - "environment": [ - "beta.agent_session_environment_state.AgentSessionEnvironmentState" - ], - "event_id": [], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_environment_ready_event.AgentSessionEnvironmentReadyEvent": { - "environment": [ - "beta.agent_session_environment_state.AgentSessionEnvironmentState" - ], - "event_id": [], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_environment_state.AgentSessionEnvironmentState": { - "error": [ - "beta.agent_session_environment_state.Error" - ], - "id": [], - "status": [], - "type": [] - }, - "beta.agent_session_environment_state.Error": { - "code": [], - "message": [], - "type": [] - }, - "beta.agent_session_error_event.AgentSessionErrorEvent": { - "error": [ - "beta.session_error.SessionError" - ], - "event_id": [], - "session_id": [], - "type": [] - }, - "beta.agent_session_failed_event.AgentSessionFailedEvent": { - "event_id": [], - "session": [ - "beta.agent_session.AgentSession" - ], - "type": [] - }, - "beta.agent_session_idle_event.AgentSessionIdleEvent": { - "event_id": [], - "session": [ - "beta.agent_session.AgentSession" - ], - "type": [] - }, - "beta.agent_session_in_progress_event.AgentSessionInProgressEvent": { - "event_id": [], - "session": [ - "beta.agent_session.AgentSession" - ], - "type": [] - }, - "beta.agent_session_input_message_param.AgentSessionInputMessageParam": { - "content": [ - "beta.input_content_param.InputContentParamInputImage", - "beta.input_content_param.InputContentParamInputText" - ], - "role": [], - "type": [] - }, - "beta.agent_session_input_param.SessionInputParamAgentSessionInputCancel": { - "type": [] - }, - "beta.agent_session_input_param.SessionInputParamAgentSessionInputMessage": { - "input": [ - "beta.agent_session_input_message_param.AgentSessionInputMessageParam" - ], - "type": [] - }, - "beta.agent_session_input_param.SessionInputParamAgentSessionInputToolResult": { - "call_id": [], - "error": [], - "output": [ - "beta.input_content_param.InputContentParamInputImage", - "beta.input_content_param.InputContentParamInputText" - ], - "success": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_item.AgentMessageItemResource": { - "content": [ - "beta.agent_content.EncryptedContentResource", - "beta.output_text.OutputText" - ], - "id": [], - "recipient_agent_id": [], - "sender_agent_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_item.FunctionCallOutputItemResource": { - "call_id": [], - "error": [], - "id": [], - "output": [ - "beta.input_content.InputContentResourceInputImage", - "beta.input_content.InputContentResourceInputText" - ], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_message.AgentSessionMessage": { - "content": [ - "beta.agent_session_message_content.MessageContentResourceInputImage", - "beta.agent_session_message_content.MessageContentResourceInputText", - "beta.agent_session_message_content.MessageContentResourceOutputText" - ], - "id": [], - "phase": [], - "role": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_message_content.MessageContentResourceInputImage": { - "image_url": [], - "type": [] - }, - "beta.agent_session_message_content.MessageContentResourceInputText": { - "text": [], - "type": [] - }, - "beta.agent_session_message_content.MessageContentResourceOutputText": { - "text": [], - "type": [] - }, - "beta.agent_session_requires_action_event.AgentSessionRequiresActionEvent": { - "event_id": [], - "session": [ - "beta.agent_session.AgentSession" - ], - "type": [] - }, - "beta.agent_session_subagent_active_event.AgentSessionSubagentActiveEvent": { - "event_id": [], - "subagent": [ - "beta.subagent.Subagent" - ], - "type": [] - }, - "beta.agent_session_subagent_closed_event.AgentSessionSubagentClosedEvent": { - "event_id": [], - "subagent": [ - "beta.subagent.Subagent" - ], - "type": [] - }, - "beta.agent_session_subagent_created_event.AgentSessionSubagentCreatedEvent": { - "event_id": [], - "subagent": [ - "beta.subagent.Subagent" - ], - "type": [] - }, - "beta.agent_session_turn_cancelled_event.AgentSessionTurnCancelledEvent": { - "event_id": [], - "session_id": [], - "turn": [ - "beta.agents.sessions.turn.Turn" - ], - "turn_id": [], - "type": [], - "usage": [ - "beta.token_usage.TokenUsage" - ] - }, - "beta.agent_session_turn_completed_event.AgentSessionTurnCompletedEvent": { - "event_id": [], - "session_id": [], - "turn": [ - "beta.agents.sessions.turn.Turn" - ], - "turn_id": [], - "type": [], - "usage": [ - "beta.token_usage.TokenUsage" - ] - }, - "beta.agent_session_turn_content_part_added_event.AgentSessionTurnContentPartAddedEvent": { - "content_index": [], - "event_id": [], - "item_id": [], - "output_index": [], - "part": [ - "beta.output_text.OutputText" - ], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_content_part_done_event.AgentSessionTurnContentPartDoneEvent": { - "content_index": [], - "event_id": [], - "item_id": [], - "output_index": [], - "part": [ - "beta.output_text.OutputText" - ], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_created_event.AgentSessionTurnCreatedEvent": { - "event_id": [], - "session_id": [], - "turn": [ - "beta.agents.sessions.turn.Turn" - ], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_failed_event.AgentSessionTurnFailedEvent": { - "event_id": [], - "session_id": [], - "turn": [ - "beta.agents.sessions.turn.Turn" - ], - "turn_id": [], - "type": [], - "usage": [ - "beta.token_usage.TokenUsage" - ] - }, - "beta.agent_session_turn_in_progress_event.AgentSessionTurnInProgressEvent": { - "event_id": [], - "session_id": [], - "turn": [ - "beta.agents.sessions.turn.Turn" - ], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_item_added_event.AgentSessionTurnItemAddedEvent": { - "event_id": [], - "item": [ - "beta.agent_close_subagent_call_item.AgentCloseSubagentCallItem", - "beta.agent_command_execution_item.AgentCommandExecutionItem", - "beta.agent_create_subagent_call_item.AgentCreateSubagentCallItem", - "beta.agent_function_call_item.AgentFunctionCallItem", - "beta.agent_interrupt_subagent_call_item.AgentInterruptSubagentCallItem", - "beta.agent_mcp_call_item.AgentMcpCallItem", - "beta.agent_reasoning_item.AgentReasoningItem", - "beta.agent_resume_subagent_call_item.AgentResumeSubagentCallItem", - "beta.agent_send_subagent_input_call_item.AgentSendSubagentInputCallItem", - "beta.agent_session_item.AgentMessageItemResource", - "beta.agent_session_item.FunctionCallOutputItemResource", - "beta.agent_session_message.AgentSessionMessage", - "beta.agent_wait_for_subagents_call_item.AgentWaitForSubagentsCallItem", - "beta.agent_web_search_call_item.AgentWebSearchCallItem" - ], - "output_index": [], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_item_done_event.AgentSessionTurnItemDoneEvent": { - "event_id": [], - "item": [ - "beta.agent_close_subagent_call_item.AgentCloseSubagentCallItem", - "beta.agent_command_execution_item.AgentCommandExecutionItem", - "beta.agent_create_subagent_call_item.AgentCreateSubagentCallItem", - "beta.agent_function_call_item.AgentFunctionCallItem", - "beta.agent_interrupt_subagent_call_item.AgentInterruptSubagentCallItem", - "beta.agent_mcp_call_item.AgentMcpCallItem", - "beta.agent_reasoning_item.AgentReasoningItem", - "beta.agent_resume_subagent_call_item.AgentResumeSubagentCallItem", - "beta.agent_send_subagent_input_call_item.AgentSendSubagentInputCallItem", - "beta.agent_session_assistant_message.AgentSessionAssistantMessage", - "beta.agent_wait_for_subagents_call_item.AgentWaitForSubagentsCallItem", - "beta.agent_web_search_call_item.AgentWebSearchCallItem" - ], - "output_index": [], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_output_text_delta_event.AgentSessionTurnOutputTextDeltaEvent": { - "content_index": [], - "delta": [], - "event_id": [], - "item_id": [], - "output_index": [], - "session_id": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_output_text_done_event.AgentSessionTurnOutputTextDoneEvent": { - "content_index": [], - "event_id": [], - "item_id": [], - "output_index": [], - "session_id": [], - "text": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_reasoning_summary_part_added_event.AgentSessionTurnReasoningSummaryPartAddedEvent": { - "event_id": [], - "item_id": [], - "output_index": [], - "part": [ - "beta.summary_text.SummaryText" - ], - "session_id": [], - "summary_index": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_reasoning_summary_part_done_event.AgentSessionTurnReasoningSummaryPartDoneEvent": { - "event_id": [], - "item_id": [], - "output_index": [], - "part": [ - "beta.summary_text.SummaryText" - ], - "session_id": [], - "status": [], - "summary_index": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_reasoning_summary_text_delta_event.AgentSessionTurnReasoningSummaryTextDeltaEvent": { - "delta": [], - "event_id": [], - "item_id": [], - "output_index": [], - "session_id": [], - "summary_index": [], - "turn_id": [], - "type": [] - }, - "beta.agent_session_turn_reasoning_summary_text_done_event.AgentSessionTurnReasoningSummaryTextDoneEvent": { - "event_id": [], - "item_id": [], - "output_index": [], - "session_id": [], - "summary_index": [], - "text": [], - "turn_id": [], - "type": [] - }, - "beta.agent_text.AgentText": { - "format": [ - "beta.text_format.TextFormatResourceJSONSchema", - "beta.text_format.TextFormatResourceText" - ], - "verbosity": [] - }, - "beta.agent_text_param.AgentTextParam": { - "format": [ - "beta.text_format_param.TextFormatParamJSONSchema", - "beta.text_format_param.TextFormatParamText" - ], - "verbosity": [] - }, - "beta.agent_tool.AgentToolResourceFunction": { - "defer_loading": [], - "description": [], - "name": [], - "parameters": [], - "type": [] - }, - "beta.agent_tool.AgentToolResourceMcp": { - "allowed_tools": [], - "connection_origin": [], - "credential_id": [], - "request_metadata": [], - "required": [], - "server_label": [], - "transport": [ - "beta.mcp_transport.McpTransportResourceHTTP", - "beta.mcp_transport.McpTransportResourceStdio" - ], - "type": [] - }, - "beta.agent_tool.AgentToolResourceProgrammaticToolCalling": { - "enabled": [], - "type": [] - }, - "beta.agent_tool.AgentToolResourceWebSearch": { - "allowed_domains": [], - "context_size": [], - "location": [ - "beta.agent_tool.AgentToolResourceWebSearchLocation" - ], - "mode": [], - "type": [] - }, - "beta.agent_tool.AgentToolResourceWebSearchLocation": { - "city": [], - "country": [], - "region": [], - "timezone": [] - }, - "beta.agent_tool_param.AgentToolConfigParamFunction": { - "defer_loading": [], - "description": [], - "name": [], - "parameters": [], - "type": [] - }, - "beta.agent_tool_param.AgentToolConfigParamMcp": { - "allowed_tools": [], - "connection_origin": [], - "credential_id": [], - "request_metadata": [], - "required": [], - "server_label": [], - "transport": [ - "beta.mcp_transport_param.McpTransportConfigParamHTTP", - "beta.mcp_transport_param.McpTransportConfigParamStdio" - ], - "type": [] - }, - "beta.agent_tool_param.AgentToolConfigParamProgrammaticToolCalling": { - "enabled": [], - "type": [] - }, - "beta.agent_tool_param.AgentToolConfigParamToolSearch": { - "type": [] - }, - "beta.agent_tool_param.AgentToolConfigParamWebSearch": { - "allowed_domains": [], - "context_size": [], - "location": [ - "beta.agent_tool_param.AgentToolConfigParamWebSearchLocation" - ], - "mode": [], - "type": [] - }, - "beta.agent_tool_param.AgentToolConfigParamWebSearchLocation": { - "city": [], - "country": [], - "region": [], - "timezone": [] - }, - "beta.agent_update_params.AgentUpdateParams": { - "instructions": [], - "metadata": [], - "model": [], - "multi_agent": [ - "beta.multi_agent_config_param.MultiAgentConfigParam" - ], - "name": [], - "reasoning": [ - "beta.agent_reasoning_param.AgentReasoningParam" - ], - "service_tier": [], - "text": [ - "beta.agent_text_param.AgentTextParam" - ], - "tools": [ - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamFunction", - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamMcp", - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamProgrammaticToolCalling", - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamToolSearch", - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamWebSearch" - ] - }, - "beta.agent_wait_for_subagents_call_item.AgentWaitForSubagentsCallItem": { - "id": [], - "recipient_agent_ids": [], - "sender_agent_id": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agent_web_search_call_item.AgentWebSearchCallItem": { - "action": [ - "beta.web_search_action.WebSearchActionResourceFindInPage", - "beta.web_search_action.WebSearchActionResourceOpenPage", - "beta.web_search_action.WebSearchActionResourceOther", - "beta.web_search_action.WebSearchActionResourceSearch" - ], - "id": [], - "status": [], - "turn_id": [], - "type": [] - }, - "beta.agents.environment_info.EnvironmentInfo": { - "files": [ - "beta.hosted_environment_file.HostedEnvironmentFileResourceInline", - "beta.hosted_environment_file_id.HostedEnvironmentFileID" - ], - "id": [], - "object": [], - "plugins": [ - "beta.hosted_plugin.HostedPlugin" - ], - "skills": [ - "beta.hosted_skill.HostedSkillResourceInline", - "beta.hosted_skill_reference.HostedSkillReference" - ], - "status": [], - "type": [] - }, - "beta.agents.environments.environment_file.EnvironmentFile": { - "environment_id": [], - "object": [], - "path": [], - "size_bytes": [] - }, - "beta.agents.environments.environment_template.EnvironmentTemplate": { - "capability_directories": [], - "created_at": [], - "files": [ - "beta.agents.environments.environment_template.FileHostedTemplateFileResourceFileID", - "beta.agents.environments.environment_template.FileHostedTemplateFileResourceInline" - ], - "id": [], - "name": [], - "network": [ - "beta.agents.environments.environment_template.Network" - ], - "object": [], - "packages": [ - "beta.agents.environments.environment_template.Packages" - ], - "plugins": [ - "beta.hosted_plugin.HostedPlugin" - ], - "skills": [ - "beta.agents.environments.environment_template.SkillHostedTemplateSkillResourceInline", - "beta.agents.environments.environment_template.SkillHostedTemplateSkillResourceSkillReference" - ], - "updated_at": [] - }, - "beta.agents.environments.environment_template.FileHostedTemplateFileResourceFileID": { - "file_id": [], - "path": [], - "type": [] - }, - "beta.agents.environments.environment_template.FileHostedTemplateFileResourceInline": { - "path": [], - "size_bytes": [], - "type": [] - }, - "beta.agents.environments.environment_template.Network": { - "access": [], - "allowed_domains": [] - }, - "beta.agents.environments.environment_template.Packages": { - "npm": [], - "python": [], - "system": [] - }, - "beta.agents.environments.environment_template.SkillHostedTemplateSkillResourceInline": { - "description": [], - "name": [], - "type": [] - }, - "beta.agents.environments.environment_template.SkillHostedTemplateSkillResourceSkillReference": { - "skill_id": [], - "type": [], - "version": [] - }, - "beta.agents.environments.environment_template_deleted.EnvironmentTemplateDeleted": { - "deleted": [], - "id": [], - "object": [] - }, - "beta.agents.environments.file_create_params.HostedEnvironmentFileParamFileID": { - "file_id": [], - "path": [], - "type": [] - }, - "beta.agents.environments.file_create_params.HostedEnvironmentFileParamInline": { - "data": [], - "path": [], - "type": [] - }, - "beta.agents.environments.file_list_params.FileListParams": { - "limit": [], - "order": [], - "page": [], - "path": [] - }, - "beta.agents.environments.template_create_params.Network": { - "access": [], - "allowed_domains": [] - }, - "beta.agents.environments.template_create_params.Packages": { - "npm": [], - "python": [], - "system": [] - }, - "beta.agents.environments.template_create_params.TemplateCreateParams": { - "capability_directories": [], - "env": [], - "files": [ - "beta.hosted_environment_file_param.HostedEnvironmentFileParamFileID", - "beta.hosted_environment_file_param.HostedEnvironmentFileParamInline" - ], - "name": [], - "network": [ - "beta.agents.environments.template_create_params.Network" - ], - "packages": [ - "beta.agents.environments.template_create_params.Packages" - ], - "plugins": [ - "beta.hosted_plugin_param.HostedPluginParam" - ], - "setup_commands": [ - "beta.setup_command_param.SetupCommandParam" - ], - "skills": [ - "beta.hosted_skill_param.HostedSkillParamInline", - "beta.hosted_skill_param.HostedSkillParamSkillReference" - ] - }, - "beta.agents.environments.template_list_params.TemplateListParams": { - "after": [], - "limit": [], - "order": [] - }, - "beta.agents.environments.template_update_params.Network": { - "access": [], - "allowed_domains": [] - }, - "beta.agents.environments.template_update_params.Packages": { - "npm": [], - "python": [], - "system": [] - }, - "beta.agents.environments.template_update_params.TemplateUpdateParams": { - "capability_directories": [], - "env": [], - "files": [ - "beta.hosted_environment_file_param.HostedEnvironmentFileParamFileID", - "beta.hosted_environment_file_param.HostedEnvironmentFileParamInline" - ], - "name": [], - "network": [ - "beta.agents.environments.template_update_params.Network" - ], - "packages": [ - "beta.agents.environments.template_update_params.Packages" - ], - "plugins": [ - "beta.hosted_plugin_param.HostedPluginParam" - ], - "setup_commands": [ - "beta.setup_command_param.SetupCommandParam" - ], - "skills": [ - "beta.hosted_skill_param.HostedSkillParamInline", - "beta.hosted_skill_param.HostedSkillParamSkillReference" - ] - }, - "beta.agents.session_create_params.Agent": { - "instructions": [], - "model": [], - "multi_agent": [ - "beta.multi_agent_config_param.MultiAgentConfigParam" - ], - "reasoning": [ - "beta.agent_reasoning_param.AgentReasoningParam" - ], - "service_tier": [], - "text": [ - "beta.agent_text_param.AgentTextParam" - ], - "tools": [ - "beta.agent_tool_param.AgentToolConfigParamFunction", - "beta.agent_tool_param.AgentToolConfigParamMcp", - "beta.agent_tool_param.AgentToolConfigParamProgrammaticToolCalling", - "beta.agent_tool_param.AgentToolConfigParamToolSearch", - "beta.agent_tool_param.AgentToolConfigParamWebSearch" - ] - }, - "beta.agents.session_create_params.SessionCreateParamsNonStreaming": { - "agent": [ - "beta.agents.session_create_params.Agent" - ], - "agent_id": [], - "environment": [ - "beta.environment_param.EnvironmentParamNone", - "beta.environment_param.EnvironmentParamOpenAIHosted", - "beta.environment_param.EnvironmentParamSelfHosted" - ], - "input": [ - "beta.agent_session_input_message_param.AgentSessionInputMessageParam" - ], - "metadata": [], - "stream": [], - "vault_ids": [] - }, - "beta.agents.session_create_params.SessionCreateParamsStreaming": { - "agent": [ - "beta.agents.session_create_params.Agent" - ], - "agent_id": [], - "environment": [ - "beta.environment_param.EnvironmentParamNone", - "beta.environment_param.EnvironmentParamOpenAIHosted", - "beta.environment_param.EnvironmentParamSelfHosted" - ], - "input": [ - "beta.agent_session_input_message_param.AgentSessionInputMessageParam" - ], - "metadata": [], - "stream": [], - "vault_ids": [] - }, - "beta.agents.session_list_params.SessionListParams": { - "after": [], - "agent_id": [], - "limit": [], - "order": [] - }, - "beta.agents.session_update_params.SessionUpdateParams": { - "metadata": [] - }, - "beta.agents.sessions.artifact_list_params.ArtifactListParams": { - "after": [], - "environment_id": [], - "limit": [], - "order": [] - }, - "beta.agents.sessions.event_create_params.EventCreateParams": { - "Idempotency-Key": [], - "events": [ - "beta.agent_session_input_param.SessionInputParamAgentSessionInputCancel", - "beta.agent_session_input_param.SessionInputParamAgentSessionInputMessage", - "beta.agent_session_input_param.SessionInputParamAgentSessionInputToolResult" - ] - }, - "beta.agents.sessions.item_list_params.ItemListParams": { - "after": [], - "limit": [], - "order": [] - }, - "beta.agents.sessions.session_artifact.SessionArtifact": { - "created_at": [], - "environment_id": [], - "id": [], - "object": [], - "path": [], - "session_id": [], - "size_bytes": [], - "turn_id": [] - }, - "beta.agents.sessions.session_artifact_deleted.SessionArtifactDeleted": { - "deleted": [], - "id": [], - "object": [] - }, - "beta.agents.sessions.subagent_list_params.SubagentListParams": { - "after": [], - "limit": [], - "order": [] - }, - "beta.agents.sessions.subagents.item_list_params.ItemListParams": { - "after": [], - "limit": [], - "order": [], - "session_id": [] - }, - "beta.agents.sessions.subagents.turn_list_params.TurnListParams": { - "after": [], - "limit": [], - "order": [], - "session_id": [] - }, - "beta.agents.sessions.subagents.turns.item_list_params.ItemListParams": { - "after": [], - "limit": [], - "order": [], - "session_id": [], - "subagent_id": [] - }, - "beta.agents.sessions.turn.Turn": { - "agent_id": [], - "completed_at": [], - "created_at": [], - "error": [ - "beta.session_turn_error.SessionTurnError" - ], - "id": [], - "object": [], - "session_id": [], - "started_at": [], - "status": [], - "subagent_id": [], - "usage": [ - "beta.token_usage.TokenUsage" - ] - }, - "beta.agents.sessions.turn_list_params.TurnListParams": { - "after": [], - "limit": [], - "order": [] - }, - "beta.agents.vault.Vault": { - "created_at": [], - "id": [], - "metadata": [], - "name": [], - "object": [] - }, - "beta.agents.vault_create_params.VaultCreateParams": { - "metadata": [], - "name": [] - }, - "beta.agents.vault_deleted.VaultDeleted": { - "deleted": [], - "id": [], - "object": [] - }, - "beta.agents.vault_list_params.VaultListParams": { - "after": [], - "limit": [], - "order": [], - "status": [] - }, - "beta.agents.vaults.credential.Credential": { - "auth": [ - "beta.agents.vaults.credential_auth.VaultCredentialAuthResourceMcpOauth", - "beta.agents.vaults.credential_auth.VaultCredentialAuthResourceStaticBearer" - ], - "created_at": [], - "id": [], - "name": [], - "object": [], - "updated_at": [], - "vault_id": [] - }, - "beta.agents.vaults.credential_auth.VaultCredentialAuthResourceMcpOauth": { - "expires_at": [], - "mcp_server_url": [], - "refresh": [ - "beta.agents.vaults.credential_auth.VaultCredentialAuthResourceMcpOauthRefresh" - ], - "type": [] - }, - "beta.agents.vaults.credential_auth.VaultCredentialAuthResourceMcpOauthRefresh": { - "client_id": [], - "resource": [], - "scope": [], - "token_endpoint": [], - "token_endpoint_auth": [ - "beta.agents.vaults.mcp_oauth_token_endpoint_auth.McpOauthTokenEndpointAuthResourceClientSecretBasic", - "beta.agents.vaults.mcp_oauth_token_endpoint_auth.McpOauthTokenEndpointAuthResourceClientSecretPost", - "beta.agents.vaults.mcp_oauth_token_endpoint_auth.McpOauthTokenEndpointAuthResourceNone" - ] - }, - "beta.agents.vaults.credential_auth.VaultCredentialAuthResourceStaticBearer": { - "mcp_server_url": [], - "type": [] - }, - "beta.agents.vaults.credential_auth_create_param.CreateVaultCredentialAuthParamMcpOauth": { - "access_token": [], - "expires_at": [], - "mcp_server_url": [], - "refresh": [ - "beta.agents.vaults.credential_auth_create_param.CreateVaultCredentialAuthParamMcpOauthRefresh" - ], - "type": [] - }, - "beta.agents.vaults.credential_auth_create_param.CreateVaultCredentialAuthParamMcpOauthRefresh": { - "client_id": [], - "refresh_token": [], - "resource": [], - "scope": [], - "token_endpoint": [], - "token_endpoint_auth": [ - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_create_param.CreateMcpOauthTokenEndpointAuthParamClientSecretBasic", - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_create_param.CreateMcpOauthTokenEndpointAuthParamClientSecretPost", - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_create_param.CreateMcpOauthTokenEndpointAuthParamNone" - ] - }, - "beta.agents.vaults.credential_auth_create_param.CreateVaultCredentialAuthParamStaticBearer": { - "mcp_server_url": [], - "token": [], - "type": [] - }, - "beta.agents.vaults.credential_auth_rotate_param.RotateVaultCredentialAuthParamMcpOauth": { - "access_token": [], - "expires_at": [], - "refresh": [ - "beta.agents.vaults.credential_auth_rotate_param.RotateVaultCredentialAuthParamMcpOauthRefresh" - ], - "type": [] - }, - "beta.agents.vaults.credential_auth_rotate_param.RotateVaultCredentialAuthParamMcpOauthRefresh": { - "refresh_token": [], - "scope": [], - "token_endpoint_auth": [ - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_rotate_param.RotateMcpOauthTokenEndpointAuthParamClientSecretBasic", - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_rotate_param.RotateMcpOauthTokenEndpointAuthParamClientSecretPost" - ] - }, - "beta.agents.vaults.credential_auth_rotate_param.RotateVaultCredentialAuthParamStaticBearer": { - "token": [], - "type": [] - }, - "beta.agents.vaults.credential_create_params.CredentialCreateParams": { - "auth": [ - "beta.agents.vaults.credential_auth_create_param.CreateVaultCredentialAuthParamMcpOauth", - "beta.agents.vaults.credential_auth_create_param.CreateVaultCredentialAuthParamStaticBearer" - ], - "name": [] - }, - "beta.agents.vaults.credential_deleted.CredentialDeleted": { - "deleted": [], - "id": [], - "object": [] - }, - "beta.agents.vaults.credential_list_params.CredentialListParams": { - "after": [], - "limit": [], - "order": [], - "status": [] - }, - "beta.agents.vaults.credential_update_params.CredentialUpdateParams": { - "auth": [ - "beta.agents.vaults.credential_auth_rotate_param.RotateVaultCredentialAuthParamMcpOauth", - "beta.agents.vaults.credential_auth_rotate_param.RotateVaultCredentialAuthParamStaticBearer" - ], - "vault_id": [] - }, - "beta.agents.vaults.mcp_oauth_token_endpoint_auth.McpOauthTokenEndpointAuthResourceClientSecretBasic": { - "type": [] - }, - "beta.agents.vaults.mcp_oauth_token_endpoint_auth.McpOauthTokenEndpointAuthResourceClientSecretPost": { - "type": [] - }, - "beta.agents.vaults.mcp_oauth_token_endpoint_auth.McpOauthTokenEndpointAuthResourceNone": { - "type": [] - }, - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_create_param.CreateMcpOauthTokenEndpointAuthParamClientSecretBasic": { - "client_secret": [], - "type": [] - }, - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_create_param.CreateMcpOauthTokenEndpointAuthParamClientSecretPost": { - "client_secret": [], - "type": [] - }, - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_create_param.CreateMcpOauthTokenEndpointAuthParamNone": { - "type": [] - }, - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_rotate_param.RotateMcpOauthTokenEndpointAuthParamClientSecretBasic": { - "client_secret": [], - "type": [] - }, - "beta.agents.vaults.mcp_oauth_token_endpoint_auth_rotate_param.RotateMcpOauthTokenEndpointAuthParamClientSecretPost": { - "client_secret": [], - "type": [] - }, - "beta.environment.EnvironmentResourceNone": { - "type": [] - }, - "beta.environment.EnvironmentResourceOpenAIHosted": { - "capability_directories": [], - "files": [ - "beta.hosted_environment_file.HostedEnvironmentFileResourceInline", - "beta.hosted_environment_file_id.HostedEnvironmentFileID" - ], - "id": [], - "network": [ - "beta.environment.EnvironmentResourceOpenAIHostedNetwork" - ], - "packages": [ - "beta.environment.EnvironmentResourceOpenAIHostedPackages" - ], - "plugins": [ - "beta.hosted_plugin.HostedPlugin" - ], - "skills": [ - "beta.hosted_skill.HostedSkillResourceInline", - "beta.hosted_skill_reference.HostedSkillReference" - ], - "type": [] - }, - "beta.environment.EnvironmentResourceOpenAIHostedNetwork": { - "access": [], - "allowed_domains": [] - }, - "beta.environment.EnvironmentResourceOpenAIHostedPackages": { - "npm": [], - "python": [], - "system": [] - }, - "beta.environment.EnvironmentResourceSelfHosted": { - "capability_directories": [], - "id": [], - "remote_url": [], - "type": [], - "workspace_directory": [] - }, - "beta.environment_param.EnvironmentParamNone": { - "type": [] - }, - "beta.environment_param.EnvironmentParamOpenAIHosted": { - "capability_directories": [], - "env": [], - "environment_template_id": [], - "files": [ - "beta.hosted_environment_file_param.HostedEnvironmentFileParamFileID", - "beta.hosted_environment_file_param.HostedEnvironmentFileParamInline" - ], - "network": [ - "beta.environment_param.EnvironmentParamOpenAIHostedNetwork" - ], - "packages": [ - "beta.environment_param.EnvironmentParamOpenAIHostedPackages" - ], - "plugins": [ - "beta.hosted_plugin_param.HostedPluginParam" - ], - "setup_commands": [ - "beta.setup_command_param.SetupCommandParam" - ], - "skills": [ - "beta.hosted_skill_param.HostedSkillParamInline", - "beta.hosted_skill_param.HostedSkillParamSkillReference" - ], - "type": [] - }, - "beta.environment_param.EnvironmentParamOpenAIHostedNetwork": { - "access": [], - "allowed_domains": [] - }, - "beta.environment_param.EnvironmentParamOpenAIHostedPackages": { - "npm": [], - "python": [], - "system": [] - }, - "beta.environment_param.EnvironmentParamSelfHosted": { - "capability_directories": [], - "type": [], - "workspace_directory": [] - }, - "beta.hosted_environment_file.HostedEnvironmentFileResourceInline": { - "id": [], - "path": [], - "size_bytes": [], - "type": [] - }, - "beta.hosted_environment_file_id.HostedEnvironmentFileID": { - "file_id": [], - "id": [], - "path": [], - "size_bytes": [], - "type": [] - }, - "beta.hosted_environment_file_param.HostedEnvironmentFileParamFileID": { - "file_id": [], - "path": [], - "type": [] - }, - "beta.hosted_environment_file_param.HostedEnvironmentFileParamInline": { - "data": [], - "path": [], - "type": [] - }, - "beta.hosted_plugin.HostedPlugin": { - "description": [], - "name": [], - "type": [] - }, - "beta.hosted_plugin_param.HostedPluginParam": { - "description": [], - "name": [], - "source": [ - "beta.inline_capability_source_param.InlineCapabilitySourceParam" - ], - "type": [] - }, - "beta.hosted_skill.HostedSkillResourceInline": { - "description": [], - "name": [], - "type": [] - }, - "beta.hosted_skill_param.HostedSkillParamInline": { - "description": [], - "name": [], - "source": [ - "beta.inline_capability_source_param.InlineCapabilitySourceParam" - ], - "type": [] - }, - "beta.hosted_skill_param.HostedSkillParamSkillReference": { - "skill_id": [], - "type": [], - "version": [] - }, - "beta.hosted_skill_reference.HostedSkillReference": { - "description": [], - "name": [], - "skill_id": [], - "type": [], - "version": [] - }, - "beta.inline_capability_source_param.InlineCapabilitySourceParam": { - "data": [], - "media_type": [], - "type": [] - }, - "beta.input_content.InputContentResourceInputImage": { - "image_url": [], - "type": [] - }, - "beta.input_content.InputContentResourceInputText": { - "text": [], - "type": [] - }, - "beta.input_content_param.InputContentParamInputImage": { - "image_url": [], - "type": [] - }, - "beta.input_content_param.InputContentParamInputText": { - "text": [], - "type": [] - }, - "beta.mcp_transport.McpTransportResourceHTTP": { - "server_url": [], - "type": [] - }, - "beta.mcp_transport.McpTransportResourceStdio": { - "args": [], - "command": [], - "cwd": [], - "env_vars": [], - "type": [] - }, - "beta.mcp_transport_param.McpTransportConfigParamHTTP": { - "authorization": [], - "headers": [], - "server_url": [], - "type": [] - }, - "beta.mcp_transport_param.McpTransportConfigParamStdio": { - "args": [], - "command": [], - "cwd": [], - "env": [], - "env_vars": [], - "type": [] - }, - "beta.multi_agent_config.MultiAgentConfig": { - "enabled": [], - "max_concurrent_subagents": [] - }, - "beta.multi_agent_config_param.MultiAgentConfigParam": { - "enabled": [], - "max_concurrent_subagents": [] - }, - "beta.output_text.OutputText": { - "text": [], - "type": [] - }, - "beta.persisted_agent_tool.PersistedAgentToolResourceFunction": { - "defer_loading": [], - "description": [], - "name": [], - "parameters": [], - "type": [] - }, - "beta.persisted_agent_tool.PersistedAgentToolResourceMcp": { - "allowed_tools": [], - "connection_origin": [], - "credential_id": [], - "request_metadata": [], - "required": [], - "server_label": [], - "transport": [ - "beta.persisted_mcp_transport.PersistedMcpTransportResourceHTTP", - "beta.persisted_mcp_transport.PersistedMcpTransportResourceStdio" - ], - "type": [] - }, - "beta.persisted_agent_tool.PersistedAgentToolResourceProgrammaticToolCalling": { - "enabled": [], - "type": [] - }, - "beta.persisted_agent_tool.PersistedAgentToolResourceToolSearch": { - "type": [] - }, - "beta.persisted_agent_tool.PersistedAgentToolResourceWebSearch": { - "allowed_domains": [], - "context_size": [], - "location": [ - "beta.persisted_agent_tool.PersistedAgentToolResourceWebSearchLocation" - ], - "mode": [], - "type": [] - }, - "beta.persisted_agent_tool.PersistedAgentToolResourceWebSearchLocation": { - "city": [], - "country": [], - "region": [], - "timezone": [] - }, - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamFunction": { - "defer_loading": [], - "description": [], - "name": [], - "parameters": [], - "type": [] - }, - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamMcp": { - "allowed_tools": [], - "connection_origin": [], - "credential_id": [], - "request_metadata": [], - "required": [], - "server_label": [], - "transport": [ - "beta.persisted_mcp_transport_param.PersistedMcpTransportConfigParamHTTP", - "beta.persisted_mcp_transport_param.PersistedMcpTransportConfigParamStdio" - ], - "type": [] - }, - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamProgrammaticToolCalling": { - "enabled": [], - "type": [] - }, - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamToolSearch": { - "type": [] - }, - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamWebSearch": { - "allowed_domains": [], - "context_size": [], - "location": [ - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamWebSearchLocation" - ], - "mode": [], - "type": [] - }, - "beta.persisted_agent_tool_param.PersistedAgentToolConfigParamWebSearchLocation": { - "city": [], - "country": [], - "region": [], - "timezone": [] - }, - "beta.persisted_mcp_transport.PersistedMcpTransportResourceHTTP": { - "headers": [], - "server_url": [], - "type": [] - }, - "beta.persisted_mcp_transport.PersistedMcpTransportResourceStdio": { - "args": [], - "command": [], - "cwd": [], - "env_vars": [], - "type": [] - }, - "beta.persisted_mcp_transport_param.PersistedMcpTransportConfigParamHTTP": { - "headers": [], - "server_url": [], - "type": [] - }, - "beta.persisted_mcp_transport_param.PersistedMcpTransportConfigParamStdio": { - "args": [], - "command": [], - "cwd": [], - "env_vars": [], - "type": [] - }, - "beta.session_error.SessionError": { - "code": [], - "message": [], - "param": [], - "type": [] - }, - "beta.session_turn_error.SessionTurnError": { - "code": [], - "message": [] - }, - "beta.setup_command_param.SetupCommandParam": { - "command": [], - "cwd": [] - }, - "beta.subagent.Subagent": { - "closed_at": [], - "id": [], - "instructions": [ - "beta.agent_content.EncryptedContentResource", - "beta.output_text.OutputText" - ], - "name": [], - "object": [], - "opened_at": [], - "parent_agent_id": [], - "session_id": [], - "status": [] - }, - "beta.summary_text.SummaryText": { - "text": [], - "type": [] - }, - "beta.text_format.TextFormatResourceJSONSchema": { - "schema": [], - "type": [] - }, - "beta.text_format.TextFormatResourceText": { - "type": [] - }, - "beta.text_format_param.TextFormatParamJSONSchema": { - "schema": [], - "type": [] - }, - "beta.text_format_param.TextFormatParamText": { - "type": [] - }, - "beta.token_usage.InputTokensDetails": { - "cached_tokens": [] - }, - "beta.token_usage.OutputTokensDetails": { - "reasoning_tokens": [] - }, - "beta.token_usage.TokenUsage": { - "input_tokens": [], - "input_tokens_details": [ - "beta.token_usage.InputTokensDetails" - ], - "output_tokens": [], - "output_tokens_details": [ - "beta.token_usage.OutputTokensDetails" - ], - "total_tokens": [] - }, - "beta.web_search_action.WebSearchActionResourceFindInPage": { - "pattern": [], - "type": [], - "url": [] - }, - "beta.web_search_action.WebSearchActionResourceOpenPage": { - "type": [], - "url": [] - }, - "beta.web_search_action.WebSearchActionResourceOther": { - "type": [] - }, - "beta.web_search_action.WebSearchActionResourceSearch": { - "queries": [], - "query": [], - "type": [] - }, - "deleted_skill.DeletedSkill": { - "deleted": [], - "id": [], - "object": [] - }, - "file_create_params.ExpiresAfter": { - "anchor": [], - "seconds": [] - }, - "file_create_params.FileCreateParams": { - "expires_after": [ - "file_create_params.ExpiresAfter" - ], - "file": [], - "purpose": [] - }, - "file_deleted.FileDeleted": { - "deleted": [], - "id": [], - "object": [] - }, - "file_list_params.FileListParams": { - "after": [], - "limit": [], - "order": [], - "purpose": [] - }, - "file_object.FileObject": { - "bytes": [], - "created_at": [], - "expires_at": [], - "filename": [], - "id": [], - "object": [], - "purpose": [], - "status": [], - "status_details": [] - }, - "pagination.SyncCursorPage[beta.agent.Agent]": { - "data": [ - "beta.agent.Agent" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[beta.agent_close_subagent_call_item.AgentCloseSubagentCallItem | beta.agent_command_execution_item.AgentCommandExecutionItem | beta.agent_create_subagent_call_item.AgentCreateSubagentCallItem | beta.agent_function_call_item.AgentFunctionCallItem | beta.agent_interrupt_subagent_call_item.AgentInterruptSubagentCallItem | beta.agent_mcp_call_item.AgentMcpCallItem | beta.agent_reasoning_item.AgentReasoningItem | beta.agent_resume_subagent_call_item.AgentResumeSubagentCallItem | beta.agent_send_subagent_input_call_item.AgentSendSubagentInputCallItem | beta.agent_session_item.AgentMessageItemResource | beta.agent_session_item.FunctionCallOutputItemResource | beta.agent_session_message.AgentSessionMessage | beta.agent_wait_for_subagents_call_item.AgentWaitForSubagentsCallItem | beta.agent_web_search_call_item.AgentWebSearchCallItem]": { - "data": [ - "beta.agent_close_subagent_call_item.AgentCloseSubagentCallItem", - "beta.agent_command_execution_item.AgentCommandExecutionItem", - "beta.agent_create_subagent_call_item.AgentCreateSubagentCallItem", - "beta.agent_function_call_item.AgentFunctionCallItem", - "beta.agent_interrupt_subagent_call_item.AgentInterruptSubagentCallItem", - "beta.agent_mcp_call_item.AgentMcpCallItem", - "beta.agent_reasoning_item.AgentReasoningItem", - "beta.agent_resume_subagent_call_item.AgentResumeSubagentCallItem", - "beta.agent_send_subagent_input_call_item.AgentSendSubagentInputCallItem", - "beta.agent_session_item.AgentMessageItemResource", - "beta.agent_session_item.FunctionCallOutputItemResource", - "beta.agent_session_message.AgentSessionMessage", - "beta.agent_wait_for_subagents_call_item.AgentWaitForSubagentsCallItem", - "beta.agent_web_search_call_item.AgentWebSearchCallItem" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[beta.agent_session.AgentSession]": { - "data": [ - "beta.agent_session.AgentSession" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[beta.agents.environments.environment_template.EnvironmentTemplate]": { - "data": [ - "beta.agents.environments.environment_template.EnvironmentTemplate" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[beta.agents.sessions.session_artifact.SessionArtifact]": { - "data": [ - "beta.agents.sessions.session_artifact.SessionArtifact" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[beta.agents.sessions.turn.Turn]": { - "data": [ - "beta.agents.sessions.turn.Turn" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[beta.agents.vault.Vault]": { - "data": [ - "beta.agents.vault.Vault" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[beta.agents.vaults.credential.Credential]": { - "data": [ - "beta.agents.vaults.credential.Credential" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[beta.subagent.Subagent]": { - "data": [ - "beta.subagent.Subagent" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[file_object.FileObject]": { - "data": [ - "file_object.FileObject" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[skill.Skill]": { - "data": [ - "skill.Skill" - ], - "has_more": [] - }, - "pagination.SyncCursorPage[skills.skill_version.SkillVersion]": { - "data": [ - "skills.skill_version.SkillVersion" - ], - "has_more": [] - }, - "pagination.SyncTokenPage[beta.agents.environments.environment_file.EnvironmentFile]": { - "data": [ - "beta.agents.environments.environment_file.EnvironmentFile" - ], - "has_more": [], - "next": [] - }, - "skill.Skill": { - "created_at": [], - "default_version": [], - "description": [], - "id": [], - "latest_version": [], - "name": [], - "object": [] - }, - "skill_create_params.SkillCreateParams": { - "files": [] - }, - "skill_list_params.SkillListParams": { - "after": [], - "limit": [], - "order": [] - }, - "skill_update_params.SkillUpdateParams": { - "default_version": [] - }, - "skills.deleted_skill_version.DeletedSkillVersion": { - "deleted": [], - "id": [], - "object": [], - "version": [] - }, - "skills.skill_version.SkillVersion": { - "created_at": [], - "description": [], - "id": [], - "name": [], - "object": [], - "skill_id": [], - "version": [] - }, - "skills.version_create_params.VersionCreateParams": { - "default": [], - "files": [] - }, - "skills.version_list_params.VersionListParams": { - "after": [], - "limit": [], - "order": [] - } - } -} diff --git a/contracts/agents-api/upstream-routes.json b/contracts/agents-api/upstream-routes.json index 3326834f2..1f305b93a 100644 --- a/contracts/agents-api/upstream-routes.json +++ b/contracts/agents-api/upstream-routes.json @@ -1,12 +1,6 @@ { - "sdk_version": "3.13.0", - "commit": "d7c41efee1b0802b79f3f88a678ef2052b06e9ce", - "generator": "scripts/extract-agents-api-upstream.py", - "resources": [ - "openai.resources.beta.agents", - "openai.resources.files", - "openai.resources.skills" - ], + "openapi_commit": "046a2a0f325bf11f97966f2729219f27281ba71e", + "generator": "scripts/generate-public-api.py", "routes": [ "DELETE /agents/environments/templates/{}", "DELETE /agents/sessions/{}", diff --git a/contracts/agents-api/upstream.json b/contracts/agents-api/upstream.json index adc71d0bb..253e00627 100644 --- a/contracts/agents-api/upstream.json +++ b/contracts/agents-api/upstream.json @@ -4,5 +4,11 @@ "sdk_version": "3.13.0", "resource_path": "src/openai/resources/beta/agents", "type_path": "src/openai/types/beta", - "beta_header": "agents=v1" + "beta_header": "agents=v1", + "openapi": { + "repository": "https://github.com/openai/openai-openapi", + "commit": "046a2a0f325bf11f97966f2729219f27281ba71e", + "path": "openapi.json", + "sha256": "c96d974b164ff4750fa5ea5a1727f7ce6691540e257c177db02b660b0db58f26" + } } diff --git a/contracts/agents-api/upstream/LICENSE b/contracts/agents-api/upstream/LICENSE new file mode 100644 index 000000000..4f14854c3 --- /dev/null +++ b/contracts/agents-api/upstream/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) OpenAI (https://openai.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/contracts/agents-api/upstream/openapi.json b/contracts/agents-api/upstream/openapi.json new file mode 100644 index 000000000..f966c2530 --- /dev/null +++ b/contracts/agents-api/upstream/openapi.json @@ -0,0 +1,107618 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAI API", + "description": "The OpenAI REST API. Please see https://platform.openai.com/docs/api-reference for more details.", + "version": "2.3.0", + "termsOfService": "https://openai.com/policies/terms-of-use", + "contact": { + "name": "OpenAI Support", + "url": "https://help.openai.com/" + }, + "license": { + "name": "MIT", + "identifier": "MIT" + } + }, + "servers": [ + { + "url": "https://api.openai.com/v1" + } + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "tags": [ + { + "name": "Assistants", + "description": "Build Assistants that can call models and use tools." + }, + { + "name": "Audio", + "description": "Turn audio into text or text into audio." + }, + { + "name": "Chat", + "description": "Given a list of messages comprising a conversation, the model will return a response." + }, + { + "name": "Conversations", + "description": "Manage conversations and conversation items." + }, + { + "name": "Completions", + "description": "Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position." + }, + { + "name": "Embeddings", + "description": "Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms." + }, + { + "name": "Evals", + "description": "Manage and run evals in the OpenAI platform." + }, + { + "name": "Fine-tuning", + "description": "Manage fine-tuning jobs to tailor a model to your specific training data." + }, + { + "name": "Graders", + "description": "Manage and run graders in the OpenAI platform." + }, + { + "name": "Batch", + "description": "Create large batches of API requests to run asynchronously." + }, + { + "name": "Files", + "description": "Files are used to upload documents that can be used with features like Assistants and Fine-tuning." + }, + { + "name": "Uploads", + "description": "Use Uploads to upload large files in multiple parts." + }, + { + "name": "Images", + "description": "Given a prompt and/or an input image, the model will generate a new image." + }, + { + "name": "Models", + "description": "List and describe the various models available in the API." + }, + { + "name": "Moderations", + "description": "Given text and/or image inputs, classifies if those inputs are potentially harmful." + }, + { + "name": "Audit Logs", + "description": "List user actions and configuration changes within this organization." + } + ], + "paths": { + "/assistants": { + "get": { + "operationId": "listAssistants", + "tags": [ + "Assistants" + ], + "summary": "Returns a list of assistants.", + "deprecated": true, + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order.\n", + "schema": { + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "schema": { + "type": "string" + } + }, + { + "name": "before", + "in": "query", + "description": "A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list.\n", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListAssistantsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List assistants", + "group": "assistants", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/assistants?order=desc&limit=20\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"OpenAI-Beta: assistants=v2\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nmy_assistants = client.beta.assistants.list(\n order=\"desc\",\n limit=\"20\",\n)\nprint(my_assistants.data)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const myAssistants = await openai.beta.assistants.list({\n order: \"desc\",\n limit: \"20\",\n });\n\n console.log(myAssistants.data);\n}\n\nmain();" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"asst_abc123\",\n \"object\": \"assistant\",\n \"created_at\": 1698982736,\n \"name\": \"Coding Tutor\",\n \"description\": null,\n \"model\": \"gpt-5\",\n \"instructions\": \"You are a helpful assistant designed to make me better at coding!\",\n \"tools\": [],\n \"tool_resources\": {},\n \"metadata\": {},\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"response_format\": \"auto\"\n },\n {\n \"id\": \"asst_abc456\",\n \"object\": \"assistant\",\n \"created_at\": 1698982718,\n \"name\": \"My Assistant\",\n \"description\": null,\n \"model\": \"gpt-5\",\n \"instructions\": \"You are a helpful assistant designed to make me better at coding!\",\n \"tools\": [],\n \"tool_resources\": {},\n \"metadata\": {},\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"response_format\": \"auto\"\n },\n {\n \"id\": \"asst_abc789\",\n \"object\": \"assistant\",\n \"created_at\": 1698982643,\n \"name\": null,\n \"description\": null,\n \"model\": \"gpt-5\",\n \"instructions\": null,\n \"tools\": [],\n \"tool_resources\": {},\n \"metadata\": {},\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"response_format\": \"auto\"\n }\n ],\n \"first_id\": \"asst_abc123\",\n \"last_id\": \"asst_abc789\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "operationId": "createAssistant", + "tags": [ + "Assistants" + ], + "summary": "Create an assistant with a model and instructions.", + "deprecated": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAssistantRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssistantObject" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Create assistant", + "group": "assistants", + "examples": [ + { + "title": "Code Interpreter", + "request": { + "curl": "curl \"https://api.openai.com/v1/assistants\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"OpenAI-Beta: assistants=v2\" \\\n -d '{\n \"instructions\": \"You are a personal math tutor. When asked a question, write and run Python code to answer the question.\",\n \"name\": \"Math Tutor\",\n \"tools\": [{\"type\": \"code_interpreter\"}],\n \"model\": \"gpt-5\"\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nmy_assistant = client.beta.assistants.create(\n instructions=\"You are a personal math tutor. When asked a question, write and run Python code to answer the question.\",\n name=\"Math Tutor\",\n tools=[{\"type\": \"code_interpreter\"}],\n model=\"gpt-5\",\n)\nprint(my_assistant)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const myAssistant = await openai.beta.assistants.create({\n instructions:\n \"You are a personal math tutor. When asked a question, write and run Python code to answer the question.\",\n name: \"Math Tutor\",\n tools: [{ type: \"code_interpreter\" }],\n model: \"gpt-5\",\n });\n\n console.log(myAssistant);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"asst_abc123\",\n \"object\": \"assistant\",\n \"created_at\": 1698984975,\n \"name\": \"Math Tutor\",\n \"description\": null,\n \"model\": \"gpt-5\",\n \"instructions\": \"You are a personal math tutor. When asked a question, write and run Python code to answer the question.\",\n \"tools\": [\n {\n \"type\": \"code_interpreter\"\n }\n ],\n \"metadata\": {},\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"response_format\": \"auto\"\n}\n" + }, + { + "title": "Files", + "request": { + "curl": "curl https://api.openai.com/v1/assistants \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"OpenAI-Beta: assistants=v2\" \\\n -d '{\n \"instructions\": \"You are an HR bot, and you have access to files to answer employee questions about company policies.\",\n \"tools\": [{\"type\": \"file_search\"}],\n \"tool_resources\": {\"file_search\": {\"vector_store_ids\": [\"vs_123\"]}},\n \"model\": \"gpt-5\"\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nmy_assistant = client.beta.assistants.create(\n instructions=\"You are an HR bot, and you have access to files to answer employee questions about company policies.\",\n name=\"HR Helper\",\n tools=[{\"type\": \"file_search\"}],\n tool_resources={\"file_search\": {\"vector_store_ids\": [\"vs_123\"]}},\n model=\"gpt-5\"\n)\nprint(my_assistant)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const myAssistant = await openai.beta.assistants.create({\n instructions:\n \"You are an HR bot, and you have access to files to answer employee questions about company policies.\",\n name: \"HR Helper\",\n tools: [{ type: \"file_search\" }],\n tool_resources: {\n file_search: {\n vector_store_ids: [\"vs_123\"]\n }\n },\n model: \"gpt-5\"\n });\n\n console.log(myAssistant);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"asst_abc123\",\n \"object\": \"assistant\",\n \"created_at\": 1699009403,\n \"name\": \"HR Helper\",\n \"description\": null,\n \"model\": \"gpt-5\",\n \"instructions\": \"You are an HR bot, and you have access to files to answer employee questions about company policies.\",\n \"tools\": [\n {\n \"type\": \"file_search\"\n }\n ],\n \"tool_resources\": {\n \"file_search\": {\n \"vector_store_ids\": [\"vs_123\"]\n }\n },\n \"metadata\": {},\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"response_format\": \"auto\"\n}\n" + } + ] + } + } + }, + "/assistants/{assistant_id}": { + "get": { + "operationId": "getAssistant", + "tags": [ + "Assistants" + ], + "summary": "Retrieves an assistant.", + "deprecated": true, + "parameters": [ + { + "in": "path", + "name": "assistant_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the assistant to retrieve." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssistantObject" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve assistant", + "group": "assistants", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/assistants/asst_abc123 \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"OpenAI-Beta: assistants=v2\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nmy_assistant = client.beta.assistants.retrieve(\"asst_abc123\")\nprint(my_assistant)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const myAssistant = await openai.beta.assistants.retrieve(\n \"asst_abc123\"\n );\n\n console.log(myAssistant);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"asst_abc123\",\n \"object\": \"assistant\",\n \"created_at\": 1699009709,\n \"name\": \"HR Helper\",\n \"description\": null,\n \"model\": \"gpt-5\",\n \"instructions\": \"You are an HR bot, and you have access to files to answer employee questions about company policies.\",\n \"tools\": [\n {\n \"type\": \"file_search\"\n }\n ],\n \"metadata\": {},\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"response_format\": \"auto\"\n}\n" + } + } + }, + "post": { + "operationId": "modifyAssistant", + "tags": [ + "Assistants" + ], + "summary": "Modifies an assistant.", + "deprecated": true, + "parameters": [ + { + "in": "path", + "name": "assistant_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the assistant to modify." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModifyAssistantRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssistantObject" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Modify assistant", + "group": "assistants", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/assistants/asst_abc123 \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"OpenAI-Beta: assistants=v2\" \\\n -d '{\n \"instructions\": \"You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.\",\n \"tools\": [{\"type\": \"file_search\"}],\n \"model\": \"gpt-5\"\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nmy_updated_assistant = client.beta.assistants.update(\n \"asst_abc123\",\n instructions=\"You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.\",\n name=\"HR Helper\",\n tools=[{\"type\": \"file_search\"}],\n model=\"gpt-5\"\n)\n\nprint(my_updated_assistant)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const myUpdatedAssistant = await openai.beta.assistants.update(\n \"asst_abc123\",\n {\n instructions:\n \"You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.\",\n name: \"HR Helper\",\n tools: [{ type: \"file_search\" }],\n model: \"gpt-5\"\n }\n );\n\n console.log(myUpdatedAssistant);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"asst_123\",\n \"object\": \"assistant\",\n \"created_at\": 1699009709,\n \"name\": \"HR Helper\",\n \"description\": null,\n \"model\": \"gpt-5\",\n \"instructions\": \"You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.\",\n \"tools\": [\n {\n \"type\": \"file_search\"\n }\n ],\n \"tool_resources\": {\n \"file_search\": {\n \"vector_store_ids\": []\n }\n },\n \"metadata\": {},\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"response_format\": \"auto\"\n}\n" + } + } + }, + "delete": { + "operationId": "deleteAssistant", + "tags": [ + "Assistants" + ], + "summary": "Delete an assistant.", + "deprecated": true, + "parameters": [ + { + "in": "path", + "name": "assistant_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the assistant to delete." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteAssistantResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete assistant", + "group": "assistants", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/assistants/asst_abc123 \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"OpenAI-Beta: assistants=v2\" \\\n -X DELETE\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nresponse = client.beta.assistants.delete(\"asst_abc123\")\nprint(response)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const response = await openai.beta.assistants.delete(\"asst_abc123\");\n\n console.log(response);\n}\nmain();" + }, + "response": "{\n \"id\": \"asst_abc123\",\n \"object\": \"assistant.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/audio/speech": { + "post": { + "operationId": "createSpeech", + "tags": [ + "Audio" + ], + "summary": "Generates audio from the input text.\n\nReturns the audio file content, or a stream of audio events.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSpeechRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "Transfer-Encoding": { + "schema": { + "type": "string" + }, + "description": "chunked" + } + }, + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/CreateSpeechResponseStreamEvent" + } + } + } + }, + "429": { + "$ref": "#/components/responses/InferenceRateLimited" + }, + "503": { + "$ref": "#/components/responses/InferenceServiceUnavailable" + } + }, + "x-oaiMeta": { + "name": "Create speech", + "group": "audio", + "examples": [ + { + "title": "Default", + "request": { + "curl": "curl https://api.openai.com/v1/audio/speech \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"model\": \"gpt-4o-mini-tts\",\n \"input\": \"The quick brown fox jumped over the lazy dog.\",\n \"voice\": \"alloy\"\n }' \\\n --output speech.mp3\n", + "python": "from pathlib import Path\nimport openai\n\nspeech_file_path = Path(__file__).parent / \"speech.mp3\"\nwith openai.audio.speech.with_streaming_response.create(\n model=\"gpt-4o-mini-tts\",\n voice=\"alloy\",\n input=\"The quick brown fox jumped over the lazy dog.\"\n) as response:\n response.stream_to_file(speech_file_path)\n", + "javascript": "import fs from \"fs\";\nimport path from \"path\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst speechFile = path.resolve(\"./speech.mp3\");\n\nasync function main() {\n const mp3 = await openai.audio.speech.create({\n model: \"gpt-4o-mini-tts\",\n voice: \"alloy\",\n input: \"Today is a wonderful day to build something people love!\",\n });\n console.log(speechFile);\n const buffer = Buffer.from(await mp3.arrayBuffer());\n await fs.promises.writeFile(speechFile, buffer);\n}\nmain();\n", + "csharp": "using System;\nusing System.IO;\n\nusing OpenAI.Audio;\n\nAudioClient client = new(\n model: \"gpt-4o-mini-tts\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nBinaryData speech = client.GenerateSpeech(\n text: \"The quick brown fox jumped over the lazy dog.\",\n voice: GeneratedSpeechVoice.Alloy\n);\n\nusing FileStream stream = File.OpenWrite(\"speech.mp3\");\nspeech.ToStream().CopyTo(stream);\n" + } + }, + { + "title": "SSE Stream Format", + "request": { + "curl": "curl https://api.openai.com/v1/audio/speech \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"model\": \"gpt-4o-mini-tts\",\n \"input\": \"The quick brown fox jumped over the lazy dog.\",\n \"voice\": \"alloy\",\n \"stream_format\": \"sse\"\n }'\n" + } + } + ] + } + } + }, + "/audio/transcriptions": { + "post": { + "operationId": "createTranscription", + "tags": [ + "Audio" + ], + "summary": "Transcribes audio into the input language.\n\nReturns a transcription object in `json`, `diarized_json`, or `verbose_json`\nformat, or a stream of transcript events.\n", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateTranscriptionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateTranscriptionResponseJson" + }, + { + "$ref": "#/components/schemas/CreateTranscriptionResponseDiarizedJson" + }, + { + "$ref": "#/components/schemas/CreateTranscriptionResponseVerboseJson" + } + ] + } + }, + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/CreateTranscriptionResponseStreamEvent" + } + } + } + }, + "429": { + "$ref": "#/components/responses/InferenceRateLimited" + }, + "503": { + "$ref": "#/components/responses/InferenceServiceUnavailable" + } + }, + "x-oaiMeta": { + "name": "Create transcription", + "group": "audio", + "examples": [ + { + "title": "Default", + "request": { + "curl": "curl https://api.openai.com/v1/audio/transcriptions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: multipart/form-data\" \\\n -F file=\"@/path/to/file/audio.mp3\" \\\n -F model=\"gpt-4o-transcribe\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\naudio_file = open(\"speech.mp3\", \"rb\")\ntranscript = client.audio.transcriptions.create(\n model=\"gpt-4o-transcribe\",\n file=audio_file\n)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const transcription = await openai.audio.transcriptions.create({\n file: fs.createReadStream(\"audio.mp3\"),\n model: \"gpt-4o-transcribe\",\n });\n\n console.log(transcription.text);\n}\nmain();\n", + "csharp": "using System;\n\nusing OpenAI.Audio;\nstring audioFilePath = \"audio.mp3\";\n\nAudioClient client = new(\n model: \"gpt-4o-transcribe\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nAudioTranscription transcription = client.TranscribeAudio(audioFilePath);\n\nConsole.WriteLine($\"{transcription.Text}\");\n" + }, + "response": "{\n \"text\": \"Imagine the wildest idea that you've ever had, and you're curious about how it might scale to something that's a 100, a 1,000 times bigger. This is a place where you can get to do that.\",\n \"usage\": {\n \"type\": \"tokens\",\n \"input_tokens\": 14,\n \"input_token_details\": {\n \"text_tokens\": 0,\n \"audio_tokens\": 14\n },\n \"output_tokens\": 45,\n \"total_tokens\": 59\n }\n}\n" + }, + { + "title": "Diarization", + "request": { + "curl": "curl https://api.openai.com/v1/audio/transcriptions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: multipart/form-data\" \\\n -F file=\"@/path/to/file/meeting.wav\" \\\n -F model=\"gpt-4o-transcribe-diarize\" \\\n -F response_format=\"diarized_json\" \\\n -F chunking_strategy=auto \\\n -F 'known_speaker_names[]=agent' \\\n -F 'known_speaker_references[]=data:audio/wav;base64,AAA...'\n", + "python": "import base64\nfrom openai import OpenAI\n\nclient = OpenAI()\n\ndef to_data_url(path: str) -> str:\n with open(path, \"rb\") as fh:\n return \"data:audio/wav;base64,\" + base64.b64encode(fh.read()).decode(\"utf-8\")\n\nwith open(\"meeting.wav\", \"rb\") as audio_file:\n transcript = client.audio.transcriptions.create(\n model=\"gpt-4o-transcribe-diarize\",\n file=audio_file,\n response_format=\"diarized_json\",\n chunking_strategy=\"auto\",\n extra_body={\n \"known_speaker_names\": [\"agent\"],\n \"known_speaker_references\": [to_data_url(\"agent.wav\")],\n },\n )\n\nprint(transcript.segments)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst speakerRef = fs.readFileSync(\"agent.wav\").toString(\"base64\");\n\nconst transcript = await openai.audio.transcriptions.create({\n file: fs.createReadStream(\"meeting.wav\"),\n model: \"gpt-4o-transcribe-diarize\",\n response_format: \"diarized_json\",\n chunking_strategy: \"auto\",\n extra_body: {\n known_speaker_names: [\"agent\"],\n known_speaker_references: [`data:audio/wav;base64,${speakerRef}`],\n },\n});\n\nconsole.log(transcript.segments);\n" + }, + "response": "{\n \"task\": \"transcribe\",\n \"duration\": 27.4,\n \"text\": \"Agent: Thanks for calling OpenAI support.\\nA: Hi, I'm trying to enable diarization.\\nAgent: Happy to walk you through the steps.\",\n \"segments\": [\n {\n \"type\": \"transcript.text.segment\",\n \"id\": \"seg_001\",\n \"start\": 0.0,\n \"end\": 4.7,\n \"text\": \"Thanks for calling OpenAI support.\",\n \"speaker\": \"agent\"\n },\n {\n \"type\": \"transcript.text.segment\",\n \"id\": \"seg_002\",\n \"start\": 4.7,\n \"end\": 11.8,\n \"text\": \"Hi, I'm trying to enable diarization.\",\n \"speaker\": \"A\"\n },\n {\n \"type\": \"transcript.text.segment\",\n \"id\": \"seg_003\",\n \"start\": 12.1,\n \"end\": 18.5,\n \"text\": \"Happy to walk you through the steps.\",\n \"speaker\": \"agent\"\n }\n ],\n \"usage\": {\n \"type\": \"duration\",\n \"seconds\": 27\n }\n}\n" + }, + { + "title": "Streaming", + "request": { + "curl": "curl https://api.openai.com/v1/audio/transcriptions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: multipart/form-data\" \\\n -F file=\"@/path/to/file/audio.mp3\" \\\n -F model=\"gpt-4o-mini-transcribe\" \\\n -F stream=true\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\naudio_file = open(\"speech.mp3\", \"rb\")\nstream = client.audio.transcriptions.create(\n file=audio_file,\n model=\"gpt-4o-mini-transcribe\",\n stream=True\n)\n\nfor event in stream:\n print(event)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst stream = await openai.audio.transcriptions.create({\n file: fs.createReadStream(\"audio.mp3\"),\n model: \"gpt-4o-mini-transcribe\",\n stream: true,\n});\n\nfor await (const event of stream) {\n console.log(event);\n}\n" + }, + "response": "data: {\"type\":\"transcript.text.delta\",\"delta\":\"I\",\"logprobs\":[{\"token\":\"I\",\"logprob\":-0.00007588794,\"bytes\":[73]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" see\",\"logprobs\":[{\"token\":\" see\",\"logprob\":-3.1281633e-7,\"bytes\":[32,115,101,101]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" skies\",\"logprobs\":[{\"token\":\" skies\",\"logprob\":-2.3392786e-6,\"bytes\":[32,115,107,105,101,115]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" of\",\"logprobs\":[{\"token\":\" of\",\"logprob\":-3.1281633e-7,\"bytes\":[32,111,102]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" blue\",\"logprobs\":[{\"token\":\" blue\",\"logprob\":-1.0280384e-6,\"bytes\":[32,98,108,117,101]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" and\",\"logprobs\":[{\"token\":\" and\",\"logprob\":-0.0005108566,\"bytes\":[32,97,110,100]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" clouds\",\"logprobs\":[{\"token\":\" clouds\",\"logprob\":-1.9361265e-7,\"bytes\":[32,99,108,111,117,100,115]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" of\",\"logprobs\":[{\"token\":\" of\",\"logprob\":-1.9361265e-7,\"bytes\":[32,111,102]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" white\",\"logprobs\":[{\"token\":\" white\",\"logprob\":-7.89631e-7,\"bytes\":[32,119,104,105,116,101]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\",\",\"logprobs\":[{\"token\":\",\",\"logprob\":-0.0014890312,\"bytes\":[44]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" the\",\"logprobs\":[{\"token\":\" the\",\"logprob\":-0.0110956915,\"bytes\":[32,116,104,101]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" bright\",\"logprobs\":[{\"token\":\" bright\",\"logprob\":0.0,\"bytes\":[32,98,114,105,103,104,116]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" blessed\",\"logprobs\":[{\"token\":\" blessed\",\"logprob\":-0.000045848617,\"bytes\":[32,98,108,101,115,115,101,100]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" days\",\"logprobs\":[{\"token\":\" days\",\"logprob\":-0.000010802739,\"bytes\":[32,100,97,121,115]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\",\",\"logprobs\":[{\"token\":\",\",\"logprob\":-0.00001700133,\"bytes\":[44]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" the\",\"logprobs\":[{\"token\":\" the\",\"logprob\":-0.0000118755715,\"bytes\":[32,116,104,101]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" dark\",\"logprobs\":[{\"token\":\" dark\",\"logprob\":-5.5122365e-7,\"bytes\":[32,100,97,114,107]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" sacred\",\"logprobs\":[{\"token\":\" sacred\",\"logprob\":-5.4385737e-6,\"bytes\":[32,115,97,99,114,101,100]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" nights\",\"logprobs\":[{\"token\":\" nights\",\"logprob\":-4.00813e-6,\"bytes\":[32,110,105,103,104,116,115]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\",\",\"logprobs\":[{\"token\":\",\",\"logprob\":-0.0036910512,\"bytes\":[44]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" and\",\"logprobs\":[{\"token\":\" and\",\"logprob\":-0.0031903093,\"bytes\":[32,97,110,100]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" I\",\"logprobs\":[{\"token\":\" I\",\"logprob\":-1.504853e-6,\"bytes\":[32,73]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" think\",\"logprobs\":[{\"token\":\" think\",\"logprob\":-4.3202e-7,\"bytes\":[32,116,104,105,110,107]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" to\",\"logprobs\":[{\"token\":\" to\",\"logprob\":-1.9361265e-7,\"bytes\":[32,116,111]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" myself\",\"logprobs\":[{\"token\":\" myself\",\"logprob\":-1.7432603e-6,\"bytes\":[32,109,121,115,101,108,102]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\",\",\"logprobs\":[{\"token\":\",\",\"logprob\":-0.29254505,\"bytes\":[44]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" what\",\"logprobs\":[{\"token\":\" what\",\"logprob\":-0.016815351,\"bytes\":[32,119,104,97,116]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" a\",\"logprobs\":[{\"token\":\" a\",\"logprob\":-3.1281633e-7,\"bytes\":[32,97]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" wonderful\",\"logprobs\":[{\"token\":\" wonderful\",\"logprob\":-2.1008714e-6,\"bytes\":[32,119,111,110,100,101,114,102,117,108]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\" world\",\"logprobs\":[{\"token\":\" world\",\"logprob\":-8.180258e-6,\"bytes\":[32,119,111,114,108,100]}]}\n\ndata: {\"type\":\"transcript.text.delta\",\"delta\":\".\",\"logprobs\":[{\"token\":\".\",\"logprob\":-0.014231676,\"bytes\":[46]}]}\n\ndata: {\"type\":\"transcript.text.done\",\"text\":\"I see skies of blue and clouds of white, the bright blessed days, the dark sacred nights, and I think to myself, what a wonderful world.\",\"logprobs\":[{\"token\":\"I\",\"logprob\":-0.00007588794,\"bytes\":[73]},{\"token\":\" see\",\"logprob\":-3.1281633e-7,\"bytes\":[32,115,101,101]},{\"token\":\" skies\",\"logprob\":-2.3392786e-6,\"bytes\":[32,115,107,105,101,115]},{\"token\":\" of\",\"logprob\":-3.1281633e-7,\"bytes\":[32,111,102]},{\"token\":\" blue\",\"logprob\":-1.0280384e-6,\"bytes\":[32,98,108,117,101]},{\"token\":\" and\",\"logprob\":-0.0005108566,\"bytes\":[32,97,110,100]},{\"token\":\" clouds\",\"logprob\":-1.9361265e-7,\"bytes\":[32,99,108,111,117,100,115]},{\"token\":\" of\",\"logprob\":-1.9361265e-7,\"bytes\":[32,111,102]},{\"token\":\" white\",\"logprob\":-7.89631e-7,\"bytes\":[32,119,104,105,116,101]},{\"token\":\",\",\"logprob\":-0.0014890312,\"bytes\":[44]},{\"token\":\" the\",\"logprob\":-0.0110956915,\"bytes\":[32,116,104,101]},{\"token\":\" bright\",\"logprob\":0.0,\"bytes\":[32,98,114,105,103,104,116]},{\"token\":\" blessed\",\"logprob\":-0.000045848617,\"bytes\":[32,98,108,101,115,115,101,100]},{\"token\":\" days\",\"logprob\":-0.000010802739,\"bytes\":[32,100,97,121,115]},{\"token\":\",\",\"logprob\":-0.00001700133,\"bytes\":[44]},{\"token\":\" the\",\"logprob\":-0.0000118755715,\"bytes\":[32,116,104,101]},{\"token\":\" dark\",\"logprob\":-5.5122365e-7,\"bytes\":[32,100,97,114,107]},{\"token\":\" sacred\",\"logprob\":-5.4385737e-6,\"bytes\":[32,115,97,99,114,101,100]},{\"token\":\" nights\",\"logprob\":-4.00813e-6,\"bytes\":[32,110,105,103,104,116,115]},{\"token\":\",\",\"logprob\":-0.0036910512,\"bytes\":[44]},{\"token\":\" and\",\"logprob\":-0.0031903093,\"bytes\":[32,97,110,100]},{\"token\":\" I\",\"logprob\":-1.504853e-6,\"bytes\":[32,73]},{\"token\":\" think\",\"logprob\":-4.3202e-7,\"bytes\":[32,116,104,105,110,107]},{\"token\":\" to\",\"logprob\":-1.9361265e-7,\"bytes\":[32,116,111]},{\"token\":\" myself\",\"logprob\":-1.7432603e-6,\"bytes\":[32,109,121,115,101,108,102]},{\"token\":\",\",\"logprob\":-0.29254505,\"bytes\":[44]},{\"token\":\" what\",\"logprob\":-0.016815351,\"bytes\":[32,119,104,97,116]},{\"token\":\" a\",\"logprob\":-3.1281633e-7,\"bytes\":[32,97]},{\"token\":\" wonderful\",\"logprob\":-2.1008714e-6,\"bytes\":[32,119,111,110,100,101,114,102,117,108]},{\"token\":\" world\",\"logprob\":-8.180258e-6,\"bytes\":[32,119,111,114,108,100]},{\"token\":\".\",\"logprob\":-0.014231676,\"bytes\":[46]}],\"usage\":{\"input_tokens\":14,\"input_token_details\":{\"text_tokens\":0,\"audio_tokens\":14},\"output_tokens\":45,\"total_tokens\":59}}\n" + }, + { + "title": "Logprobs", + "request": { + "curl": "curl https://api.openai.com/v1/audio/transcriptions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: multipart/form-data\" \\\n -F file=\"@/path/to/file/audio.mp3\" \\\n -F \"include[]=logprobs\" \\\n -F model=\"gpt-4o-transcribe\" \\\n -F response_format=\"json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\naudio_file = open(\"speech.mp3\", \"rb\")\ntranscript = client.audio.transcriptions.create(\n file=audio_file,\n model=\"gpt-4o-transcribe\",\n response_format=\"json\",\n include=[\"logprobs\"]\n)\n\nprint(transcript)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const transcription = await openai.audio.transcriptions.create({\n file: fs.createReadStream(\"audio.mp3\"),\n model: \"gpt-4o-transcribe\",\n response_format: \"json\",\n include: [\"logprobs\"]\n });\n\n console.log(transcription);\n}\nmain();\n" + }, + "response": "{\n \"text\": \"Hey, my knee is hurting and I want to see the doctor tomorrow ideally.\",\n \"logprobs\": [\n { \"token\": \"Hey\", \"logprob\": -1.0415299, \"bytes\": [72, 101, 121] },\n { \"token\": \",\", \"logprob\": -9.805982e-5, \"bytes\": [44] },\n { \"token\": \" my\", \"logprob\": -0.00229799, \"bytes\": [32, 109, 121] },\n {\n \"token\": \" knee\",\n \"logprob\": -4.7159858e-5,\n \"bytes\": [32, 107, 110, 101, 101]\n },\n { \"token\": \" is\", \"logprob\": -0.043909557, \"bytes\": [32, 105, 115] },\n {\n \"token\": \" hurting\",\n \"logprob\": -1.1041146e-5,\n \"bytes\": [32, 104, 117, 114, 116, 105, 110, 103]\n },\n { \"token\": \" and\", \"logprob\": -0.011076359, \"bytes\": [32, 97, 110, 100] },\n { \"token\": \" I\", \"logprob\": -5.3193703e-6, \"bytes\": [32, 73] },\n {\n \"token\": \" want\",\n \"logprob\": -0.0017156356,\n \"bytes\": [32, 119, 97, 110, 116]\n },\n { \"token\": \" to\", \"logprob\": -7.89631e-7, \"bytes\": [32, 116, 111] },\n { \"token\": \" see\", \"logprob\": -5.5122365e-7, \"bytes\": [32, 115, 101, 101] },\n { \"token\": \" the\", \"logprob\": -0.0040786397, \"bytes\": [32, 116, 104, 101] },\n {\n \"token\": \" doctor\",\n \"logprob\": -2.3392786e-6,\n \"bytes\": [32, 100, 111, 99, 116, 111, 114]\n },\n {\n \"token\": \" tomorrow\",\n \"logprob\": -7.89631e-7,\n \"bytes\": [32, 116, 111, 109, 111, 114, 114, 111, 119]\n },\n {\n \"token\": \" ideally\",\n \"logprob\": -0.5800861,\n \"bytes\": [32, 105, 100, 101, 97, 108, 108, 121]\n },\n { \"token\": \".\", \"logprob\": -0.00011093382, \"bytes\": [46] }\n ],\n \"usage\": {\n \"type\": \"tokens\",\n \"input_tokens\": 14,\n \"input_token_details\": {\n \"text_tokens\": 0,\n \"audio_tokens\": 14\n },\n \"output_tokens\": 45,\n \"total_tokens\": 59\n }\n}\n" + }, + { + "title": "Word timestamps", + "request": { + "curl": "curl https://api.openai.com/v1/audio/transcriptions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: multipart/form-data\" \\\n -F file=\"@/path/to/file/audio.mp3\" \\\n -F \"timestamp_granularities[]=word\" \\\n -F model=\"whisper-1\" \\\n -F response_format=\"verbose_json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\naudio_file = open(\"speech.mp3\", \"rb\")\ntranscript = client.audio.transcriptions.create(\n file=audio_file,\n model=\"whisper-1\",\n response_format=\"verbose_json\",\n timestamp_granularities=[\"word\"]\n)\n\nprint(transcript.words)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const transcription = await openai.audio.transcriptions.create({\n file: fs.createReadStream(\"audio.mp3\"),\n model: \"whisper-1\",\n response_format: \"verbose_json\",\n timestamp_granularities: [\"word\"]\n });\n\n console.log(transcription.text);\n}\nmain();\n", + "csharp": "using System;\n\nusing OpenAI.Audio;\n\nstring audioFilePath = \"audio.mp3\";\n\nAudioClient client = new(\n model: \"whisper-1\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nAudioTranscriptionOptions options = new()\n{\n ResponseFormat = AudioTranscriptionFormat.Verbose,\n TimestampGranularities = AudioTimestampGranularities.Word,\n};\n\nAudioTranscription transcription = client.TranscribeAudio(audioFilePath, options);\n\nConsole.WriteLine($\"{transcription.Text}\");\n" + }, + "response": "{\n \"task\": \"transcribe\",\n \"language\": \"english\",\n \"duration\": 8.470000267028809,\n \"text\": \"The beach was a popular spot on a hot summer day. People were swimming in the ocean, building sandcastles, and playing beach volleyball.\",\n \"words\": [\n {\n \"word\": \"The\",\n \"start\": 0.0,\n \"end\": 0.23999999463558197\n },\n ...\n {\n \"word\": \"volleyball\",\n \"start\": 7.400000095367432,\n \"end\": 7.900000095367432\n }\n ],\n \"usage\": {\n \"type\": \"duration\",\n \"seconds\": 9\n }\n}\n" + }, + { + "title": "Segment timestamps", + "request": { + "curl": "curl https://api.openai.com/v1/audio/transcriptions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: multipart/form-data\" \\\n -F file=\"@/path/to/file/audio.mp3\" \\\n -F \"timestamp_granularities[]=segment\" \\\n -F model=\"whisper-1\" \\\n -F response_format=\"verbose_json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\naudio_file = open(\"speech.mp3\", \"rb\")\ntranscript = client.audio.transcriptions.create(\n file=audio_file,\n model=\"whisper-1\",\n response_format=\"verbose_json\",\n timestamp_granularities=[\"segment\"]\n)\n\nprint(transcript.words)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const transcription = await openai.audio.transcriptions.create({\n file: fs.createReadStream(\"audio.mp3\"),\n model: \"whisper-1\",\n response_format: \"verbose_json\",\n timestamp_granularities: [\"segment\"]\n });\n\n console.log(transcription.text);\n}\nmain();\n", + "csharp": "using System;\n\nusing OpenAI.Audio;\n\nstring audioFilePath = \"audio.mp3\";\n\nAudioClient client = new(\n model: \"whisper-1\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nAudioTranscriptionOptions options = new()\n{\n ResponseFormat = AudioTranscriptionFormat.Verbose,\n TimestampGranularities = AudioTimestampGranularities.Segment,\n};\n\nAudioTranscription transcription = client.TranscribeAudio(audioFilePath, options);\n\nConsole.WriteLine($\"{transcription.Text}\");\n" + }, + "response": "{\n \"task\": \"transcribe\",\n \"language\": \"english\",\n \"duration\": 8.470000267028809,\n \"text\": \"The beach was a popular spot on a hot summer day. People were swimming in the ocean, building sandcastles, and playing beach volleyball.\",\n \"segments\": [\n {\n \"id\": 0,\n \"seek\": 0,\n \"start\": 0.0,\n \"end\": 3.319999933242798,\n \"text\": \" The beach was a popular spot on a hot summer day.\",\n \"tokens\": [\n 50364, 440, 7534, 390, 257, 3743, 4008, 322, 257, 2368, 4266, 786, 13, 50530\n ],\n \"temperature\": 0.0,\n \"avg_logprob\": -0.2860786020755768,\n \"compression_ratio\": 1.2363636493682861,\n \"no_speech_prob\": 0.00985979475080967\n },\n ...\n ],\n \"usage\": {\n \"type\": \"duration\",\n \"seconds\": 9\n }\n}\n" + } + ] + } + } + }, + "/audio/translations": { + "post": { + "operationId": "createTranslation", + "tags": [ + "Audio" + ], + "summary": "Translates audio into English.", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateTranslationRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateTranslationResponseJson" + }, + { + "$ref": "#/components/schemas/CreateTranslationResponseVerboseJson" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/InferenceRateLimited" + }, + "503": { + "$ref": "#/components/responses/InferenceServiceUnavailable" + } + }, + "x-oaiMeta": { + "name": "Create translation", + "group": "audio", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/audio/translations \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: multipart/form-data\" \\\n -F file=\"@/path/to/file/german.m4a\" \\\n -F model=\"whisper-1\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\naudio_file = open(\"speech.mp3\", \"rb\")\ntranscript = client.audio.translations.create(\n model=\"whisper-1\",\n file=audio_file\n)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const translation = await openai.audio.translations.create({\n file: fs.createReadStream(\"speech.mp3\"),\n model: \"whisper-1\",\n });\n\n console.log(translation.text);\n}\nmain();\n", + "csharp": "using System;\n\nusing OpenAI.Audio;\n\nstring audioFilePath = \"audio.mp3\";\n\nAudioClient client = new(\n model: \"whisper-1\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nAudioTranscription transcription = client.TranscribeAudio(audioFilePath);\n\nConsole.WriteLine($\"{transcription.Text}\");\n" + }, + "response": "{\n \"text\": \"Hello, my name is Wolfgang and I come from Germany. Where are you heading today?\"\n}\n" + } + } + } + }, + "/audio/voice_consents": { + "post": { + "operationId": "createVoiceConsent", + "tags": [ + "Audio" + ], + "summary": "Upload a voice consent recording.", + "description": "Upload a consent recording that authorizes creation of a custom voice.\n\nSee the [custom voices guide](https://developers.openai.com/api/docs/guides/text-to-speech#custom-voices) for requirements and best practices. Custom voices are limited to eligible customers.\n", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateVoiceConsentRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoiceConsentResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create voice consent", + "group": "audio", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/audio/voice_consents \\\n -X POST \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -F \"name=John Doe\" \\\n -F \"language=en-US\" \\\n -F \"recording=@$HOME/consent_recording.wav;type=audio/x-wav\"\n" + } + } + } + }, + "get": { + "operationId": "listVoiceConsents", + "tags": [ + "Audio" + ], + "summary": "Returns a list of voice consent recordings.", + "description": "List consent recordings available to your organization for creating custom voices.\n\nSee the [custom voices guide](https://developers.openai.com/api/docs/guides/text-to-speech#custom-voices). Custom voices are limited to eligible customers.\n", + "parameters": [ + { + "in": "query", + "name": "after", + "required": false, + "schema": { + "type": "string" + }, + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n" + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoiceConsentListResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List voice consents", + "group": "audio", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/audio/voice_consents?limit=20 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + } + } + } + } + }, + "/audio/voice_consents/{consent_id}": { + "get": { + "operationId": "getVoiceConsent", + "tags": [ + "Audio" + ], + "summary": "Retrieves a voice consent recording.", + "description": "Retrieve consent recording metadata used for creating custom voices.\n\nSee the [custom voices guide](https://developers.openai.com/api/docs/guides/text-to-speech#custom-voices). Custom voices are limited to eligible customers.\n", + "parameters": [ + { + "in": "path", + "name": "consent_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the consent recording to retrieve." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoiceConsentResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve voice consent", + "group": "audio", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/audio/voice_consents/cons_1234 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + } + } + } + }, + "post": { + "operationId": "updateVoiceConsent", + "tags": [ + "Audio" + ], + "summary": "Updates a voice consent recording (metadata only).", + "description": "Update consent recording metadata used for creating custom voices. This endpoint updates metadata only and does not replace the underlying audio.\n\nSee the [custom voices guide](https://developers.openai.com/api/docs/guides/text-to-speech#custom-voices). Custom voices are limited to eligible customers.\n", + "parameters": [ + { + "in": "path", + "name": "consent_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the consent recording to update." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateVoiceConsentRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoiceConsentResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update voice consent", + "group": "audio", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/audio/voice_consents/cons_1234 \\\n -X POST \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"John Doe\"\n }'\n" + } + } + } + }, + "delete": { + "operationId": "deleteVoiceConsent", + "tags": [ + "Audio" + ], + "summary": "Deletes a voice consent recording.", + "description": "Delete a consent recording that was uploaded for creating custom voices.\n\nSee the [custom voices guide](https://developers.openai.com/api/docs/guides/text-to-speech#custom-voices). Custom voices are limited to eligible customers.\n", + "parameters": [ + { + "in": "path", + "name": "consent_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the consent recording to delete." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoiceConsentDeletedResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete voice consent", + "group": "audio", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/audio/voice_consents/cons_1234 \\\n -X DELETE \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + } + } + } + } + }, + "/audio/voices": { + "post": { + "operationId": "createVoice", + "tags": [ + "Audio" + ], + "summary": "Creates a custom voice.", + "description": "Create a custom voice you can use for audio output (for example, in Text-to-Speech and the Realtime API). This requires an audio sample and a previously uploaded consent recording.\n\nSee the [custom voices guide](https://developers.openai.com/api/docs/guides/text-to-speech#custom-voices) for requirements and best practices. Custom voices are limited to eligible customers.\n", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateVoiceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoiceResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create voice", + "group": "audio", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/audio/voices \\\n -X POST \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -F \"name=My new voice\" \\\n -F \"consent=cons_1234\" \\\n -F \"audio_sample=@$HOME/audio_sample.wav;type=audio/x-wav\"\n" + } + } + } + } + }, + "/batches": { + "post": { + "summary": "Creates and executes a batch from an uploaded file of requests", + "operationId": "createBatch", + "tags": [ + "Batch" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBatchRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Batch created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Batch" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create batch", + "group": "batch", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/batches \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"input_file_id\": \"file-abc123\",\n \"endpoint\": \"/v1/chat/completions\",\n \"completion_window\": \"24h\"\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.batches.create(\n input_file_id=\"file-abc123\",\n endpoint=\"/v1/chat/completions\",\n completion_window=\"24h\"\n)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const batch = await openai.batches.create({\n input_file_id: \"file-abc123\",\n endpoint: \"/v1/chat/completions\",\n completion_window: \"24h\"\n });\n\n console.log(batch);\n}\n\nmain();\n" + }, + "response": "{\n \"id\": \"batch_abc123\",\n \"object\": \"batch\",\n \"endpoint\": \"/v1/chat/completions\",\n \"errors\": null,\n \"input_file_id\": \"file-abc123\",\n \"completion_window\": \"24h\",\n \"status\": \"validating\",\n \"output_file_id\": null,\n \"error_file_id\": null,\n \"created_at\": 1711471533,\n \"in_progress_at\": null,\n \"expires_at\": null,\n \"finalizing_at\": null,\n \"completed_at\": null,\n \"failed_at\": null,\n \"expired_at\": null,\n \"cancelling_at\": null,\n \"cancelled_at\": null,\n \"request_counts\": {\n \"total\": 0,\n \"completed\": 0,\n \"failed\": 0\n },\n \"metadata\": {\n \"customer_id\": \"user_123456789\",\n \"batch_description\": \"Nightly eval job\",\n }\n}\n" + } + } + }, + "get": { + "operationId": "listBatches", + "tags": [ + "Batch" + ], + "summary": "List your organization's batches.", + "parameters": [ + { + "in": "query", + "name": "after", + "required": false, + "schema": { + "type": "string" + }, + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n" + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Batch listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListBatchesResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List batches", + "group": "batch", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/batches?limit=2 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.batches.list()\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const list = await openai.batches.list();\n\n for await (const batch of list) {\n console.log(batch);\n }\n}\n\nmain();\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"batch_abc123\",\n \"object\": \"batch\",\n \"endpoint\": \"/v1/chat/completions\",\n \"errors\": null,\n \"input_file_id\": \"file-abc123\",\n \"completion_window\": \"24h\",\n \"status\": \"completed\",\n \"output_file_id\": \"file-cvaTdG\",\n \"error_file_id\": \"file-HOWS94\",\n \"created_at\": 1711471533,\n \"in_progress_at\": 1711471538,\n \"expires_at\": 1711557933,\n \"finalizing_at\": 1711493133,\n \"completed_at\": 1711493163,\n \"failed_at\": null,\n \"expired_at\": null,\n \"cancelling_at\": null,\n \"cancelled_at\": null,\n \"request_counts\": {\n \"total\": 100,\n \"completed\": 95,\n \"failed\": 5\n },\n \"metadata\": {\n \"customer_id\": \"user_123456789\",\n \"batch_description\": \"Nightly job\",\n }\n },\n { ... },\n ],\n \"first_id\": \"batch_abc123\",\n \"last_id\": \"batch_abc456\",\n \"has_more\": true\n}\n" + } + } + } + }, + "/batches/{batch_id}": { + "get": { + "operationId": "retrieveBatch", + "tags": [ + "Batch" + ], + "summary": "Retrieves a batch.", + "parameters": [ + { + "in": "path", + "name": "batch_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the batch to retrieve." + } + ], + "responses": { + "200": { + "description": "Batch retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Batch" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve batch", + "group": "batch", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/batches/batch_abc123 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.batches.retrieve(\"batch_abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const batch = await openai.batches.retrieve(\"batch_abc123\");\n\n console.log(batch);\n}\n\nmain();\n" + }, + "response": "{\n \"id\": \"batch_abc123\",\n \"object\": \"batch\",\n \"endpoint\": \"/v1/completions\",\n \"errors\": null,\n \"input_file_id\": \"file-abc123\",\n \"completion_window\": \"24h\",\n \"status\": \"completed\",\n \"output_file_id\": \"file-cvaTdG\",\n \"error_file_id\": \"file-HOWS94\",\n \"created_at\": 1711471533,\n \"in_progress_at\": 1711471538,\n \"expires_at\": 1711557933,\n \"finalizing_at\": 1711493133,\n \"completed_at\": 1711493163,\n \"failed_at\": null,\n \"expired_at\": null,\n \"cancelling_at\": null,\n \"cancelled_at\": null,\n \"request_counts\": {\n \"total\": 100,\n \"completed\": 95,\n \"failed\": 5\n },\n \"metadata\": {\n \"customer_id\": \"user_123456789\",\n \"batch_description\": \"Nightly eval job\",\n }\n}\n" + } + } + } + }, + "/batches/{batch_id}/cancel": { + "post": { + "operationId": "cancelBatch", + "tags": [ + "Batch" + ], + "summary": "Cancels an in-progress batch. The batch will be in status `cancelling` for up to 10 minutes, before changing to `cancelled`, where it will have partial results (if any) available in the output file.", + "parameters": [ + { + "in": "path", + "name": "batch_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the batch to cancel." + } + ], + "responses": { + "200": { + "description": "Batch is cancelling. Returns the cancelling batch's details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Batch" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Cancel batch", + "group": "batch", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/batches/batch_abc123/cancel \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -X POST\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.batches.cancel(\"batch_abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const batch = await openai.batches.cancel(\"batch_abc123\");\n\n console.log(batch);\n}\n\nmain();\n" + }, + "response": "{\n \"id\": \"batch_abc123\",\n \"object\": \"batch\",\n \"endpoint\": \"/v1/chat/completions\",\n \"errors\": null,\n \"input_file_id\": \"file-abc123\",\n \"completion_window\": \"24h\",\n \"status\": \"cancelling\",\n \"output_file_id\": null,\n \"error_file_id\": null,\n \"created_at\": 1711471533,\n \"in_progress_at\": 1711471538,\n \"expires_at\": 1711557933,\n \"finalizing_at\": null,\n \"completed_at\": null,\n \"failed_at\": null,\n \"expired_at\": null,\n \"cancelling_at\": 1711475133,\n \"cancelled_at\": null,\n \"request_counts\": {\n \"total\": 100,\n \"completed\": 23,\n \"failed\": 1\n },\n \"metadata\": {\n \"customer_id\": \"user_123456789\",\n \"batch_description\": \"Nightly eval job\",\n }\n}\n" + } + } + } + }, + "/chat/completions": { + "get": { + "operationId": "listChatCompletions", + "tags": [ + "Chat" + ], + "summary": "List stored Chat Completions. Only Chat Completions that have been stored\nwith the `store` parameter set to `true` will be returned.\n", + "parameters": [ + { + "name": "model", + "in": "query", + "description": "The model used to generate the Chat Completions.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "metadata", + "in": "query", + "description": "A list of metadata keys to filter the Chat Completions by. Example:\n\n`metadata[key1]=value1&metadata[key2]=value2`\n", + "required": false, + "schema": { + "$ref": "#/components/schemas/Metadata" + } + }, + { + "name": "after", + "in": "query", + "description": "Identifier for the last chat completion from the previous pagination request.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of Chat Completions to retrieve.", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for Chat Completions by timestamp. Use `asc` for ascending order or `desc` for descending order. Defaults to `asc`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + } + ], + "responses": { + "200": { + "description": "A list of Chat Completions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionList" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List Chat Completions", + "group": "chat", + "path": "list", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/chat/completions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncompletions = client.chat.completions.list()\nprint(completions)\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"chat.completion\",\n \"id\": \"chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2\",\n \"model\": \"gpt-6-astra\",\n \"created\": 1738960610,\n \"request_id\": \"req_ded8ab984ec4bf840f37566c1011c417\",\n \"tool_choice\": null,\n \"usage\": {\n \"total_tokens\": 31,\n \"completion_tokens\": 18,\n \"prompt_tokens\": 13\n },\n \"seed\": 4944116822809979520,\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"presence_penalty\": 0.0,\n \"frequency_penalty\": 0.0,\n \"system_fingerprint\": \"fp_50cad350e4\",\n \"input_user\": null,\n \"service_tier\": \"default\",\n \"tools\": null,\n \"metadata\": {},\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"content\": \"Mind of circuits hum, \\nLearning patterns in silence— \\nFuture's quiet spark.\",\n \"role\": \"assistant\",\n \"tool_calls\": null,\n \"function_call\": null\n },\n \"finish_reason\": \"stop\",\n \"logprobs\": null\n }\n ],\n \"response_format\": null\n }\n ],\n \"first_id\": \"chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2\",\n \"last_id\": \"chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "operationId": "createChatCompletion", + "tags": [ + "Chat" + ], + "summary": "**Starting a new project?** We recommend trying [Responses](https://developers.openai.com/api/reference/resources/responses)\nto take advantage of the latest OpenAI platform features. Compare\n[Chat Completions with Responses](https://developers.openai.com/api/docs/guides/migrate-to-responses?api-mode=responses).\n\n---\n\nCreates a model response for the given chat conversation. Learn more in the\n[text generation](https://developers.openai.com/api/docs/guides/text), [vision](https://developers.openai.com/api/docs/guides/images-vision),\nand [audio](https://developers.openai.com/api/docs/guides/audio) guides.\n\nParameter support can differ depending on the model used to generate the\nresponse, particularly for newer reasoning models. Parameters that are only\nsupported for reasoning models are noted below. For the current state of\nunsupported parameters in reasoning models,\n[refer to the reasoning guide](https://developers.openai.com/api/docs/guides/reasoning).\n\nReturns a chat completion object, or a streamed sequence of chat completion\nchunk objects if the request is streamed.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateChatCompletionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateChatCompletionResponse" + } + }, + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/CreateChatCompletionStreamResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/InferenceRateLimited" + }, + "503": { + "$ref": "#/components/responses/InferenceServiceUnavailable" + } + }, + "x-oaiMeta": { + "name": "Create chat completion", + "group": "chat", + "path": "create", + "examples": [ + { + "title": "Default", + "request": { + "curl": "curl https://api.openai.com/v1/chat/completions \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"model\": \"gpt-6-astra\",\n \"messages\": [\n {\n \"role\": \"developer\",\n \"content\": \"You are a helpful assistant.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Hello!\"\n }\n ]\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncompletion = client.chat.completions.create(\n model=\"gpt-6-astra\",\n messages=[\n {\"role\": \"developer\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Hello!\"}\n ]\n)\n\nprint(completion.choices[0].message)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const completion = await openai.chat.completions.create({\n messages: [{ role: \"developer\", content: \"You are a helpful assistant.\" }],\n model: \"gpt-6-astra\",\n store: true,\n });\n\n console.log(completion.choices[0]);\n}\n\nmain();\n", + "csharp": "using System;\nusing System.Collections.Generic;\n\nusing OpenAI.Chat;\n\nChatClient client = new(\n model: \"gpt-6-astra\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nList messages =\n[\n new SystemChatMessage(\"You are a helpful assistant.\"),\n new UserChatMessage(\"Hello!\")\n];\n\nChatCompletion completion = client.CompleteChat(messages);\n\nConsole.WriteLine(completion.Content[0].Text);\n" + }, + "response": "{\n \"id\": \"chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT\",\n \"object\": \"chat.completion\",\n \"created\": 1741569952,\n \"model\": \"gpt-6-astra\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Hello! How can I assist you today?\",\n \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 19,\n \"completion_tokens\": 10,\n \"total_tokens\": 29,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": \"default\"\n}\n" + }, + { + "title": "Image input", + "request": { + "curl": "curl https://api.openai.com/v1/chat/completions \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"model\": \"gpt-6-astra\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"What is in this image?\"\n },\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"\n }\n }\n ]\n }\n ],\n \"max_tokens\": 300\n }'\n", + "python": "from openai import OpenAI\n\nclient = OpenAI()\n\nresponse = client.chat.completions.create(\n model=\"gpt-6-astra\",\n messages=[\n {\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": \"What's in this image?\"},\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\",\n }\n },\n ],\n }\n ],\n max_tokens=300,\n)\n\nprint(response.choices[0])\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const response = await openai.chat.completions.create({\n model: \"gpt-6-astra\",\n messages: [\n {\n role: \"user\",\n content: [\n { type: \"text\", text: \"What's in this image?\" },\n {\n type: \"image_url\",\n image_url: {\n \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\",\n },\n }\n ],\n },\n ],\n });\n console.log(response.choices[0]);\n}\nmain();\n", + "csharp": "using System;\nusing System.Collections.Generic;\n\nusing OpenAI.Chat;\n\nChatClient client = new(\n model: \"gpt-6-astra\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nList messages =\n[\n new UserChatMessage(\n [\n ChatMessageContentPart.CreateTextPart(\"What's in this image?\"),\n ChatMessageContentPart.CreateImagePart(new Uri(\"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\"))\n ])\n];\n\nChatCompletion completion = client.CompleteChat(messages);\n\nConsole.WriteLine(completion.Content[0].Text);\n" + }, + "response": "{\n \"id\": \"chatcmpl-B9MHDbslfkBeAs8l4bebGdFOJ6PeG\",\n \"object\": \"chat.completion\",\n \"created\": 1741570283,\n \"model\": \"gpt-6-astra\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"The image shows a wooden boardwalk path running through a lush green field or meadow. The sky is bright blue with some scattered clouds, giving the scene a serene and peaceful atmosphere. Trees and shrubs are visible in the background.\",\n \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 1117,\n \"completion_tokens\": 46,\n \"total_tokens\": 1163,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": \"default\"\n}\n" + }, + { + "title": "Streaming", + "request": { + "curl": "curl https://api.openai.com/v1/chat/completions \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"model\": \"gpt-6-astra\",\n \"messages\": [\n {\n \"role\": \"developer\",\n \"content\": \"You are a helpful assistant.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Hello!\"\n }\n ],\n \"stream\": true\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncompletion = client.chat.completions.create(\n model=\"gpt-6-astra\",\n messages=[\n {\"role\": \"developer\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Hello!\"}\n ],\n stream=True\n)\n\nfor chunk in completion:\n print(chunk.choices[0].delta)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const completion = await openai.chat.completions.create({\n model: \"gpt-6-astra\",\n messages: [\n {\"role\": \"developer\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Hello!\"}\n ],\n stream: true,\n });\n\n for await (const chunk of completion) {\n console.log(chunk.choices[0].delta.content);\n }\n}\n\nmain();\n", + "csharp": "using System;\nusing System.ClientModel;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nusing OpenAI.Chat;\n\nChatClient client = new(\n model: \"gpt-6-astra\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nList messages =\n[\n new SystemChatMessage(\"You are a helpful assistant.\"),\n new UserChatMessage(\"Hello!\")\n];\n\nAsyncCollectionResult completionUpdates = client.CompleteChatStreamingAsync(messages);\n\nawait foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates)\n{\n if (completionUpdate.ContentUpdate.Count > 0)\n {\n Console.Write(completionUpdate.ContentUpdate[0].Text);\n }\n}\n" + }, + "response": "{\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1694268190,\"model\":\"gpt-6-astra\", \"system_fingerprint\": \"fp_44709d6fcb\", \"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\n{\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1694268190,\"model\":\"gpt-6-astra\", \"system_fingerprint\": \"fp_44709d6fcb\", \"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}]}\n\n....\n\n{\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1694268190,\"model\":\"gpt-6-astra\", \"system_fingerprint\": \"fp_44709d6fcb\", \"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n" + }, + { + "title": "Functions", + "request": { + "curl": "curl https://api.openai.com/v1/chat/completions \\\n-H \"Content-Type: application/json\" \\\n-H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n-d '{\n \"model\": \"gpt-6-astra\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"What is the weather like in Boston today?\"\n }\n ],\n \"tools\": [\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_current_weather\",\n \"description\": \"Get the current weather in a given location\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"location\": {\n \"type\": \"string\",\n \"description\": \"The city and state, e.g. San Francisco, CA\"\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\"celsius\", \"fahrenheit\"]\n }\n },\n \"required\": [\"location\"]\n }\n }\n }\n ],\n \"tool_choice\": \"auto\"\n}'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ntools = [\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_current_weather\",\n \"description\": \"Get the current weather in a given location\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"location\": {\n \"type\": \"string\",\n \"description\": \"The city and state, e.g. San Francisco, CA\",\n },\n \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]},\n },\n \"required\": [\"location\"],\n },\n }\n }\n]\nmessages = [{\"role\": \"user\", \"content\": \"What's the weather like in Boston today?\"}]\ncompletion = client.chat.completions.create(\n model=\"gpt-6-astra\",\n messages=messages,\n tools=tools,\n tool_choice=\"auto\"\n)\n\nprint(completion)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const messages = [{\"role\": \"user\", \"content\": \"What's the weather like in Boston today?\"}];\n const tools = [\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_current_weather\",\n \"description\": \"Get the current weather in a given location\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"location\": {\n \"type\": \"string\",\n \"description\": \"The city and state, e.g. San Francisco, CA\",\n },\n \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]},\n },\n \"required\": [\"location\"],\n },\n }\n }\n ];\n\n const response = await openai.chat.completions.create({\n model: \"gpt-6-astra\",\n messages: messages,\n tools: tools,\n tool_choice: \"auto\",\n });\n\n console.log(response);\n}\n\nmain();\n", + "csharp": "using System;\nusing System.Collections.Generic;\n\nusing OpenAI.Chat;\n\nChatClient client = new(\n model: \"gpt-6-astra\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nChatTool getCurrentWeatherTool = ChatTool.CreateFunctionTool(\n functionName: \"get_current_weather\",\n functionDescription: \"Get the current weather in a given location\",\n functionParameters: BinaryData.FromString(\"\"\"\n {\n \"type\": \"object\",\n \"properties\": {\n \"location\": {\n \"type\": \"string\",\n \"description\": \"The city and state, e.g. San Francisco, CA\"\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [ \"celsius\", \"fahrenheit\" ]\n }\n },\n \"required\": [ \"location\" ]\n }\n \"\"\")\n);\n\nList messages =\n[\n new UserChatMessage(\"What's the weather like in Boston today?\"),\n];\n\nChatCompletionOptions options = new()\n{\n Tools =\n {\n getCurrentWeatherTool\n },\n ToolChoice = ChatToolChoice.CreateAutoChoice(),\n};\n\nChatCompletion completion = client.CompleteChat(messages, options);\n" + }, + "response": "{\n \"id\": \"chatcmpl-abc123\",\n \"object\": \"chat.completion\",\n \"created\": 1699896916,\n \"model\": \"gpt-6-astra\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n \"id\": \"call_abc123\",\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_current_weather\",\n \"arguments\": \"{\\n\\\"location\\\": \\\"Boston, MA\\\"\\n}\"\n }\n }\n ]\n },\n \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 82,\n \"completion_tokens\": 17,\n \"total_tokens\": 99,\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n }\n}\n" + }, + { + "title": "Logprobs", + "request": { + "curl": "curl https://api.openai.com/v1/chat/completions \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"model\": \"gpt-6-astra\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Hello!\"\n }\n ],\n \"reasoning_effort\": \"none\",\n \"logprobs\": true,\n \"top_logprobs\": 2\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncompletion = client.chat.completions.create(\n model=\"gpt-6-astra\",\n messages=[\n {\"role\": \"user\", \"content\": \"Hello!\"}\n ],\n reasoning_effort=\"none\",\n logprobs=True,\n top_logprobs=2\n)\n\nprint(completion.choices[0].message)\nprint(completion.choices[0].logprobs)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const completion = await openai.chat.completions.create({\n messages: [{ role: \"user\", content: \"Hello!\" }],\n model: \"gpt-6-astra\",\n reasoning_effort: \"none\",\n logprobs: true,\n top_logprobs: 2,\n });\n\n console.log(completion.choices[0]);\n}\n\nmain();\n", + "csharp": "using System;\nusing System.Collections.Generic;\n\nusing OpenAI.Chat;\n\nChatClient client = new(\n model: \"gpt-6-astra\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nList messages =\n[\n new UserChatMessage(\"Hello!\")\n];\n\nChatCompletionOptions options = new()\n{\n ReasoningEffortLevel = ChatReasoningEffortLevel.None,\n IncludeLogProbabilities = true,\n TopLogProbabilityCount = 2\n};\n\nChatCompletion completion = client.CompleteChat(messages, options);\n\nConsole.WriteLine(completion.Content[0].Text);\n" + }, + "response": "{\n \"id\": \"chatcmpl-123\",\n \"object\": \"chat.completion\",\n \"created\": 1702685778,\n \"model\": \"gpt-6-astra\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Hello! How can I assist you today?\"\n },\n \"logprobs\": {\n \"content\": [\n {\n \"token\": \"Hello\",\n \"logprob\": -0.31725305,\n \"bytes\": [72, 101, 108, 108, 111],\n \"top_logprobs\": [\n {\n \"token\": \"Hello\",\n \"logprob\": -0.31725305,\n \"bytes\": [72, 101, 108, 108, 111]\n },\n {\n \"token\": \"Hi\",\n \"logprob\": -1.3190403,\n \"bytes\": [72, 105]\n }\n ]\n },\n {\n \"token\": \"!\",\n \"logprob\": -0.02380986,\n \"bytes\": [\n 33\n ],\n \"top_logprobs\": [\n {\n \"token\": \"!\",\n \"logprob\": -0.02380986,\n \"bytes\": [33]\n },\n {\n \"token\": \" there\",\n \"logprob\": -3.787621,\n \"bytes\": [32, 116, 104, 101, 114, 101]\n }\n ]\n },\n {\n \"token\": \" How\",\n \"logprob\": -0.000054669687,\n \"bytes\": [32, 72, 111, 119],\n \"top_logprobs\": [\n {\n \"token\": \" How\",\n \"logprob\": -0.000054669687,\n \"bytes\": [32, 72, 111, 119]\n },\n {\n \"token\": \"<|end|>\",\n \"logprob\": -10.953937,\n \"bytes\": null\n }\n ]\n },\n {\n \"token\": \" can\",\n \"logprob\": -0.015801601,\n \"bytes\": [32, 99, 97, 110],\n \"top_logprobs\": [\n {\n \"token\": \" can\",\n \"logprob\": -0.015801601,\n \"bytes\": [32, 99, 97, 110]\n },\n {\n \"token\": \" may\",\n \"logprob\": -4.161023,\n \"bytes\": [32, 109, 97, 121]\n }\n ]\n },\n {\n \"token\": \" I\",\n \"logprob\": -3.7697225e-6,\n \"bytes\": [\n 32,\n 73\n ],\n \"top_logprobs\": [\n {\n \"token\": \" I\",\n \"logprob\": -3.7697225e-6,\n \"bytes\": [32, 73]\n },\n {\n \"token\": \" assist\",\n \"logprob\": -13.596657,\n \"bytes\": [32, 97, 115, 115, 105, 115, 116]\n }\n ]\n },\n {\n \"token\": \" assist\",\n \"logprob\": -0.04571125,\n \"bytes\": [32, 97, 115, 115, 105, 115, 116],\n \"top_logprobs\": [\n {\n \"token\": \" assist\",\n \"logprob\": -0.04571125,\n \"bytes\": [32, 97, 115, 115, 105, 115, 116]\n },\n {\n \"token\": \" help\",\n \"logprob\": -3.1089056,\n \"bytes\": [32, 104, 101, 108, 112]\n }\n ]\n },\n {\n \"token\": \" you\",\n \"logprob\": -5.4385737e-6,\n \"bytes\": [32, 121, 111, 117],\n \"top_logprobs\": [\n {\n \"token\": \" you\",\n \"logprob\": -5.4385737e-6,\n \"bytes\": [32, 121, 111, 117]\n },\n {\n \"token\": \" today\",\n \"logprob\": -12.807695,\n \"bytes\": [32, 116, 111, 100, 97, 121]\n }\n ]\n },\n {\n \"token\": \" today\",\n \"logprob\": -0.0040071653,\n \"bytes\": [32, 116, 111, 100, 97, 121],\n \"top_logprobs\": [\n {\n \"token\": \" today\",\n \"logprob\": -0.0040071653,\n \"bytes\": [32, 116, 111, 100, 97, 121]\n },\n {\n \"token\": \"?\",\n \"logprob\": -5.5247097,\n \"bytes\": [63]\n }\n ]\n },\n {\n \"token\": \"?\",\n \"logprob\": -0.0008108172,\n \"bytes\": [63],\n \"top_logprobs\": [\n {\n \"token\": \"?\",\n \"logprob\": -0.0008108172,\n \"bytes\": [63]\n },\n {\n \"token\": \"?\\n\",\n \"logprob\": -7.184561,\n \"bytes\": [63, 10]\n }\n ]\n }\n ]\n },\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 9,\n \"completion_tokens\": 9,\n \"total_tokens\": 18,\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"system_fingerprint\": null\n}\n" + } + ] + } + } + }, + "/chat/completions/{completion_id}": { + "get": { + "operationId": "getChatCompletion", + "tags": [ + "Chat" + ], + "summary": "Get a stored chat completion. Only Chat Completions that have been created\nwith the `store` parameter set to `true` will be returned.\n", + "parameters": [ + { + "in": "path", + "name": "completion_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the chat completion to retrieve." + } + ], + "responses": { + "200": { + "description": "A chat completion", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateChatCompletionResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Get chat completion", + "group": "chat", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/chat/completions/chatcmpl-abc123 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncompletions = client.chat.completions.list()\nfirst_id = completions[0].id\nfirst_completion = client.chat.completions.retrieve(completion_id=first_id)\nprint(first_completion)\n" + }, + "response": "{\n \"object\": \"chat.completion\",\n \"id\": \"chatcmpl-abc123\",\n \"model\": \"gpt-6-astra\",\n \"created\": 1738960610,\n \"request_id\": \"req_ded8ab984ec4bf840f37566c1011c417\",\n \"tool_choice\": null,\n \"usage\": {\n \"total_tokens\": 31,\n \"completion_tokens\": 18,\n \"prompt_tokens\": 13\n },\n \"seed\": 4944116822809979520,\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"presence_penalty\": 0.0,\n \"frequency_penalty\": 0.0,\n \"system_fingerprint\": \"fp_50cad350e4\",\n \"input_user\": null,\n \"service_tier\": \"default\",\n \"tools\": null,\n \"metadata\": {},\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"content\": \"Mind of circuits hum, \\nLearning patterns in silence— \\nFuture's quiet spark.\",\n \"role\": \"assistant\",\n \"tool_calls\": null,\n \"function_call\": null\n },\n \"finish_reason\": \"stop\",\n \"logprobs\": null\n }\n ],\n \"response_format\": null\n}\n" + } + } + }, + "post": { + "operationId": "updateChatCompletion", + "tags": [ + "Chat" + ], + "summary": "Modify a stored chat completion. Only Chat Completions that have been\ncreated with the `store` parameter set to `true` can be modified. Currently,\nthe only supported modification is to update the `metadata` field.\n", + "parameters": [ + { + "in": "path", + "name": "completion_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the chat completion to update." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "metadata" + ], + "properties": { + "metadata": { + "$ref": "#/components/schemas/Metadata" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "A chat completion", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateChatCompletionResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update chat completion", + "group": "chat", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/chat/completions/chat_abc123 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"metadata\": {\"foo\": \"bar\"}}'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncompletions = client.chat.completions.list()\nfirst_id = completions[0].id\nupdated_completion = client.chat.completions.update(completion_id=first_id, request_body={\"metadata\": {\"foo\": \"bar\"}})\nprint(updated_completion)\n" + }, + "response": "{\n \"object\": \"chat.completion\",\n \"id\": \"chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2\",\n \"model\": \"gpt-6-astra\",\n \"created\": 1738960610,\n \"request_id\": \"req_ded8ab984ec4bf840f37566c1011c417\",\n \"tool_choice\": null,\n \"usage\": {\n \"total_tokens\": 31,\n \"completion_tokens\": 18,\n \"prompt_tokens\": 13\n },\n \"seed\": 4944116822809979520,\n \"top_p\": 1.0,\n \"temperature\": 1.0,\n \"presence_penalty\": 0.0,\n \"frequency_penalty\": 0.0,\n \"system_fingerprint\": \"fp_50cad350e4\",\n \"input_user\": null,\n \"service_tier\": \"default\",\n \"tools\": null,\n \"metadata\": {\n \"foo\": \"bar\"\n },\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"content\": \"Mind of circuits hum, \\nLearning patterns in silence— \\nFuture's quiet spark.\",\n \"role\": \"assistant\",\n \"tool_calls\": null,\n \"function_call\": null\n },\n \"finish_reason\": \"stop\",\n \"logprobs\": null\n }\n ],\n \"response_format\": null\n}\n" + } + } + }, + "delete": { + "operationId": "deleteChatCompletion", + "tags": [ + "Chat" + ], + "summary": "Delete a stored chat completion. Only Chat Completions that have been\ncreated with the `store` parameter set to `true` can be deleted.\n", + "parameters": [ + { + "in": "path", + "name": "completion_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the chat completion to delete." + } + ], + "responses": { + "200": { + "description": "The chat completion was deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionDeleted" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete chat completion", + "group": "chat", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/chat/completions/chat_abc123 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncompletions = client.chat.completions.list()\nfirst_id = completions[0].id\ndelete_response = client.chat.completions.delete(completion_id=first_id)\nprint(delete_response)\n" + }, + "response": "{\n \"object\": \"chat.completion.deleted\",\n \"id\": \"chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/chat/completions/{completion_id}/messages": { + "get": { + "operationId": "getChatCompletionMessages", + "tags": [ + "Chat" + ], + "summary": "Get the messages in a stored chat completion. Only Chat Completions that\nhave been created with the `store` parameter set to `true` will be\nreturned.\n", + "parameters": [ + { + "in": "path", + "name": "completion_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the chat completion to retrieve messages from." + }, + { + "name": "after", + "in": "query", + "description": "Identifier for the last message from the previous pagination request.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of messages to retrieve.", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for messages by timestamp. Use `asc` for ascending order or `desc` for descending order. Defaults to `asc`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + } + ], + "responses": { + "200": { + "description": "A list of messages", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionMessageList" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Get chat messages", + "group": "chat", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/chat/completions/chat_abc123/messages \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncompletions = client.chat.completions.list()\nfirst_id = completions[0].id\nfirst_completion = client.chat.completions.retrieve(completion_id=first_id)\nmessages = client.chat.completions.messages.list(completion_id=first_id)\nprint(messages)\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2-0\",\n \"role\": \"user\",\n \"content\": \"write a haiku about ai\",\n \"name\": null,\n \"content_parts\": null\n }\n ],\n \"first_id\": \"chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2-0\",\n \"last_id\": \"chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2-0\",\n \"has_more\": false\n}\n" + } + } + } + }, + "/completions": { + "post": { + "operationId": "createCompletion", + "tags": [ + "Completions" + ], + "summary": "Creates a completion for the provided prompt and parameters.\n\nReturns a completion object, or a sequence of completion objects if the request is streamed.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCompletionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCompletionResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/InferenceRateLimited" + }, + "503": { + "$ref": "#/components/responses/InferenceServiceUnavailable" + } + }, + "x-oaiMeta": { + "name": "Create completion", + "group": "completions", + "legacy": true, + "examples": [ + { + "title": "No streaming", + "request": { + "curl": "curl https://api.openai.com/v1/completions \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"model\": \"gpt-3.5-turbo-instruct\",\n \"prompt\": \"Say this is a test\",\n \"max_tokens\": 7,\n \"temperature\": 0\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.completions.create(\n model=\"gpt-3.5-turbo-instruct\",\n prompt=\"Say this is a test\",\n max_tokens=7,\n temperature=0\n)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const completion = await openai.completions.create({\n model: \"gpt-3.5-turbo-instruct\",\n prompt: \"Say this is a test.\",\n max_tokens: 7,\n temperature: 0,\n });\n\n console.log(completion);\n}\nmain();" + }, + "response": "{\n \"id\": \"cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7\",\n \"object\": \"text_completion\",\n \"created\": 1589478378,\n \"model\": \"gpt-3.5-turbo-instruct\",\n \"system_fingerprint\": \"fp_44709d6fcb\",\n \"choices\": [\n {\n \"text\": \"\\n\\nThis is indeed a test\",\n \"index\": 0,\n \"logprobs\": null,\n \"finish_reason\": \"length\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 5,\n \"completion_tokens\": 7,\n \"total_tokens\": 12\n }\n}\n" + }, + { + "title": "Streaming", + "request": { + "curl": "curl https://api.openai.com/v1/completions \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"model\": \"gpt-3.5-turbo-instruct\",\n \"prompt\": \"Say this is a test\",\n \"max_tokens\": 7,\n \"temperature\": 0,\n \"stream\": true\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nfor chunk in client.completions.create(\n model=\"gpt-3.5-turbo-instruct\",\n prompt=\"Say this is a test\",\n max_tokens=7,\n temperature=0,\n stream=True\n):\n print(chunk.choices[0].text)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const stream = await openai.completions.create({\n model: \"gpt-3.5-turbo-instruct\",\n prompt: \"Say this is a test.\",\n stream: true,\n });\n\n for await (const chunk of stream) {\n console.log(chunk.choices[0].text)\n }\n}\nmain();" + }, + "response": "{\n \"id\": \"cmpl-7iA7iJjj8V2zOkCGvWF2hAkDWBQZe\",\n \"object\": \"text_completion\",\n \"created\": 1690759702,\n \"choices\": [\n {\n \"text\": \"This\",\n \"index\": 0,\n \"logprobs\": null,\n \"finish_reason\": null\n }\n ],\n \"model\": \"gpt-3.5-turbo-instruct\"\n \"system_fingerprint\": \"fp_44709d6fcb\",\n}\n" + } + ] + } + } + }, + "/containers": { + "get": { + "summary": "List Containers", + "description": "Lists containers.", + "operationId": "ListContainers", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order.\n", + "schema": { + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "description": "Filter results by container name.", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List containers", + "group": "containers", + "path": "get", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/containers \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"cntr_682dfebaacac8198bbfe9c2474fb6f4a085685cbe3cb5863\",\n \"object\": \"container\",\n \"created_at\": 1747844794,\n \"status\": \"running\",\n \"expires_after\": {\n \"anchor\": \"last_active_at\",\n \"minutes\": 20\n },\n \"last_active_at\": 1747844794,\n \"memory_limit\": \"4g\",\n \"name\": \"My Container\"\n }\n ],\n \"first_id\": \"container_123\",\n \"last_id\": \"container_123\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "summary": "Create Container", + "description": "Creates a container.", + "operationId": "CreateContainer", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateContainerBody" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Create container", + "group": "containers", + "path": "post", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/containers \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Container\",\n \"memory_limit\": \"4g\",\n \"skills\": [\n {\n \"type\": \"skill_reference\",\n \"skill_id\": \"skill_4db6f1a2c9e73508b41f9da06e2c7b5f\"\n },\n {\n \"type\": \"skill_reference\",\n \"skill_id\": \"openai-spreadsheets\",\n \"version\": \"latest\"\n }\n ],\n \"network_policy\": {\n \"type\": \"allowlist\",\n \"allowed_domains\": [\"api.buildkite.com\"]\n }\n }'\n" + }, + "response": "{\n \"id\": \"cntr_682e30645a488191b6363a0cbefc0f0a025ec61b66250591\",\n \"object\": \"container\",\n \"created_at\": 1747857508,\n \"status\": \"running\",\n \"expires_after\": {\n \"anchor\": \"last_active_at\",\n \"minutes\": 20\n },\n \"last_active_at\": 1747857508,\n \"network_policy\": {\n \"type\": \"allowlist\",\n \"allowed_domains\": [\"api.buildkite.com\"]\n },\n \"memory_limit\": \"4g\",\n \"name\": \"My Container\"\n}\n" + } + } + } + }, + "/containers/{container_id}": { + "get": { + "summary": "Retrieve Container", + "description": "Retrieves a container.", + "operationId": "RetrieveContainer", + "parameters": [ + { + "name": "container_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve container", + "group": "containers", + "path": "get", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/containers/cntr_682dfebaacac8198bbfe9c2474fb6f4a085685cbe3cb5863 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "{\n \"id\": \"cntr_682dfebaacac8198bbfe9c2474fb6f4a085685cbe3cb5863\",\n \"object\": \"container\",\n \"created_at\": 1747844794,\n \"status\": \"running\",\n \"expires_after\": {\n \"anchor\": \"last_active_at\",\n \"minutes\": 20\n },\n \"last_active_at\": 1747844794,\n \"memory_limit\": \"4g\",\n \"name\": \"My Container\"\n}\n" + } + } + }, + "delete": { + "operationId": "DeleteContainer", + "summary": "Delete Container", + "description": "Delete a container.", + "parameters": [ + { + "name": "container_id", + "in": "path", + "description": "The ID of the container to delete.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete a container", + "group": "containers", + "path": "delete", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/containers/cntr_682dfebaacac8198bbfe9c2474fb6f4a085685cbe3cb5863 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "{\n \"id\": \"cntr_682dfebaacac8198bbfe9c2474fb6f4a085685cbe3cb5863\",\n \"object\": \"container.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/containers/{container_id}/files": { + "post": { + "summary": "Create a Container File\n\nYou can send either a multipart/form-data request with the raw file content, or a JSON request with a file ID.\n", + "description": "Creates a container file.\n", + "operationId": "CreateContainerFile", + "parameters": [ + { + "name": "container_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateContainerFileBody" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateContainerFileBody" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerFileResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Create container file", + "group": "containers", + "path": "post", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/containers/cntr_682e0e7318108198aa783fd921ff305e08e78805b9fdbb04/files \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -F file=\"@example.txt\"\n" + }, + "response": "{\n \"id\": \"cfile_682e0e8a43c88191a7978f477a09bdf5\",\n \"object\": \"container.file\",\n \"created_at\": 1747848842,\n \"bytes\": 880,\n \"container_id\": \"cntr_682e0e7318108198aa783fd921ff305e08e78805b9fdbb04\",\n \"path\": \"/mnt/data/88e12fa445d32636f190a0b33daed6cb-tsconfig.json\",\n \"source\": \"user\"\n}\n" + } + } + }, + "get": { + "summary": "List Container files", + "description": "Lists container files.", + "operationId": "ListContainerFiles", + "parameters": [ + { + "name": "container_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order.\n", + "schema": { + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerFileListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List container files", + "group": "containers", + "path": "get", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/containers/cntr_682e0e7318108198aa783fd921ff305e08e78805b9fdbb04/files \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"cfile_682e0e8a43c88191a7978f477a09bdf5\",\n \"object\": \"container.file\",\n \"created_at\": 1747848842,\n \"bytes\": 880,\n \"container_id\": \"cntr_682e0e7318108198aa783fd921ff305e08e78805b9fdbb04\",\n \"path\": \"/mnt/data/88e12fa445d32636f190a0b33daed6cb-tsconfig.json\",\n \"source\": \"user\"\n }\n ],\n \"first_id\": \"cfile_682e0e8a43c88191a7978f477a09bdf5\",\n \"has_more\": false,\n \"last_id\": \"cfile_682e0e8a43c88191a7978f477a09bdf5\"\n}\n" + } + } + } + }, + "/containers/{container_id}/files/{file_id}": { + "get": { + "summary": "Retrieve Container File", + "description": "Retrieves a container file.", + "operationId": "RetrieveContainerFile", + "parameters": [ + { + "name": "container_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerFileResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve container file", + "group": "containers", + "path": "get", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/containers/container_123/files/file_456 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "{\n \"id\": \"cfile_682e0e8a43c88191a7978f477a09bdf5\",\n \"object\": \"container.file\",\n \"created_at\": 1747848842,\n \"bytes\": 880,\n \"container_id\": \"cntr_682e0e7318108198aa783fd921ff305e08e78805b9fdbb04\",\n \"path\": \"/mnt/data/88e12fa445d32636f190a0b33daed6cb-tsconfig.json\",\n \"source\": \"user\"\n}\n" + } + } + }, + "delete": { + "operationId": "DeleteContainerFile", + "summary": "Delete Container File", + "description": "Delete a container file.", + "parameters": [ + { + "name": "container_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete a container file", + "group": "containers", + "path": "delete", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/containers/cntr_682dfebaacac8198bbfe9c2474fb6f4a085685cbe3cb5863/files/cfile_682e0e8a43c88191a7978f477a09bdf5 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "{\n \"id\": \"cfile_682e0e8a43c88191a7978f477a09bdf5\",\n \"object\": \"container.file.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/containers/{container_id}/files/{file_id}/content": { + "get": { + "summary": "Retrieve Container File Content", + "description": "Retrieves a container file content.", + "operationId": "RetrieveContainerFileContent", + "parameters": [ + { + "name": "container_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve container file content", + "group": "containers", + "path": "get", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/containers/container_123/files/cfile_456/content \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "\n" + } + } + } + }, + "/conversations/{conversation_id}/items": { + "post": { + "operationId": "createConversationItems", + "tags": [ + "Conversations" + ], + "summary": "Create items in a conversation with the given ID.", + "parameters": [ + { + "in": "path", + "name": "conversation_id", + "required": true, + "schema": { + "type": "string", + "example": "conv_123" + }, + "description": "The ID of the conversation to add the item to." + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncludeEnum" + } + }, + "description": "Additional fields to include in the response. See the `include`\nparameter for [listing Conversation items above](https://developers.openai.com/api/reference/resources/conversations/subresources/items/methods/list#%28resource%29%20conversations.items%20%3E%20%28method%29%20list%20%3E%20%28params%29%20default%20%3E%20%28param%29%20include%20%3E%20%28schema%29) for more information.\n" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "items": { + "type": "array", + "description": "The items to add to the conversation. You may add up to 20 items at a time.\n", + "items": { + "$ref": "#/components/schemas/InputItem" + }, + "maxItems": 20 + } + }, + "required": [ + "items" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationItemList" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Create items", + "group": "conversations", + "path": "create-item", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/conversations/conv_123/items \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"items\": [\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"input_text\", \"text\": \"Hello!\"}\n ]\n },\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"input_text\", \"text\": \"How are you?\"}\n ]\n }\n ]\n }'\n", + "javascript": "import OpenAI from \"openai\";\nconst client = new OpenAI();\n\nconst items = await client.conversations.items.create(\n \"conv_123\",\n {\n items: [\n {\n type: \"message\",\n role: \"user\",\n content: [{ type: \"input_text\", text: \"Hello!\" }],\n },\n {\n type: \"message\",\n role: \"user\",\n content: [{ type: \"input_text\", text: \"How are you?\" }],\n },\n ],\n }\n);\nconsole.log(items.data);\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nitems = client.conversations.items.create(\n \"conv_123\",\n items=[\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": [{\"type\": \"input_text\", \"text\": \"Hello!\"}],\n },\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": [{\"type\": \"input_text\", \"text\": \"How are you?\"}],\n }\n ],\n)\nprint(items.data)\n", + "csharp": "using System;\nusing System.Collections.Generic;\nusing OpenAI.Conversations;\n\nOpenAIConversationClient client = new(\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nConversationItemList created = client.ConversationItems.Create(\n conversationId: \"conv_123\",\n new CreateConversationItemsOptions\n {\n Items = new List\n {\n new ConversationMessage\n {\n Role = \"user\",\n Content =\n {\n new ConversationInputText { Text = \"Hello!\" }\n }\n },\n new ConversationMessage\n {\n Role = \"user\",\n Content =\n {\n new ConversationInputText { Text = \"How are you?\" }\n }\n }\n }\n }\n);\nConsole.WriteLine(created.Data.Count);\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"type\": \"message\",\n \"id\": \"msg_abc\",\n \"status\": \"completed\",\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"input_text\", \"text\": \"Hello!\"}\n ]\n },\n {\n \"type\": \"message\",\n \"id\": \"msg_def\",\n \"status\": \"completed\",\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"input_text\", \"text\": \"How are you?\"}\n ]\n }\n ],\n \"first_id\": \"msg_abc\",\n \"last_id\": \"msg_def\",\n \"has_more\": false\n}\n" + } + } + }, + "get": { + "operationId": "listConversationItems", + "tags": [ + "Conversations" + ], + "summary": "List all items for a conversation with the given ID.", + "parameters": [ + { + "in": "path", + "name": "conversation_id", + "required": true, + "schema": { + "type": "string", + "example": "conv_123" + }, + "description": "The ID of the conversation to list items for." + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between\n1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "in": "query", + "name": "order", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "description": "The order to return the input items in. Default is `desc`.\n- `asc`: Return the input items in ascending order.\n- `desc`: Return the input items in descending order.\n" + }, + { + "in": "query", + "name": "after", + "schema": { + "type": "string" + }, + "description": "An item ID to list items after, used in pagination.\n" + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncludeEnum" + } + }, + "description": "Specify additional output data to include in the model response. Currently supported values are:\n- `web_search_call.action.sources`: Include the sources of the web search tool call.\n- `code_interpreter_call.outputs`: Includes the outputs of python code execution in code interpreter tool call items.\n- `computer_call_output.output.image_url`: Include image urls from the computer call output.\n- `file_search_call.results`: Include the search results of the file search tool call.\n- `message.input_image.image_url`: Include image urls from the input message.\n- `message.output_text.logprobs`: Include logprobs with assistant messages.\n- `reasoning.encrypted_content`: Includes an encrypted version of reasoning tokens in reasoning item outputs. This enables reasoning items to be used in multi-turn conversations when using the Responses API statelessly (like when the `store` parameter is set to `false`, or when an organization is enrolled in the zero data retention program)." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationItemList" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List items", + "group": "conversations", + "path": "list-items", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/conversations/conv_123/items?limit=10\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "javascript": "import OpenAI from \"openai\";\nconst client = new OpenAI();\n\nconst items = await client.conversations.items.list(\"conv_123\", { limit: 10 });\nconsole.log(items.data);\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nitems = client.conversations.items.list(\"conv_123\", limit=10)\nprint(items.data)\n", + "csharp": "using System;\nusing OpenAI.Conversations;\n\nOpenAIConversationClient client = new(\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nConversationItemList items = client.ConversationItems.List(\n conversationId: \"conv_123\",\n new ListConversationItemsOptions { Limit = 10 }\n);\nConsole.WriteLine(items.Data.Count);\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"type\": \"message\",\n \"id\": \"msg_abc\",\n \"status\": \"completed\",\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"input_text\", \"text\": \"Hello!\"}\n ]\n }\n ],\n \"first_id\": \"msg_abc\",\n \"last_id\": \"msg_abc\",\n \"has_more\": false\n}\n" + } + } + } + }, + "/conversations/{conversation_id}/items/{item_id}": { + "get": { + "operationId": "getConversationItem", + "tags": [ + "Conversations" + ], + "summary": "Get a single item from a conversation with the given IDs.", + "parameters": [ + { + "in": "path", + "name": "conversation_id", + "required": true, + "schema": { + "type": "string", + "example": "conv_123" + }, + "description": "The ID of the conversation that contains the item." + }, + { + "in": "path", + "name": "item_id", + "required": true, + "schema": { + "type": "string", + "example": "msg_abc" + }, + "description": "The ID of the item to retrieve." + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncludeEnum" + } + }, + "description": "Additional fields to include in the response. See the `include`\nparameter for [listing Conversation items above](https://developers.openai.com/api/reference/resources/conversations/subresources/items/methods/list#%28resource%29%20conversations.items%20%3E%20%28method%29%20list%20%3E%20%28params%29%20default%20%3E%20%28param%29%20include%20%3E%20%28schema%29) for more information.\n" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationItem" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve an item", + "group": "conversations", + "path": "get-item", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/conversations/conv_123/items/msg_abc \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "javascript": "import OpenAI from \"openai\";\nconst client = new OpenAI();\n\nconst item = await client.conversations.items.retrieve(\n \"conv_123\",\n \"msg_abc\"\n);\nconsole.log(item);\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nitem = client.conversations.items.retrieve(\"conv_123\", \"msg_abc\")\nprint(item)\n", + "csharp": "using System;\nusing OpenAI.Conversations;\n\nOpenAIConversationClient client = new(\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nConversationItem item = client.ConversationItems.Get(\n conversationId: \"conv_123\",\n itemId: \"msg_abc\"\n);\nConsole.WriteLine(item.Id);\n" + }, + "response": "{\n \"type\": \"message\",\n \"id\": \"msg_abc\",\n \"status\": \"completed\",\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"input_text\", \"text\": \"Hello!\"}\n ]\n}\n" + } + } + }, + "delete": { + "operationId": "deleteConversationItem", + "tags": [ + "Conversations" + ], + "summary": "Delete an item from a conversation with the given IDs.", + "parameters": [ + { + "in": "path", + "name": "conversation_id", + "required": true, + "schema": { + "type": "string", + "example": "conv_123" + }, + "description": "The ID of the conversation that contains the item." + }, + { + "in": "path", + "name": "item_id", + "required": true, + "schema": { + "type": "string", + "example": "msg_abc" + }, + "description": "The ID of the item to delete." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete an item", + "group": "conversations", + "path": "delete-item", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/conversations/conv_123/items/msg_abc \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "javascript": "import OpenAI from \"openai\";\nconst client = new OpenAI();\n\nconst conversation = await client.conversations.items.delete(\n \"conv_123\",\n \"msg_abc\"\n);\nconsole.log(conversation);\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nconversation = client.conversations.items.delete(\"conv_123\", \"msg_abc\")\nprint(conversation)\n", + "csharp": "using System;\nusing OpenAI.Conversations;\n\nOpenAIConversationClient client = new(\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nConversation conversation = client.ConversationItems.Delete(\n conversationId: \"conv_123\",\n itemId: \"msg_abc\"\n);\nConsole.WriteLine(conversation.Id);\n" + }, + "response": "{\n \"id\": \"conv_123\",\n \"object\": \"conversation\",\n \"created_at\": 1741900000,\n \"metadata\": {\"topic\": \"demo\"}\n}\n" + } + } + } + }, + "/embeddings": { + "post": { + "operationId": "createEmbedding", + "tags": [ + "Embeddings" + ], + "summary": "Creates an embedding vector representing the input text.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEmbeddingRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEmbeddingResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/InferenceRateLimited" + }, + "503": { + "$ref": "#/components/responses/InferenceServiceUnavailable" + } + }, + "x-oaiMeta": { + "name": "Create embeddings", + "group": "embeddings", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/embeddings \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"input\": \"The food was delicious and the waiter...\",\n \"model\": \"text-embedding-ada-002\",\n \"encoding_format\": \"float\"\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.embeddings.create(\n model=\"text-embedding-ada-002\",\n input=\"The food was delicious and the waiter...\",\n encoding_format=\"float\"\n)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const embedding = await openai.embeddings.create({\n model: \"text-embedding-ada-002\",\n input: \"The quick brown fox jumped over the lazy dog\",\n encoding_format: \"float\",\n });\n\n console.log(embedding);\n}\n\nmain();\n", + "csharp": "using System;\n\nusing OpenAI.Embeddings;\n\nEmbeddingClient client = new(\n model: \"text-embedding-3-small\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nOpenAIEmbedding embedding = client.GenerateEmbedding(input: \"The quick brown fox jumped over the lazy dog\");\nReadOnlyMemory vector = embedding.ToFloats();\n\nfor (int i = 0; i < vector.Length; i++)\n{\n Console.WriteLine($\" [{i,4}] = {vector.Span[i]}\");\n}\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"embedding\",\n \"embedding\": [\n 0.0023064255,\n -0.009327292,\n .... (1536 floats total for ada-002)\n -0.0028842222,\n ],\n \"index\": 0\n }\n ],\n \"model\": \"text-embedding-ada-002\",\n \"usage\": {\n \"prompt_tokens\": 8,\n \"total_tokens\": 8\n }\n}\n" + } + } + } + }, + "/evals": { + "get": { + "operationId": "listEvals", + "tags": [ + "Evals" + ], + "summary": "List evaluations for a project.\n", + "parameters": [ + { + "name": "after", + "in": "query", + "description": "Identifier for the last eval from the previous pagination request.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of evals to retrieve.", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for evals by timestamp. Use `asc` for ascending order or `desc` for descending order.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + }, + { + "name": "order_by", + "in": "query", + "description": "Evals can be ordered by creation time or last updated time. Use\n`created_at` for creation time or `updated_at` for last updated time.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "created_at", + "updated_at" + ], + "default": "created_at" + } + } + ], + "responses": { + "200": { + "description": "A list of evals", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvalList" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List evals", + "group": "evals", + "path": "list", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals?limit=1 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nevals = client.evals.list(limit=1)\nprint(evals)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst evals = await openai.evals.list({ limit: 1 });\nconsole.log(evals);\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"object\": \"eval\",\n \"data_source_config\": {\n \"type\": \"stored_completions\",\n \"metadata\": {\n \"usecase\": \"push_notifications_summarizer\"\n },\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"item\": {\n \"type\": \"object\"\n },\n \"sample\": {\n \"type\": \"object\"\n }\n },\n \"required\": [\n \"item\",\n \"sample\"\n ]\n }\n },\n \"testing_criteria\": [\n {\n \"name\": \"Push Notification Summary Grader\",\n \"id\": \"Push Notification Summary Grader-9b876f24-4762-4be9-aff4-db7a9b31c673\",\n \"type\": \"label_model\",\n \"model\": \"o3-mini\",\n \"input\": [\n {\n \"type\": \"message\",\n \"role\": \"developer\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"\\nLabel the following push notification summary as either correct or incorrect.\\nThe push notification and the summary will be provided below.\\nA good push notificiation summary is concise and snappy.\\nIf it is good, then label it as correct, if not, then incorrect.\\n\"\n }\n },\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"\\nPush notifications: {{item.input}}\\nSummary: {{sample.output_text}}\\n\"\n }\n }\n ],\n \"passing_labels\": [\n \"correct\"\n ],\n \"labels\": [\n \"correct\",\n \"incorrect\"\n ],\n \"sampling_params\": null\n }\n ],\n \"name\": \"Push Notification Summary Grader\",\n \"created_at\": 1739314509,\n \"metadata\": {\n \"description\": \"A stored completions eval for push notification summaries\"\n }\n }\n ],\n \"first_id\": \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"last_id\": \"eval_67aa884cf6688190b58f657d4441c8b7\",\n \"has_more\": true\n}\n" + } + } + }, + "post": { + "operationId": "createEval", + "tags": [ + "Evals" + ], + "summary": "Create the structure of an evaluation that can be used to test a model's performance.\nAn evaluation is a set of testing criteria and the config for a data source, which dictates the schema of the data used in the evaluation. After creating an evaluation, you can run it on different models and model parameters. We support several types of graders and datasources.\nFor more information, see the [Evals guide](https://developers.openai.com/api/docs/guides/evals).\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEvalRequest" + } + } + } + }, + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Eval" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Create eval", + "group": "evals", + "path": "post", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Sentiment\",\n \"data_source_config\": {\n \"type\": \"stored_completions\",\n \"metadata\": {\n \"usecase\": \"chatbot\"\n }\n },\n \"testing_criteria\": [\n {\n \"type\": \"label_model\",\n \"model\": \"o3-mini\",\n \"input\": [\n {\n \"role\": \"developer\",\n \"content\": \"Classify the sentiment of the following statement as one of 'positive', 'neutral', or 'negative'\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Statement: {{item.input}}\"\n }\n ],\n \"passing_labels\": [\n \"positive\"\n ],\n \"labels\": [\n \"positive\",\n \"neutral\",\n \"negative\"\n ],\n \"name\": \"Example label grader\"\n }\n ]\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\neval_obj = client.evals.create(\n name=\"Sentiment\",\n data_source_config={\n \"type\": \"stored_completions\",\n \"metadata\": {\"usecase\": \"chatbot\"}\n },\n testing_criteria=[\n {\n \"type\": \"label_model\",\n \"model\": \"o3-mini\",\n \"input\": [\n {\"role\": \"developer\", \"content\": \"Classify the sentiment of the following statement as one of 'positive', 'neutral', or 'negative'\"},\n {\"role\": \"user\", \"content\": \"Statement: {{item.input}}\"}\n ],\n \"passing_labels\": [\"positive\"],\n \"labels\": [\"positive\", \"neutral\", \"negative\"],\n \"name\": \"Example label grader\"\n }\n ]\n)\nprint(eval_obj)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst evalObj = await openai.evals.create({\n name: \"Sentiment\",\n data_source_config: {\n type: \"stored_completions\",\n metadata: { usecase: \"chatbot\" }\n },\n testing_criteria: [\n {\n type: \"label_model\",\n model: \"o3-mini\",\n input: [\n { role: \"developer\", content: \"Classify the sentiment of the following statement as one of 'positive', 'neutral', or 'negative'\" },\n { role: \"user\", content: \"Statement: {{item.input}}\" }\n ],\n passing_labels: [\"positive\"],\n labels: [\"positive\", \"neutral\", \"negative\"],\n name: \"Example label grader\"\n }\n ]\n});\nconsole.log(evalObj);\n" + }, + "response": "{\n \"object\": \"eval\",\n \"id\": \"eval_67b7fa9a81a88190ab4aa417e397ea21\",\n \"data_source_config\": {\n \"type\": \"stored_completions\",\n \"metadata\": {\n \"usecase\": \"chatbot\"\n },\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"item\": {\n \"type\": \"object\"\n },\n \"sample\": {\n \"type\": \"object\"\n }\n },\n \"required\": [\n \"item\",\n \"sample\"\n ]\n },\n \"testing_criteria\": [\n {\n \"name\": \"Example label grader\",\n \"type\": \"label_model\",\n \"model\": \"o3-mini\",\n \"input\": [\n {\n \"type\": \"message\",\n \"role\": \"developer\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"Classify the sentiment of the following statement as one of positive, neutral, or negative\"\n }\n },\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"Statement: {{item.input}}\"\n }\n }\n ],\n \"passing_labels\": [\n \"positive\"\n ],\n \"labels\": [\n \"positive\",\n \"neutral\",\n \"negative\"\n ]\n }\n ],\n \"name\": \"Sentiment\",\n \"created_at\": 1740110490,\n \"metadata\": {\n \"description\": \"An eval for sentiment analysis\"\n }\n}\n" + } + } + } + }, + "/evals/{eval_id}": { + "get": { + "operationId": "getEval", + "tags": [ + "Evals" + ], + "summary": "Get an evaluation by ID.\n", + "parameters": [ + { + "name": "eval_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation to retrieve." + } + ], + "responses": { + "200": { + "description": "The evaluation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Eval" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Get an eval", + "group": "evals", + "path": "get", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/eval_67abd54d9b0081909a86353f6fb9317a \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\neval_obj = client.evals.retrieve(\"eval_67abd54d9b0081909a86353f6fb9317a\")\nprint(eval_obj)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst evalObj = await openai.evals.retrieve(\"eval_67abd54d9b0081909a86353f6fb9317a\");\nconsole.log(evalObj);\n" + }, + "response": "{\n \"object\": \"eval\",\n \"id\": \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"data_source_config\": {\n \"type\": \"custom\",\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"item\": {\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\"\n },\n \"ground_truth\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"input\",\n \"ground_truth\"\n ]\n }\n },\n \"required\": [\n \"item\"\n ]\n }\n },\n \"testing_criteria\": [\n {\n \"name\": \"String check\",\n \"id\": \"String check-2eaf2d8d-d649-4335-8148-9535a7ca73c2\",\n \"type\": \"string_check\",\n \"input\": \"{{item.input}}\",\n \"reference\": \"{{item.ground_truth}}\",\n \"operation\": \"eq\"\n }\n ],\n \"name\": \"External Data Eval\",\n \"created_at\": 1739314509,\n \"metadata\": {},\n}\n" + } + } + }, + "post": { + "operationId": "updateEval", + "tags": [ + "Evals" + ], + "summary": "Update certain properties of an evaluation.\n", + "parameters": [ + { + "name": "eval_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation to update." + } + ], + "requestBody": { + "description": "Request to update an evaluation", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Rename the evaluation." + }, + "metadata": { + "$ref": "#/components/schemas/Metadata" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The updated evaluation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Eval" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Update an eval", + "group": "evals", + "path": "update", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/eval_67abd54d9b0081909a86353f6fb9317a \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\": \"Updated Eval\", \"metadata\": {\"description\": \"Updated description\"}}'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nupdated_eval = client.evals.update(\n \"eval_67abd54d9b0081909a86353f6fb9317a\",\n name=\"Updated Eval\",\n metadata={\"description\": \"Updated description\"}\n)\nprint(updated_eval)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst updatedEval = await openai.evals.update(\n \"eval_67abd54d9b0081909a86353f6fb9317a\",\n {\n name: \"Updated Eval\",\n metadata: { description: \"Updated description\" }\n }\n);\nconsole.log(updatedEval);\n" + }, + "response": "{\n \"object\": \"eval\",\n \"id\": \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"data_source_config\": {\n \"type\": \"custom\",\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"item\": {\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\"\n },\n \"ground_truth\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"input\",\n \"ground_truth\"\n ]\n }\n },\n \"required\": [\n \"item\"\n ]\n }\n },\n \"testing_criteria\": [\n {\n \"name\": \"String check\",\n \"id\": \"String check-2eaf2d8d-d649-4335-8148-9535a7ca73c2\",\n \"type\": \"string_check\",\n \"input\": \"{{item.input}}\",\n \"reference\": \"{{item.ground_truth}}\",\n \"operation\": \"eq\"\n }\n ],\n \"name\": \"Updated Eval\",\n \"created_at\": 1739314509,\n \"metadata\": {\"description\": \"Updated description\"},\n}\n" + } + } + }, + "delete": { + "operationId": "deleteEval", + "tags": [ + "Evals" + ], + "summary": "Delete an evaluation.\n", + "parameters": [ + { + "name": "eval_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation to delete." + } + ], + "responses": { + "200": { + "description": "Successfully deleted the evaluation.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "object": { + "type": "string", + "example": "eval.deleted" + }, + "deleted": { + "type": "boolean", + "example": true + }, + "eval_id": { + "type": "string", + "example": "eval_abc123" + } + }, + "required": [ + "object", + "deleted", + "eval_id" + ] + } + } + } + }, + "404": { + "description": "Evaluation not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete an eval", + "group": "evals", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/eval_abc123 \\\n -X DELETE \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ndeleted = client.evals.delete(\"eval_abc123\")\nprint(deleted)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst deleted = await openai.evals.delete(\"eval_abc123\");\nconsole.log(deleted);\n" + }, + "response": "{\n \"object\": \"eval.deleted\",\n \"deleted\": true,\n \"eval_id\": \"eval_abc123\"\n}\n" + } + } + } + }, + "/evals/{eval_id}/runs": { + "get": { + "operationId": "getEvalRuns", + "tags": [ + "Evals" + ], + "summary": "Get a list of runs for an evaluation.\n", + "parameters": [ + { + "name": "eval_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation to retrieve runs for." + }, + { + "name": "after", + "in": "query", + "description": "Identifier for the last run from the previous pagination request.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of runs to retrieve.", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for runs by timestamp. Use `asc` for ascending order or `desc` for descending order. Defaults to `asc`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter runs by status. One of `queued` | `in_progress` | `failed` | `completed` | `canceled`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "queued", + "in_progress", + "completed", + "canceled", + "failed" + ] + } + } + ], + "responses": { + "200": { + "description": "A list of runs for the evaluation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvalRunList" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Get eval runs", + "group": "evals", + "path": "get-runs", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/egroup_67abd54d9b0081909a86353f6fb9317a/runs \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nruns = client.evals.runs.list(\"egroup_67abd54d9b0081909a86353f6fb9317a\")\nprint(runs)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst runs = await openai.evals.runs.list(\"egroup_67abd54d9b0081909a86353f6fb9317a\");\nconsole.log(runs);\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"eval.run\",\n \"id\": \"evalrun_67e0c7d31560819090d60c0780591042\",\n \"eval_id\": \"eval_67e0c726d560819083f19a957c4c640b\",\n \"report_url\": \"https://platform.openai.com/evaluations/eval_67e0c726d560819083f19a957c4c640b\",\n \"status\": \"completed\",\n \"model\": \"o3-mini\",\n \"name\": \"bulk_with_negative_examples_o3-mini\",\n \"created_at\": 1742784467,\n \"result_counts\": {\n \"total\": 1,\n \"errored\": 0,\n \"failed\": 0,\n \"passed\": 1\n },\n \"per_model_usage\": [\n {\n \"model_name\": \"o3-mini\",\n \"invocation_count\": 1,\n \"prompt_tokens\": 563,\n \"completion_tokens\": 874,\n \"total_tokens\": 1437,\n \"cached_tokens\": 0\n }\n ],\n \"per_testing_criteria_results\": [\n {\n \"testing_criteria\": \"Push Notification Summary Grader-1808cd0b-eeec-4e0b-a519-337e79f4f5d1\",\n \"passed\": 1,\n \"failed\": 0\n }\n ],\n \"data_source\": {\n \"type\": \"completions\",\n \"source\": {\n \"type\": \"file_content\",\n \"content\": [\n {\n \"item\": {\n \"notifications\": \"\\n- New message from Sarah: \\\"Can you call me later?\\\"\\n- Your package has been delivered!\\n- Flash sale: 20% off electronics for the next 2 hours!\\n\"\n }\n }\n ]\n },\n \"input_messages\": {\n \"type\": \"template\",\n \"template\": [\n {\n \"type\": \"message\",\n \"role\": \"developer\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"\\n\\n\\n\\nYou are a helpful assistant that takes in an array of push notifications and returns a collapsed summary of them.\\nThe push notification will be provided as follows:\\n\\n...notificationlist...\\n\\n\\nYou should return just the summary and nothing else.\\n\\n\\nYou should return a summary that is concise and snappy.\\n\\n\\nHere is an example of a good summary:\\n\\n- Traffic alert: Accident reported on Main Street.- Package out for delivery: Expected by 5 PM.- New friend suggestion: Connect with Emma.\\n\\n\\nTraffic alert, package expected by 5pm, suggestion for new friend (Emily).\\n\\n\\n\\nHere is an example of a bad summary:\\n\\n- Traffic alert: Accident reported on Main Street.- Package out for delivery: Expected by 5 PM.- New friend suggestion: Connect with Emma.\\n\\n\\nTraffic alert reported on main street. You have a package that will arrive by 5pm, Emily is a new friend suggested for you.\\n\\n\"\n }\n },\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"{{item.notifications}}\"\n }\n }\n ]\n },\n \"model\": \"o3-mini\",\n \"sampling_params\": null\n },\n \"error\": null,\n \"metadata\": {}\n }\n ],\n \"first_id\": \"evalrun_67e0c7d31560819090d60c0780591042\",\n \"last_id\": \"evalrun_67e0c7d31560819090d60c0780591042\",\n \"has_more\": true\n}\n" + } + } + }, + "post": { + "operationId": "createEvalRun", + "tags": [ + "Evals" + ], + "summary": "Kicks off a new run for a given evaluation, specifying the data source, and what model configuration to use to test. The datasource will be validated against the schema specified in the config of the evaluation.\n", + "parameters": [ + { + "in": "path", + "name": "eval_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation to create a run for." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEvalRunRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Successfully created a run for the evaluation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvalRun" + } + } + } + }, + "400": { + "description": "Bad request (for example, missing eval object)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Create eval run", + "group": "evals", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/eval_67e579652b548190aaa83ada4b125f47/runs \\\n -X POST \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"gpt-6-astra\",\"data_source\":{\"type\":\"completions\",\"input_messages\":{\"type\":\"template\",\"template\":[{\"role\":\"developer\",\"content\":\"Categorize a given news headline into one of the following topics: Technology, Markets, World, Business, or Sports.\\n\\n# Steps\\n\\n1. Analyze the content of the news headline to understand its primary focus.\\n2. Extract the subject matter, identifying any key indicators or keywords.\\n3. Use the identified indicators to determine the most suitable category out of the five options: Technology, Markets, World, Business, or Sports.\\n4. Ensure only one category is selected per headline.\\n\\n# Output Format\\n\\nRespond with the chosen category as a single word. For instance: \\\"Technology\\\", \\\"Markets\\\", \\\"World\\\", \\\"Business\\\", or \\\"Sports\\\".\\n\\n# Examples\\n\\n**Input**: \\\"Apple Unveils New iPhone Model, Featuring Advanced AI Features\\\" \\n**Output**: \\\"Technology\\\"\\n\\n**Input**: \\\"Global Stocks Mixed as Investors Await Central Bank Decisions\\\" \\n**Output**: \\\"Markets\\\"\\n\\n**Input**: \\\"War in Ukraine: Latest Updates on Negotiation Status\\\" \\n**Output**: \\\"World\\\"\\n\\n**Input**: \\\"Microsoft in Talks to Acquire Gaming Company for $2 Billion\\\" \\n**Output**: \\\"Business\\\"\\n\\n**Input**: \\\"Manchester United Secures Win in Premier League Football Match\\\" \\n**Output**: \\\"Sports\\\" \\n\\n# Notes\\n\\n- If the headline appears to fit into more than one category, choose the most dominant theme.\\n- Keywords or phrases such as \\\"stocks\\\", \\\"company acquisition\\\", \\\"match\\\", or technological brands can be good indicators for classification.\\n\"} , {\"role\":\"user\",\"content\":\"{{item.input}}\"}]} ,\"sampling_params\":{\"max_completions_tokens\":2048},\"model\":\"gpt-6-astra\",\"source\":{\"type\":\"file_content\",\"content\":[{\"item\":{\"input\":\"Tech Company Launches Advanced Artificial Intelligence Platform\",\"ground_truth\":\"Technology\"}}]}}}'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nrun = client.evals.runs.create(\n \"eval_67e579652b548190aaa83ada4b125f47\",\n name=\"gpt-6-astra\",\n data_source={\n \"type\": \"completions\",\n \"input_messages\": {\n \"type\": \"template\",\n \"template\": [\n {\n \"role\": \"developer\",\n \"content\": \"Categorize a given news headline into one of the following topics: Technology, Markets, World, Business, or Sports.\\n\\n# Steps\\n\\n1. Analyze the content of the news headline to understand its primary focus.\\n2. Extract the subject matter, identifying any key indicators or keywords.\\n3. Use the identified indicators to determine the most suitable category out of the five options: Technology, Markets, World, Business, or Sports.\\n4. Ensure only one category is selected per headline.\\n\\n# Output Format\\n\\nRespond with the chosen category as a single word. For instance: \\\"Technology\\\", \\\"Markets\\\", \\\"World\\\", \\\"Business\\\", or \\\"Sports\\\".\\n\\n# Examples\\n\\n**Input**: \\\"Apple Unveils New iPhone Model, Featuring Advanced AI Features\\\" \\n**Output**: \\\"Technology\\\"\\n\\n**Input**: \\\"Global Stocks Mixed as Investors Await Central Bank Decisions\\\" \\n**Output**: \\\"Markets\\\"\\n\\n**Input**: \\\"War in Ukraine: Latest Updates on Negotiation Status\\\" \\n**Output**: \\\"World\\\"\\n\\n**Input**: \\\"Microsoft in Talks to Acquire Gaming Company for $2 Billion\\\" \\n**Output**: \\\"Business\\\"\\n\\n**Input**: \\\"Manchester United Secures Win in Premier League Football Match\\\" \\n**Output**: \\\"Sports\\\" \\n\\n# Notes\\n\\n- If the headline appears to fit into more than one category, choose the most dominant theme.\\n- Keywords or phrases such as \\\"stocks\\\", \\\"company acquisition\\\", \\\"match\\\", or technological brands can be good indicators for classification.\\n\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{item.input}}\"\n }\n ]\n },\n \"sampling_params\": {\n \"max_completions_tokens\": 2048\n },\n \"model\": \"gpt-6-astra\",\n \"source\": {\n \"type\": \"file_content\",\n \"content\": [\n {\n \"item\": {\n \"input\": \"Tech Company Launches Advanced Artificial Intelligence Platform\",\n \"ground_truth\": \"Technology\"\n }\n }\n ]\n }\n }\n)\nprint(run)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst run = await openai.evals.runs.create(\n \"eval_67e579652b548190aaa83ada4b125f47\",\n {\n name: \"gpt-6-astra\",\n data_source: {\n type: \"completions\",\n input_messages: {\n type: \"template\",\n template: [\n {\n role: \"developer\",\n content: \"Categorize a given news headline into one of the following topics: Technology, Markets, World, Business, or Sports.\\n\\n# Steps\\n\\n1. Analyze the content of the news headline to understand its primary focus.\\n2. Extract the subject matter, identifying any key indicators or keywords.\\n3. Use the identified indicators to determine the most suitable category out of the five options: Technology, Markets, World, Business, or Sports.\\n4. Ensure only one category is selected per headline.\\n\\n# Output Format\\n\\nRespond with the chosen category as a single word. For instance: \\\"Technology\\\", \\\"Markets\\\", \\\"World\\\", \\\"Business\\\", or \\\"Sports\\\".\\n\\n# Examples\\n\\n**Input**: \\\"Apple Unveils New iPhone Model, Featuring Advanced AI Features\\\" \\n**Output**: \\\"Technology\\\"\\n\\n**Input**: \\\"Global Stocks Mixed as Investors Await Central Bank Decisions\\\" \\n**Output**: \\\"Markets\\\"\\n\\n**Input**: \\\"War in Ukraine: Latest Updates on Negotiation Status\\\" \\n**Output**: \\\"World\\\"\\n\\n**Input**: \\\"Microsoft in Talks to Acquire Gaming Company for $2 Billion\\\" \\n**Output**: \\\"Business\\\"\\n\\n**Input**: \\\"Manchester United Secures Win in Premier League Football Match\\\" \\n**Output**: \\\"Sports\\\" \\n\\n# Notes\\n\\n- If the headline appears to fit into more than one category, choose the most dominant theme.\\n- Keywords or phrases such as \\\"stocks\\\", \\\"company acquisition\\\", \\\"match\\\", or technological brands can be good indicators for classification.\\n\"\n },\n {\n role: \"user\",\n content: \"{{item.input}}\"\n }\n ]\n },\n sampling_params: {\n max_completions_tokens: 2048\n },\n model: \"gpt-6-astra\",\n source: {\n type: \"file_content\",\n content: [\n {\n item: {\n input: \"Tech Company Launches Advanced Artificial Intelligence Platform\",\n ground_truth: \"Technology\"\n }\n }\n ]\n }\n }\n }\n);\nconsole.log(run);\n" + }, + "response": "{\n \"object\": \"eval.run\",\n \"id\": \"evalrun_67e57965b480819094274e3a32235e4c\",\n \"eval_id\": \"eval_67e579652b548190aaa83ada4b125f47\",\n \"report_url\": \"https://platform.openai.com/evaluations/eval_67e579652b548190aaa83ada4b125f47&run_id=evalrun_67e57965b480819094274e3a32235e4c\",\n \"status\": \"queued\",\n \"model\": \"gpt-6-astra\",\n \"name\": \"gpt-6-astra\",\n \"created_at\": 1743092069,\n \"result_counts\": {\n \"total\": 0,\n \"errored\": 0,\n \"failed\": 0,\n \"passed\": 0\n },\n \"per_model_usage\": null,\n \"per_testing_criteria_results\": null,\n \"data_source\": {\n \"type\": \"completions\",\n \"source\": {\n \"type\": \"file_content\",\n \"content\": [\n {\n \"item\": {\n \"input\": \"Tech Company Launches Advanced Artificial Intelligence Platform\",\n \"ground_truth\": \"Technology\"\n }\n }\n ]\n },\n \"input_messages\": {\n \"type\": \"template\",\n \"template\": [\n {\n \"type\": \"message\",\n \"role\": \"developer\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"Categorize a given news headline into one of the following topics: Technology, Markets, World, Business, or Sports.\\n\\n# Steps\\n\\n1. Analyze the content of the news headline to understand its primary focus.\\n2. Extract the subject matter, identifying any key indicators or keywords.\\n3. Use the identified indicators to determine the most suitable category out of the five options: Technology, Markets, World, Business, or Sports.\\n4. Ensure only one category is selected per headline.\\n\\n# Output Format\\n\\nRespond with the chosen category as a single word. For instance: \\\"Technology\\\", \\\"Markets\\\", \\\"World\\\", \\\"Business\\\", or \\\"Sports\\\".\\n\\n# Examples\\n\\n**Input**: \\\"Apple Unveils New iPhone Model, Featuring Advanced AI Features\\\" \\n**Output**: \\\"Technology\\\"\\n\\n**Input**: \\\"Global Stocks Mixed as Investors Await Central Bank Decisions\\\" \\n**Output**: \\\"Markets\\\"\\n\\n**Input**: \\\"War in Ukraine: Latest Updates on Negotiation Status\\\" \\n**Output**: \\\"World\\\"\\n\\n**Input**: \\\"Microsoft in Talks to Acquire Gaming Company for $2 Billion\\\" \\n**Output**: \\\"Business\\\"\\n\\n**Input**: \\\"Manchester United Secures Win in Premier League Football Match\\\" \\n**Output**: \\\"Sports\\\" \\n\\n# Notes\\n\\n- If the headline appears to fit into more than one category, choose the most dominant theme.\\n- Keywords or phrases such as \\\"stocks\\\", \\\"company acquisition\\\", \\\"match\\\", or technological brands can be good indicators for classification.\\n\"\n }\n },\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"{{item.input}}\"\n }\n }\n ]\n },\n \"model\": \"gpt-6-astra\",\n \"sampling_params\": {\n \"max_completions_tokens\": 2048\n }\n },\n \"error\": null,\n \"metadata\": {}\n}\n" + } + } + } + }, + "/evals/{eval_id}/runs/{run_id}": { + "get": { + "operationId": "getEvalRun", + "tags": [ + "Evals" + ], + "summary": "Get an evaluation run by ID.\n", + "parameters": [ + { + "name": "eval_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation to retrieve runs for." + }, + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the run to retrieve." + } + ], + "responses": { + "200": { + "description": "The evaluation run", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvalRun" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Get an eval run", + "group": "evals", + "path": "get", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/eval_67abd54d9b0081909a86353f6fb9317a/runs/evalrun_67abd54d60ec8190832b46859da808f7 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nrun = client.evals.runs.retrieve(\n \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"evalrun_67abd54d60ec8190832b46859da808f7\"\n)\nprint(run)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst run = await openai.evals.runs.retrieve(\n \"evalrun_67abd54d60ec8190832b46859da808f7\",\n { eval_id: \"eval_67abd54d9b0081909a86353f6fb9317a\" }\n);\nconsole.log(run);\n" + }, + "response": "{\n \"object\": \"eval.run\",\n \"id\": \"evalrun_67abd54d60ec8190832b46859da808f7\",\n \"eval_id\": \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"report_url\": \"https://platform.openai.com/evaluations/eval_67abd54d9b0081909a86353f6fb9317a?run_id=evalrun_67abd54d60ec8190832b46859da808f7\",\n \"status\": \"queued\",\n \"model\": \"gpt-6-astra\",\n \"name\": \"gpt-6-astra\",\n \"created_at\": 1743092069,\n \"result_counts\": {\n \"total\": 0,\n \"errored\": 0,\n \"failed\": 0,\n \"passed\": 0\n },\n \"per_model_usage\": null,\n \"per_testing_criteria_results\": null,\n \"data_source\": {\n \"type\": \"completions\",\n \"source\": {\n \"type\": \"file_content\",\n \"content\": [\n {\n \"item\": {\n \"input\": \"Tech Company Launches Advanced Artificial Intelligence Platform\",\n \"ground_truth\": \"Technology\"\n }\n },\n {\n \"item\": {\n \"input\": \"Central Bank Increases Interest Rates Amid Inflation Concerns\",\n \"ground_truth\": \"Markets\"\n }\n },\n {\n \"item\": {\n \"input\": \"International Summit Addresses Climate Change Strategies\",\n \"ground_truth\": \"World\"\n }\n },\n {\n \"item\": {\n \"input\": \"Major Retailer Reports Record-Breaking Holiday Sales\",\n \"ground_truth\": \"Business\"\n }\n },\n {\n \"item\": {\n \"input\": \"National Team Qualifies for World Championship Finals\",\n \"ground_truth\": \"Sports\"\n }\n },\n {\n \"item\": {\n \"input\": \"Stock Markets Rally After Positive Economic Data Released\",\n \"ground_truth\": \"Markets\"\n }\n },\n {\n \"item\": {\n \"input\": \"Global Manufacturer Announces Merger with Competitor\",\n \"ground_truth\": \"Business\"\n }\n },\n {\n \"item\": {\n \"input\": \"Breakthrough in Renewable Energy Technology Unveiled\",\n \"ground_truth\": \"Technology\"\n }\n },\n {\n \"item\": {\n \"input\": \"World Leaders Sign Historic Climate Agreement\",\n \"ground_truth\": \"World\"\n }\n },\n {\n \"item\": {\n \"input\": \"Professional Athlete Sets New Record in Championship Event\",\n \"ground_truth\": \"Sports\"\n }\n },\n {\n \"item\": {\n \"input\": \"Financial Institutions Adapt to New Regulatory Requirements\",\n \"ground_truth\": \"Business\"\n }\n },\n {\n \"item\": {\n \"input\": \"Tech Conference Showcases Advances in Artificial Intelligence\",\n \"ground_truth\": \"Technology\"\n }\n },\n {\n \"item\": {\n \"input\": \"Global Markets Respond to Oil Price Fluctuations\",\n \"ground_truth\": \"Markets\"\n }\n },\n {\n \"item\": {\n \"input\": \"International Cooperation Strengthened Through New Treaty\",\n \"ground_truth\": \"World\"\n }\n },\n {\n \"item\": {\n \"input\": \"Sports League Announces Revised Schedule for Upcoming Season\",\n \"ground_truth\": \"Sports\"\n }\n }\n ]\n },\n \"input_messages\": {\n \"type\": \"template\",\n \"template\": [\n {\n \"type\": \"message\",\n \"role\": \"developer\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"Categorize a given news headline into one of the following topics: Technology, Markets, World, Business, or Sports.\\n\\n# Steps\\n\\n1. Analyze the content of the news headline to understand its primary focus.\\n2. Extract the subject matter, identifying any key indicators or keywords.\\n3. Use the identified indicators to determine the most suitable category out of the five options: Technology, Markets, World, Business, or Sports.\\n4. Ensure only one category is selected per headline.\\n\\n# Output Format\\n\\nRespond with the chosen category as a single word. For instance: \\\"Technology\\\", \\\"Markets\\\", \\\"World\\\", \\\"Business\\\", or \\\"Sports\\\".\\n\\n# Examples\\n\\n**Input**: \\\"Apple Unveils New iPhone Model, Featuring Advanced AI Features\\\" \\n**Output**: \\\"Technology\\\"\\n\\n**Input**: \\\"Global Stocks Mixed as Investors Await Central Bank Decisions\\\" \\n**Output**: \\\"Markets\\\"\\n\\n**Input**: \\\"War in Ukraine: Latest Updates on Negotiation Status\\\" \\n**Output**: \\\"World\\\"\\n\\n**Input**: \\\"Microsoft in Talks to Acquire Gaming Company for $2 Billion\\\" \\n**Output**: \\\"Business\\\"\\n\\n**Input**: \\\"Manchester United Secures Win in Premier League Football Match\\\" \\n**Output**: \\\"Sports\\\" \\n\\n# Notes\\n\\n- If the headline appears to fit into more than one category, choose the most dominant theme.\\n- Keywords or phrases such as \\\"stocks\\\", \\\"company acquisition\\\", \\\"match\\\", or technological brands can be good indicators for classification.\\n\"\n }\n },\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"{{item.input}}\"\n }\n }\n ]\n },\n \"model\": \"gpt-6-astra\",\n \"sampling_params\": {\n \"max_completions_tokens\": 2048\n }\n },\n \"error\": null,\n \"metadata\": {}\n}\n" + } + } + }, + "post": { + "operationId": "cancelEvalRun", + "tags": [ + "Evals" + ], + "summary": "Cancel an ongoing evaluation run.\n", + "parameters": [ + { + "name": "eval_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation whose run you want to cancel." + }, + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the run to cancel." + } + ], + "responses": { + "200": { + "description": "The canceled eval run object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvalRun" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Cancel eval run", + "group": "evals", + "path": "post", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/eval_67abd54d9b0081909a86353f6fb9317a/runs/evalrun_67abd54d60ec8190832b46859da808f7/cancel \\\n -X POST \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncanceled_run = client.evals.runs.cancel(\n \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"evalrun_67abd54d60ec8190832b46859da808f7\"\n)\nprint(canceled_run)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst canceledRun = await openai.evals.runs.cancel(\n \"evalrun_67abd54d60ec8190832b46859da808f7\",\n { eval_id: \"eval_67abd54d9b0081909a86353f6fb9317a\" }\n);\nconsole.log(canceledRun);\n" + }, + "response": "{\n \"object\": \"eval.run\",\n \"id\": \"evalrun_67abd54d60ec8190832b46859da808f7\",\n \"eval_id\": \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"report_url\": \"https://platform.openai.com/evaluations/eval_67abd54d9b0081909a86353f6fb9317a?run_id=evalrun_67abd54d60ec8190832b46859da808f7\",\n \"status\": \"canceled\",\n \"model\": \"gpt-6-astra\",\n \"name\": \"gpt-6-astra\",\n \"created_at\": 1743092069,\n \"result_counts\": {\n \"total\": 0,\n \"errored\": 0,\n \"failed\": 0,\n \"passed\": 0\n },\n \"per_model_usage\": null,\n \"per_testing_criteria_results\": null,\n \"data_source\": {\n \"type\": \"completions\",\n \"source\": {\n \"type\": \"file_content\",\n \"content\": [\n {\n \"item\": {\n \"input\": \"Tech Company Launches Advanced Artificial Intelligence Platform\",\n \"ground_truth\": \"Technology\"\n }\n },\n {\n \"item\": {\n \"input\": \"Central Bank Increases Interest Rates Amid Inflation Concerns\",\n \"ground_truth\": \"Markets\"\n }\n },\n {\n \"item\": {\n \"input\": \"International Summit Addresses Climate Change Strategies\",\n \"ground_truth\": \"World\"\n }\n },\n {\n \"item\": {\n \"input\": \"Major Retailer Reports Record-Breaking Holiday Sales\",\n \"ground_truth\": \"Business\"\n }\n },\n {\n \"item\": {\n \"input\": \"National Team Qualifies for World Championship Finals\",\n \"ground_truth\": \"Sports\"\n }\n },\n {\n \"item\": {\n \"input\": \"Stock Markets Rally After Positive Economic Data Released\",\n \"ground_truth\": \"Markets\"\n }\n },\n {\n \"item\": {\n \"input\": \"Global Manufacturer Announces Merger with Competitor\",\n \"ground_truth\": \"Business\"\n }\n },\n {\n \"item\": {\n \"input\": \"Breakthrough in Renewable Energy Technology Unveiled\",\n \"ground_truth\": \"Technology\"\n }\n },\n {\n \"item\": {\n \"input\": \"World Leaders Sign Historic Climate Agreement\",\n \"ground_truth\": \"World\"\n }\n },\n {\n \"item\": {\n \"input\": \"Professional Athlete Sets New Record in Championship Event\",\n \"ground_truth\": \"Sports\"\n }\n },\n {\n \"item\": {\n \"input\": \"Financial Institutions Adapt to New Regulatory Requirements\",\n \"ground_truth\": \"Business\"\n }\n },\n {\n \"item\": {\n \"input\": \"Tech Conference Showcases Advances in Artificial Intelligence\",\n \"ground_truth\": \"Technology\"\n }\n },\n {\n \"item\": {\n \"input\": \"Global Markets Respond to Oil Price Fluctuations\",\n \"ground_truth\": \"Markets\"\n }\n },\n {\n \"item\": {\n \"input\": \"International Cooperation Strengthened Through New Treaty\",\n \"ground_truth\": \"World\"\n }\n },\n {\n \"item\": {\n \"input\": \"Sports League Announces Revised Schedule for Upcoming Season\",\n \"ground_truth\": \"Sports\"\n }\n }\n ]\n },\n \"input_messages\": {\n \"type\": \"template\",\n \"template\": [\n {\n \"type\": \"message\",\n \"role\": \"developer\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"Categorize a given news headline into one of the following topics: Technology, Markets, World, Business, or Sports.\\n\\n# Steps\\n\\n1. Analyze the content of the news headline to understand its primary focus.\\n2. Extract the subject matter, identifying any key indicators or keywords.\\n3. Use the identified indicators to determine the most suitable category out of the five options: Technology, Markets, World, Business, or Sports.\\n4. Ensure only one category is selected per headline.\\n\\n# Output Format\\n\\nRespond with the chosen category as a single word. For instance: \\\"Technology\\\", \\\"Markets\\\", \\\"World\\\", \\\"Business\\\", or \\\"Sports\\\".\\n\\n# Examples\\n\\n**Input**: \\\"Apple Unveils New iPhone Model, Featuring Advanced AI Features\\\" \\n**Output**: \\\"Technology\\\"\\n\\n**Input**: \\\"Global Stocks Mixed as Investors Await Central Bank Decisions\\\" \\n**Output**: \\\"Markets\\\"\\n\\n**Input**: \\\"War in Ukraine: Latest Updates on Negotiation Status\\\" \\n**Output**: \\\"World\\\"\\n\\n**Input**: \\\"Microsoft in Talks to Acquire Gaming Company for $2 Billion\\\" \\n**Output**: \\\"Business\\\"\\n\\n**Input**: \\\"Manchester United Secures Win in Premier League Football Match\\\" \\n**Output**: \\\"Sports\\\" \\n\\n# Notes\\n\\n- If the headline appears to fit into more than one category, choose the most dominant theme.\\n- Keywords or phrases such as \\\"stocks\\\", \\\"company acquisition\\\", \\\"match\\\", or technological brands can be good indicators for classification.\\n\"\n }\n },\n {\n \"type\": \"message\",\n \"role\": \"user\",\n \"content\": {\n \"type\": \"input_text\",\n \"text\": \"{{item.input}}\"\n }\n }\n ]\n },\n \"model\": \"gpt-6-astra\",\n \"sampling_params\": {\n \"max_completions_tokens\": 2048\n }\n },\n \"error\": null,\n \"metadata\": {}\n}\n" + } + } + }, + "delete": { + "operationId": "deleteEvalRun", + "tags": [ + "Evals" + ], + "summary": "Delete an eval run.\n", + "parameters": [ + { + "name": "eval_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation to delete the run from." + }, + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the run to delete." + } + ], + "responses": { + "200": { + "description": "Successfully deleted the eval run", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "object": { + "type": "string", + "example": "eval.run.deleted" + }, + "deleted": { + "type": "boolean", + "example": true + }, + "run_id": { + "type": "string", + "example": "evalrun_677469f564d48190807532a852da3afb" + } + } + } + } + } + }, + "404": { + "description": "Run not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete eval run", + "group": "evals", + "path": "delete", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/eval_123abc/runs/evalrun_abc456 \\\n -X DELETE \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ndeleted = client.evals.runs.delete(\n \"eval_123abc\",\n \"evalrun_abc456\"\n)\nprint(deleted)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst deleted = await openai.evals.runs.delete(\n \"eval_123abc\",\n \"evalrun_abc456\"\n);\nconsole.log(deleted);\n" + }, + "response": "{\n \"object\": \"eval.run.deleted\",\n \"deleted\": true,\n \"run_id\": \"evalrun_abc456\"\n}\n" + } + } + } + }, + "/evals/{eval_id}/runs/{run_id}/output_items": { + "get": { + "operationId": "getEvalRunOutputItems", + "tags": [ + "Evals" + ], + "summary": "Get a list of output items for an evaluation run.\n", + "parameters": [ + { + "name": "eval_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation to retrieve runs for." + }, + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the run to retrieve output items for." + }, + { + "name": "after", + "in": "query", + "description": "Identifier for the last output item from the previous pagination request.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of output items to retrieve.", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "status", + "in": "query", + "description": "Filter output items by status. Use `failed` to filter by failed output\nitems or `pass` to filter by passed output items.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "fail", + "pass" + ] + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for output items by timestamp. Use `asc` for ascending order or `desc` for descending order. Defaults to `asc`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + } + ], + "responses": { + "200": { + "description": "A list of output items for the evaluation run", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvalRunOutputItemList" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Get eval run output items", + "group": "evals", + "path": "get", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/egroup_67abd54d9b0081909a86353f6fb9317a/runs/erun_67abd54d60ec8190832b46859da808f7/output_items \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\noutput_items = client.evals.runs.output_items.list(\n \"egroup_67abd54d9b0081909a86353f6fb9317a\",\n \"erun_67abd54d60ec8190832b46859da808f7\"\n)\nprint(output_items)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst outputItems = await openai.evals.runs.outputItems.list(\n \"egroup_67abd54d9b0081909a86353f6fb9317a\",\n \"erun_67abd54d60ec8190832b46859da808f7\"\n);\nconsole.log(outputItems);\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"eval.run.output_item\",\n \"id\": \"outputitem_67e5796c28e081909917bf79f6e6214d\",\n \"created_at\": 1743092076,\n \"run_id\": \"evalrun_67abd54d60ec8190832b46859da808f7\",\n \"eval_id\": \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"status\": \"pass\",\n \"datasource_item_id\": 5,\n \"datasource_item\": {\n \"input\": \"Stock Markets Rally After Positive Economic Data Released\",\n \"ground_truth\": \"Markets\"\n },\n \"results\": [\n {\n \"name\": \"String check-a2486074-d803-4445-b431-ad2262e85d47\",\n \"sample\": null,\n \"passed\": true,\n \"score\": 1.0\n }\n ],\n \"sample\": {\n \"input\": [\n {\n \"role\": \"developer\",\n \"content\": \"Categorize a given news headline into one of the following topics: Technology, Markets, World, Business, or Sports.\\n\\n# Steps\\n\\n1. Analyze the content of the news headline to understand its primary focus.\\n2. Extract the subject matter, identifying any key indicators or keywords.\\n3. Use the identified indicators to determine the most suitable category out of the five options: Technology, Markets, World, Business, or Sports.\\n4. Ensure only one category is selected per headline.\\n\\n# Output Format\\n\\nRespond with the chosen category as a single word. For instance: \\\"Technology\\\", \\\"Markets\\\", \\\"World\\\", \\\"Business\\\", or \\\"Sports\\\".\\n\\n# Examples\\n\\n**Input**: \\\"Apple Unveils New iPhone Model, Featuring Advanced AI Features\\\" \\n**Output**: \\\"Technology\\\"\\n\\n**Input**: \\\"Global Stocks Mixed as Investors Await Central Bank Decisions\\\" \\n**Output**: \\\"Markets\\\"\\n\\n**Input**: \\\"War in Ukraine: Latest Updates on Negotiation Status\\\" \\n**Output**: \\\"World\\\"\\n\\n**Input**: \\\"Microsoft in Talks to Acquire Gaming Company for $2 Billion\\\" \\n**Output**: \\\"Business\\\"\\n\\n**Input**: \\\"Manchester United Secures Win in Premier League Football Match\\\" \\n**Output**: \\\"Sports\\\" \\n\\n# Notes\\n\\n- If the headline appears to fit into more than one category, choose the most dominant theme.\\n- Keywords or phrases such as \\\"stocks\\\", \\\"company acquisition\\\", \\\"match\\\", or technological brands can be good indicators for classification.\\n\",\n \"tool_call_id\": null,\n \"tool_calls\": null,\n \"function_call\": null\n },\n {\n \"role\": \"user\",\n \"content\": \"Stock Markets Rally After Positive Economic Data Released\",\n \"tool_call_id\": null,\n \"tool_calls\": null,\n \"function_call\": null\n }\n ],\n \"output\": [\n {\n \"role\": \"assistant\",\n \"content\": \"Markets\",\n \"tool_call_id\": null,\n \"tool_calls\": null,\n \"function_call\": null\n }\n ],\n \"finish_reason\": \"stop\",\n \"model\": \"gpt-6-astra\",\n \"usage\": {\n \"total_tokens\": 325,\n \"completion_tokens\": 2,\n \"prompt_tokens\": 323,\n \"cached_tokens\": 0\n },\n \"error\": null,\n \"temperature\": 1.0,\n \"max_completion_tokens\": 2048,\n \"top_p\": 1.0,\n \"seed\": 42\n }\n }\n ],\n \"first_id\": \"outputitem_67e5796c28e081909917bf79f6e6214d\",\n \"last_id\": \"outputitem_67e5796c28e081909917bf79f6e6214d\",\n \"has_more\": true\n}\n" + } + } + } + }, + "/evals/{eval_id}/runs/{run_id}/output_items/{output_item_id}": { + "get": { + "operationId": "getEvalRunOutputItem", + "tags": [ + "Evals" + ], + "summary": "Get an evaluation run output item by ID.\n", + "parameters": [ + { + "name": "eval_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the evaluation to retrieve runs for." + }, + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the run to retrieve." + }, + { + "name": "output_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the output item to retrieve." + } + ], + "responses": { + "200": { + "description": "The evaluation run output item", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvalRunOutputItem" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Get an output item of an eval run", + "group": "evals", + "path": "get", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/evals/eval_67abd54d9b0081909a86353f6fb9317a/runs/evalrun_67abd54d60ec8190832b46859da808f7/output_items/outputitem_67abd55eb6548190bb580745d5644a33 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\noutput_item = client.evals.runs.output_items.retrieve(\n \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"evalrun_67abd54d60ec8190832b46859da808f7\",\n \"outputitem_67abd55eb6548190bb580745d5644a33\"\n)\nprint(output_item)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst outputItem = await openai.evals.runs.outputItems.retrieve(\n \"outputitem_67abd55eb6548190bb580745d5644a33\",\n {\n eval_id: \"eval_67abd54d9b0081909a86353f6fb9317a\",\n run_id: \"evalrun_67abd54d60ec8190832b46859da808f7\",\n }\n);\nconsole.log(outputItem);\n" + }, + "response": "{\n \"object\": \"eval.run.output_item\",\n \"id\": \"outputitem_67e5796c28e081909917bf79f6e6214d\",\n \"created_at\": 1743092076,\n \"run_id\": \"evalrun_67abd54d60ec8190832b46859da808f7\",\n \"eval_id\": \"eval_67abd54d9b0081909a86353f6fb9317a\",\n \"status\": \"pass\",\n \"datasource_item_id\": 5,\n \"datasource_item\": {\n \"input\": \"Stock Markets Rally After Positive Economic Data Released\",\n \"ground_truth\": \"Markets\"\n },\n \"results\": [\n {\n \"name\": \"String check-a2486074-d803-4445-b431-ad2262e85d47\",\n \"sample\": null,\n \"passed\": true,\n \"score\": 1.0\n }\n ],\n \"sample\": {\n \"input\": [\n {\n \"role\": \"developer\",\n \"content\": \"Categorize a given news headline into one of the following topics: Technology, Markets, World, Business, or Sports.\\n\\n# Steps\\n\\n1. Analyze the content of the news headline to understand its primary focus.\\n2. Extract the subject matter, identifying any key indicators or keywords.\\n3. Use the identified indicators to determine the most suitable category out of the five options: Technology, Markets, World, Business, or Sports.\\n4. Ensure only one category is selected per headline.\\n\\n# Output Format\\n\\nRespond with the chosen category as a single word. For instance: \\\"Technology\\\", \\\"Markets\\\", \\\"World\\\", \\\"Business\\\", or \\\"Sports\\\".\\n\\n# Examples\\n\\n**Input**: \\\"Apple Unveils New iPhone Model, Featuring Advanced AI Features\\\" \\n**Output**: \\\"Technology\\\"\\n\\n**Input**: \\\"Global Stocks Mixed as Investors Await Central Bank Decisions\\\" \\n**Output**: \\\"Markets\\\"\\n\\n**Input**: \\\"War in Ukraine: Latest Updates on Negotiation Status\\\" \\n**Output**: \\\"World\\\"\\n\\n**Input**: \\\"Microsoft in Talks to Acquire Gaming Company for $2 Billion\\\" \\n**Output**: \\\"Business\\\"\\n\\n**Input**: \\\"Manchester United Secures Win in Premier League Football Match\\\" \\n**Output**: \\\"Sports\\\" \\n\\n# Notes\\n\\n- If the headline appears to fit into more than one category, choose the most dominant theme.\\n- Keywords or phrases such as \\\"stocks\\\", \\\"company acquisition\\\", \\\"match\\\", or technological brands can be good indicators for classification.\\n\",\n \"tool_call_id\": null,\n \"tool_calls\": null,\n \"function_call\": null\n },\n {\n \"role\": \"user\",\n \"content\": \"Stock Markets Rally After Positive Economic Data Released\",\n \"tool_call_id\": null,\n \"tool_calls\": null,\n \"function_call\": null\n }\n ],\n \"output\": [\n {\n \"role\": \"assistant\",\n \"content\": \"Markets\",\n \"tool_call_id\": null,\n \"tool_calls\": null,\n \"function_call\": null\n }\n ],\n \"finish_reason\": \"stop\",\n \"model\": \"gpt-6-astra\",\n \"usage\": {\n \"total_tokens\": 325,\n \"completion_tokens\": 2,\n \"prompt_tokens\": 323,\n \"cached_tokens\": 0\n },\n \"error\": null,\n \"temperature\": 1.0,\n \"max_completion_tokens\": 2048,\n \"top_p\": 1.0,\n \"seed\": 42\n }\n}\n" + } + } + } + }, + "/files": { + "get": { + "operationId": "listFiles", + "tags": [ + "Files" + ], + "summary": "Returns a list of files.", + "parameters": [ + { + "in": "query", + "name": "purpose", + "required": false, + "schema": { + "type": "string" + }, + "description": "Only return files with the given purpose." + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 10,000, and the default is 10,000.\n", + "required": false, + "schema": { + "type": "integer", + "default": 10000 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order.\n", + "schema": { + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFilesResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List files", + "group": "files", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.files.list()\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const list = await openai.files.list();\n\n for await (const file of list) {\n console.log(file);\n }\n}\n\nmain();" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"file-abc123\",\n \"object\": \"file\",\n \"bytes\": 175,\n \"created_at\": 1613677385,\n \"expires_at\": 1677614202,\n \"filename\": \"salesOverview.pdf\",\n \"purpose\": \"assistants\",\n },\n {\n \"id\": \"file-abc456\",\n \"object\": \"file\",\n \"bytes\": 140,\n \"created_at\": 1613779121,\n \"expires_at\": 1677614202,\n \"filename\": \"puppy.jsonl\",\n \"purpose\": \"fine-tune\",\n }\n ],\n \"first_id\": \"file-abc123\",\n \"last_id\": \"file-abc456\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "operationId": "createFile", + "tags": [ + "Files" + ], + "summary": "Upload a file that can be used across various endpoints. Individual files\ncan be up to 512 MB, and each project can store up to 2.5 TB of files in\ntotal. There is no organization-wide storage limit. Uploads to this\nendpoint are rate-limited to 1,000 requests per minute per authenticated\nuser.\n\n- The Assistants API supports files up to 2 million tokens and of specific\n file types. See the [Assistants Tools guide](https://developers.openai.com/api/docs/guides/tools) for\n details.\n- The Fine-tuning API only supports `.jsonl` files. The input also has\n certain required formats for fine-tuning\n [chat](https://developers.openai.com/api/docs/guides/supervised-fine-tuning#formatting-your-data) or\n [completions](https://developers.openai.com/api/docs/guides/supervised-fine-tuning#formatting-your-data) models.\n- The Batch API only supports `.jsonl` files up to 200 MB in size. The input\n also has a specific required\n [format](https://developers.openai.com/api/docs/guides/batch#1-prepare-your-batch-file).\n- For Retrieval or `file_search` ingestion, upload files here first. If\n you need to attach multiple uploaded files to the same vector store, use\n [`/vector_stores/{vector_store_id}/file_batches`](https://developers.openai.com/api/reference/resources/vector_stores/subresources/file_batches/methods/create)\n instead of attaching them one by one. Vector store attachment has separate\n limits from file upload, including 2,000 attached files per minute per\n organization.\n\nPlease [contact us](https://help.openai.com/) if you need to increase these\nstorage limits.\n", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateFileRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OpenAIFile" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Upload file", + "group": "files", + "description": "Uploads a file for later use across OpenAI APIs. Uploads to this endpoint are rate-limited to 1,000 requests per minute per authenticated user. For Retrieval or `file_search` ingestion, upload files here first. If you need to attach multiple uploaded files to the same vector store, use vector store file batches instead of attaching them one by one.\n", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -F purpose=\"fine-tune\" \\\n -F file=\"@mydata.jsonl\"\n -F expires_after[anchor]=\"created_at\"\n -F expires_after[seconds]=2592000\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.files.create(\n file=open(\"mydata.jsonl\", \"rb\"),\n purpose=\"fine-tune\",\n expires_after={\n \"anchor\": \"created_at\",\n \"seconds\": 2592000\n }\n)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const file = await openai.files.create({\n file: fs.createReadStream(\"mydata.jsonl\"),\n purpose: \"fine-tune\",\n expires_after: {\n anchor: \"created_at\",\n seconds: 2592000\n }\n });\n\n console.log(file);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"file-abc123\",\n \"object\": \"file\",\n \"bytes\": 120000,\n \"created_at\": 1677610602,\n \"expires_at\": 1677614202,\n \"filename\": \"mydata.jsonl\",\n \"purpose\": \"fine-tune\",\n}\n" + } + } + } + }, + "/files/{file_id}": { + "delete": { + "operationId": "deleteFile", + "tags": [ + "Files" + ], + "summary": "Delete a file and remove it from all vector stores.", + "parameters": [ + { + "in": "path", + "name": "file_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the file to use for this request." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteFileResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete file", + "group": "files", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files/file-abc123 \\\n -X DELETE \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.files.delete(\"file-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const file = await openai.files.delete(\"file-abc123\");\n\n console.log(file);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"file-abc123\",\n \"object\": \"file\",\n \"deleted\": true\n}\n" + } + } + }, + "get": { + "operationId": "retrieveFile", + "tags": [ + "Files" + ], + "summary": "Returns information about a specific file.", + "parameters": [ + { + "in": "path", + "name": "file_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the file to use for this request." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OpenAIFile" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve file", + "group": "files", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files/file-abc123 \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.files.retrieve(\"file-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const file = await openai.files.retrieve(\"file-abc123\");\n\n console.log(file);\n}\n\nmain();" + }, + "response": "{\n \"id\": \"file-abc123\",\n \"object\": \"file\",\n \"bytes\": 120000,\n \"created_at\": 1677610602,\n \"expires_at\": 1677614202,\n \"filename\": \"mydata.jsonl\",\n \"purpose\": \"fine-tune\",\n}\n" + } + } + } + }, + "/files/{file_id}/content": { + "get": { + "operationId": "downloadFile", + "tags": [ + "Files" + ], + "summary": "Returns a response containing the contents of the specified file.", + "parameters": [ + { + "in": "path", + "name": "file_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the file to use for this request." + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve file content", + "group": "files", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/files/file-abc123/content \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" > file.jsonl\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\ncontent = client.files.content(\"file-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const response = await openai.files.content(\"file-abc123\");\n const content = await response.text();\n\n console.log(content);\n}\n\nmain();\n" + } + } + } + } + }, + "/fine_tuning/alpha/graders/run": { + "post": { + "operationId": "runGrader", + "tags": [ + "Fine-tuning" + ], + "summary": "Run a grader.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunGraderRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunGraderResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Run grader", + "beta": true, + "group": "graders", + "examples": [ + { + "title": "Score text alignment", + "request": { + "curl": "curl -X POST https://api.openai.com/v1/fine_tuning/alpha/graders/run \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"grader\": {\n \"type\": \"score_model\",\n \"name\": \"Example score model grader\",\n \"input\": [\n {\n \"role\": \"user\",\n \"content\": [\n {\n \"type\": \"input_text\",\n \"text\": \"Score how close the reference answer is to the model answer on a 0-1 scale. Return only the score.\\n\\nReference answer: {{item.reference_answer}}\\n\\nModel answer: {{sample.output_text}}\"\n }\n ]\n }\n ],\n \"model\": \"gpt-5-mini\",\n \"sampling_params\": {\n \"temperature\": 1,\n \"top_p\": 1,\n \"seed\": 42\n }\n },\n \"item\": {\n \"reference_answer\": \"fuzzy wuzzy was a bear\"\n },\n \"model_sample\": \"fuzzy wuzzy was a bear\"\n }'\n", + "python": "from openai import OpenAI\n\nclient = OpenAI()\nresult = client.fine_tuning.alpha.graders.run(\n grader={\n \"type\": \"score_model\",\n \"name\": \"Example score model grader\",\n \"input\": [\n {\n \"role\": \"user\",\n \"content\": [\n {\n \"type\": \"input_text\",\n \"text\": \"Score how close the reference answer is to the model answer on a 0-1 scale. Return only the score.\\n\\nReference answer: {{item.reference_answer}}\\n\\nModel answer: {{sample.output_text}}\",\n }\n ],\n }\n ],\n \"model\": \"gpt-5-mini\",\n \"sampling_params\": {\"temperature\": 1, \"top_p\": 1, \"seed\": 42},\n },\n item={\"reference_answer\": \"fuzzy wuzzy was a bear\"},\n model_sample=\"fuzzy wuzzy was a bear\",\n)\nprint(result)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nconst result = await openai.fineTuning.alpha.graders.run({\n grader: {\n type: \"score_model\",\n name: \"Example score model grader\",\n input: [\n {\n role: \"user\",\n content: [\n {\n type: \"input_text\",\n text: \"Score how close the reference answer is to the model answer on a 0-1 scale. Return only the score.\\n\\nReference answer: {{item.reference_answer}}\\n\\nModel answer: {{sample.output_text}}\",\n },\n ],\n },\n ],\n model: \"gpt-5-mini\",\n sampling_params: { temperature: 1, top_p: 1, seed: 42 },\n },\n item: { reference_answer: \"fuzzy wuzzy was a bear\" },\n model_sample: \"fuzzy wuzzy was a bear\",\n});\nconsole.log(result);\n" + }, + "response": "{\n \"reward\": 1.0,\n \"metadata\": {\n \"name\": \"Example score model grader\",\n \"type\": \"score_model\",\n \"errors\": {\n \"formula_parse_error\": false,\n \"sample_parse_error\": false,\n \"truncated_observation_error\": false,\n \"unresponsive_reward_error\": false,\n \"invalid_variable_error\": false,\n \"other_error\": false,\n \"python_grader_server_error\": false,\n \"python_grader_server_error_type\": null,\n \"python_grader_runtime_error\": false,\n \"python_grader_runtime_error_details\": null,\n \"model_grader_server_error\": false,\n \"model_grader_refusal_error\": false,\n \"model_grader_parse_error\": false,\n \"model_grader_server_error_details\": null\n },\n \"execution_time\": 4.365238428115845,\n \"scores\": {},\n \"token_usage\": {\n \"prompt_tokens\": 190,\n \"total_tokens\": 324,\n \"completion_tokens\": 134,\n \"cached_tokens\": 0\n },\n \"sampled_model_name\": \"gpt-5-mini\"\n },\n \"sub_rewards\": {},\n \"model_grader_token_usage_per_model\": {\n \"gpt-5-mini\": {\n \"prompt_tokens\": 190,\n \"total_tokens\": 324,\n \"completion_tokens\": 134,\n \"cached_tokens\": 0\n }\n }\n}\n" + }, + { + "title": "Score an image caption", + "request": { + "curl": "curl -X POST https://api.openai.com/v1/fine_tuning/alpha/graders/run \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"grader\": {\n \"type\": \"score_model\",\n \"name\": \"Image caption grader\",\n \"input\": [\n {\n \"role\": \"user\",\n \"content\": [\n {\n \"type\": \"input_text\",\n \"text\": \"Score how well the provided caption matches the image on a 0-1 scale. Only return the score.\\n\\nCaption: {{sample.output_text}}\"\n },\n {\n \"type\": \"input_image\",\n \"image_url\": \"https://example.com/dog-catching-ball.png\",\n \"file_id\": null,\n \"detail\": \"high\"\n }\n ]\n }\n ],\n \"model\": \"gpt-5-mini\",\n \"sampling_params\": {\n \"temperature\": 0.2\n }\n },\n \"item\": {\n \"expected_caption\": \"A golden retriever jumps to catch a tennis ball\"\n },\n \"model_sample\": \"A dog leaps to grab a tennis ball mid-air\"\n }'\n" + } + }, + { + "title": "Score an audio response", + "request": { + "curl": "curl -X POST https://api.openai.com/v1/fine_tuning/alpha/graders/run \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"grader\": {\n \"type\": \"score_model\",\n \"name\": \"Audio clarity grader\",\n \"input\": [\n {\n \"role\": \"user\",\n \"content\": [\n {\n \"type\": \"input_text\",\n \"text\": \"Listen to the clip and return a confidence score from 0 to 1 that the speaker said: {{item.target_phrase}}\"\n },\n {\n \"type\": \"input_audio\",\n \"input_audio\": {\n \"data\": \"{{item.audio_clip_b64}}\",\n \"format\": \"mp3\"\n }\n }\n ]\n }\n ],\n \"model\": \"gpt-audio\",\n \"sampling_params\": {\n \"temperature\": 0.2,\n \"top_p\": 1,\n \"seed\": 123\n }\n },\n \"item\": {\n \"target_phrase\": \"Please deliver the package on Tuesday\",\n \"audio_clip_b64\": \"\"\n },\n \"model_sample\": \"Please deliver the package on Tuesday\"\n }'\n" + } + } + ] + } + } + }, + "/fine_tuning/alpha/graders/validate": { + "post": { + "operationId": "validateGrader", + "tags": [ + "Fine-tuning" + ], + "summary": "Validate a grader.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateGraderRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateGraderResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Validate grader", + "beta": true, + "group": "graders", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/alpha/graders/validate \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"grader\": {\n \"type\": \"string_check\",\n \"name\": \"Example string check grader\",\n \"input\": \"{{sample.output_text}}\",\n \"reference\": \"{{item.label}}\",\n \"operation\": \"eq\"\n }\n }'\n" + }, + "response": "{\n \"grader\": {\n \"type\": \"string_check\",\n \"name\": \"Example string check grader\",\n \"input\": \"{{sample.output_text}}\",\n \"reference\": \"{{item.label}}\",\n \"operation\": \"eq\"\n }\n}\n" + } + } + } + }, + "/fine_tuning/checkpoints/{fine_tuned_model_checkpoint}/permissions": { + "get": { + "operationId": "listFineTuningCheckpointPermissions", + "tags": [ + "Fine-tuning" + ], + "summary": "**NOTE:** This endpoint requires an [admin API key](https://developers.openai.com/api/reference/resources/admin/subresources/organization/subresources/admin_api_keys).\n\nOrganization owners can use this endpoint to view all permissions for a fine-tuned model checkpoint.\n", + "parameters": [ + { + "in": "path", + "name": "fine_tuned_model_checkpoint", + "required": true, + "schema": { + "type": "string", + "example": "ft-AF1WoRqd3aJAHsqc9NY7iL8F" + }, + "description": "The ID of the fine-tuned model checkpoint to get permissions for.\n" + }, + { + "name": "project_id", + "in": "query", + "description": "The ID of the project to get permissions for.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "after", + "in": "query", + "description": "Identifier for the last permission ID from the previous pagination request.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of permissions to retrieve.", + "required": false, + "schema": { + "type": "integer", + "default": 10 + } + }, + { + "name": "order", + "in": "query", + "description": "The order in which to retrieve permissions.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "ascending", + "descending" + ], + "default": "descending" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFineTuningCheckpointPermissionResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List checkpoint permissions", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/checkpoints/ft:gpt-4o-mini-2024-07-18:org:weather:B7R9VjQd/permissions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"checkpoint.permission\",\n \"id\": \"cp_zc4Q7MP6XxulcVzj4MZdwsAB\",\n \"created_at\": 1721764867,\n \"project_id\": \"proj_abGMw1llN8IrBb6SvvY5A1iH\"\n },\n {\n \"object\": \"checkpoint.permission\",\n \"id\": \"cp_enQCFmOTGj3syEpYVhBRLTSy\",\n \"created_at\": 1721764800,\n \"project_id\": \"proj_iqGMw1llN8IrBb6SvvY5A1oF\"\n },\n ],\n \"first_id\": \"cp_zc4Q7MP6XxulcVzj4MZdwsAB\",\n \"last_id\": \"cp_enQCFmOTGj3syEpYVhBRLTSy\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "operationId": "createFineTuningCheckpointPermission", + "tags": [ + "Fine-tuning" + ], + "summary": "**NOTE:** Calling this endpoint requires an [admin API key](https://developers.openai.com/api/reference/resources/admin/subresources/organization/subresources/admin_api_keys).\n\nThis enables organization owners to share fine-tuned models with other projects in their organization.\n", + "parameters": [ + { + "in": "path", + "name": "fine_tuned_model_checkpoint", + "required": true, + "schema": { + "type": "string", + "example": "ft:gpt-4o-mini-2024-07-18:org:weather:B7R9VjQd" + }, + "description": "The ID of the fine-tuned model checkpoint to create a permission for.\n" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFineTuningCheckpointPermissionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFineTuningCheckpointPermissionResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create checkpoint permissions", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/checkpoints/ft:gpt-4o-mini-2024-07-18:org:weather:B7R9VjQd/permissions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n -d '{\"project_ids\": [\"proj_abGMw1llN8IrBb6SvvY5A1iH\"]}'\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"checkpoint.permission\",\n \"id\": \"cp_zc4Q7MP6XxulcVzj4MZdwsAB\",\n \"created_at\": 1721764867,\n \"project_id\": \"proj_abGMw1llN8IrBb6SvvY5A1iH\"\n }\n ],\n \"first_id\": \"cp_zc4Q7MP6XxulcVzj4MZdwsAB\",\n \"last_id\": \"cp_zc4Q7MP6XxulcVzj4MZdwsAB\",\n \"has_more\": false\n}\n" + } + } + } + }, + "/fine_tuning/checkpoints/{fine_tuned_model_checkpoint}/permissions/{permission_id}": { + "delete": { + "operationId": "deleteFineTuningCheckpointPermission", + "tags": [ + "Fine-tuning" + ], + "summary": "**NOTE:** This endpoint requires an [admin API key](https://developers.openai.com/api/reference/resources/admin/subresources/organization/subresources/admin_api_keys).\n\nOrganization owners can use this endpoint to delete a permission for a fine-tuned model checkpoint.\n", + "parameters": [ + { + "in": "path", + "name": "fine_tuned_model_checkpoint", + "required": true, + "schema": { + "type": "string", + "example": "ft:gpt-4o-mini-2024-07-18:org:weather:B7R9VjQd" + }, + "description": "The ID of the fine-tuned model checkpoint to delete a permission for.\n" + }, + { + "in": "path", + "name": "permission_id", + "required": true, + "schema": { + "type": "string", + "example": "cp_zc4Q7MP6XxulcVzj4MZdwsAB" + }, + "description": "The ID of the fine-tuned model checkpoint permission to delete.\n" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteFineTuningCheckpointPermissionResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete checkpoint permission", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/checkpoints/ft:gpt-4o-mini-2024-07-18:org:weather:B7R9VjQd/permissions/cp_zc4Q7MP6XxulcVzj4MZdwsAB \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "{\n \"object\": \"checkpoint.permission\",\n \"id\": \"cp_zc4Q7MP6XxulcVzj4MZdwsAB\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/fine_tuning/jobs": { + "post": { + "operationId": "createFineTuningJob", + "tags": [ + "Fine-tuning" + ], + "summary": "Creates a fine-tuning job which begins the process of creating a new model from a given dataset.\n\nResponse includes details of the enqueued job including job status and the name of the fine-tuned models once complete.\n\n[Learn more about fine-tuning](https://developers.openai.com/api/docs/guides/model-optimization)\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFineTuningJobRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FineTuningJob" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create fine-tuning job", + "group": "fine-tuning", + "examples": [ + { + "title": "Default", + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"training_file\": \"file-BK7bzQj3FfZFXr7DbL6xJwfo\",\n \"model\": \"gpt-4o-mini\"\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.fine_tuning.jobs.create(\n training_file=\"file-abc123\",\n model=\"gpt-4o-mini\"\n)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const fineTune = await openai.fineTuning.jobs.create({\n training_file: \"file-abc123\"\n });\n\n console.log(fineTune);\n}\n\nmain();\n" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"created_at\": 1721764800,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-123\",\n \"result_files\": [],\n \"status\": \"queued\",\n \"validation_file\": null,\n \"training_file\": \"file-abc123\",\n \"method\": {\n \"type\": \"supervised\",\n \"supervised\": {\n \"hyperparameters\": {\n \"batch_size\": \"auto\",\n \"learning_rate_multiplier\": \"auto\",\n \"n_epochs\": \"auto\",\n }\n }\n },\n \"metadata\": null\n}\n" + }, + { + "title": "Epochs", + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"training_file\": \"file-abc123\",\n \"model\": \"gpt-4o-mini\",\n \"method\": {\n \"type\": \"supervised\",\n \"supervised\": {\n \"hyperparameters\": {\n \"n_epochs\": 2\n }\n }\n }\n }'\n", + "python": "from openai import OpenAI\nfrom openai.types.fine_tuning import SupervisedMethod, SupervisedHyperparameters\n\nclient = OpenAI()\n\nclient.fine_tuning.jobs.create(\n training_file=\"file-abc123\",\n model=\"gpt-4o-mini\",\n method={\n \"type\": \"supervised\",\n \"supervised\": SupervisedMethod(\n hyperparameters=SupervisedHyperparameters(\n n_epochs=2\n )\n )\n }\n)\n", + "javascript": "import OpenAI from \"openai\";\nimport { SupervisedMethod, SupervisedHyperparameters } from \"openai/resources/fine-tuning/methods\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const fineTune = await openai.fineTuning.jobs.create({\n training_file: \"file-abc123\",\n model: \"gpt-4o-mini\",\n method: {\n type: \"supervised\",\n supervised: {\n hyperparameters: {\n n_epochs: 2\n }\n }\n }\n });\n\n console.log(fineTune);\n}\n\nmain();\n" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"gpt-4o-mini\",\n \"created_at\": 1721764800,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-123\",\n \"result_files\": [],\n \"status\": \"queued\",\n \"validation_file\": null,\n \"training_file\": \"file-abc123\",\n \"hyperparameters\": {\n \"batch_size\": \"auto\",\n \"learning_rate_multiplier\": \"auto\",\n \"n_epochs\": 2\n },\n \"method\": {\n \"type\": \"supervised\",\n \"supervised\": {\n \"hyperparameters\": {\n \"batch_size\": \"auto\",\n \"learning_rate_multiplier\": \"auto\",\n \"n_epochs\": 2\n }\n }\n },\n \"metadata\": null,\n \"error\": {\n \"code\": null,\n \"message\": null,\n \"param\": null\n },\n \"finished_at\": null,\n \"seed\": 683058546,\n \"trained_tokens\": null,\n \"estimated_finish\": null,\n \"integrations\": [],\n \"user_provided_suffix\": null,\n \"usage_metrics\": null,\n \"shared_with_openai\": false\n}\n" + }, + { + "title": "DPO", + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"training_file\": \"file-abc123\",\n \"validation_file\": \"file-abc123\",\n \"model\": \"gpt-4o-mini\",\n \"method\": {\n \"type\": \"dpo\",\n \"dpo\": {\n \"hyperparameters\": {\n \"beta\": 0.1\n }\n }\n }\n }'\n", + "python": "from openai import OpenAI\nfrom openai.types.fine_tuning import DpoMethod, DpoHyperparameters\n\nclient = OpenAI()\n\nclient.fine_tuning.jobs.create(\n training_file=\"file-abc\",\n validation_file=\"file-123\",\n model=\"gpt-4o-mini\",\n method={\n \"type\": \"dpo\",\n \"dpo\": DpoMethod(\n hyperparameters=DpoHyperparameters(beta=0.1)\n )\n }\n)\n" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc\",\n \"model\": \"gpt-4o-mini\",\n \"created_at\": 1746130590,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-abc\",\n \"result_files\": [],\n \"status\": \"queued\",\n \"validation_file\": \"file-123\",\n \"training_file\": \"file-abc\",\n \"method\": {\n \"type\": \"dpo\",\n \"dpo\": {\n \"hyperparameters\": {\n \"beta\": 0.1,\n \"batch_size\": \"auto\",\n \"learning_rate_multiplier\": \"auto\",\n \"n_epochs\": \"auto\"\n }\n }\n },\n \"metadata\": null,\n \"error\": {\n \"code\": null,\n \"message\": null,\n \"param\": null\n },\n \"finished_at\": null,\n \"hyperparameters\": null,\n \"seed\": 1036326793,\n \"estimated_finish\": null,\n \"integrations\": [],\n \"user_provided_suffix\": null,\n \"usage_metrics\": null,\n \"shared_with_openai\": false\n}\n" + }, + { + "title": "Reinforcement", + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"training_file\": \"file-abc\",\n \"validation_file\": \"file-123\",\n \"model\": \"o4-mini\",\n \"method\": {\n \"type\": \"reinforcement\",\n \"reinforcement\": {\n \"grader\": {\n \"type\": \"string_check\",\n \"name\": \"Example string check grader\",\n \"input\": \"{{sample.output_text}}\",\n \"reference\": \"{{item.label}}\",\n \"operation\": \"eq\"\n },\n \"hyperparameters\": {\n \"reasoning_effort\": \"medium\"\n }\n }\n }\n }'\n", + "python": "from openai import OpenAI\nfrom openai.types.fine_tuning import ReinforcementMethod, ReinforcementHyperparameters\nfrom openai.types.graders import StringCheckGrader\n\nclient = OpenAI()\n\nclient.fine_tuning.jobs.create(\n training_file=\"file-abc\",\n validation_file=\"file-123\",\n model=\"o4-mini\",\n method={\n \"type\": \"reinforcement\",\n \"reinforcement\": ReinforcementMethod(\n grader=StringCheckGrader(\n name=\"Example string check grader\",\n type=\"string_check\",\n input=\"{{item.label}}\",\n operation=\"eq\",\n reference=\"{{sample.output_text}}\"\n ),\n hyperparameters=ReinforcementHyperparameters(\n reasoning_effort=\"medium\",\n )\n )\n }, \n seed=42,\n)\n" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"o4-mini\",\n \"created_at\": 1721764800,\n \"finished_at\": null,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-123\",\n \"result_files\": [],\n \"status\": \"validating_files\",\n \"validation_file\": \"file-123\",\n \"training_file\": \"file-abc\",\n \"trained_tokens\": null,\n \"error\": {},\n \"user_provided_suffix\": null,\n \"seed\": 950189191,\n \"estimated_finish\": null,\n \"integrations\": [],\n \"method\": {\n \"type\": \"reinforcement\",\n \"reinforcement\": {\n \"hyperparameters\": {\n \"batch_size\": \"auto\",\n \"learning_rate_multiplier\": \"auto\",\n \"n_epochs\": \"auto\",\n \"eval_interval\": \"auto\",\n \"eval_samples\": \"auto\",\n \"compute_multiplier\": \"auto\",\n \"reasoning_effort\": \"medium\"\n },\n \"grader\": {\n \"type\": \"string_check\",\n \"name\": \"Example string check grader\",\n \"input\": \"{{sample.output_text}}\",\n \"reference\": \"{{item.label}}\",\n \"operation\": \"eq\"\n },\n \"response_format\": null\n }\n },\n \"metadata\": null,\n \"usage_metrics\": null,\n \"shared_with_openai\": false\n}\n \n" + }, + { + "title": "Validation file", + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"training_file\": \"file-abc123\",\n \"validation_file\": \"file-abc123\",\n \"model\": \"gpt-4o-mini\"\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.fine_tuning.jobs.create(\n training_file=\"file-abc123\",\n validation_file=\"file-def456\",\n model=\"gpt-4o-mini\"\n)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const fineTune = await openai.fineTuning.jobs.create({\n training_file: \"file-abc123\",\n validation_file: \"file-abc123\"\n });\n\n console.log(fineTune);\n}\n\nmain();\n" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"created_at\": 1721764800,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-123\",\n \"result_files\": [],\n \"status\": \"queued\",\n \"validation_file\": \"file-abc123\",\n \"training_file\": \"file-abc123\",\n \"method\": {\n \"type\": \"supervised\",\n \"supervised\": {\n \"hyperparameters\": {\n \"batch_size\": \"auto\",\n \"learning_rate_multiplier\": \"auto\",\n \"n_epochs\": \"auto\",\n }\n }\n },\n \"metadata\": null\n}\n" + }, + { + "title": "W&B Integration", + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"training_file\": \"file-abc123\",\n \"validation_file\": \"file-abc123\",\n \"model\": \"gpt-4o-mini\",\n \"integrations\": [\n {\n \"type\": \"wandb\",\n \"wandb\": {\n \"project\": \"my-wandb-project\",\n \"name\": \"ft-run-display-name\"\n \"tags\": [\n \"first-experiment\", \"v2\"\n ]\n }\n }\n ]\n }'\n" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"created_at\": 1721764800,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-123\",\n \"result_files\": [],\n \"status\": \"queued\",\n \"validation_file\": \"file-abc123\",\n \"training_file\": \"file-abc123\",\n \"integrations\": [\n {\n \"type\": \"wandb\",\n \"wandb\": {\n \"project\": \"my-wandb-project\",\n \"entity\": None,\n \"run_id\": \"ftjob-abc123\"\n }\n }\n ],\n \"method\": {\n \"type\": \"supervised\",\n \"supervised\": {\n \"hyperparameters\": {\n \"batch_size\": \"auto\",\n \"learning_rate_multiplier\": \"auto\",\n \"n_epochs\": \"auto\",\n }\n }\n },\n \"metadata\": null\n}\n" + } + ] + } + }, + "get": { + "operationId": "listPaginatedFineTuningJobs", + "tags": [ + "Fine-tuning" + ], + "summary": "List your organization's fine-tuning jobs\n", + "parameters": [ + { + "name": "after", + "in": "query", + "description": "Identifier for the last job from the previous pagination request.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of fine-tuning jobs to retrieve.", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "in": "query", + "name": "metadata", + "required": false, + "schema": { + "type": "object", + "nullable": true, + "additionalProperties": { + "type": "string" + } + }, + "style": "deepObject", + "explode": true, + "description": "Optional metadata filter. To filter, use the syntax `metadata[k]=v`. Alternatively, set `metadata=null` to indicate no metadata.\n" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListPaginatedFineTuningJobsResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List fine-tuning jobs", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs?limit=2&metadata[key]=value \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.fine_tuning.jobs.list()\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const list = await openai.fineTuning.jobs.list();\n\n for await (const fineTune of list) {\n console.log(fineTune);\n }\n}\n\nmain();" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"created_at\": 1721764800,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-123\",\n \"result_files\": [],\n \"status\": \"queued\",\n \"validation_file\": null,\n \"training_file\": \"file-abc123\",\n \"metadata\": {\n \"key\": \"value\"\n }\n },\n { ... },\n { ... }\n ], \"has_more\": true\n}\n" + } + } + } + }, + "/fine_tuning/jobs/{fine_tuning_job_id}": { + "get": { + "operationId": "retrieveFineTuningJob", + "tags": [ + "Fine-tuning" + ], + "summary": "Get info about a fine-tuning job.\n\n[Learn more about fine-tuning](https://developers.openai.com/api/docs/guides/model-optimization)\n", + "parameters": [ + { + "in": "path", + "name": "fine_tuning_job_id", + "required": true, + "schema": { + "type": "string", + "example": "ft-AF1WoRqd3aJAHsqc9NY7iL8F" + }, + "description": "The ID of the fine-tuning job.\n" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FineTuningJob" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve fine-tuning job", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs/ft-AF1WoRqd3aJAHsqc9NY7iL8F \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.fine_tuning.jobs.retrieve(\"ftjob-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const fineTune = await openai.fineTuning.jobs.retrieve(\"ftjob-abc123\");\n\n console.log(fineTune);\n}\n\nmain();\n" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"davinci-002\",\n \"created_at\": 1692661014,\n \"finished_at\": 1692661190,\n \"fine_tuned_model\": \"ft:davinci-002:my-org:custom_suffix:7q8mpxmy\",\n \"organization_id\": \"org-123\",\n \"result_files\": [\n \"file-abc123\"\n ],\n \"status\": \"succeeded\",\n \"validation_file\": null,\n \"training_file\": \"file-abc123\",\n \"hyperparameters\": {\n \"n_epochs\": 4,\n \"batch_size\": 1,\n \"learning_rate_multiplier\": 1.0\n },\n \"trained_tokens\": 5768,\n \"integrations\": [],\n \"seed\": 0,\n \"estimated_finish\": 0,\n \"method\": {\n \"type\": \"supervised\",\n \"supervised\": {\n \"hyperparameters\": {\n \"n_epochs\": 4,\n \"batch_size\": 1,\n \"learning_rate_multiplier\": 1.0\n }\n }\n }\n}\n" + } + } + } + }, + "/fine_tuning/jobs/{fine_tuning_job_id}/cancel": { + "post": { + "operationId": "cancelFineTuningJob", + "tags": [ + "Fine-tuning" + ], + "summary": "Immediately cancel a fine-tune job.\n", + "parameters": [ + { + "in": "path", + "name": "fine_tuning_job_id", + "required": true, + "schema": { + "type": "string", + "example": "ft-AF1WoRqd3aJAHsqc9NY7iL8F" + }, + "description": "The ID of the fine-tuning job to cancel.\n" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FineTuningJob" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Cancel fine-tuning", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/fine_tuning/jobs/ftjob-abc123/cancel \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.fine_tuning.jobs.cancel(\"ftjob-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const fineTune = await openai.fineTuning.jobs.cancel(\"ftjob-abc123\");\n\n console.log(fineTune);\n}\nmain();" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"created_at\": 1721764800,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-123\",\n \"result_files\": [],\n \"status\": \"cancelled\",\n \"validation_file\": \"file-abc123\",\n \"training_file\": \"file-abc123\"\n}\n" + } + } + } + }, + "/fine_tuning/jobs/{fine_tuning_job_id}/checkpoints": { + "get": { + "operationId": "listFineTuningJobCheckpoints", + "tags": [ + "Fine-tuning" + ], + "summary": "List checkpoints for a fine-tuning job.\n", + "parameters": [ + { + "in": "path", + "name": "fine_tuning_job_id", + "required": true, + "schema": { + "type": "string", + "example": "ft-AF1WoRqd3aJAHsqc9NY7iL8F" + }, + "description": "The ID of the fine-tuning job to get checkpoints for.\n" + }, + { + "name": "after", + "in": "query", + "description": "Identifier for the last checkpoint ID from the previous pagination request.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of checkpoints to retrieve.", + "required": false, + "schema": { + "type": "integer", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFineTuningJobCheckpointsResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List fine-tuning checkpoints", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs/ftjob-abc123/checkpoints \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"fine_tuning.job.checkpoint\",\n \"id\": \"ftckpt_zc4Q7MP6XxulcVzj4MZdwsAB\",\n \"created_at\": 1721764867,\n \"fine_tuned_model_checkpoint\": \"ft:gpt-4o-mini-2024-07-18:my-org:custom-suffix:96olL566:ckpt-step-2000\",\n \"metrics\": {\n \"full_valid_loss\": 0.134,\n \"full_valid_mean_token_accuracy\": 0.874\n },\n \"fine_tuning_job_id\": \"ftjob-abc123\",\n \"step_number\": 2000\n },\n {\n \"object\": \"fine_tuning.job.checkpoint\",\n \"id\": \"ftckpt_enQCFmOTGj3syEpYVhBRLTSy\",\n \"created_at\": 1721764800,\n \"fine_tuned_model_checkpoint\": \"ft:gpt-4o-mini-2024-07-18:my-org:custom-suffix:7q8mpxmy:ckpt-step-1000\",\n \"metrics\": {\n \"full_valid_loss\": 0.167,\n \"full_valid_mean_token_accuracy\": 0.781\n },\n \"fine_tuning_job_id\": \"ftjob-abc123\",\n \"step_number\": 1000\n }\n ],\n \"first_id\": \"ftckpt_zc4Q7MP6XxulcVzj4MZdwsAB\",\n \"last_id\": \"ftckpt_enQCFmOTGj3syEpYVhBRLTSy\",\n \"has_more\": true\n}\n" + } + } + } + }, + "/fine_tuning/jobs/{fine_tuning_job_id}/events": { + "get": { + "operationId": "listFineTuningEvents", + "tags": [ + "Fine-tuning" + ], + "summary": "Get status updates for a fine-tuning job.\n", + "parameters": [ + { + "in": "path", + "name": "fine_tuning_job_id", + "required": true, + "schema": { + "type": "string", + "example": "ft-AF1WoRqd3aJAHsqc9NY7iL8F" + }, + "description": "The ID of the fine-tuning job to get events for.\n" + }, + { + "name": "after", + "in": "query", + "description": "Identifier for the last event from the previous pagination request.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of events to retrieve.", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFineTuningJobEventsResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List fine-tuning events", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/fine_tuning/jobs/ftjob-abc123/events \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.fine_tuning.jobs.list_events(\n fine_tuning_job_id=\"ftjob-abc123\",\n limit=2\n)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const list = await openai.fineTuning.list_events(id=\"ftjob-abc123\", limit=2);\n\n for await (const fineTune of list) {\n console.log(fineTune);\n }\n}\n\nmain();" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"fine_tuning.job.event\",\n \"id\": \"ft-event-ddTJfwuMVpfLXseO0Am0Gqjm\",\n \"created_at\": 1721764800,\n \"level\": \"info\",\n \"message\": \"Fine tuning job successfully completed\",\n \"data\": null,\n \"type\": \"message\"\n },\n {\n \"object\": \"fine_tuning.job.event\",\n \"id\": \"ft-event-tyiGuB72evQncpH87xe505Sv\",\n \"created_at\": 1721764800,\n \"level\": \"info\",\n \"message\": \"New fine-tuned model created: ft:gpt-4o-mini:openai::7p4lURel\",\n \"data\": null,\n \"type\": \"message\"\n }\n ],\n \"has_more\": true\n}\n" + } + } + } + }, + "/fine_tuning/jobs/{fine_tuning_job_id}/pause": { + "post": { + "operationId": "pauseFineTuningJob", + "tags": [ + "Fine-tuning" + ], + "summary": "Pause a fine-tune job.\n", + "parameters": [ + { + "in": "path", + "name": "fine_tuning_job_id", + "required": true, + "schema": { + "type": "string", + "example": "ft-AF1WoRqd3aJAHsqc9NY7iL8F" + }, + "description": "The ID of the fine-tuning job to pause.\n" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FineTuningJob" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Pause fine-tuning", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/fine_tuning/jobs/ftjob-abc123/pause \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.fine_tuning.jobs.pause(\"ftjob-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const fineTune = await openai.fineTuning.jobs.pause(\"ftjob-abc123\");\n\n console.log(fineTune);\n}\nmain();" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"created_at\": 1721764800,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-123\",\n \"result_files\": [],\n \"status\": \"paused\",\n \"validation_file\": \"file-abc123\",\n \"training_file\": \"file-abc123\"\n}\n" + } + } + } + }, + "/fine_tuning/jobs/{fine_tuning_job_id}/resume": { + "post": { + "operationId": "resumeFineTuningJob", + "tags": [ + "Fine-tuning" + ], + "summary": "Resume a fine-tune job.\n", + "parameters": [ + { + "in": "path", + "name": "fine_tuning_job_id", + "required": true, + "schema": { + "type": "string", + "example": "ft-AF1WoRqd3aJAHsqc9NY7iL8F" + }, + "description": "The ID of the fine-tuning job to resume.\n" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FineTuningJob" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Resume fine-tuning", + "group": "fine-tuning", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/fine_tuning/jobs/ftjob-abc123/resume \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.fine_tuning.jobs.resume(\"ftjob-abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const fineTune = await openai.fineTuning.jobs.resume(\"ftjob-abc123\");\n\n console.log(fineTune);\n}\nmain();" + }, + "response": "{\n \"object\": \"fine_tuning.job\",\n \"id\": \"ftjob-abc123\",\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"created_at\": 1721764800,\n \"fine_tuned_model\": null,\n \"organization_id\": \"org-123\",\n \"result_files\": [],\n \"status\": \"queued\",\n \"validation_file\": \"file-abc123\",\n \"training_file\": \"file-abc123\"\n}\n" + } + } + } + }, + "/images/edits": { + "post": { + "operationId": "createImageEdit", + "tags": [ + "Images" + ], + "summary": "Creates an edited or extended image given one or more source images and a prompt. This endpoint supports GPT Image models and `dall-e-2`.", + "description": "You can call this endpoint with either:\n\n- `multipart/form-data`: use binary uploads via `image` (and optional `mask`).\n- `application/json`: use `images` (and optional `mask`) as references with either `image_url` or `file_id`.\n\nNote that JSON requests use `images` (array) instead of the multipart `image` field.\n", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateImageEditRequest" + }, + "examples": { + "multipart_edit": { + "summary": "Multipart form upload (binary image + prompt)", + "value": { + "model": "gpt-image-1.5", + "prompt": "Add a watercolor effect to this image", + "image": "", + "size": "1024x1024", + "quality": "high" + } + } + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditImageBodyJsonParam" + }, + "examples": { + "json_with_url": { + "summary": "JSON request with image URL", + "value": { + "model": "gpt-image-1.5", + "prompt": "Add a watercolor effect to this image", + "images": [ + { + "image_url": "https://example.com/source-image.png" + } + ], + "size": "1024x1024", + "quality": "high" + } + }, + "json_with_file_id": { + "summary": "JSON request with uploaded file id", + "value": { + "model": "gpt-image-1.5", + "prompt": "Replace the background with a snowy mountain scene", + "images": [ + { + "file_id": "file-abc123" + } + ], + "mask": { + "file_id": "file-mask123" + }, + "output_format": "png", + "output_compression": 100 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImagesResponse" + } + }, + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/ImageEditStreamEvent" + } + } + } + }, + "429": { + "$ref": "#/components/responses/InferenceRateLimited" + }, + "503": { + "$ref": "#/components/responses/InferenceServiceUnavailable" + } + }, + "x-oaiMeta": { + "name": "Create image edit", + "group": "images", + "examples": [ + { + "title": "Edit image", + "request": { + "curl": "curl -s -D >(grep -i x-request-id >&2) \\\n -o >(jq -r '.data[0].b64_json' | base64 --decode > gift-basket.png) \\\n -X POST \"https://api.openai.com/v1/images/edits\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -F \"model=gpt-image-1.5\" \\\n -F \"image[]=@body-lotion.png\" \\\n -F \"image[]=@bath-bomb.png\" \\\n -F \"image[]=@incense-kit.png\" \\\n -F \"image[]=@soap.png\" \\\n -F 'prompt=Create a lovely gift basket with these four items in it'\n", + "python": "import base64\nfrom openai import OpenAI\nclient = OpenAI()\n\nprompt = \"\"\"\nGenerate a photorealistic image of a gift basket on a white background\nlabeled 'Relax & Unwind' with a ribbon and handwriting-like font,\ncontaining all the items in the reference pictures.\n\"\"\"\n\nresult = client.images.edit(\n model=\"gpt-image-1.5\",\n image=[\n open(\"body-lotion.png\", \"rb\"),\n open(\"bath-bomb.png\", \"rb\"),\n open(\"incense-kit.png\", \"rb\"),\n open(\"soap.png\", \"rb\"),\n ],\n prompt=prompt\n)\n\nimage_base64 = result.data[0].b64_json\nimage_bytes = base64.b64decode(image_base64)\n\n# Save the image to a file\nwith open(\"gift-basket.png\", \"wb\") as f:\n f.write(image_bytes)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI, { toFile } from \"openai\";\n\nconst client = new OpenAI();\n\nconst imageFiles = [\n \"bath-bomb.png\",\n \"body-lotion.png\",\n \"incense-kit.png\",\n \"soap.png\",\n];\n\nconst images = await Promise.all(\n imageFiles.map(async (file) =>\n await toFile(fs.createReadStream(file), null, {\n type: \"image/png\",\n })\n ),\n);\n\nconst rsp = await client.images.edit({\n model: \"gpt-image-1.5\",\n image: images,\n prompt: \"Create a lovely gift basket with these four items in it\",\n});\n\n// Save the image to a file\nconst image_base64 = rsp.data[0].b64_json;\nconst image_bytes = Buffer.from(image_base64, \"base64\");\nfs.writeFileSync(\"basket.png\", image_bytes);\n" + } + }, + { + "title": "Streaming", + "request": { + "curl": "curl -s -N -X POST \"https://api.openai.com/v1/images/edits\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -F \"model=gpt-image-1.5\" \\\n -F \"image[]=@body-lotion.png\" \\\n -F \"image[]=@bath-bomb.png\" \\\n -F \"image[]=@incense-kit.png\" \\\n -F \"image[]=@soap.png\" \\\n -F 'prompt=Create a lovely gift basket with these four items in it' \\\n -F \"stream=true\"\n", + "python": "from openai import OpenAI\n\nclient = OpenAI()\n\nprompt = \"\"\"\nGenerate a photorealistic image of a gift basket on a white background\nlabeled 'Relax & Unwind' with a ribbon and handwriting-like font,\ncontaining all the items in the reference pictures.\n\"\"\"\n\nstream = client.images.edit(\n model=\"gpt-image-1.5\",\n image=[\n open(\"body-lotion.png\", \"rb\"),\n open(\"bath-bomb.png\", \"rb\"),\n open(\"incense-kit.png\", \"rb\"),\n open(\"soap.png\", \"rb\"),\n ],\n prompt=prompt,\n stream=True\n)\n\nfor event in stream:\n print(event)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI, { toFile } from \"openai\";\n\nconst client = new OpenAI();\n\nconst imageFiles = [\n \"bath-bomb.png\",\n \"body-lotion.png\",\n \"incense-kit.png\",\n \"soap.png\",\n];\n\nconst images = await Promise.all(\n imageFiles.map(async (file) =>\n await toFile(fs.createReadStream(file), null, {\n type: \"image/png\",\n })\n ),\n);\n\nconst stream = await client.images.edit({\n model: \"gpt-image-1.5\",\n image: images,\n prompt: \"Create a lovely gift basket with these four items in it\",\n stream: true,\n});\n\nfor await (const event of stream) {\n console.log(event);\n}\n" + }, + "response": "event: image_edit.partial_image\ndata: {\"type\":\"image_edit.partial_image\",\"b64_json\":\"...\",\"partial_image_index\":0}\n\nevent: image_edit.completed\ndata: {\"type\":\"image_edit.completed\",\"b64_json\":\"...\",\"usage\":{\"total_tokens\":100,\"input_tokens\":50,\"output_tokens\":50,\"input_tokens_details\":{\"text_tokens\":10,\"image_tokens\":40}}}\n" + } + ] + } + } + }, + "/images/generations": { + "post": { + "operationId": "createImage", + "tags": [ + "Images" + ], + "summary": "Creates an image given a prompt. [Learn more](https://developers.openai.com/api/docs/guides/images-vision).\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateImageRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImagesResponse" + } + }, + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/ImageGenStreamEvent" + } + } + } + }, + "429": { + "$ref": "#/components/responses/InferenceRateLimited" + }, + "503": { + "$ref": "#/components/responses/InferenceServiceUnavailable" + } + }, + "x-oaiMeta": { + "name": "Create image", + "group": "images", + "examples": [ + { + "title": "Generate image", + "request": { + "curl": "curl https://api.openai.com/v1/images/generations \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"model\": \"gpt-image-1.5\",\n \"prompt\": \"A cute baby sea otter\",\n \"n\": 1,\n \"size\": \"1024x1024\"\n }'\n", + "python": "import base64\nfrom openai import OpenAI\nclient = OpenAI()\n\nimg = client.images.generate(\n model=\"gpt-image-1.5\",\n prompt=\"A cute baby sea otter\",\n n=1,\n size=\"1024x1024\"\n)\n\nimage_bytes = base64.b64decode(img.data[0].b64_json)\nwith open(\"output.png\", \"wb\") as f:\n f.write(image_bytes)\n", + "javascript": "import OpenAI from \"openai\";\nimport { writeFile } from \"fs/promises\";\n\nconst client = new OpenAI();\n\nconst img = await client.images.generate({\n model: \"gpt-image-1.5\",\n prompt: \"A cute baby sea otter\",\n n: 1,\n size: \"1024x1024\"\n});\n\nconst imageBuffer = Buffer.from(img.data[0].b64_json, \"base64\");\nawait writeFile(\"output.png\", imageBuffer);\n" + }, + "response": "{\n \"created\": 1713833628,\n \"data\": [\n {\n \"b64_json\": \"...\"\n }\n ],\n \"usage\": {\n \"total_tokens\": 100,\n \"input_tokens\": 50,\n \"output_tokens\": 50,\n \"input_tokens_details\": {\n \"text_tokens\": 10,\n \"image_tokens\": 40\n }\n }\n}\n" + }, + { + "title": "Streaming", + "request": { + "curl": "curl https://api.openai.com/v1/images/generations \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"model\": \"gpt-image-1.5\",\n \"prompt\": \"A cute baby sea otter\",\n \"n\": 1,\n \"size\": \"1024x1024\",\n \"stream\": true\n }' \\\n --no-buffer\n", + "python": "from openai import OpenAI\n\nclient = OpenAI()\n\nstream = client.images.generate(\n model=\"gpt-image-1.5\",\n prompt=\"A cute baby sea otter\",\n n=1,\n size=\"1024x1024\",\n stream=True\n)\n\nfor event in stream:\n print(event)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst client = new OpenAI();\n\nconst stream = await client.images.generate({\n model: \"gpt-image-1.5\",\n prompt: \"A cute baby sea otter\",\n n: 1,\n size: \"1024x1024\",\n stream: true,\n});\n\nfor await (const event of stream) {\n console.log(event);\n}\n" + }, + "response": "event: image_generation.partial_image\ndata: {\"type\":\"image_generation.partial_image\",\"b64_json\":\"...\",\"partial_image_index\":0}\n\nevent: image_generation.completed\ndata: {\"type\":\"image_generation.completed\",\"b64_json\":\"...\",\"usage\":{\"total_tokens\":100,\"input_tokens\":50,\"output_tokens\":50,\"input_tokens_details\":{\"text_tokens\":10,\"image_tokens\":40}}}\n" + } + ] + } + } + }, + "/images/variations": { + "post": { + "operationId": "createImageVariation", + "tags": [ + "Images" + ], + "summary": "Creates a variation of a given image. This endpoint only supports `dall-e-2`.", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateImageVariationRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImagesResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create image variation", + "group": "images", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/images/variations \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -F image=\"@otter.png\" \\\n -F n=2 \\\n -F size=\"1024x1024\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nresponse = client.images.create_variation(\n image=open(\"image_edit_original.png\", \"rb\"),\n n=2,\n size=\"1024x1024\"\n)\n", + "javascript": "import fs from \"fs\";\nimport OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const image = await openai.images.createVariation({\n image: fs.createReadStream(\"otter.png\"),\n });\n\n console.log(image.data);\n}\nmain();", + "csharp": "using System;\n\nusing OpenAI.Images;\n\nImageClient client = new(\n model: \"dall-e-2\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nGeneratedImage image = client.GenerateImageVariation(imageFilePath: \"otter.png\");\n\nConsole.WriteLine(image.ImageUri);\n" + }, + "response": "{\n \"created\": 1589478378,\n \"data\": [\n {\n \"url\": \"https://...\"\n },\n {\n \"url\": \"https://...\"\n }\n ]\n}\n" + } + } + } + }, + "/live/sessions": { + "post": { + "operationId": "create-live", + "summary": "Create a Live WebRTC session. Start with the [Live prompting guide](https://developers.openai.com/api/docs/guides/live-prompting).", + "description": "Send JSON containing session configuration and transport with type webrtc and the SDP offer. The request starts the session. Apply transport.sdp from the response as the remote answer and wait for session.started on the data channel before sending commands. Audio uses the negotiated media track; omit audio.format and do not send session.start on the data channel. Before integrating, follow the [Live prompting guide](https://developers.openai.com/api/docs/guides/live-prompting) to write frontend conversation and delegation instructions and a separate backend prompt.", + "tags": [ + "Live" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveCreateRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Live session created with a WebRTC answer.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveCreateResponse" + }, + "example": { + "session": { + "id": "live_123" + }, + "transport": { + "type": "webrtc", + "sdp": "" + } + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create session", + "group": "live", + "returns": "Returns 201 Created with the session identifier in session.id and SDP answer in transport.sdp.", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/live/sessions \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session\":{\"model\":\"gpt-live-1\",\"instructions\":\"Be concise. Ask for clarification when needed.\"},\"transport\":{\"type\":\"webrtc\",\"sdp\":\"\"}}'" + } + } + } + } + }, + "/live/sessions/{session_id}/accept": { + "post": { + "operationId": "accept-live-session", + "summary": "Accept an incoming SIP call. Supply session with type live, the model, and startup configuration. Before accepting calls, follow the [Live prompting guide](https://developers.openai.com/api/docs/guides/live-prompting) to write frontend conversation instructions and a separate backend prompt. SIP media format is negotiated; omit audio.format.", + "description": "Accept an incoming SIP call. Supply session with type live, the model, and startup configuration. Before accepting calls, follow the [Live prompting guide](https://developers.openai.com/api/docs/guides/live-prompting) to write frontend conversation instructions and a separate backend prompt. SIP media format is negotiated; omit audio.format.", + "tags": [ + "Live" + ], + "parameters": [ + { + "in": "path", + "name": "session_id", + "required": true, + "description": "Opaque Live session identifier from the creation response or incoming-call webhook. Preserve the returned value unchanged, including its prefix.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveCallAcceptRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Session accept request accepted." + } + }, + "x-oaiMeta": { + "name": "Accept call", + "group": "live-sessions", + "returns": "Returns 200 OK when the control request succeeds." + } + } + }, + "/live/sessions/{session_id}/fork": { + "post": { + "operationId": "fork-live-session", + "summary": "Fork a stored Live session onto a new WebRTC connection.", + "description": "Resume the stored conversation using a new WebRTC connection. The model, voice, and frontend instructions are inherited. Omit session or send an empty object to inherit the remaining configuration. Only Responses delegation settings, storage, and frontend data-channel permissions can be overridden. Apply transport.sdp as the remote answer and wait for session.started before sending commands. Do not send session.start on the data channel or supply audio.format; WebRTC negotiates its media format.", + "tags": [ + "Live" + ], + "parameters": [ + { + "in": "path", + "name": "session_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the stored Live session to fork." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveForkRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Forked Live session created with a WebRTC answer.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveCreateResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Fork session", + "group": "live", + "returns": "The new session identifier in session.id and SDP answer in transport.sdp.", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/live/sessions/live_123/fork \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session\":{},\"transport\":{\"type\":\"webrtc\",\"sdp\":\"\"}}'" + } + } + } + } + }, + "/live/sessions/{session_id}/hangup": { + "post": { + "operationId": "hangup-live-session", + "summary": "End a SIP call identified by session_id.", + "description": "End a SIP call identified by session_id.", + "tags": [ + "Live" + ], + "parameters": [ + { + "in": "path", + "name": "session_id", + "required": true, + "description": "Opaque Live session identifier from the creation response or incoming-call webhook. Preserve the returned value unchanged, including its prefix.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Session hangup request accepted." + } + }, + "x-oaiMeta": { + "name": "Hang up session", + "group": "live-sessions", + "returns": "Returns 200 OK when the control request succeeds." + } + } + }, + "/live/sessions/{session_id}/refer": { + "post": { + "operationId": "refer-live-session", + "summary": "Transfer a SIP call to another destination. Supply a nonblank target_uri for the SIP Refer-To header.", + "description": "Transfer a SIP call to another destination. Supply a nonblank target_uri for the SIP Refer-To header.", + "tags": [ + "Live" + ], + "parameters": [ + { + "in": "path", + "name": "session_id", + "required": true, + "description": "Opaque Live session identifier from the creation response or incoming-call webhook. Preserve the returned value unchanged, including its prefix.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveCallReferRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Session refer request accepted." + } + }, + "x-oaiMeta": { + "name": "Transfer call", + "group": "live-sessions", + "returns": "Returns 200 OK when the control request succeeds." + } + } + }, + "/live/sessions/{session_id}/reject": { + "post": { + "operationId": "reject-live-session", + "summary": "Reject an incoming SIP call. Send a required SIP rejection status_code between 300 and 699.", + "description": "Reject an incoming SIP call. Send a required SIP rejection status_code between 300 and 699.", + "tags": [ + "Live" + ], + "parameters": [ + { + "in": "path", + "name": "session_id", + "required": true, + "description": "Opaque Live session identifier from the creation response or incoming-call webhook. Preserve the returned value unchanged, including its prefix.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveCallRejectRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Session reject request accepted." + } + }, + "x-oaiMeta": { + "name": "Reject call", + "group": "live-sessions", + "returns": "Returns 200 OK when the control request succeeds." + } + } + }, + "/models": { + "get": { + "operationId": "listModels", + "tags": [ + "Models" + ], + "summary": "Lists the currently available models, and provides basic information about each one such as the owner and availability.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListModelsResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List models", + "group": "models", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/models \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.models.list()\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const list = await openai.models.list();\n\n for await (const model of list) {\n console.log(model);\n }\n}\nmain();", + "csharp": "using System;\n\nusing OpenAI.Models;\n\nOpenAIModelClient client = new(\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nforeach (var model in client.GetModels().Value)\n{\n Console.WriteLine(model.Id);\n}\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"model-id-0\",\n \"object\": \"model\",\n \"created\": 1686935002,\n \"owned_by\": \"organization-owner\",\n \"shutdown_date\": null\n },\n {\n \"id\": \"model-id-1\",\n \"object\": \"model\",\n \"created\": 1686935002,\n \"owned_by\": \"organization-owner\",\n \"shutdown_date\": null\n },\n {\n \"id\": \"model-id-2\",\n \"object\": \"model\",\n \"created\": 1686935002,\n \"owned_by\": \"openai\",\n \"shutdown_date\": \"2026-10-23\"\n },\n ]\n}\n" + } + } + } + }, + "/models/{model}": { + "get": { + "operationId": "retrieveModel", + "tags": [ + "Models" + ], + "summary": "Retrieves a model instance, providing basic information about the model such as the owner and permissioning.", + "parameters": [ + { + "in": "path", + "name": "model", + "required": true, + "schema": { + "type": "string", + "example": "gpt-6-astra" + }, + "description": "The ID of the model to use for this request" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Model" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve model", + "group": "models", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/models/gpt-6-astra \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.models.retrieve(\"gpt-6-astra\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const model = await openai.models.retrieve(\"gpt-6-astra\");\n\n console.log(model);\n}\n\nmain();", + "csharp": "using System;\nusing System.ClientModel;\n\nusing OpenAI.Models;\n\n OpenAIModelClient client = new(\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nClientResult model = client.GetModel(\"babbage-002\");\nConsole.WriteLine(model.Value.Id);\n" + }, + "response": "{\n \"id\": \"gpt-6-astra\",\n \"object\": \"model\",\n \"created\": 1686935002,\n \"owned_by\": \"openai\",\n \"shutdown_date\": \"2026-10-23\"\n}\n" + } + } + }, + "delete": { + "operationId": "deleteModel", + "tags": [ + "Models" + ], + "summary": "Delete a fine-tuned model. You must have the Owner role in your organization to delete a model.", + "parameters": [ + { + "in": "path", + "name": "model", + "required": true, + "schema": { + "type": "string", + "example": "ft:gpt-4o-mini:acemeco:suffix:abc123" + }, + "description": "The model to delete" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteModelResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete a fine-tuned model", + "group": "models", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/models/ft:gpt-4o-mini:acemeco:suffix:abc123 \\\n -X DELETE \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\"\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nclient.models.delete(\"ft:gpt-4o-mini:acemeco:suffix:abc123\")\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const model = await openai.models.delete(\"ft:gpt-4o-mini:acemeco:suffix:abc123\");\n \n console.log(model);\n}\nmain();", + "csharp": "using System;\nusing System.ClientModel;\n\nusing OpenAI.Models;\n\nOpenAIModelClient client = new(\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nClientResult success = client.DeleteModel(\"ft:gpt-4o-mini:acemeco:suffix:abc123\");\nConsole.WriteLine(success);\n" + }, + "response": "{\n \"id\": \"ft:gpt-4o-mini:acemeco:suffix:abc123\",\n \"object\": \"model\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/moderations": { + "post": { + "operationId": "createModeration", + "tags": [ + "Moderations" + ], + "summary": "Classifies if text and/or image inputs are potentially harmful. Learn\nmore in the [moderation guide](https://developers.openai.com/api/docs/guides/moderation).\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateModerationRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateModerationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/InferenceRateLimited" + }, + "503": { + "$ref": "#/components/responses/InferenceServiceUnavailable" + } + }, + "x-oaiMeta": { + "name": "Create moderation", + "group": "moderations", + "examples": [ + { + "title": "Single string", + "request": { + "curl": "curl https://api.openai.com/v1/moderations \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"input\": \"I want to kill them.\"\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nmoderation = client.moderations.create(input=\"I want to kill them.\")\nprint(moderation)\n", + "javascript": "import OpenAI from \"openai\";\n\nconst openai = new OpenAI();\n\nasync function main() {\n const moderation = await openai.moderations.create({ input: \"I want to kill them.\" });\n\n console.log(moderation);\n}\nmain();\n", + "csharp": "using System;\nusing System.ClientModel;\n\nusing OpenAI.Moderations;\n\nModerationClient client = new(\n model: \"omni-moderation-latest\",\n apiKey: Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")\n);\n\nClientResult moderation = client.ClassifyText(\"I want to kill them.\");\n" + }, + "response": "{\n \"id\": \"modr-AB8CjOTu2jiq12hp1AQPfeqFWaORR\",\n \"model\": \"text-moderation-007\",\n \"results\": [\n {\n \"flagged\": true,\n \"categories\": {\n \"sexual\": false,\n \"hate\": false,\n \"harassment\": true,\n \"self-harm\": false,\n \"sexual/minors\": false,\n \"hate/threatening\": false,\n \"violence/graphic\": false,\n \"self-harm/intent\": false,\n \"self-harm/instructions\": false,\n \"harassment/threatening\": true,\n \"violence\": true\n },\n \"category_scores\": {\n \"sexual\": 0.000011726012417057063,\n \"hate\": 0.22706663608551025,\n \"harassment\": 0.5215635299682617,\n \"self-harm\": 2.227119921371923e-6,\n \"sexual/minors\": 7.107352217872176e-8,\n \"hate/threatening\": 0.023547329008579254,\n \"violence/graphic\": 0.00003391829886822961,\n \"self-harm/intent\": 1.646940972932498e-6,\n \"self-harm/instructions\": 1.1198755256458526e-9,\n \"harassment/threatening\": 0.5694745779037476,\n \"violence\": 0.9971134662628174\n }\n }\n ]\n}\n" + }, + { + "title": "Image and text", + "request": { + "curl": "curl https://api.openai.com/v1/moderations \\\n -X POST \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -d '{\n \"model\": \"omni-moderation-latest\",\n \"input\": [\n { \"type\": \"text\", \"text\": \"...text to classify goes here...\" },\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": \"https://example.com/image.png\"\n }\n }\n ]\n }'\n", + "python": "from openai import OpenAI\nclient = OpenAI()\n\nresponse = client.moderations.create(\n model=\"omni-moderation-latest\",\n input=[\n {\"type\": \"text\", \"text\": \"...text to classify goes here...\"},\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": \"https://example.com/image.png\",\n # can also use base64 encoded image URLs\n # \"url\": \"data:image/jpeg;base64,abcdefg...\"\n }\n },\n ],\n)\n\nprint(response)\n", + "javascript": "import OpenAI from \"openai\";\nconst openai = new OpenAI();\n\nconst moderation = await openai.moderations.create({\n model: \"omni-moderation-latest\",\n input: [\n { type: \"text\", text: \"...text to classify goes here...\" },\n {\n type: \"image_url\",\n image_url: {\n url: \"https://example.com/image.png\"\n // can also use base64 encoded image URLs\n // url: \"data:image/jpeg;base64,abcdefg...\"\n }\n }\n ],\n});\n\nconsole.log(moderation);\n" + }, + "response": "{\n \"id\": \"modr-0d9740456c391e43c445bf0f010940c7\",\n \"model\": \"omni-moderation-latest\",\n \"results\": [\n {\n \"flagged\": true,\n \"categories\": {\n \"harassment\": true,\n \"harassment/threatening\": true,\n \"sexual\": false,\n \"hate\": false,\n \"hate/threatening\": false,\n \"illicit\": false,\n \"illicit/violent\": false,\n \"self-harm/intent\": false,\n \"self-harm/instructions\": false,\n \"self-harm\": false,\n \"sexual/minors\": false,\n \"violence\": true,\n \"violence/graphic\": true\n },\n \"category_scores\": {\n \"harassment\": 0.8189693396524255,\n \"harassment/threatening\": 0.804985420696006,\n \"sexual\": 1.573112165348997e-6,\n \"hate\": 0.007562942636942845,\n \"hate/threatening\": 0.004208854591835476,\n \"illicit\": 0.030535955153511665,\n \"illicit/violent\": 0.008925306722380033,\n \"self-harm/intent\": 0.00023023930975076432,\n \"self-harm/instructions\": 0.0002293869201073356,\n \"self-harm\": 0.012598046106750154,\n \"sexual/minors\": 2.212566909570261e-8,\n \"violence\": 0.9999992735124786,\n \"violence/graphic\": 0.843064871157054\n },\n \"category_applied_input_types\": {\n \"harassment\": [\n \"text\"\n ],\n \"harassment/threatening\": [\n \"text\"\n ],\n \"sexual\": [\n \"text\",\n \"image\"\n ],\n \"hate\": [\n \"text\"\n ],\n \"hate/threatening\": [\n \"text\"\n ],\n \"illicit\": [\n \"text\"\n ],\n \"illicit/violent\": [\n \"text\"\n ],\n \"self-harm/intent\": [\n \"text\",\n \"image\"\n ],\n \"self-harm/instructions\": [\n \"text\",\n \"image\"\n ],\n \"self-harm\": [\n \"text\",\n \"image\"\n ],\n \"sexual/minors\": [\n \"text\"\n ],\n \"violence\": [\n \"text\",\n \"image\"\n ],\n \"violence/graphic\": [\n \"text\",\n \"image\"\n ]\n }\n }\n ]\n}\n" + } + ] + } + } + }, + "/organization/admin_api_keys": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "List organization API keys", + "operationId": "admin-api-keys-list", + "description": "Retrieve a paginated list of organization admin API keys.", + "parameters": [ + { + "in": "query", + "name": "after", + "required": false, + "schema": { + "type": "string", + "nullable": true, + "description": "Return keys with IDs that come after this ID in the pagination order." + } + }, + { + "in": "query", + "name": "order", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc", + "description": "Order results by creation time, ascending or descending." + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "integer", + "default": 20, + "description": "Maximum number of keys to return." + } + } + ], + "responses": { + "200": { + "description": "A list of organization API keys.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyList" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List all organization and project API keys.", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/admin_api_keys?after=key_abc&limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"organization.admin_api_key\",\n \"id\": \"key_abc\",\n \"name\": \"Main Admin Key\",\n \"redacted_value\": \"sk-admin...def\",\n \"created_at\": 1711471533,\n \"expires_at\": 1714063533,\n \"last_used_at\": 1711471534,\n \"owner\": {\n \"type\": \"service_account\",\n \"object\": \"organization.service_account\",\n \"id\": \"sa_456\",\n \"name\": \"My Service Account\",\n \"created_at\": 1711471533,\n \"role\": \"member\"\n }\n }\n ],\n \"first_id\": \"key_abc\",\n \"last_id\": \"key_abc\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Create an organization admin API key", + "operationId": "admin-api-keys-create", + "description": "Create a new admin-level API key for the organization.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "example": "New Admin Key" + }, + "expires_in_seconds": { + "type": "integer", + "minimum": 1, + "maximum": 31536000, + "example": 2592000, + "description": "The number of seconds until the API key expires. Omit this field for a key that does not expire." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The newly created admin API key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminApiKeyCreateResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create admin API key", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/admin_api_keys \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"New Admin Key\",\n \"expires_in_seconds\": 2592000\n }'\n" + }, + "response": "{\n \"object\": \"organization.admin_api_key\",\n \"id\": \"key_xyz\",\n \"name\": \"New Admin Key\",\n \"redacted_value\": \"sk-admin...xyz\",\n \"created_at\": 1711471533,\n \"expires_at\": 1714063533,\n \"last_used_at\": 1711471534,\n \"owner\": {\n \"type\": \"user\",\n \"object\": \"organization.user\",\n \"id\": \"user_123\",\n \"name\": \"John Doe\",\n \"created_at\": 1711471533,\n \"role\": \"owner\"\n },\n \"value\": \"sk-admin-1234abcd\"\n}\n" + } + } + } + }, + "/organization/admin_api_keys/{key_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieve a single organization API key", + "operationId": "admin-api-keys-get", + "description": "Get details for a specific organization API key by its ID.", + "parameters": [ + { + "in": "path", + "name": "key_id", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the API key." + } + } + ], + "responses": { + "200": { + "description": "Details of the requested API key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminApiKey" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve admin API key", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/admin_api_keys/key_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.admin_api_key\",\n \"id\": \"key_abc\",\n \"name\": \"Main Admin Key\",\n \"redacted_value\": \"sk-admin...xyz\",\n \"created_at\": 1711471533,\n \"last_used_at\": 1711471534,\n \"owner\": {\n \"type\": \"user\",\n \"object\": \"organization.user\",\n \"id\": \"user_123\",\n \"name\": \"John Doe\",\n \"created_at\": 1711471533,\n \"role\": \"owner\"\n }\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Delete an organization admin API key", + "operationId": "admin-api-keys-delete", + "description": "Delete the specified admin API key.", + "parameters": [ + { + "in": "path", + "name": "key_id", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the API key to be deleted." + } + } + ], + "responses": { + "200": { + "description": "Confirmation that the API key was deleted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "key_abc" + }, + "object": { + "type": "string", + "enum": [ + "organization.admin_api_key.deleted" + ], + "example": "organization.admin_api_key.deleted", + "x-stainless-const": true + }, + "deleted": { + "type": "boolean", + "example": true + } + }, + "required": [ + "id", + "object", + "deleted" + ] + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete admin API key", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/admin_api_keys/key_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"key_abc\",\n \"object\": \"organization.admin_api_key.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/audit_logs": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "List user actions and configuration changes within this organization.", + "operationId": "list-audit-logs", + "tags": [ + "Audit Logs" + ], + "parameters": [ + { + "name": "effective_at", + "in": "query", + "description": "Return only events whose `effective_at` (Unix seconds) is in this range.", + "required": false, + "schema": { + "type": "object", + "properties": { + "gt": { + "type": "integer", + "description": "Return only events whose `effective_at` (Unix seconds) is greater than this value." + }, + "gte": { + "type": "integer", + "description": "Return only events whose `effective_at` (Unix seconds) is greater than or equal to this value." + }, + "lt": { + "type": "integer", + "description": "Return only events whose `effective_at` (Unix seconds) is less than this value." + }, + "lte": { + "type": "integer", + "description": "Return only events whose `effective_at` (Unix seconds) is less than or equal to this value." + } + } + } + }, + { + "name": "project_ids[]", + "in": "query", + "description": "Return only events for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "event_types[]", + "in": "query", + "description": "Return only events with a `type` in one of these values. For example, `project.created`. For all options, see the documentation for the [audit log object](https://developers.openai.com/api/reference/resources/admin/subresources/organization/subresources/audit_logs).", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditLogEventType" + } + } + }, + { + "name": "actor_ids[]", + "in": "query", + "description": "Return only events performed by these actors. Can be a user ID, a service account ID, or an api key tracking ID.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "actor_emails[]", + "in": "query", + "description": "Return only events performed by users with these emails.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "resource_ids[]", + "in": "query", + "description": "Return only events performed on these targets. For example, a project ID updated. For ChatGPT connector role events, use the workspace connector resource ID shown in `details.id`, such as `__`.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "tenant_only", + "in": "query", + "description": "Return only tenant-scoped events associated with this organization. Required for tenant-scoped events such as `role.bound_to_resource` and `role.unbound_from_resource`. When `true`, all supplied event types must be tenant-scoped.", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "schema": { + "type": "string" + } + }, + { + "name": "before", + "in": "query", + "description": "A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list.\n", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Audit logs listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListAuditLogsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List audit logs", + "group": "audit-logs", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/audit_logs \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"audit_log-xxx_yyyymmdd\",\n \"type\": \"project.archived\",\n \"effective_at\": 1722461446,\n \"actor\": {\n \"type\": \"api_key\",\n \"api_key\": {\n \"type\": \"user\",\n \"user\": {\n \"id\": \"user-xxx\",\n \"email\": \"user@example.com\"\n }\n }\n },\n \"project.archived\": {\n \"id\": \"proj_abc\"\n },\n },\n {\n \"id\": \"audit_log-yyy__20240101\",\n \"type\": \"api_key.updated\",\n \"effective_at\": 1720804190,\n \"actor\": {\n \"type\": \"session\",\n \"session\": {\n \"user\": {\n \"id\": \"user-xxx\",\n \"email\": \"user@example.com\"\n },\n \"ip_address\": \"127.0.0.1\",\n \"user_agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\",\n \"ja3\": \"a497151ce4338a12c4418c44d375173e\",\n \"ja4\": \"q13d0313h3_55b375c5d22e_c7319ce65786\",\n \"ip_address_details\": {\n \"country\": \"US\",\n \"city\": \"San Francisco\",\n \"region\": \"California\",\n \"region_code\": \"CA\",\n \"asn\": \"1234\",\n \"latitude\": \"37.77490\",\n \"longitude\": \"-122.41940\"\n }\n }\n },\n \"api_key.updated\": {\n \"id\": \"key_xxxx\",\n \"data\": {\n \"scopes\": [\"resource_2.operation_2\"]\n }\n },\n }\n ],\n \"first_id\": \"audit_log-xxx__20240101\",\n \"last_id\": \"audit_log_yyy__20240101\",\n \"has_more\": true\n}\n" + } + } + } + }, + "/organization/certificates": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "List uploaded certificates for this organization.", + "operationId": "listOrganizationCertificates", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order.\n", + "schema": { + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ] + } + } + ], + "responses": { + "200": { + "description": "Certificates listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListCertificatesResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List organization certificates", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/certificates \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"organization.certificate\",\n \"id\": \"cert_abc\",\n \"name\": \"My Example Certificate\",\n \"active\": true,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n ],\n \"first_id\": \"cert_abc\",\n \"last_id\": \"cert_abc\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Upload a certificate to the organization. This does **not** automatically activate the certificate.\n\nOrganizations can upload up to 50 certificates.\n", + "operationId": "uploadCertificate", + "tags": [ + "Certificates" + ], + "requestBody": { + "description": "The certificate upload payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadCertificateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Certificate uploaded successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Certificate" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Upload certificate", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/certificates \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n \"name\": \"My Example Certificate\",\n \"certificate\": \"-----BEGIN CERTIFICATE-----\\\\nMIIDeT...\\\\n-----END CERTIFICATE-----\"\n}'\n" + }, + "response": "{\n \"object\": \"certificate\",\n \"id\": \"cert_abc\",\n \"name\": \"My Example Certificate\",\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n}\n" + } + } + } + }, + "/organization/certificates/activate": { + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Activate certificates at the organization level.\n\nYou can atomically and idempotently activate up to 10 certificates at a time.\n", + "operationId": "activateOrganizationCertificates", + "tags": [ + "Certificates" + ], + "requestBody": { + "description": "The certificate activation payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToggleCertificatesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Certificates activated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationCertificateActivationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Activate certificates for organization", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/certificates/activate \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n \"certificate_ids\": [\"cert_abc\", \"cert_def\"]\n}'\n" + }, + "response": "{\n \"object\": \"organization.certificate.activation\",\n \"data\": [\n {\n \"object\": \"organization.certificate\",\n \"id\": \"cert_abc\",\n \"name\": \"My Example Certificate\",\n \"active\": true,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n {\n \"object\": \"organization.certificate\",\n \"id\": \"cert_def\",\n \"name\": \"My Example Certificate 2\",\n \"active\": true,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n ],\n}\n" + } + } + } + }, + "/organization/certificates/deactivate": { + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deactivate certificates at the organization level.\n\nYou can atomically and idempotently deactivate up to 10 certificates at a time.\n", + "operationId": "deactivateOrganizationCertificates", + "tags": [ + "Certificates" + ], + "requestBody": { + "description": "The certificate deactivation payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToggleCertificatesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Certificates deactivated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationCertificateDeactivationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Deactivate certificates for organization", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/certificates/deactivate \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n \"certificate_ids\": [\"cert_abc\", \"cert_def\"]\n}'\n" + }, + "response": "{\n \"object\": \"organization.certificate.deactivation\",\n \"data\": [\n {\n \"object\": \"organization.certificate\",\n \"id\": \"cert_abc\",\n \"name\": \"My Example Certificate\",\n \"active\": false,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n {\n \"object\": \"organization.certificate\",\n \"id\": \"cert_def\",\n \"name\": \"My Example Certificate 2\",\n \"active\": false,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n ],\n}\n" + } + } + } + }, + "/organization/certificates/{certificate_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get a certificate that has been uploaded to the organization.\n\nYou can get a certificate regardless of whether it is active or not.\n", + "operationId": "getCertificate", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "name": "certificate_id", + "in": "path", + "description": "Unique ID of the certificate to retrieve.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "include", + "in": "query", + "description": "A list of additional fields to include in the response. Currently the only supported value is `content` to fetch the PEM content of the certificate.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "content" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Certificate retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Certificate" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Get certificate", + "group": "administration", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/certificates/cert_abc?include[]=content\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\"\n" + }, + "response": "{\n \"object\": \"certificate\",\n \"id\": \"cert_abc\",\n \"name\": \"My Example Certificate\",\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 1234567,\n \"expires_at\": 12345678,\n \"content\": \"-----BEGIN CERTIFICATE-----MIIDeT...-----END CERTIFICATE-----\"\n }\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Modify a certificate. Note that only the name can be modified.\n", + "operationId": "modifyCertificate", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "name": "certificate_id", + "in": "path", + "description": "Unique ID of the certificate to modify.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The certificate modification payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModifyCertificateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Certificate modified successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Certificate" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Modify certificate", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/certificates/cert_abc \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n \"name\": \"Renamed Certificate\"\n}'\n" + }, + "response": "{\n \"object\": \"certificate\",\n \"id\": \"cert_abc\",\n \"name\": \"Renamed Certificate\",\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Delete a certificate from the organization.\n\nThe certificate must be inactive for the organization and all projects.\n", + "operationId": "deleteCertificate", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "name": "certificate_id", + "in": "path", + "description": "Unique ID of the certificate to delete.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Certificate deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteCertificateResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete certificate", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/certificates/cert_abc \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\"\n" + }, + "response": "{\n \"object\": \"certificate.deleted\",\n \"id\": \"cert_abc\"\n}\n" + } + } + } + }, + "/organization/costs": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get costs details for the organization.", + "operationId": "usage-costs", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently only `1d` is supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only costs for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "api_key_ids", + "in": "query", + "description": "Return only costs for these API keys.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "line_items", + "in": "query", + "description": "Return only costs for these exact line item names. Each value must match the complete `line_item` value, for example `gpt-6-astra, input_tokens`.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the costs by the specified fields. Support fields include `project_id`, `line_item`, `api_key_id` and any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id", + "line_item", + "api_key_id" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of buckets to be returned. Limit can range between 1 and 180, and the default is 7.\n", + "required": false, + "schema": { + "type": "integer", + "default": 7 + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Costs data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Costs", + "group": "usage-costs", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/costs?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.costs.result\",\n \"amount\": {\n \"value\": 0.06,\n \"currency\": \"usd\"\n },\n \"line_item\": null,\n \"project_id\": null,\n \"api_key_id\": null,\n \"quantity\": null,\n \"quantity_unit\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/data_retention": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves organization data retention controls.", + "operationId": "retrieve-organization-data-retention", + "tags": [ + "Data retention" + ], + "responses": { + "200": { + "description": "Organization data retention controls retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationDataRetention" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve organization data retention", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/data_retention \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.data_retention\",\n \"type\": \"modified_abuse_monitoring\"\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates organization data retention controls.", + "operationId": "update-organization-data-retention", + "tags": [ + "Data retention" + ], + "requestBody": { + "description": "The desired organization data retention setting.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationDataRetentionBody" + } + } + } + }, + "responses": { + "200": { + "description": "Organization data retention controls updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationDataRetention" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update organization data retention", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/data_retention \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"retention_type\": \"modified_abuse_monitoring\"\n }'\n" + }, + "response": "{\n \"object\": \"organization.data_retention\",\n \"type\": \"modified_abuse_monitoring\"\n}\n" + } + } + } + }, + "/organization/groups": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists all groups in the organization.", + "operationId": "list-groups", + "tags": [ + "Groups" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "A limit on the number of groups to be returned. Limit can range between 0 and 1000, and the default is 100.\n", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000, + "default": 100 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is a group ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with group_abc, your subsequent call can include `after=group_abc` in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Specifies the sort order of the returned groups.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + } + ], + "responses": { + "200": { + "description": "Groups listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List groups", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/groups?limit=20&order=asc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"group\",\n \"id\": \"group_01J1F8ABCDXYZ\",\n \"name\": \"Support Team\",\n \"created_at\": 1711471533,\n \"is_scim_managed\": false\n }\n ],\n \"has_more\": false,\n \"next\": null\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Creates a new group in the organization.", + "operationId": "create-group", + "tags": [ + "Groups" + ], + "requestBody": { + "description": "Parameters for the group you want to create.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGroupBody" + } + } + } + }, + "responses": { + "200": { + "description": "Group created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Create group", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/groups \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Support Team\"\n }'\n" + }, + "response": "{\n \"object\": \"group\",\n \"id\": \"group_01J1F8ABCDXYZ\",\n \"name\": \"Support Team\",\n \"created_at\": 1711471533,\n \"is_scim_managed\": false\n}\n" + } + } + } + }, + "/organization/groups/{group_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a group.", + "operationId": "retrieve-group", + "tags": [ + "Groups" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to retrieve.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Group retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve group", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"group_01J1F8ABCDXYZ\",\n \"name\": \"Support Team\",\n \"created_at\": 1711471533,\n \"is_scim_managed\": false,\n \"group_type\": \"group\"\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates a group's information.", + "operationId": "update-group", + "tags": [ + "Groups" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "New attributes to set on the group.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateGroupBody" + } + } + } + }, + "responses": { + "200": { + "description": "Group updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupResourceWithSuccess" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Update group", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Escalations\"\n }'\n" + }, + "response": "{\n \"id\": \"group_01J1F8ABCDXYZ\",\n \"name\": \"Escalations\",\n \"created_at\": 1711471533,\n \"is_scim_managed\": false\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes a group from the organization.", + "operationId": "delete-group", + "tags": [ + "Groups" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to delete.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Group deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupDeletedResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete group", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"group.deleted\",\n \"id\": \"group_01J1F8ABCDXYZ\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/groups/{group_id}/roles": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists the organization roles assigned to a group within the organization.", + "operationId": "list-group-role-assignments", + "tags": [ + "Group organization role assignments" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group whose organization role assignments you want to list.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of organization role assignments to return.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000 + } + }, + { + "name": "after", + "in": "query", + "description": "Cursor for pagination. Provide the value from the previous response's `next` field to continue listing organization roles.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for the returned organization roles.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + ], + "responses": { + "200": { + "description": "Group organization role assignments listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List group organization role assignments", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false,\n \"description\": \"Allows managing organization groups\",\n \"created_at\": 1711471533,\n \"updated_at\": 1711472599,\n \"created_by\": \"user_abc123\",\n \"created_by_user_obj\": {\n \"id\": \"user_abc123\",\n \"name\": \"Ada Lovelace\",\n \"email\": \"ada@example.com\"\n },\n \"metadata\": {}\n }\n ],\n \"has_more\": false,\n \"next\": null\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Assigns an organization role to a group within the organization.", + "operationId": "assign-group-role", + "tags": [ + "Group organization role assignments" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group that should receive the organization role.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Identifies the organization role to assign to the group.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicAssignOrganizationGroupRoleBody" + } + } + } + }, + "responses": { + "200": { + "description": "Organization role assigned to the group successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupRoleAssignment" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Assign organization role to group", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role_id\": \"role_01J1F8ROLE01\"\n }'\n" + }, + "response": "{\n \"object\": \"group.role\",\n \"group\": {\n \"object\": \"group\",\n \"id\": \"group_01J1F8ABCDXYZ\",\n \"name\": \"Support Team\",\n \"created_at\": 1711471533,\n \"scim_managed\": false\n },\n \"role\": {\n \"object\": \"role\",\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"description\": \"Allows managing organization groups\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false\n }\n}\n" + } + } + } + }, + "/organization/groups/{group_id}/roles/{role_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves an organization role assigned to a group.", + "operationId": "retrieve-group-role", + "tags": [ + "Group organization role assignments" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the organization role to retrieve for the group.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization role retrieved for the group successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignedRoleDetails" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve group organization role", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ/roles/role_01J1F8ROLE01 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false,\n \"description\": \"Allows managing organization groups\",\n \"created_at\": 1711471533,\n \"updated_at\": 1711472599,\n \"created_by\": \"user_abc123\",\n \"created_by_user_obj\": null,\n \"metadata\": {},\n \"assignment_sources\": null\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Unassigns an organization role from a group within the organization.", + "operationId": "unassign-group-role", + "tags": [ + "Group organization role assignments" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to modify.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the organization role to remove from the group.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization role unassigned from the group successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedRoleAssignmentResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Unassign organization role from group", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ/roles/role_01J1F8ROLE01 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"group.role.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/groups/{group_id}/users": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists the users assigned to a group.", + "operationId": "list-group-users", + "tags": [ + "Group users" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of users to be returned. Limit can range between 0 and 1000, and the default is 100.\n", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000, + "default": 100 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. Provide the ID of the last user from the previous list response to retrieve the next page.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Specifies the sort order of users in the list.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + } + } + ], + "responses": { + "200": { + "description": "Group users listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List group users", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ/users?limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"user_abc123\",\n \"name\": \"Ada Lovelace\",\n \"email\": \"ada@example.com\"\n }\n ],\n \"has_more\": false,\n \"next\": null\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Adds a user to a group.", + "operationId": "add-group-user", + "tags": [ + "Group users" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Identifies the user that should be added to the group.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGroupUserBody" + } + } + } + }, + "responses": { + "200": { + "description": "User added to the group successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupUserAssignment" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Add group user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ/users \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"user_id\": \"user_abc123\"\n }'\n" + }, + "response": "{\n \"object\": \"group.user\",\n \"user_id\": \"user_abc123\",\n \"group_id\": \"group_01J1F8ABCDXYZ\"\n}\n" + } + } + } + }, + "/organization/groups/{group_id}/users/{user_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a user in a group.", + "operationId": "retrieve-group-user", + "tags": [ + "Group users" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "description": "The ID of the user to retrieve from the group.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "User retrieved from the group successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupMemberUser" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve group user", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ/users/user_abc123 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"user_abc123\",\n \"name\": \"Ada Lovelace\",\n \"email\": \"ada@example.com\",\n \"picture\": null,\n \"is_service_account\": false,\n \"user_type\": \"user\"\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Removes a user from a group.", + "operationId": "remove-group-user", + "tags": [ + "Group users" + ], + "parameters": [ + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to update.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "description": "The ID of the user to remove from the group.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "User removed from the group successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupUserDeletedResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Remove group user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/groups/group_01J1F8ABCDXYZ/users/user_abc123 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"group.user.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/invites": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Returns a list of invites in the organization.", + "operationId": "list-invites", + "tags": [ + "Invites" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Invites listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InviteListResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List invites", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/invites?after=invite-abc&limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"organization.invite\",\n \"id\": \"invite-abc\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"status\": \"accepted\",\n \"created_at\": 1711471533,\n \"expires_at\": 1711471533,\n \"accepted_at\": 1711471533\n }\n ],\n \"first_id\": \"invite-abc\",\n \"last_id\": \"invite-abc\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Create an invite for a user to the organization. The invite must be accepted by the user before they have access to the organization.", + "operationId": "inviteUser", + "tags": [ + "Invites" + ], + "requestBody": { + "description": "The invite request payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InviteRequest" + } + } + } + }, + "responses": { + "200": { + "description": "User invited successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invite" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create invite", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/invites \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"email\": \"anotheruser@example.com\",\n \"role\": \"reader\",\n \"projects\": [\n {\n \"id\": \"project-xyz\",\n \"role\": \"member\"\n },\n {\n \"id\": \"project-abc\",\n \"role\": \"owner\"\n }\n ]\n }'\n" + }, + "response": "{\n \"object\": \"organization.invite\",\n \"id\": \"invite-def\",\n \"email\": \"anotheruser@example.com\",\n \"role\": \"reader\",\n \"status\": \"pending\",\n \"created_at\": 1711471533,\n \"expires_at\": 1711471533,\n \"accepted_at\": null,\n \"projects\": [\n {\n \"id\": \"project-xyz\",\n \"role\": \"member\"\n },\n {\n \"id\": \"project-abc\",\n \"role\": \"owner\"\n }\n ]\n}\n" + } + } + } + }, + "/organization/invites/{invite_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves an invite.", + "operationId": "retrieve-invite", + "tags": [ + "Invites" + ], + "parameters": [ + { + "in": "path", + "name": "invite_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the invite to retrieve." + } + ], + "responses": { + "200": { + "description": "Invite retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invite" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve invite", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/invites/invite-abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.invite\",\n \"id\": \"invite-abc\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"status\": \"accepted\",\n \"created_at\": 1711471533,\n \"expires_at\": 1711471533,\n \"accepted_at\": 1711471533\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Delete an invite. If the invite has already been accepted, it cannot be deleted.", + "operationId": "delete-invite", + "tags": [ + "Invites" + ], + "parameters": [ + { + "in": "path", + "name": "invite_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of the invite to delete." + } + ], + "responses": { + "200": { + "description": "Invite deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InviteDeleteResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete invite", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/invites/invite-abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.invite.deleted\",\n \"id\": \"invite-abc\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/projects": { + "get": { + "summary": "Returns a list of projects.", + "operationId": "list-projects", + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "include_archived", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "description": "If `true` returns all projects including those that have been `archived`. Archived projects are not included by default." + } + ], + "responses": { + "200": { + "description": "Projects listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectListResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List projects", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects?after=proj_abc&limit=20&include_archived=false \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"proj_abc\",\n \"object\": \"organization.project\",\n \"name\": \"Project example\",\n \"created_at\": 1711471533,\n \"archived_at\": null,\n \"status\": \"active\"\n }\n ],\n \"first_id\": \"proj-abc\",\n \"last_id\": \"proj-xyz\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "summary": "Create a new project in the organization. Projects can be created and archived, but cannot be deleted.", + "operationId": "create-project", + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "tags": [ + "Projects" + ], + "requestBody": { + "description": "The project create request payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Project created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create project", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Project ABC\"\n }'\n" + }, + "response": "{\n \"id\": \"proj_abc\",\n \"object\": \"organization.project\",\n \"name\": \"Project ABC\",\n \"created_at\": 1711471533,\n \"archived_at\": null,\n \"status\": \"active\"\n}\n" + } + } + } + }, + "/organization/projects/{project_id}": { + "get": { + "summary": "Retrieves a project.", + "operationId": "retrieve-project", + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve project", + "group": "administration", + "description": "Retrieve a project.", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"proj_abc\",\n \"object\": \"organization.project\",\n \"name\": \"Project example\",\n \"created_at\": 1711471533,\n \"archived_at\": null,\n \"status\": \"active\"\n}\n" + } + } + }, + "post": { + "summary": "Modifies a project in the organization.", + "operationId": "modify-project", + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The project update request payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Project updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "400": { + "description": "Error response when updating the default project.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Modify project", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Project DEF\"\n }'\n" + } + } + } + } + }, + "/organization/projects/{project_id}/api_keys": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Returns a list of API keys in the project.", + "operationId": "list-project-api-keys", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "owner_project_access", + "in": "query", + "description": "Filter API keys by whether the owner currently has effective access to the project. Use `active` for owners with access, `inactive` for owners without access, or `any` for all enabled project API keys. If omitted, the endpoint applies its existing membership-based visibility rules, which may exclude some enabled keys.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "active", + "inactive", + "any" + ] + } + } + ], + "responses": { + "200": { + "description": "Project API keys listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectApiKeyListResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List project API keys", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/api_keys?after=key_abc&limit=20&owner_project_access=any \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"organization.project.api_key\",\n \"redacted_value\": \"sk-abc...def\",\n \"name\": \"My API Key\",\n \"created_at\": 1711471533,\n \"last_used_at\": 1711471534,\n \"id\": \"key_abc\",\n \"owner_project_access\": \"active\",\n \"owner\": {\n \"type\": \"user\",\n \"user\": {\n \"id\": \"user_abc\",\n \"name\": \"First Last\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"created_at\": 1711471533\n }\n }\n }\n ],\n \"first_id\": \"key_abc\",\n \"last_id\": \"key_xyz\",\n \"has_more\": false\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/api_keys/{api_key_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves an API key in the project.", + "operationId": "retrieve-project-api-key", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api_key_id", + "in": "path", + "description": "The ID of the API key.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project API key retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectApiKey" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve project API key", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/api_keys/key_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.project.api_key\",\n \"redacted_value\": \"sk-abc...def\",\n \"name\": \"My API Key\",\n \"created_at\": 1711471533,\n \"last_used_at\": 1711471534,\n \"id\": \"key_abc\",\n \"owner_project_access\": \"active\",\n \"owner\": {\n \"type\": \"user\",\n \"user\": {\n \"id\": \"user_abc\",\n \"name\": \"First Last\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"created_at\": 1711471533\n }\n }\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes an API key from the project.\n\nReturns confirmation of the key deletion, or an error if the key belonged to\na service account.\n", + "operationId": "delete-project-api-key", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api_key_id", + "in": "path", + "description": "The ID of the API key.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project API key deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectApiKeyDeleteResponse" + } + } + } + }, + "400": { + "description": "Error response for various conditions.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete project API key", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/projects/proj_abc/api_keys/key_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.project.api_key.deleted\",\n \"id\": \"key_abc\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/archive": { + "post": { + "summary": "Archives a project in the organization. Archived projects cannot be used or updated.", + "operationId": "archive-project", + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project archived successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Archive project", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/archive \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"proj_abc\",\n \"object\": \"organization.project\",\n \"name\": \"Project DEF\",\n \"created_at\": 1711471533,\n \"archived_at\": 1711471533,\n \"status\": \"archived\"\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/certificates": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "List certificates for this project.", + "operationId": "listProjectCertificates", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order.\n", + "schema": { + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ] + } + } + ], + "responses": { + "200": { + "description": "Certificates listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListProjectCertificatesResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List project certificates", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/certificates \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"organization.project.certificate\",\n \"id\": \"cert_abc\",\n \"name\": \"My Example Certificate\",\n \"active\": true,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n ],\n \"first_id\": \"cert_abc\",\n \"last_id\": \"cert_abc\",\n \"has_more\": false\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/certificates/activate": { + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Activate certificates at the project level.\n\nYou can atomically and idempotently activate up to 10 certificates at a time.\n", + "operationId": "activateProjectCertificates", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The certificate activation payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToggleCertificatesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Certificates activated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationProjectCertificateActivationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Activate certificates for project", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/certificates/activate \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n \"certificate_ids\": [\"cert_abc\", \"cert_def\"]\n}'\n" + }, + "response": "{\n \"object\": \"organization.project.certificate.activation\",\n \"data\": [\n {\n \"object\": \"organization.project.certificate\",\n \"id\": \"cert_abc\",\n \"name\": \"My Example Certificate\",\n \"active\": true,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n {\n \"object\": \"organization.project.certificate\",\n \"id\": \"cert_def\",\n \"name\": \"My Example Certificate 2\",\n \"active\": true,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n ],\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/certificates/deactivate": { + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deactivate certificates at the project level. You can atomically and \nidempotently deactivate up to 10 certificates at a time.\n", + "operationId": "deactivateProjectCertificates", + "tags": [ + "Certificates" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The certificate deactivation payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToggleCertificatesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Certificates deactivated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationProjectCertificateDeactivationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Deactivate certificates for project", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/certificates/deactivate \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n \"certificate_ids\": [\"cert_abc\", \"cert_def\"]\n}'\n" + }, + "response": "{\n \"object\": \"organization.project.certificate.deactivation\",\n \"data\": [\n {\n \"object\": \"organization.project.certificate\",\n \"id\": \"cert_abc\",\n \"name\": \"My Example Certificate\",\n \"active\": false,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n {\n \"object\": \"organization.project.certificate\",\n \"id\": \"cert_def\",\n \"name\": \"My Example Certificate 2\",\n \"active\": false,\n \"created_at\": 1234567,\n \"certificate_details\": {\n \"valid_at\": 12345667,\n \"expires_at\": 12345678\n }\n },\n ],\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/data_retention": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves project data retention controls.", + "operationId": "retrieve-project-data-retention", + "tags": [ + "Data retention" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to inspect.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project data retention controls retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectDataRetention" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve project data retention", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/data_retention \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"project.data_retention\",\n \"type\": \"organization_default\"\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates project data retention controls.", + "operationId": "update-project-data-retention", + "tags": [ + "Data retention" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The desired project data retention setting.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProjectDataRetentionBody" + } + } + } + }, + "responses": { + "200": { + "description": "Project data retention controls updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectDataRetention" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update project data retention", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/data_retention \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"retention_type\": \"modified_abuse_monitoring\"\n }'\n" + }, + "response": "{\n \"object\": \"project.data_retention\",\n \"type\": \"modified_abuse_monitoring\"\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/groups": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists the groups that have access to a project.", + "operationId": "list-project-groups", + "tags": [ + "Project groups" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of project groups to return. Defaults to 20.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "Cursor for pagination. Provide the ID of the last group from the previous response to fetch the next page.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for the returned groups.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + } + ], + "responses": { + "200": { + "description": "Project groups listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectGroupListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List project groups", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc123/groups?limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"project.group\",\n \"project_id\": \"proj_abc123\",\n \"group_id\": \"group_01J1F8ABCDXYZ\",\n \"group_name\": \"Support Team\",\n \"created_at\": 1711471533\n }\n ],\n \"has_more\": false,\n \"next\": null\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Grants a group access to a project.", + "operationId": "add-project-group", + "tags": [ + "Project groups" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Identifies the group and role to assign to the project.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InviteProjectGroupBody" + } + } + } + }, + "responses": { + "200": { + "description": "Group granted access to the project successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectGroup" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Add project group", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc123/groups \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"group_id\": \"group_01J1F8ABCDXYZ\",\n \"role\": \"role_01J1F8PROJ\"\n }'\n" + }, + "response": "{\n \"object\": \"project.group\",\n \"project_id\": \"proj_abc123\",\n \"group_id\": \"group_01J1F8ABCDXYZ\",\n \"group_name\": \"Support Team\",\n \"created_at\": 1711471533\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/groups/{group_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a project's group.", + "operationId": "retrieve-project-group", + "tags": [ + "Project groups" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to retrieve.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group_type", + "in": "query", + "description": "The type of group to retrieve.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "group", + "tenant_group" + ], + "default": "group" + } + } + ], + "responses": { + "200": { + "description": "Project group retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectGroup" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve project group", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc123/groups/group_01J1F8ABCDXYZ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"project.group\",\n \"project_id\": \"proj_abc123\",\n \"group_id\": \"group_01J1F8ABCDXYZ\",\n \"group_name\": \"Support Team\",\n \"group_type\": \"group\",\n \"created_at\": 1711471533\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Revokes a group's access to a project.", + "operationId": "remove-project-group", + "tags": [ + "Project groups" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to remove from the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Group removed from the project successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectGroupDeletedResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Remove project group", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/projects/proj_abc123/groups/group_01J1F8ABCDXYZ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"project.group.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/hosted_tool_permissions": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Returns hosted tool permissions for a project.", + "operationId": "retrieve-project-hosted-tool-permissions", + "tags": [ + "Hosted tools" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project hosted tool permissions retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectHostedToolPermissions" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve project hosted tool permissions", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/hosted_tool_permissions \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"file_search\": {\n \"enabled\": true\n },\n \"web_search\": {\n \"enabled\": true\n },\n \"image_generation\": {\n \"enabled\": true\n },\n \"mcp\": {\n \"enabled\": true\n },\n \"code_interpreter\": {\n \"enabled\": true\n }\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates hosted tool permissions for a project.", + "operationId": "update-project-hosted-tool-permissions", + "tags": [ + "Hosted tools" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The project hosted tool permissions update request payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectHostedToolPermissionsUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Project hosted tool permissions updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectHostedToolPermissions" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Modify project hosted tool permissions", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/hosted_tool_permissions \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"file_search\": {\n \"enabled\": true\n },\n \"image_generation\": {\n \"enabled\": false\n }\n }'\n" + }, + "response": "{\n \"file_search\": {\n \"enabled\": true\n },\n \"web_search\": {\n \"enabled\": true\n },\n \"image_generation\": {\n \"enabled\": false\n },\n \"mcp\": {\n \"enabled\": true\n },\n \"code_interpreter\": {\n \"enabled\": true\n }\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/model_permissions": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Returns model permissions for a project.", + "operationId": "retrieve-project-model-permissions", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project model permissions retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectModelPermissions" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve project model permissions", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/model_permissions \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"project.model_permissions\",\n \"mode\": \"allow_list\",\n \"model_ids\": [\n \"gpt-6-astra\",\n \"o3\"\n ]\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates model permissions for a project.", + "operationId": "update-project-model-permissions", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The project model permissions update request payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectModelPermissionsUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Project model permissions updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectModelPermissions" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Modify project model permissions", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/model_permissions \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"mode\": \"deny_list\",\n \"model_ids\": [\n \"o3\"\n ]\n }'\n" + }, + "response": "{\n \"object\": \"project.model_permissions\",\n \"mode\": \"deny_list\",\n \"model_ids\": [\n \"o3\"\n ]\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes model permissions for a project.", + "operationId": "delete-project-model-permissions", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project model permissions deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectModelPermissionsDeleteResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete project model permissions", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/projects/proj_abc/model_permissions \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"project.model_permissions.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/rate_limits": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Returns the rate limits per model for a project.", + "operationId": "list-project-rate-limits", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. The default is 100.\n", + "required": false, + "schema": { + "type": "integer", + "default": 100 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "before", + "in": "query", + "description": "A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, beginning with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project rate limits listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRateLimitListResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List project rate limits", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/rate_limits?after=rl_xxx&limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"project.rate_limit\",\n \"id\": \"rl-ada\",\n \"model\": \"ada\",\n \"max_requests_per_1_minute\": 600,\n \"max_tokens_per_1_minute\": 150000,\n \"max_images_per_1_minute\": 10\n }\n ],\n \"first_id\": \"rl-ada\",\n \"last_id\": \"rl-ada\",\n \"has_more\": false\n}\n", + "error_response": "{\n \"code\": 404,\n \"message\": \"The project {project_id} was not found\"\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/rate_limits/{rate_limit_id}": { + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates a project rate limit.", + "operationId": "update-project-rate-limits", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "rate_limit_id", + "in": "path", + "description": "The ID of the rate limit.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The project rate limit update request payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRateLimitUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Project rate limit updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRateLimit" + } + } + } + }, + "400": { + "description": "Error response for various conditions.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Modify project rate limit", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/rate_limits/rl_xxx \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"max_requests_per_1_minute\": 500\n }'\n" + }, + "response": "{\n \"object\": \"project.rate_limit\",\n \"id\": \"rl-ada\",\n \"model\": \"ada\",\n \"max_requests_per_1_minute\": 600,\n \"max_tokens_per_1_minute\": 150000,\n \"max_images_per_1_minute\": 10\n }\n", + "error_response": "{\n \"code\": 404,\n \"message\": \"The project {project_id} was not found\"\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/service_accounts": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Returns a list of service accounts in the project.", + "operationId": "list-project-service-accounts", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project service accounts listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectServiceAccountListResponse" + } + } + } + }, + "400": { + "description": "Error response when project is archived.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List project service accounts", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/service_accounts?after=custom_id&limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"organization.project.service_account\",\n \"id\": \"svc_acct_abc\",\n \"name\": \"Service Account\",\n \"role\": \"owner\",\n \"created_at\": 1711471533\n }\n ],\n \"first_id\": \"svc_acct_abc\",\n \"last_id\": \"svc_acct_xyz\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Creates a new service account in the project. By default, this also returns an unredacted API key for the service account.", + "operationId": "create-project-service-account", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The project service account create request payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectServiceAccountCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Project service account created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectServiceAccountCreateResponse" + } + } + } + }, + "400": { + "description": "Error response when project is archived.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create project service account", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/service_accounts \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Production App\"\n }'\n" + }, + "response": "{\n \"object\": \"organization.project.service_account\",\n \"id\": \"svc_acct_abc\",\n \"name\": \"Production App\",\n \"role\": \"member\",\n \"created_at\": 1711471533,\n \"api_key\": {\n \"object\": \"organization.project.service_account.api_key\",\n \"value\": \"sk-abcdefghijklmnop123\",\n \"name\": \"Secret Key\",\n \"created_at\": 1711471533,\n \"id\": \"key_abc\"\n }\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/service_accounts/{service_account_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a service account in the project.", + "operationId": "retrieve-project-service-account", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "service_account_id", + "in": "path", + "description": "The ID of the service account.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project service account retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectServiceAccount" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve project service account", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/service_accounts/svc_acct_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.project.service_account\",\n \"id\": \"svc_acct_abc\",\n \"name\": \"Service Account\",\n \"role\": \"owner\",\n \"created_at\": 1711471533\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates a service account in the project.", + "operationId": "update-project-service-account", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "service_account_id", + "in": "path", + "description": "The ID of the service account.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Fields to update on the service account.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProjectServiceAccountBody" + } + } + } + }, + "responses": { + "200": { + "description": "Project service account updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectServiceAccount" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update project service account", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/service_accounts/svc_acct_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Updated service account\",\n \"role\": \"member\"\n }'\n" + }, + "response": "{\n \"object\": \"organization.project.service_account\",\n \"id\": \"svc_acct_abc\",\n \"name\": \"Updated service account\",\n \"role\": \"member\",\n \"created_at\": 1711471533\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes a service account from the project.\n\nReturns confirmation of service account deletion, or an error if the project\nis archived (archived projects have no service accounts).\n", + "operationId": "delete-project-service-account", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "service_account_id", + "in": "path", + "description": "The ID of the service account.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project service account deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectServiceAccountDeleteResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete project service account", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/projects/proj_abc/service_accounts/svc_acct_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.project.service_account.deleted\",\n \"id\": \"svc_acct_abc\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/spend_alerts": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists project spend alerts.", + "operationId": "list-project-spend-alerts", + "tags": [ + "Spend alerts" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of spend alerts to return. Defaults to 20.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for the returned spend alerts.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + }, + { + "name": "after", + "in": "query", + "description": "Cursor for pagination. Provide the ID of the last spend alert from the previous response to fetch the next page.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "before", + "in": "query", + "description": "Cursor for pagination. Provide the ID of the first spend alert from the previous response to fetch the previous page.", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project spend alerts listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectSpendAlertListResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List project spend alerts", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/spend_alerts?limit=20&order=asc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"alert_abc123\",\n \"object\": \"project.spend_alert\",\n \"threshold_amount\": 100000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n }\n ],\n \"first_id\": \"alert_abc123\",\n \"last_id\": \"alert_abc123\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Creates a project spend alert.", + "operationId": "create-project-spend-alert", + "tags": [ + "Spend alerts" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Parameters for the project spend alert you want to create.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSpendAlertBody" + } + } + } + }, + "responses": { + "200": { + "description": "Project spend alert created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectSpendAlert" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create project spend alert", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/spend_alerts \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"threshold_amount\": 100000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n }'\n" + }, + "response": "{\n \"id\": \"alert_abc123\",\n \"object\": \"project.spend_alert\",\n \"threshold_amount\": 100000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/spend_alerts/{alert_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a project spend alert.", + "operationId": "retrieve-project-spend-alert", + "tags": [ + "Spend alerts" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alert_id", + "in": "path", + "description": "The ID of the spend alert to retrieve.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project spend alert retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectSpendAlert" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve project spend alert", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/spend_alerts/alert_abc123 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"alert_abc123\",\n \"object\": \"project.spend_alert\",\n \"threshold_amount\": 150000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates a project spend alert.", + "operationId": "update-project-spend-alert", + "tags": [ + "Spend alerts" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alert_id", + "in": "path", + "description": "The ID of the spend alert to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Fields to update on the project spend alert.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSpendAlertBody" + } + } + } + }, + "responses": { + "200": { + "description": "Project spend alert updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectSpendAlert" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update project spend alert", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/spend_alerts/alert_abc123 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"threshold_amount\": 150000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n }'\n" + }, + "response": "{\n \"id\": \"alert_abc123\",\n \"object\": \"project.spend_alert\",\n \"threshold_amount\": 150000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes a project spend alert.", + "operationId": "delete-project-spend-alert", + "tags": [ + "Spend alerts" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alert_id", + "in": "path", + "description": "The ID of the spend alert to delete.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project spend alert deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectSpendAlertDeletedResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete project spend alert", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/projects/proj_abc/spend_alerts/alert_abc123 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"alert_abc123\",\n \"object\": \"project.spend_alert.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/users": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Returns a list of users in the project.", + "operationId": "list-project-users", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project users listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUserListResponse" + } + } + } + }, + "400": { + "description": "Error response when project is archived.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List project users", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/users?after=user_abc&limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"organization.project.user\",\n \"id\": \"user_abc\",\n \"name\": \"First Last\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"added_at\": 1711471533\n }\n ],\n \"first_id\": \"user-abc\",\n \"last_id\": \"user-xyz\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Adds a user to the project. Users must already be members of the organization to be added to a project.", + "operationId": "create-project-user", + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Projects" + ], + "requestBody": { + "description": "The project user create request payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUserCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "User added to project successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUser" + } + } + } + }, + "400": { + "description": "Error response for various conditions.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create project user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/users \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"user_id\": \"user_abc\",\n \"role\": \"member\"\n }'\n" + }, + "response": "{\n \"object\": \"organization.project.user\",\n \"id\": \"user_abc\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"added_at\": 1711471533\n}\n" + } + } + } + }, + "/organization/projects/{project_id}/users/{user_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a user in the project.", + "operationId": "retrieve-project-user", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "description": "The ID of the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project user retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUser" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve project user", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/projects/proj_abc/users/user_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.project.user\",\n \"id\": \"user_abc\",\n \"name\": \"First Last\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"added_at\": 1711471533\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Modifies a user's role in the project.", + "operationId": "modify-project-user", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "description": "The ID of the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The project user update request payload.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUserUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Project user's role updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUser" + } + } + } + }, + "400": { + "description": "Error response for various conditions.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Modify project user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/users/user_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role\": \"owner\"\n }'\n" + }, + "response": "{\n \"object\": \"organization.project.user\",\n \"id\": \"user_abc\",\n \"name\": \"First Last\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"added_at\": 1711471533\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes a user from the project.\n\nReturns confirmation of project user deletion, or an error if the project is\narchived (archived projects have no users).\n", + "operationId": "delete-project-user", + "tags": [ + "Projects" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "description": "The ID of the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project user deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectUserDeleteResponse" + } + } + } + }, + "400": { + "description": "Error response for various conditions.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete project user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/projects/proj_abc/users/user_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.project.user.deleted\",\n \"id\": \"user_abc\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/roles": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists the roles configured for the organization.", + "operationId": "list-roles", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "A limit on the number of roles to return. Defaults to 1000.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000, + "default": 1000 + } + }, + { + "name": "after", + "in": "query", + "description": "Cursor for pagination. Provide the value from the previous response's `next` field to continue listing roles.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for the returned roles.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + } + ], + "responses": { + "200": { + "description": "Roles listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicRoleListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List organization roles", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/roles?limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"role\",\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"description\": \"Allows managing organization groups\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false\n }\n ],\n \"has_more\": false,\n \"next\": null\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Creates a custom role for the organization.", + "operationId": "create-role", + "tags": [ + "Roles" + ], + "requestBody": { + "description": "Parameters for the role you want to create.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicCreateOrganizationRoleBody" + } + } + } + }, + "responses": { + "200": { + "description": "Role created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Role" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Create organization role", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role_name\": \"API Group Manager\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"description\": \"Allows managing organization groups\"\n }'\n" + }, + "response": "{\n \"object\": \"role\",\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"description\": \"Allows managing organization groups\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false\n}\n" + } + } + } + }, + "/organization/roles/{role_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves an organization role.", + "operationId": "retrieve-role", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "role_id", + "in": "path", + "description": "The ID of the role to retrieve.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Role retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Role" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve organization role", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/roles/role_01J1F8ROLE01 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"role\",\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"description\": \"Allows managing organization groups\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates an existing organization role.", + "operationId": "update-role", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "role_id", + "in": "path", + "description": "The ID of the role to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Fields to update on the role.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicUpdateOrganizationRoleBody" + } + } + } + }, + "responses": { + "200": { + "description": "Role updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Role" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Update organization role", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/roles/role_01J1F8ROLE01 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role_name\": \"API Group Manager\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"description\": \"Allows managing organization groups\"\n }'\n" + }, + "response": "{\n \"object\": \"role\",\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"description\": \"Allows managing organization groups\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes a custom role from the organization.", + "operationId": "delete-role", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "role_id", + "in": "path", + "description": "The ID of the role to delete.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Role deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleDeletedResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete organization role", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/roles/role_01J1F8ROLE01 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"role.deleted\",\n \"id\": \"role_01J1F8ROLE01\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/spend_alerts": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists organization spend alerts.", + "operationId": "list-organization-spend-alerts", + "tags": [ + "Spend alerts" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "A limit on the number of spend alerts to return. Defaults to 20.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for the returned spend alerts.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + }, + { + "name": "after", + "in": "query", + "description": "Cursor for pagination. Provide the ID of the last spend alert from the previous response to fetch the next page.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "before", + "in": "query", + "description": "Cursor for pagination. Provide the ID of the first spend alert from the previous response to fetch the previous page.", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization spend alerts listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationSpendAlertListResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List organization spend alerts", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/spend_alerts?limit=20&order=asc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"alert_abc123\",\n \"object\": \"organization.spend_alert\",\n \"threshold_amount\": 100000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n }\n ],\n \"first_id\": \"alert_abc123\",\n \"last_id\": \"alert_abc123\",\n \"has_more\": false\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Creates an organization spend alert.", + "operationId": "create-organization-spend-alert", + "tags": [ + "Spend alerts" + ], + "requestBody": { + "description": "Parameters for the organization spend alert you want to create.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSpendAlertBody" + } + } + } + }, + "responses": { + "200": { + "description": "Organization spend alert created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationSpendAlert" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create organization spend alert", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/spend_alerts \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"threshold_amount\": 100000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n }'\n" + }, + "response": "{\n \"id\": \"alert_abc123\",\n \"object\": \"organization.spend_alert\",\n \"threshold_amount\": 100000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n}\n" + } + } + } + }, + "/organization/spend_alerts/{alert_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves an organization spend alert.", + "operationId": "retrieve-organization-spend-alert", + "tags": [ + "Spend alerts" + ], + "parameters": [ + { + "name": "alert_id", + "in": "path", + "description": "The ID of the spend alert to retrieve.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization spend alert retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationSpendAlert" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve organization spend alert", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/spend_alerts/alert_abc123 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"alert_abc123\",\n \"object\": \"organization.spend_alert\",\n \"threshold_amount\": 150000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates an organization spend alert.", + "operationId": "update-organization-spend-alert", + "tags": [ + "Spend alerts" + ], + "parameters": [ + { + "name": "alert_id", + "in": "path", + "description": "The ID of the spend alert to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Fields to update on the organization spend alert.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSpendAlertBody" + } + } + } + }, + "responses": { + "200": { + "description": "Organization spend alert updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationSpendAlert" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Update organization spend alert", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/spend_alerts/alert_abc123 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"threshold_amount\": 150000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n }'\n" + }, + "response": "{\n \"id\": \"alert_abc123\",\n \"object\": \"organization.spend_alert\",\n \"threshold_amount\": 150000,\n \"currency\": \"USD\",\n \"interval\": \"month\",\n \"notification_channel\": {\n \"type\": \"email\",\n \"recipients\": [\"finance@example.com\"],\n \"subject_prefix\": \"OpenAI spend alert\"\n }\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes an organization spend alert.", + "operationId": "delete-organization-spend-alert", + "tags": [ + "Spend alerts" + ], + "parameters": [ + { + "name": "alert_id", + "in": "path", + "description": "The ID of the spend alert to delete.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization spend alert deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationSpendAlertDeletedResource" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete organization spend alert", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/spend_alerts/alert_abc123 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"alert_abc123\",\n \"object\": \"organization.spend_alert.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/usage/audio_speeches": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get audio speeches usage details for the organization.", + "operationId": "usage-audio-speeches", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "user_ids", + "in": "query", + "description": "Return only usage for these users.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "api_key_ids", + "in": "query", + "description": "Return only usage for these API keys.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "models", + "in": "query", + "description": "Return only usage for these models.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`, `user_id`, `api_key_id`, `model` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id", + "user_id", + "api_key_id", + "model" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Audio speeches", + "group": "usage-audio-speeches", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/audio_speeches?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.audio_speeches.result\",\n \"characters\": 45,\n \"num_model_requests\": 1,\n \"project_id\": null,\n \"user_id\": null,\n \"api_key_id\": null,\n \"model\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/usage/audio_transcriptions": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get audio transcriptions usage details for the organization.", + "operationId": "usage-audio-transcriptions", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "user_ids", + "in": "query", + "description": "Return only usage for these users.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "api_key_ids", + "in": "query", + "description": "Return only usage for these API keys.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "models", + "in": "query", + "description": "Return only usage for these models.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`, `user_id`, `api_key_id`, `model` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id", + "user_id", + "api_key_id", + "model" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Audio transcriptions", + "group": "usage-audio-transcriptions", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/audio_transcriptions?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.audio_transcriptions.result\",\n \"seconds\": 20,\n \"num_model_requests\": 1,\n \"project_id\": null,\n \"user_id\": null,\n \"api_key_id\": null,\n \"model\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/usage/code_interpreter_sessions": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get code interpreter sessions usage details for the organization.", + "operationId": "usage-code-interpreter-sessions", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Code interpreter sessions", + "group": "usage-code-interpreter-sessions", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/code_interpreter_sessions?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.code_interpreter_sessions.result\",\n \"num_sessions\": 1,\n \"project_id\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/usage/completions": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get completions usage details for the organization.", + "operationId": "usage-completions", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "user_ids", + "in": "query", + "description": "Return only usage for these users.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "api_key_ids", + "in": "query", + "description": "Return only usage for these API keys.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "models", + "in": "query", + "description": "Return only usage for these models.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "batch", + "in": "query", + "description": "If `true`, return batch jobs only. If `false`, return non-batch jobs only. By default, return both.\n", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`, `user_id`, `api_key_id`, `model`, `batch`, `service_tier` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id", + "user_id", + "api_key_id", + "model", + "batch", + "service_tier" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Completions", + "group": "usage-completions", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/completions?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.completions.result\",\n \"input_tokens\": 1000,\n \"input_cached_tokens\": 400,\n \"input_cache_write_tokens\": 100,\n \"input_uncached_tokens\": 500,\n \"output_tokens\": 500,\n \"input_text_tokens\": 400,\n \"output_text_tokens\": 400,\n \"input_cached_text_tokens\": 300,\n \"input_audio_tokens\": 50,\n \"input_cached_audio_tokens\": 50,\n \"output_audio_tokens\": 50,\n \"input_image_tokens\": 50,\n \"input_cached_image_tokens\": 50,\n \"output_image_tokens\": 50,\n \"num_model_requests\": 5,\n \"project_id\": null,\n \"user_id\": null,\n \"api_key_id\": null,\n \"model\": null,\n \"batch\": null,\n \"service_tier\": null\n }\n ]\n }\n ],\n \"has_more\": true,\n \"next_page\": \"page_AAAAAGdGxdEiJdKOAAAAAGcqsYA=\"\n}\n" + } + } + } + }, + "/organization/usage/embeddings": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get embeddings usage details for the organization.", + "operationId": "usage-embeddings", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "user_ids", + "in": "query", + "description": "Return only usage for these users.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "api_key_ids", + "in": "query", + "description": "Return only usage for these API keys.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "models", + "in": "query", + "description": "Return only usage for these models.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`, `user_id`, `api_key_id`, `model` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id", + "user_id", + "api_key_id", + "model" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Embeddings", + "group": "usage-embeddings", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/embeddings?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.embeddings.result\",\n \"input_tokens\": 16,\n \"num_model_requests\": 2,\n \"project_id\": null,\n \"user_id\": null,\n \"api_key_id\": null,\n \"model\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/usage/file_search_calls": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get file search calls usage details for the organization.", + "operationId": "usage-file-search-calls", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "user_ids", + "in": "query", + "description": "Return only usage for these users.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "api_key_ids", + "in": "query", + "description": "Return only usage for these API keys.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "vector_store_ids", + "in": "query", + "description": "Return only usage for these vector stores.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`, `user_id`, `api_key_id`, `vector_store_id` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id", + "user_id", + "api_key_id", + "vector_store_id" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "File search calls", + "group": "usage-file-search-calls", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/file_search_calls?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.file_searches.result\",\n \"num_requests\": 2,\n \"project_id\": null,\n \"user_id\": null,\n \"api_key_id\": null,\n \"vector_store_id\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/usage/images": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get images usage details for the organization.", + "operationId": "usage-images", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "sources", + "in": "query", + "description": "Return only usages for these sources. Possible values are `image.generation`, `image.edit`, `image.variation` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image.generation", + "image.edit", + "image.variation" + ] + } + } + }, + { + "name": "sizes", + "in": "query", + "description": "Return only usages for these image sizes. Possible values are `256x256`, `512x512`, `1024x1024`, `1792x1792`, `1024x1792` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "256x256", + "512x512", + "1024x1024", + "1792x1792", + "1024x1792" + ] + } + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "user_ids", + "in": "query", + "description": "Return only usage for these users.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "api_key_ids", + "in": "query", + "description": "Return only usage for these API keys.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "models", + "in": "query", + "description": "Return only usage for these models.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`, `user_id`, `api_key_id`, `model`, `size`, `source` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id", + "user_id", + "api_key_id", + "model", + "size", + "source" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Images", + "group": "usage-images", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/images?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.images.result\",\n \"images\": 2,\n \"num_model_requests\": 2,\n \"size\": null,\n \"source\": null,\n \"project_id\": null,\n \"user_id\": null,\n \"api_key_id\": null,\n \"model\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/usage/moderations": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get moderations usage details for the organization.", + "operationId": "usage-moderations", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "user_ids", + "in": "query", + "description": "Return only usage for these users.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "api_key_ids", + "in": "query", + "description": "Return only usage for these API keys.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "models", + "in": "query", + "description": "Return only usage for these models.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`, `user_id`, `api_key_id`, `model` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id", + "user_id", + "api_key_id", + "model" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Moderations", + "group": "usage-moderations", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/moderations?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.moderations.result\",\n \"input_tokens\": 16,\n \"num_model_requests\": 2,\n \"project_id\": null,\n \"user_id\": null,\n \"api_key_id\": null,\n \"model\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/usage/vector_stores": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get vector stores usage details for the organization.", + "operationId": "usage-vector-stores", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Vector stores", + "group": "usage-vector-stores", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/vector_stores?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.vector_stores.result\",\n \"usage_bytes\": 1024,\n \"project_id\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/usage/web_search_calls": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Get web search calls usage details for the organization.", + "operationId": "usage-web-search-calls", + "tags": [ + "Usage" + ], + "parameters": [ + { + "name": "start_time", + "in": "query", + "description": "Start time (Unix seconds) of the query time range, inclusive.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "end_time", + "in": "query", + "description": "End time (Unix seconds) of the query time range, exclusive.", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "bucket_width", + "in": "query", + "description": "Width of each time bucket in response. Currently `1m`, `1h` and `1d` are supported, default to `1d`.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1m", + "1h", + "1d" + ], + "default": "1d" + } + }, + { + "name": "project_ids", + "in": "query", + "description": "Return only usage for these projects.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "user_ids", + "in": "query", + "description": "Return only usage for these users.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "api_key_ids", + "in": "query", + "description": "Return only usage for these API keys.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "models", + "in": "query", + "description": "Return only usage for these models.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "context_levels", + "in": "query", + "description": "Return only web search usage for these context levels.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + } + } + }, + { + "name": "group_by", + "in": "query", + "description": "Group the usage data by the specified fields. Support fields include `project_id`, `user_id`, `api_key_id`, `model`, `context_level` or any combination of them.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "project_id", + "user_id", + "api_key_id", + "model", + "context_level" + ] + } + } + }, + { + "name": "limit", + "in": "query", + "description": "Specifies the number of buckets to return.\n- `bucket_width=1d`: default: 7, max: 31\n- `bucket_width=1h`: default: 24, max: 168\n- `bucket_width=1m`: default: 60, max: 1440\n", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "A cursor for use in pagination. Corresponding to the `next_page` field from the previous response.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Usage data retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Web search calls", + "group": "usage-web-search-calls", + "examples": { + "request": { + "curl": "curl \"https://api.openai.com/v1/organization/usage/web_search_calls?start_time=1730419200&limit=1\" \\\n-H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n-H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"page\",\n \"data\": [\n {\n \"object\": \"bucket\",\n \"start_time\": 1730419200,\n \"end_time\": 1730505600,\n \"results\": [\n {\n \"object\": \"organization.usage.web_searches.result\",\n \"num_model_requests\": 2,\n \"num_requests\": 2,\n \"project_id\": null,\n \"user_id\": null,\n \"api_key_id\": null,\n \"model\": null,\n \"context_level\": null\n }\n ]\n }\n ],\n \"has_more\": false,\n \"next_page\": null\n}\n" + } + } + } + }, + "/organization/users": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists all of the users in the organization.", + "operationId": "list-users", + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.\n", + "required": false, + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "after", + "in": "query", + "description": "A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "emails", + "in": "query", + "description": "Filter by the email address of users.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "Users listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserListResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "List users", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/users?after=user_abc&limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"organization.user\",\n \"id\": \"user_abc\",\n \"name\": \"First Last\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"added_at\": 1711471533\n }\n ],\n \"first_id\": \"user-abc\",\n \"last_id\": \"user-xyz\",\n \"has_more\": false\n}\n" + } + } + } + }, + "/organization/users/{user_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a user by their identifier.", + "operationId": "retrieve-user", + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "description": "The ID of the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "User retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Retrieve user", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/users/user_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.user\",\n \"id\": \"user_abc\",\n \"name\": \"First Last\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"added_at\": 1711471533\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Modifies a user's role in the organization.", + "operationId": "modify-user", + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "description": "The ID of the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The new user role to modify. This must be one of `owner` or `member`.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRoleUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "User role updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Modify user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/users/user_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role\": \"owner\"\n }'\n" + }, + "response": "{\n \"object\": \"organization.user\",\n \"id\": \"user_abc\",\n \"name\": \"First Last\",\n \"email\": \"user@example.com\",\n \"role\": \"owner\",\n \"added_at\": 1711471533\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes a user from the organization.", + "operationId": "delete-user", + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "description": "The ID of the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "User deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDeleteResponse" + } + } + } + } + }, + "x-oaiMeta": { + "name": "Delete user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/users/user_abc \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"organization.user.deleted\",\n \"id\": \"user_abc\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/organization/users/{user_id}/roles": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists the organization roles assigned to a user within the organization.", + "operationId": "list-user-role-assignments", + "tags": [ + "User organization role assignments" + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "description": "The ID of the user to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of organization role assignments to return.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000 + } + }, + { + "name": "after", + "in": "query", + "description": "Cursor for pagination. Provide the value from the previous response's `next` field to continue listing organization roles.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for the returned organization roles.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + ], + "responses": { + "200": { + "description": "User organization role assignments listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List user organization role assignments", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/users/user_abc123/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false,\n \"description\": \"Allows managing organization groups\",\n \"created_at\": 1711471533,\n \"updated_at\": 1711472599,\n \"created_by\": \"user_abc123\",\n \"created_by_user_obj\": {\n \"id\": \"user_abc123\",\n \"name\": \"Ada Lovelace\",\n \"email\": \"ada@example.com\"\n },\n \"metadata\": {}\n }\n ],\n \"has_more\": false,\n \"next\": null\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Assigns an organization role to a user within the organization.", + "operationId": "assign-user-role", + "tags": [ + "User organization role assignments" + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "description": "The ID of the user that should receive the organization role.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Identifies the organization role to assign to the user.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicAssignOrganizationGroupRoleBody" + } + } + } + }, + "responses": { + "200": { + "description": "Organization role assigned to the user successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRoleAssignment" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Assign organization role to user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/organization/users/user_abc123/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role_id\": \"role_01J1F8ROLE01\"\n }'\n" + }, + "response": "{\n \"object\": \"user.role\",\n \"user\": {\n \"object\": \"organization.user\",\n \"id\": \"user_abc123\",\n \"name\": \"Ada Lovelace\",\n \"email\": \"ada@example.com\",\n \"role\": \"owner\",\n \"added_at\": 1711470000\n },\n \"role\": {\n \"object\": \"role\",\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"description\": \"Allows managing organization groups\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false\n }\n}\n" + } + } + } + }, + "/organization/users/{user_id}/roles/{role_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves an organization role assigned to a user.", + "operationId": "retrieve-user-role", + "tags": [ + "User organization role assignments" + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "description": "The ID of the user to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the organization role to retrieve for the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization role retrieved for the user successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignedRoleDetails" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve user organization role", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/organization/users/user_abc123/roles/role_01J1F8ROLE01 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"role_01J1F8ROLE01\",\n \"name\": \"API Group Manager\",\n \"permissions\": [\n \"api.groups.read\",\n \"api.groups.write\"\n ],\n \"resource_type\": \"api.organization\",\n \"predefined_role\": false,\n \"description\": \"Allows managing organization groups\",\n \"created_at\": 1711471533,\n \"updated_at\": 1711472599,\n \"created_by\": \"user_abc123\",\n \"created_by_user_obj\": null,\n \"metadata\": {},\n \"assignment_sources\": null\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Unassigns an organization role from a user within the organization.", + "operationId": "unassign-user-role", + "tags": [ + "User organization role assignments" + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "description": "The ID of the user to modify.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the organization role to remove from the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization role unassigned from the user successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedRoleAssignmentResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Unassign organization role from user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/organization/users/user_abc123/roles/role_01J1F8ROLE01 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"user.role.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/projects/{project_id}/groups/{group_id}/roles": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists the project roles assigned to a group within a project.", + "operationId": "list-project-group-role-assignments", + "tags": [ + "Project group role assignments" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of project role assignments to return.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000 + } + }, + { + "name": "after", + "in": "query", + "description": "Cursor for pagination. Provide the value from the previous response's `next` field to continue listing project roles.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for the returned project roles.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + ], + "responses": { + "200": { + "description": "Project group role assignments listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List project group role assignments", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/projects/proj_abc123/groups/group_01J1F8ABCDXYZ/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false,\n \"description\": \"Allows managing API keys for the project\",\n \"created_at\": 1711471533,\n \"updated_at\": 1711472599,\n \"created_by\": \"user_abc123\",\n \"created_by_user_obj\": {\n \"id\": \"user_abc123\",\n \"name\": \"Ada Lovelace\",\n \"email\": \"ada@example.com\"\n },\n \"metadata\": {}\n }\n ],\n \"has_more\": false,\n \"next\": null\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Assigns a project role to a group within a project.", + "operationId": "assign-project-group-role", + "tags": [ + "Project group role assignments" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group_id", + "in": "path", + "description": "The ID of the group that should receive the project role.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Identifies the project role to assign to the group.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicAssignOrganizationGroupRoleBody" + } + } + } + }, + "responses": { + "200": { + "description": "Project role assigned to the group successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupRoleAssignment" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Assign project role to group", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/projects/proj_abc123/groups/group_01J1F8ABCDXYZ/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role_id\": \"role_01J1F8PROJ\"\n }'\n" + }, + "response": "{\n \"object\": \"group.role\",\n \"group\": {\n \"object\": \"group\",\n \"id\": \"group_01J1F8ABCDXYZ\",\n \"name\": \"Support Team\",\n \"created_at\": 1711471533,\n \"scim_managed\": false\n },\n \"role\": {\n \"object\": \"role\",\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"description\": \"Allows managing API keys for the project\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false\n }\n}\n" + } + } + } + }, + "/projects/{project_id}/groups/{group_id}/roles/{role_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a project role assigned to a group.", + "operationId": "retrieve-project-group-role", + "tags": [ + "Project group role assignments" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group_id", + "in": "path", + "description": "The ID of the group to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the project role to retrieve for the group.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project role retrieved for the group successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignedRoleDetails" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve project group role", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/projects/proj_abc123/groups/group_01J1F8ABCDXYZ/roles/role_01J1F8PROJ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false,\n \"description\": \"Allows managing API keys for the project\",\n \"created_at\": 1711471533,\n \"updated_at\": 1711472599,\n \"created_by\": \"user_abc123\",\n \"created_by_user_obj\": null,\n \"metadata\": {},\n \"assignment_sources\": null\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Unassigns a project role from a group within a project.", + "operationId": "unassign-project-group-role", + "tags": [ + "Project group role assignments" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to modify.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group_id", + "in": "path", + "description": "The ID of the group whose project role assignment should be removed.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the project role to remove from the group.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project role unassigned from the group successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedRoleAssignmentResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Unassign project role from group", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/projects/proj_abc123/groups/group_01J1F8ABCDXYZ/roles/role_01J1F8PROJ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"group.role.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/projects/{project_id}/roles": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists the roles configured for a project.", + "operationId": "list-project-roles", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of roles to return. Defaults to 1000.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000, + "default": 1000 + } + }, + { + "name": "after", + "in": "query", + "description": "Cursor for pagination. Provide the value from the previous response's `next` field to continue listing roles.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for the returned roles.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "asc" + } + } + ], + "responses": { + "200": { + "description": "Project roles listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicRoleListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List project roles", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/projects/proj_abc123/roles?limit=20 \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": \"role\",\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"description\": \"Allows managing API keys for the project\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false\n }\n ],\n \"has_more\": false,\n \"next\": null\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Creates a custom role for a project.", + "operationId": "create-project-role", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Parameters for the project role you want to create.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicCreateOrganizationRoleBody" + } + } + } + }, + "responses": { + "200": { + "description": "Project role created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Role" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Create project role", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/projects/proj_abc123/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role_name\": \"API Project Key Manager\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"description\": \"Allows managing API keys for the project\"\n }'\n" + }, + "response": "{\n \"object\": \"role\",\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"description\": \"Allows managing API keys for the project\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false\n}\n" + } + } + } + }, + "/projects/{project_id}/roles/{role_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a project role.", + "operationId": "retrieve-project-role", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the role to retrieve.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project role retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Role" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve project role", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/projects/proj_abc123/roles/role_01J1F8PROJ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"role\",\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"description\": \"Allows managing API keys for the project\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Updates an existing project role.", + "operationId": "update-project-role", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the role to update.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Fields to update on the project role.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicUpdateOrganizationRoleBody" + } + } + } + }, + "responses": { + "200": { + "description": "Project role updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Role" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Update project role", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/projects/proj_abc123/roles/role_01J1F8PROJ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role_name\": \"API Project Key Manager\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"description\": \"Allows managing API keys for the project\"\n }'\n" + }, + "response": "{\n \"object\": \"role\",\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"description\": \"Allows managing API keys for the project\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Deletes a custom role from a project.", + "operationId": "delete-project-role", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the role to delete.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project role deleted successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleDeletedResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Delete project role", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/projects/proj_abc123/roles/role_01J1F8PROJ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"role.deleted\",\n \"id\": \"role_01J1F8PROJ\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/projects/{project_id}/users/{user_id}/roles": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Lists the project roles assigned to a user within a project.", + "operationId": "list-project-user-role-assignments", + "tags": [ + "Project user role assignments" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "description": "The ID of the user to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "A limit on the number of project role assignments to return.", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000 + } + }, + { + "name": "after", + "in": "query", + "description": "Cursor for pagination. Provide the value from the previous response's `next` field to continue listing project roles.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "order", + "in": "query", + "description": "Sort order for the returned project roles.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + ], + "responses": { + "200": { + "description": "Project user role assignments listed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleListResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "List project user role assignments", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/projects/proj_abc123/users/user_abc123/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false,\n \"description\": \"Allows managing API keys for the project\",\n \"created_at\": 1711471533,\n \"updated_at\": 1711472599,\n \"created_by\": \"user_abc123\",\n \"created_by_user_obj\": {\n \"id\": \"user_abc123\",\n \"name\": \"Ada Lovelace\",\n \"email\": \"ada@example.com\"\n },\n \"metadata\": {}\n }\n ],\n \"has_more\": false,\n \"next\": null\n}\n" + } + } + }, + "post": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Assigns a project role to a user within a project.", + "operationId": "assign-project-user-role", + "tags": [ + "Project user role assignments" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to update.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "description": "The ID of the user that should receive the project role.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Identifies the project role to assign to the user.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicAssignOrganizationGroupRoleBody" + } + } + } + }, + "responses": { + "200": { + "description": "Project role assigned to the user successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRoleAssignment" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Assign project role to user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/projects/proj_abc123/users/user_abc123/roles \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"role_id\": \"role_01J1F8PROJ\"\n }'\n" + }, + "response": "{\n \"object\": \"user.role\",\n \"user\": {\n \"object\": \"organization.user\",\n \"id\": \"user_abc123\",\n \"name\": \"Ada Lovelace\",\n \"email\": \"ada@example.com\",\n \"role\": \"owner\",\n \"added_at\": 1711470000\n },\n \"role\": {\n \"object\": \"role\",\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"description\": \"Allows managing API keys for the project\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false\n }\n}\n" + } + } + } + }, + "/projects/{project_id}/users/{user_id}/roles/{role_id}": { + "get": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Retrieves a project role assigned to a user.", + "operationId": "retrieve-project-user-role", + "tags": [ + "Project user role assignments" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "description": "The ID of the user to inspect.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the project role to retrieve for the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project role retrieved for the user successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignedRoleDetails" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Retrieve project user role", + "group": "administration", + "examples": { + "request": { + "curl": "curl https://api.openai.com/v1/projects/proj_abc123/users/user_abc123/roles/role_01J1F8PROJ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"id\": \"role_01J1F8PROJ\",\n \"name\": \"API Project Key Manager\",\n \"permissions\": [\n \"api.organization.projects.api_keys.read\",\n \"api.organization.projects.api_keys.write\"\n ],\n \"resource_type\": \"api.project\",\n \"predefined_role\": false,\n \"description\": \"Allows managing API keys for the project\",\n \"created_at\": 1711471533,\n \"updated_at\": 1711472599,\n \"created_by\": \"user_abc123\",\n \"created_by_user_obj\": null,\n \"metadata\": {},\n \"assignment_sources\": null\n}\n" + } + } + }, + "delete": { + "security": [ + { + "AdminApiKeyAuth": [] + } + ], + "summary": "Unassigns a project role from a user within a project.", + "operationId": "unassign-project-user-role", + "tags": [ + "Project user role assignments" + ], + "parameters": [ + { + "name": "project_id", + "in": "path", + "description": "The ID of the project to modify.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "description": "The ID of the user whose project role assignment should be removed.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role_id", + "in": "path", + "description": "The ID of the project role to remove from the user.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project role unassigned from the user successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedRoleAssignmentResource" + } + } + } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" + } + }, + "x-oaiMeta": { + "name": "Unassign project role from user", + "group": "administration", + "examples": { + "request": { + "curl": "curl -X DELETE https://api.openai.com/v1/projects/proj_abc123/users/user_abc123/roles/role_01J1F8PROJ \\\n -H \"Authorization: Bearer $OPENAI_ADMIN_KEY\" \\\n -H \"Content-Type: application/json\"\n" + }, + "response": "{\n \"object\": \"user.role.deleted\",\n \"deleted\": true\n}\n" + } + } + } + }, + "/realtime/calls": { + "post": { + "summary": "Create a new Realtime API call over WebRTC and receive the SDP answer needed\nto complete the peer connection.", + "operationId": "create-realtime-call", + "tags": [ + "Realtime" + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/RealtimeCallCreateRequest" + }, + "encoding": { + "sdp": { + "contentType": "application/sdp" + }, + "session": { + "contentType": "application/json" + } + } + }, + "application/sdp": { + "schema": { + "type": "string", + "description": "WebRTC SDP offer. Use this variant when you have previously created an\nephemeral **session token** and are authenticating the request with it.\nRealtime session parameters will be retrieved from the session token." + } + } + } + }, + "responses": { + "201": { + "description": "Realtime call created successfully.", + "headers": { + "Location": { + "description": "Relative URL containing the call ID for subsequent control requests.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/sdp": { + "schema": { + "type": "string", + "description": "SDP answer produced by OpenAI for the peer connection." + } + } + } + } + }, + "x-oaiMeta": { + "name": "Create call", + "group": "realtime", + "returns": "Returns `201 Created` with the SDP answer in the response body. The\n`Location` response header includes the call ID for follow-up requests,\ne.g., establishing a monitoring WebSocket or hanging up the call.", + "examples": { + "request": { + "curl": "curl -X POST https://api.openai.com/v1/realtime/calls \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -F \"sdp=