Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/sync-openapi-spec/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ Summarize:

## Sync policy

The policy is encoded in `scripts/sync_openapi.py` as `EXCLUDED_TAGS` and `EXCLUDED_PATHS`. See `references/sync-policy.md` for the rationale behind each entry and the rules for adding new ones.
The policy is encoded in `scripts/sync_openapi.py` as `EXCLUDED_TAGS`, `EXCLUDED_PATHS`, `EXCLUDED_PATH_PREFIXES`, and `EXCLUDED_RUN_SOURCE_VALUES`. See `references/sync-policy.md` for the rationale behind each entry and the rules for adding new ones.

## Schedule

Expand Down
16 changes: 13 additions & 3 deletions .agents/skills/sync-openapi-spec/references/sync-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ 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.).
6. Recursively strip every key in `STRIP_FLAGS` from whatever survives
steps 1-5, wherever it appears in the tree (operations, schemas,
5. Keep only reusable component entries that are reachable from surviving paths via `$ref` walking (recursive over `allOf`/`oneOf`/`anyOf`/`items`/`additionalProperties`/etc.).
6. Remove Factory-only values and matching description lines from `RunSourceType`.
7. Recursively strip every key in `STRIP_FLAGS` from whatever survives
steps 1-6, wherever it appears in the tree (operations, schemas,
individual properties, parameters).

Rule 1 mirrors warp-server's own filter, so a surface the server team marks private stays private here without anyone having to maintain a matching allowlist entry.
Expand Down Expand Up @@ -101,6 +102,15 @@ If any of these become stable public surfaces, remove them from `EXCLUDED_PATHS`

`EXCLUDED_PATH_PREFIXES` drops a path by prefix regardless of how its operations are tagged. Today it holds a single entry, `/factory`, because some Factory operations are tagged `agent` upstream — `GET /factory/scorers/{scorer_id}/results` is one — so a tags-only rule leaks them into the public reference. Use a prefix only when a whole URL namespace is private; prefer a tag or an explicit path everywhere else.

## Excluded enum values in public schemas

`RunSourceType` is used by public run endpoints but includes three values that
describe Factory-only behavior. `EXCLUDED_RUN_SOURCE_VALUES` removes
`BENCHMARK_TRIAL`, `CREATE_BENCHMARK_TASK`, and `CUSTOM_WEBHOOK`, plus their
matching description lines, from the docs subset. Keep `RUN_SCORER`: its
description identifies a generic run-scoring judge rather than a Factory-only
surface.

## `x-internal` operations are dropped

Operations marked `x-internal: true` are removed, and a path loses its entry when all of its operations are internal. This covers the `/agent/messages/*` and `/agent/events/*` orchestration-messaging operations, `/agent/runs/{runId}/client-events`, `/agent/conversations/{conversation_id}/rename`, and `/agent/sessions/{sessionUuid}/redirect`.
Expand Down
121 changes: 115 additions & 6 deletions .agents/skills/sync-openapi-spec/scripts/sync_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
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
surviving paths via $ref walking
* components/schemas and components/responses are pruned to entries
reachable from the surviving paths via $ref walking
* Factory-only values are removed from the mixed public/private
``RunSourceType`` schema
* every key in STRIP_FLAGS (implementation-only extensions such as
``x-go-type`` and ``x-stainless-naming``) is removed recursively from
whatever survives the filtering above, wherever it appears in the tree
Expand Down Expand Up @@ -104,6 +106,10 @@
# tags-only rule would leak them into the public reference.
EXCLUDED_PATH_PREFIXES: tuple[str, ...] = ("/factory",)

EXCLUDED_RUN_SOURCE_VALUES: frozenset[str] = frozenset(
{"BENCHMARK_TRIAL", "CREATE_BENCHMARK_TASK", "CUSTOM_WEBHOOK"}
)

# Default checkout layout: docs/ and warp-server/ as siblings.
DEFAULT_SOURCE = Path("../warp-server/public_api/openapi.yaml")
DEFAULT_TARGET = Path("developers/agent-api-openapi.yaml")
Expand Down Expand Up @@ -283,6 +289,49 @@ def _collect_refs(node: Any, refs: set[str]) -> None:
for item in node:
_collect_refs(item, refs)

def _collect_response_refs(node: Any, refs: set[str]) -> None:
"""Collect component response names referenced from ``node``."""
if isinstance(node, dict):
for key, value in node.items():
if (
key == "$ref"
and isinstance(value, str)
and value.startswith("#/components/responses/")
):
refs.add(value[len("#/components/responses/") :])
else:
_collect_response_refs(value, refs)
elif isinstance(node, list):
for item in node:
_collect_response_refs(item, refs)


def _prune_run_source_values(schemas: dict[str, Any]) -> dict[str, Any]:
"""Remove Factory-only RunSourceType values and their descriptions."""
run_source_type = schemas.get("RunSourceType")
if not isinstance(run_source_type, dict):
return schemas
enum = run_source_type.get("enum")
if not isinstance(enum, list):
return schemas
pruned = dict(schemas)
filtered_run_source_type = dict(run_source_type)
filtered_run_source_type["enum"] = [
value for value in enum if value not in EXCLUDED_RUN_SOURCE_VALUES
]
description = run_source_type.get("description")
if isinstance(description, str):
filtered_run_source_type["description"] = "\n".join(
line
for line in description.splitlines()
if not any(
line.strip().startswith(f"- {value}:")
for value in EXCLUDED_RUN_SOURCE_VALUES
)
)
pruned["RunSourceType"] = filtered_run_source_type
return pruned


def _transitive_schemas(
seed_refs: set[str], schemas: dict[str, Any]
Expand Down Expand Up @@ -384,7 +433,14 @@ def transform(source: dict[str, Any]) -> dict[str, Any]:
_collect_refs(kept_paths, seed_refs)

src_components = source.get("components") or {}
src_schemas = src_components.get("schemas") or {}
src_schemas = _prune_run_source_values(src_components.get("schemas") or {})
src_responses = src_components.get("responses") or {}
response_refs: set[str] = set()
_collect_response_refs(kept_paths, response_refs)
for response_name in response_refs:
response = src_responses.get(response_name)
if isinstance(response, dict):
_collect_refs(response, seed_refs)
reachable = _transitive_schemas(seed_refs, src_schemas)

out_components: dict[str, Any] = {}
Expand All @@ -395,6 +451,12 @@ def transform(source: dict[str, Any]) -> dict[str, Any]:
for name in src_schemas
if name in reachable
}
elif ck == "responses":
out_components["responses"] = {
name: src_responses[name]
for name in response_refs
if name in src_responses
}
else:
out_components[ck] = cv
if out_components:
Expand Down Expand Up @@ -545,7 +607,10 @@ def _self_test() -> int:
"schema": {"$ref": "#/components/schemas/RunResp"}
}
},
}
},
"403": {
"$ref": "#/components/responses/PublicAccessDenied"
},
},
}
},
Expand Down Expand Up @@ -580,7 +645,8 @@ def _self_test() -> int:
"x-go-type": "models.RunReq",
"x-go-type-import": {"path": "warp.dev/warp-server/models"},
"properties": {
"config": {"$ref": "#/components/schemas/Config"}
"config": {"$ref": "#/components/schemas/Config"},
"source": {"$ref": "#/components/schemas/RunSourceType"},
},
},
"Config": {
Expand Down Expand Up @@ -613,8 +679,37 @@ def _self_test() -> int:
"x-stainless-naming": {"typescript": {"type": "Mode"}},
},
"RunResp": {"type": "object"},
"Error": {"type": "object"},
"MSItem": {"type": "object"}, # only referenced by dropped path
"Followup": {"type": "object"},
"RunSourceType": {
"type": "string",
"enum": ["API", "BENCHMARK_TRIAL", "CUSTOM_WEBHOOK"],
"description": (
"Source that created the run:\n"
"- API: Created through the API\n"
"- BENCHMARK_TRIAL: Created as a factory benchmark trial\n"
"- CUSTOM_WEBHOOK: Created by a factory automation"
),
},
},
"responses": {
"PublicAccessDenied": {
"description": "access denied",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Error"}
}
},
},
"FactoryAccessDenied": {
"description": "Factory access denied",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/MSItem"}
}
},
},
},
},
}
Expand All @@ -628,7 +723,21 @@ def _self_test() -> int:

schemas = set(out["components"]["schemas"].keys())
# Config and Mode are reachable transitively (allOf, items)
assert schemas == {"RunReq", "Config", "Mode", "RunResp"}, f"unexpected schemas: {schemas}"
assert schemas == {
"RunReq",
"Config",
"Mode",
"RunResp",
"RunSourceType",
"Error",
}, f"unexpected schemas: {schemas}"

responses = set(out["components"]["responses"].keys())
assert responses == {"PublicAccessDenied"}, f"unexpected responses: {responses}"
run_sources = out["components"]["schemas"]["RunSourceType"]
assert run_sources["enum"] == ["API"], f"unexpected run sources: {run_sources['enum']}"
assert "BENCHMARK_TRIAL" not in run_sources["description"]
assert "CUSTOM_WEBHOOK" not in run_sources["description"]

tag_names = [t["name"] for t in out.get("tags") or []]
assert tag_names == ["agent"], f"unexpected tags: {tag_names}"
Expand Down
Loading
Loading