From c4d4d78550f7682d1d3dfd0db2f7b4a48c346214 Mon Sep 17 00:00:00 2001 From: Oz Date: Mon, 24 Aug 2026 17:15:17 +0000 Subject: [PATCH] docs: sync agent-api-openapi.yaml from warp-server Prune unused entries from every shared component section, not just components.schemas, so a response referenced only by an excluded /factory path can no longer ship in the published spec. Co-Authored-By: Oz Co-Authored-By: Warp --- .../references/sync-policy.md | 18 +- .../sync-openapi-spec/scripts/sync_openapi.py | 179 ++++++++++++------ developers/agent-api-openapi.yaml | 161 ++++++++++++++-- 3 files changed, 281 insertions(+), 77 deletions(-) diff --git a/.agents/skills/sync-openapi-spec/references/sync-policy.md b/.agents/skills/sync-openapi-spec/references/sync-policy.md index 2c36c72e6..af1f35251 100644 --- a/.agents/skills/sync-openapi-spec/references/sync-policy.md +++ b/.agents/skills/sync-openapi-spec/references/sync-policy.md @@ -1,6 +1,6 @@ # Sync Policy -This document records what `developers/agent-api-openapi.yaml` keeps from `warp-server/public_api/openapi.yaml`, and why. The exclusion lists live in `scripts/sync_openapi.py` as `EXCLUDED_TAGS`, `EXCLUDED_PATHS`, and `EXCLUDED_PATH_PREFIXES`. Update both this document and the script when the policy changes. +This document records what `developers/agent-api-openapi.yaml` keeps from `warp-server/public_api/openapi.yaml`, and why. The exclusion lists live in `scripts/sync_openapi.py` as `EXCLUDED_TAGS`, `EXCLUDED_PATHS`, `EXCLUDED_PATH_PREFIXES`, and `PRUNABLE_COMPONENT_SECTIONS`. Update both this document and the script when the policy changes. ## Relationship to warp-server's release automation @@ -19,7 +19,7 @@ This skill is the manual fallback for the same job, so its output has to match t 2. Drop every tag listed in `EXCLUDED_TAGS`. 3. Drop every path whose tags are a subset of `EXCLUDED_TAGS`, plus every path listed explicitly in `EXCLUDED_PATHS` or matching a prefix in `EXCLUDED_PATH_PREFIXES`. 4. Keep top-level `openapi`, `info`, `servers`, and `components.securitySchemes` verbatim. -5. Keep only the `components.schemas` entries that are reachable from the surviving paths via `$ref` walking (recursive over `allOf`/`oneOf`/`anyOf`/`items`/`additionalProperties`/etc.). +5. Keep only the entries in each `PRUNABLE_COMPONENT_SECTIONS` section (`schemas`, `parameters`, `examples`, `headers`, `requestBodies`, `responses`, `mediaTypes`) that are reachable from the surviving paths via `$ref` walking (recursive over `allOf`/`oneOf`/`anyOf`/`items`/`additionalProperties`/etc., and across sections — a shared response pulls in the schemas it references). 6. Recursively strip every key in `STRIP_FLAGS` from whatever survives steps 1-5, wherever it appears in the tree (operations, schemas, individual properties, parameters). @@ -49,6 +49,20 @@ already covered. `STRIP_FLAGS` (rule 6) then only has to clean up the is normally nothing left, since every `x-internal: true` object is deleted outright) plus the other seven implementation-only extensions. +## Every shared component section is pruned, not just `schemas` (`PRUNABLE_COMPONENT_SECTIONS`) + +`PRUNABLE_COMPONENT_SECTIONS` mirrors the `unusedComponents` list in +`warp-server/public_api/public-openapi-filter.yaml`. An earlier version of +this script pruned `components.schemas` and copied every other section +verbatim, so a shared component referenced only by an excluded path stayed +in the published copy: `FactoryAccessDenied`, a `components.responses` entry +used solely by the private `/factory/*` operations, shipped in the Scalar +reference as an orphan definition. + +Sections outside the set are copied verbatim. Today that means +`securitySchemes`, which no operation `$ref`s — pruning it by reachability +would delete it. + ## Implementation-only extensions are stripped everywhere (`STRIP_FLAGS`) `STRIP_FLAGS` mirrors the `stripFlags` list in diff --git a/.agents/skills/sync-openapi-spec/scripts/sync_openapi.py b/.agents/skills/sync-openapi-spec/scripts/sync_openapi.py index 7ef73d01d..fe4e9f2d1 100644 --- a/.agents/skills/sync-openapi-spec/scripts/sync_openapi.py +++ b/.agents/skills/sync-openapi-spec/scripts/sync_openapi.py @@ -10,7 +10,8 @@ every operation is internal is dropped entirely * tags listed in EXCLUDED_TAGS are removed (and their paths/schemas) * paths listed in EXCLUDED_PATHS are removed - * components/schemas is pruned to only schemas reachable from the + * every section in PRUNABLE_COMPONENT_SECTIONS (schemas, responses, + parameters, ...) is pruned to only the entries reachable from the surviving paths via $ref walking * every key in STRIP_FLAGS (implementation-only extensions such as ``x-go-type`` and ``x-stainless-naming``) is removed recursively from @@ -87,6 +88,22 @@ {"get", "put", "post", "delete", "options", "head", "patch", "trace"} ) +# Component sections pruned down to entries reachable from the surviving +# paths. Mirrors `unusedComponents` in +# warp-server/public_api/public-openapi-filter.yaml. Sections outside this set +# (notably `securitySchemes`, which nothing $refs) are copied verbatim. +PRUNABLE_COMPONENT_SECTIONS: frozenset[str] = frozenset( + { + "schemas", + "parameters", + "examples", + "headers", + "requestBodies", + "responses", + "mediaTypes", + } +) + # Specific paths under otherwise-public tags that should be hidden from # the public API reference. Keep in sync with references/sync-policy.md. EXCLUDED_PATHS: frozenset[str] = frozenset( @@ -262,21 +279,20 @@ def _strip_flags(node: Any) -> Any: return node -def _collect_refs(node: Any, refs: set[str]) -> None: - """Recursively collect every component schema name referenced from ``node``. +def _collect_refs(node: Any, refs: set[tuple[str, str]]) -> None: + """Recursively collect every ``(section, name)`` component ref in ``node``. Walks dicts and lists, picking up any string under a ``$ref`` key that - points into ``#/components/schemas/``. Captures refs nested anywhere - (allOf/oneOf/anyOf, items, additionalProperties, etc.). + points into ``#/components/
/``. Captures refs nested + anywhere (allOf/oneOf/anyOf, items, additionalProperties, a shared + response under an operation's ``responses``, etc.). """ if isinstance(node, dict): for k, v in node.items(): - if ( - k == "$ref" - and isinstance(v, str) - and v.startswith("#/components/schemas/") - ): - refs.add(v[len("#/components/schemas/") :]) + if k == "$ref" and isinstance(v, str) and v.startswith("#/components/"): + section, _, name = v[len("#/components/") :].partition("/") + if section and name: + refs.add((section, name)) else: _collect_refs(v, refs) elif isinstance(node, list): @@ -284,27 +300,31 @@ def _collect_refs(node: Any, refs: set[str]) -> None: _collect_refs(item, refs) -def _transitive_schemas( - seed_refs: set[str], schemas: dict[str, Any] -) -> set[str]: - """Closure of ``seed_refs`` under transitive $ref edges in ``schemas``.""" - reachable: set[str] = set() +def _reachable_components( + seed_refs: set[tuple[str, str]], components: dict[str, Any] +) -> set[tuple[str, str]]: + """Closure of ``seed_refs`` under transitive $ref edges in ``components``. + + Component entries reference each other across sections — a shared + response $refs a schema, a schema $refs another schema — so the walk + has to follow every section, not just ``schemas``. + """ + reachable: set[tuple[str, str]] = set() pending = list(seed_refs) while pending: - name = pending.pop() - if name in reachable: + ref = pending.pop() + if ref in reachable: continue - if name not in schemas: - # Dangling ref — skip silently. The diff will surface it via - # the resulting schema set comparison. - reachable.add(name) + reachable.add(ref) + section, name = ref + entry = (components.get(section) or {}).get(name) + if entry is None: + # Dangling ref — skip silently. _validate_output surfaces it + # before apply mode writes anything. continue - reachable.add(name) - new_refs: set[str] = set() - _collect_refs(schemas[name], new_refs) - for ref in new_refs: - if ref not in reachable: - pending.append(ref) + new_refs: set[tuple[str, str]] = set() + _collect_refs(entry, new_refs) + pending.extend(r for r in new_refs if r not in reachable) return reachable @@ -380,21 +400,20 @@ def transform(source: dict[str, Any]) -> dict[str, Any]: } out["paths"] = kept_paths - seed_refs: set[str] = set() + seed_refs: set[tuple[str, str]] = set() _collect_refs(kept_paths, seed_refs) src_components = source.get("components") or {} - src_schemas = src_components.get("schemas") or {} - reachable = _transitive_schemas(seed_refs, src_schemas) + reachable = _reachable_components(seed_refs, src_components) out_components: dict[str, Any] = {} for ck, cv in src_components.items(): - if ck == "schemas": - out_components["schemas"] = { - name: src_schemas[name] - for name in src_schemas - if name in reachable + if ck in PRUNABLE_COMPONENT_SECTIONS and isinstance(cv, dict): + kept = { + name: entry for name, entry in cv.items() if (ck, name) in reachable } + if kept: + out_components[ck] = kept else: out_components[ck] = cv if out_components: @@ -437,28 +456,37 @@ def _summarize_drift( notes.append("Paths whose operations differ between source and target:") notes.extend(f" ~ {p}" for p in changed_paths) - exp_schemas = set(((expected.get("components") or {}).get("schemas") or {}).keys()) - act_schemas = set(((actual.get("components") or {}).get("schemas") or {}).keys()) - - missing_schemas = sorted(exp_schemas - act_schemas) - extra_schemas = sorted(act_schemas - exp_schemas) - - if missing_schemas: - notes.append("Schemas present in source subset but missing from target:") - notes.extend(f" + {s}" for s in missing_schemas) - if extra_schemas: - notes.append("Schemas present in target but absent from source subset:") - notes.extend(f" - {s}" for s in extra_schemas) - - common_schemas = exp_schemas & act_schemas - schema_changes = sorted( - s - for s in common_schemas - if expected["components"]["schemas"][s] != actual["components"]["schemas"][s] - ) - if schema_changes: - notes.append("Schemas whose definitions differ between source subset and target:") - notes.extend(f" ~ {s}" for s in schema_changes) + # Every pruned section is compared, not just `schemas`: a stale entry in + # `components.responses` (or any other shared section) is drift too, and + # reporting only schemas let one sit in the target unnoticed. + exp_components = expected.get("components") or {} + act_components = actual.get("components") or {} + for section in sorted(PRUNABLE_COMPONENT_SECTIONS): + exp_entries = (exp_components.get(section) or {}) + act_entries = (act_components.get(section) or {}) + exp_names = set(exp_entries.keys()) + act_names = set(act_entries.keys()) + label = "Schemas" if section == "schemas" else f"Component {section}" + + missing = sorted(exp_names - act_names) + extra = sorted(act_names - exp_names) + if missing: + notes.append(f"{label} present in source subset but missing from target:") + notes.extend(f" + {name}" for name in missing) + if extra: + notes.append(f"{label} present in target but absent from source subset:") + notes.extend(f" - {name}" for name in extra) + + changed = sorted( + name + for name in exp_names & act_names + if exp_entries[name] != act_entries[name] + ) + if changed: + notes.append( + f"{label} whose definitions differ between source subset and target:" + ) + notes.extend(f" ~ {name}" for name in changed) for top_key in ("openapi", "info", "servers"): if expected.get(top_key) != actual.get(top_key): @@ -545,7 +573,8 @@ def _self_test() -> int: "schema": {"$ref": "#/components/schemas/RunResp"} } }, - } + }, + "401": {"$ref": "#/components/responses/Unauthorized"}, }, } }, @@ -574,6 +603,20 @@ def _self_test() -> int: }, "components": { "securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer"}}, + "responses": { + # Referenced by a surviving operation, and pulls a schema of + # its own into the output. + "Unauthorized": { + "description": "auth required", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Err"} + } + }, + }, + # Referenced only by dropped Factory paths. + "FactoryAccessDenied": {"description": "factory access denied"}, + }, "schemas": { "RunReq": { "type": "object", @@ -613,6 +656,7 @@ def _self_test() -> int: "x-stainless-naming": {"typescript": {"type": "Mode"}}, }, "RunResp": {"type": "object"}, + "Err": {"type": "object"}, # only reachable via a shared response "MSItem": {"type": "object"}, # only referenced by dropped path "Followup": {"type": "object"}, }, @@ -627,8 +671,21 @@ def _self_test() -> int: }, f"unexpected paths: {paths}" schemas = set(out["components"]["schemas"].keys()) - # Config and Mode are reachable transitively (allOf, items) - assert schemas == {"RunReq", "Config", "Mode", "RunResp"}, f"unexpected schemas: {schemas}" + # Config and Mode are reachable transitively (allOf, items); Err only + # through the shared Unauthorized response. + assert schemas == { + "RunReq", + "Config", + "Mode", + "RunResp", + "Err", + }, f"unexpected schemas: {schemas}" + + # Shared components outside `schemas` are pruned the same way, so a + # response only referenced by a dropped path cannot linger in the + # published spec. + responses = set(out["components"]["responses"].keys()) + assert responses == {"Unauthorized"}, f"unexpected responses: {responses}" tag_names = [t["name"] for t in out.get("tags") or []] assert tag_names == ["agent"], f"unexpected tags: {tag_names}" diff --git a/developers/agent-api-openapi.yaml b/developers/agent-api-openapi.yaml index 0d50c00dd..37f471fbe 100644 --- a/developers/agent-api-openapi.yaml +++ b/developers/agent-api-openapi.yaml @@ -1,8 +1,8 @@ openapi: 3.0.0 info: - title: Oz Agent API + title: Warp Agent API version: 1.0.0 - description: "API for creating, managing, and querying Oz cloud agent runs.\n\nThese endpoints allow users to programmatically spawn agents, list runs, \nand retrieve detailed run information.\n" + description: "API for creating, managing, and querying Warp cloud agent runs.\n\nThese endpoints allow users to programmatically spawn agents, list runs, \nand retrieve detailed run information.\n" contact: name: Warp Support url: https://docs.warp.dev @@ -2401,7 +2401,7 @@ components: type: string description: | Short, badge-visible label for the artifact. For recording artifacts, - this is the agent-authored title shown in Oz web and blocklist badges. + this is the agent-authored title shown in Warp web and blocklist badges. Distinct from description, which is longer and shown in detail views. description: type: string @@ -2491,20 +2491,144 @@ components: type: number format: double description: | - inference_cost in US dollars, converted at a fixed rate. An - approximate cost, not a billed amount. + inference_cost in US dollars, converted at the owning team's + current credit price. An approximate cost, not a billed amount. compute_cost_usd: type: number format: double description: | - compute_cost in US dollars, converted at a fixed rate. An - approximate cost, not a billed amount. + compute_cost in US dollars, converted at the owning team's + current credit price. An approximate cost, not a billed amount. platform_cost_usd: type: number format: double description: | - platform_cost in US dollars, converted at a fixed rate. An - approximate cost, not a billed amount. + platform_cost in US dollars, converted at the owning team's + current credit price. An approximate cost, not a billed amount. + total_tokens: + type: integer + format: int64 + description: | + Total LLM token count (summed across every usage category and model) for the run's + conversation. Omitted when the data is not available. + inference_cost_breakdown_usd: + $ref: '#/components/schemas/InferenceCostBreakdownUsd' + usage_by_category: + type: object + additionalProperties: + $ref: '#/components/schemas/ChargedUsageDetail' + description: | + Full-granularity token and dollar-cost breakdown for the run's + conversation, keyed by usage category (e.g. "primary_agent", + "conversation_compaction") and model id. + This differs from total_tokens/inference_cost_breakdown_usd which + combine usage across all categories and models. + Omitted when the data is not available. + InferenceCostBreakdownUsd: + type: object + description: | + Charged dollar cost of LLM inference, split by token type. + Omitted when the data is not available. + required: + - input_cost_usd + - input_cache_read_cost_usd + - input_cache_write_cost_usd + - output_cost_usd + properties: + input_cost_usd: + type: number + format: double + description: Cost of non-cached input tokens, in US dollars. + input_cache_read_cost_usd: + type: number + format: double + description: Cost of cache-read input tokens, in US dollars. + input_cache_write_cost_usd: + type: number + format: double + description: Cost of cache-write input tokens, in US dollars. + output_cost_usd: + type: number + format: double + description: Cost of output tokens, in US dollars. + TokenCountBreakdown: + type: object + description: A per-token-type token count. + required: + - input + - output + - input_cache_read + - input_cache_write + properties: + input: + type: integer + format: int64 + description: Count of non-cached input tokens. + output: + type: integer + format: int64 + description: Count of output tokens. + input_cache_read: + type: integer + format: int64 + description: Count of cache-read input tokens. + input_cache_write: + type: integer + format: int64 + description: Count of cache-write input tokens. + InferenceUsageDetail: + type: object + description: | + Full token count and dollar-cost detail inference usage. + The counts and cost describe the same usage (e.g. token_count.input + tokens cost cost_usd.input_cost_usd in total). + required: + - token_count + - cost_usd + - web_search_count + - web_search_cost_usd + properties: + token_count: + $ref: '#/components/schemas/TokenCountBreakdown' + cost_usd: + $ref: '#/components/schemas/InferenceCostBreakdownUsd' + web_search_count: + type: integer + format: int64 + description: Number of web searches performed by this model. + web_search_cost_usd: + type: number + format: double + description: Total cost of those web searches, in US dollars. + ChargedUsageDetail: + type: object + description: | + Usage charged for a single usage category, broken down by usage type + (direct API/BYOK/custom endpoint) and, within each, by model ID. + required: + - platform_usage_usd + properties: + direct_api_inference_usage: + type: object + additionalProperties: + $ref: '#/components/schemas/InferenceUsageDetail' + description: Inference usage incurred through Warp-provided model access, keyed by model ID. + byok_inference_usage: + type: object + additionalProperties: + $ref: '#/components/schemas/InferenceUsageDetail' + description: Inference usage charged using a user's own API key, keyed by model ID. + custom_endpoint_inference_usage: + type: object + additionalProperties: + $ref: '#/components/schemas/InferenceUsageDetail' + description: | + Inference usage charged using a custom endpoint, keyed by the + custom model's config key. + platform_usage_usd: + type: number + format: double + description: Platform usage charged for this category, in US dollars. RunCreatorInfo: type: object properties: @@ -2594,7 +2718,7 @@ components: - REMOTE description: | Where the run executed: - - LOCAL: Executed in the user's local Oz environment + - LOCAL: Executed in the user's local Warp environment - REMOTE: Executed by a remote/cloud worker AmbientAgentConfig: type: object @@ -2652,6 +2776,15 @@ components: description: | Controls whether computer use is enabled for this agent. If not set, defaults to true. + computer_use_model_id: + type: string + description: | + Model the computer use subagent runs on. If not set, the subagent + picks its own model automatically. + Only applies to the built-in Oz harness; the value is accepted but + has no effect under a third-party harness or when computer use is + disabled. Requires an agent CLI version that supports the + --computer-use-model flag. idle_timeout_minutes: type: integer format: int32 @@ -2753,7 +2886,7 @@ components: description: | Model to use with a third-party harness (e.g. "claude-haiku-4-5"). Only applies when type is a non-oz harness; the top-level config - model_id targets the built-in Oz harness instead. When omitted or + model_id targets the built-in Warp harness instead. When omitted or empty, the harness uses its own default model. reasoning_level: type: string @@ -3029,7 +3162,7 @@ components: type: string description: | Short, badge-visible label for the artifact. For recording artifacts, - this is the agent-authored title shown in Oz web and blocklist badges. + this is the agent-authored title shown in Warp web and blocklist badges. Distinct from description, which is longer and shown in detail views. description: type: string @@ -4220,7 +4353,7 @@ components: 5. System defaults available: type: boolean - description: Whether this agent is within the team's plan limit and can be used for runs + description: Whether the agent is currently enabled. Defaults to true. created_at: type: string format: date-time @@ -4276,7 +4409,7 @@ components: Default harness for runs executed by this agent. The precedence order for harness resolution is: 1. The harness specified on the run itself 2. The agent's base harness - 3. Oz + 3. Warp Deprecated - use harness instead, which carries the full {type, model_id, reasoning_level} default. harness: