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
57 changes: 56 additions & 1 deletion .agents/skills/sync-openapi-spec/references/sync-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,69 @@ This skill is the manual fallback for the same job, so its output has to match t

`scripts/sync_openapi.py` applies these rules, top-down:

1. Drop every operation marked `x-internal: true`, and drop a path entirely when every one of its operations is internal.
1. Recursively drop every object marked `x-internal: true`, wherever it
appears in the tree — a path operation, a query/path parameter, a
schema property, a whole schema, a tag entry, and so on — not only
top-level path operations.
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,
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.

## `x-internal` deletes the whole marked object, not just the flag (`_prune_internal`)

`x-internal: true` mirrors openapi-format's `flagValues` semantics in
warp-server's filter: the entire object bearing the marker is deleted, not
just the `x-internal` key on it. An earlier version of this script only
applied that rule to top-level path operations (`strip_internal_operations`)
and left every other marked object's `x-internal` key to be stripped later
by the `STRIP_FLAGS` pass (rule 6 above). Stripping the key without deleting
the object it was marking leaves the object itself — now unmarked — in the
published spec. This let several server-internal fields leak through: the
`factory_uid` and `automation_id` query parameters on `GET /agent/runs`, and
the `factory_uid`/`agent_type` properties on `CreateAgentRequest`,
`UpdateAgentRequest`, and `AgentResponse`.

`_prune_internal` now runs first, before any other rule, and walks the
entire source tree deleting every marked object outright: a schema property
under `properties`, an item in a `parameters` array, a whole schema in
`components.schemas`, and so on, in addition to the path operations rule 1
already covered. `STRIP_FLAGS` (rule 6) then only has to clean up the
`x-internal` key on anything that rule 1 doesn't fully own removing (there
is normally nothing left, since every `x-internal: true` object is deleted
outright) plus the other seven implementation-only extensions.

## Implementation-only extensions are stripped everywhere (`STRIP_FLAGS`)

`STRIP_FLAGS` mirrors the `stripFlags` list in
`warp-server/public_api/public-openapi-filter.yaml` verbatim: `x-internal`,
`x-enum-varnames`, `x-go-type`, `x-go-type-import`,
`x-go-type-skip-optional-pointer`, `x-oapi-codegen-extra-tags`,
`x-stainless-deprecation-message`, and `x-stainless-naming`. These
extensions are useful for server/SDK code generation (oapi-codegen,
Stainless) but carry no meaning for a docs reader, so none of them may
reach the published Scalar reference.

An earlier version of this script only removed `x-internal` from
top-level operation objects (the key that decides whether to drop the
operation entirely). It never stripped the *other* six keys, and it never
walked into schemas, so implementation-only markers on component schemas
and their properties — `x-go-type-skip-optional-pointer` and
`x-stainless-deprecation-message` in particular — leaked into the
published copy verbatim. `_strip_flags` now walks the entire regenerated
tree after filtering and removes every `STRIP_FLAGS` key it finds,
regardless of nesting depth, matching `generate-public-openapi`'s own
post-generation check that no `x-*` key remains in warp-server's
published copy.

When warp-server adds a new entry to its `stripFlags` list, add the same
key to `STRIP_FLAGS` here so the two filters stay in lockstep.

## Excluded tags

### `memory_stores` and `memory`
Expand Down
138 changes: 135 additions & 3 deletions .agents/skills/sync-openapi-spec/scripts/sync_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
* paths listed in EXCLUDED_PATHS are removed
* components/schemas is pruned to only schemas 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
whatever survives the filtering above, wherever it appears in the tree
* the regenerated spec is validated for unresolved $refs before
being written; apply will refuse to write a broken spec

Expand All @@ -20,7 +23,9 @@
release pipeline publishes the spec. Honoring the same marker here keeps this
script from publishing a surface the server team has explicitly marked private,
instead of relying only on a hand-maintained tag allowlist that goes stale
whenever a new private tag appears.
whenever a new private tag appears. STRIP_FLAGS mirrors that same filter's
``stripFlags`` list, so implementation-only extensions never reach the
published docs copy either.

Modes:
diff Print structural drift between source and target. Exits 1
Expand Down Expand Up @@ -59,6 +64,24 @@
# `flagValues: [x-internal: true]` in warp-server/public_api/public-openapi-filter.yaml.
INTERNAL_MARKER = "x-internal"

# Implementation-only OpenAPI extensions that must never reach the published
# docs copy. Mirrors `stripFlags` in
# warp-server/public_api/public-openapi-filter.yaml: these keys are useful for
# server/SDK code generation (oapi-codegen, Stainless) but are stripped
# unconditionally from every remaining object, not just top-level operations.
STRIP_FLAGS: frozenset[str] = frozenset(
{
"x-internal",
"x-enum-varnames",
"x-go-type",
"x-go-type-import",
"x-go-type-skip-optional-pointer",
"x-oapi-codegen-extra-tags",
"x-stainless-deprecation-message",
"x-stainless-naming",
}
)

# Path-item keys that are HTTP operations rather than shared path metadata.
HTTP_METHODS: frozenset[str] = frozenset(
{"get", "put", "post", "delete", "options", "head", "patch", "trace"}
Expand Down Expand Up @@ -156,6 +179,35 @@ def _is_internal_operation(operation: Any) -> bool:
return isinstance(operation, dict) and operation.get(INTERNAL_MARKER) is True


def _prune_internal(node: Any) -> Any:
"""Recursively drop any object marked ``x-internal: true``, then recurse
into whatever remains.

Mirrors openapi-format's ``flagValues: [x-internal: true]`` semantics
(warp-server's ``public_api/public-openapi-filter.yaml``): the entire
marked node is deleted, not just the marker key. This catches internal
schema properties (e.g. ``factory_uid``, ``agent_type``) and internal
parameters (e.g. the ``automation_id`` query parameter) wherever they
appear in the tree — not only the top-level path operations that
``strip_internal_operations`` inspects. Stripping only the marker key
(see ``_strip_flags``) would otherwise leave the internal object itself,
just unmarked, in the published spec.
"""
if isinstance(node, dict):
return {
key: _prune_internal(value)
for key, value in node.items()
if not _is_internal_operation(value)
}
if isinstance(node, list):
return [
_prune_internal(item)
for item in node
if not _is_internal_operation(item)
]
return node


def strip_internal_operations(path_item: dict[str, Any]) -> dict[str, Any]:
"""Return ``path_item`` without any operation marked ``x-internal: true``.

Expand Down Expand Up @@ -191,6 +243,25 @@ def _should_keep_path(path: str, path_item: dict[str, Any]) -> bool:
return True


def _strip_flags(node: Any) -> Any:
"""Recursively remove every key in ``STRIP_FLAGS`` from ``node``.

These extensions can appear anywhere in the spec (operations, schemas,
individual properties, parameters), not only on the operation objects
that ``strip_internal_operations`` already inspects, so this walks the
entire tree rather than a fixed set of levels.
"""
if isinstance(node, dict):
return {
key: _strip_flags(value)
for key, value in node.items()
if key not in STRIP_FLAGS
}
if isinstance(node, list):
return [_strip_flags(item) for item in node]
return node


def _collect_refs(node: Any, refs: set[str]) -> None:
"""Recursively collect every component schema name referenced from ``node``.

Expand Down Expand Up @@ -281,6 +352,11 @@ def visit(node: Any, path: str) -> None:

def transform(source: dict[str, Any]) -> dict[str, Any]:
"""Produce the docs subset of the given source spec."""
# Drop every x-internal-marked object (schema properties, parameters,
# operations, tags, ...) before anything else, so a downstream pass never
# sees an internal node it would otherwise have to know how to filter.
source = _prune_internal(source)

out: dict[str, Any] = {}

for top_key in ("openapi", "info", "servers"):
Expand Down Expand Up @@ -324,7 +400,7 @@ def transform(source: dict[str, Any]) -> dict[str, Any]:
if out_components:
out["components"] = out_components

return out
return _strip_flags(out)


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -440,6 +516,20 @@ def _self_test() -> int:
"post": {
"tags": ["agent"],
"operationId": "runAgent",
"x-stainless-deprecation-message": "use /agent/runs instead",
"parameters": [
{
"name": "conversation_id",
"in": "query",
"schema": {"type": "string"},
},
{
"name": "factory_uid",
"in": "query",
"x-internal": True,
"schema": {"type": "string"},
},
],
"requestBody": {
"content": {
"application/json": {
Expand Down Expand Up @@ -487,6 +577,8 @@ def _self_test() -> int:
"schemas": {
"RunReq": {
"type": "object",
"x-go-type": "models.RunReq",
"x-go-type-import": {"path": "warp.dev/warp-server/models"},
"properties": {
"config": {"$ref": "#/components/schemas/Config"}
},
Expand All @@ -504,9 +596,22 @@ def _self_test() -> int:
{"type": "object"},
]
},
"legacy_mode": {
"type": "string",
"x-go-type-skip-optional-pointer": True,
"x-oapi-codegen-extra-tags": {"json": "legacy_mode,omitempty"},
},
"factory_agent_type": {
"allOf": [{"$ref": "#/components/schemas/Mode"}],
"x-internal": True,
},
},
},
"Mode": {"type": "string"},
"Mode": {
"type": "string",
"x-enum-varnames": ["ModeFast", "ModeSlow"],
"x-stainless-naming": {"typescript": {"type": "Mode"}},
},
"RunResp": {"type": "object"},
"MSItem": {"type": "object"}, # only referenced by dropped path
"Followup": {"type": "object"},
Expand All @@ -533,6 +638,33 @@ def _self_test() -> int:
ref_errors = _validate_output(out)
assert not ref_errors, f"unexpected unresolved refs: {ref_errors}"

# Implementation-only extensions must never survive into the output,
# regardless of whether they sit on an operation, a schema, or a nested
# property — mirrors warp-server's `stripFlags` filter.
dumped = yaml.safe_dump(out)
for flag in STRIP_FLAGS:
assert flag not in dumped, f"{flag} leaked into the regenerated spec"
# The objects that carried those flags must otherwise survive intact.
assert out["paths"]["/agent/run"]["post"]["operationId"] == "runAgent"
assert out["components"]["schemas"]["RunReq"]["type"] == "object"
assert out["components"]["schemas"]["Config"]["properties"]["legacy_mode"][
"type"
] == "string"

# An x-internal-marked object must be dropped entirely, not just have its
# marker key stripped — covers an internal query parameter and an
# internal schema property, alongside the surviving public sibling in
# each case.
run_params = {
p["name"] for p in out["paths"]["/agent/run"]["post"]["parameters"]
}
assert run_params == {"conversation_id"}, f"unexpected parameters: {run_params}"
config_props = set(out["components"]["schemas"]["Config"]["properties"].keys())
assert "factory_agent_type" not in config_props, (
f"internal property survived: {config_props}"
)
assert "legacy_mode" in config_props, f"public property dropped: {config_props}"

print("self-test: OK")
return 0

Expand Down
Loading
Loading