diff --git a/.redocly.lint-ignore.yaml b/.redocly.lint-ignore.yaml new file mode 100644 index 0000000..895e6c1 --- /dev/null +++ b/.redocly.lint-ignore.yaml @@ -0,0 +1,172 @@ +# Per-location lint exceptions; redocly.yaml explains each rule listed here. +# Regenerate with: redocly lint source/openapi.yaml --config redocly.yaml --generate-ignore-file +# and review that every new entry is a conditional branch, an SSE binding or the local server. +source/openapi.yaml: + no-server-example.com: + - '#/servers/0/url' + no-required-schema-properties-undefined: + - >- + #/components/schemas/Capabilities/properties/resources/properties/config/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/config/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/config/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/config_validate/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/config_validate/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/config_validate/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/runtime_memory/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/traffic_history/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/traffic_history/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/memory_history/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/memory_history/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/datapath/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/datapath/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/nodes/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/providers/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/providers/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/providers/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/groups/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/groups/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/groups/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/probes/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/probes/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/probes/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/probes/then/required/3 + - >- + #/components/schemas/Capabilities/properties/resources/properties/probes/then/required/4 + - >- + #/components/schemas/Capabilities/properties/resources/properties/connections/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/connections/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/flows/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/flows/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/flows/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/flows/then/required/3 + - >- + #/components/schemas/Capabilities/properties/resources/properties/flows/then/required/4 + - >- + #/components/schemas/Capabilities/properties/resources/properties/flows/then/required/5 + - >- + #/components/schemas/Capabilities/properties/resources/properties/flows/then/required/6 + - >- + #/components/schemas/Capabilities/properties/resources/properties/routing_trace/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/routing_trace/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/routing_trace/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/routing_trace/then/required/3 + - >- + #/components/schemas/Capabilities/properties/resources/properties/routing_trace/then/required/4 + - >- + #/components/schemas/Capabilities/properties/resources/properties/routing_trace/then/required/5 + - >- + #/components/schemas/Capabilities/properties/resources/properties/rules/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/events/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/events/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/events/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/events/then/required/3 + - >- + #/components/schemas/Capabilities/properties/resources/properties/events/then/required/4 + - >- + #/components/schemas/Capabilities/properties/resources/properties/logs/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/logs/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/logs/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/logs/then/required/3 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_query/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_query/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_cache/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_cache/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_cache/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_cache/then/required/3 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_cache/then/required/4 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_log/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_log/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/dns_rules/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/runtime_settings/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/geodata/allOf/0/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/geodata/allOf/0/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/geodata/allOf/1/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/geodata/allOf/2/then/required/0 + - >- + #/components/schemas/Capabilities/properties/resources/properties/geodata/allOf/2/then/required/1 + - >- + #/components/schemas/Capabilities/properties/resources/properties/geodata/allOf/2/then/required/2 + - >- + #/components/schemas/Capabilities/properties/resources/properties/geodata/allOf/2/then/required/3 + - >- + #/components/schemas/Capabilities/properties/resources/properties/operations/then/required/0 + - '#/components/schemas/EbpfAttachment/allOf/0/then/required/0' + - '#/components/schemas/EbpfAttachment/allOf/0/then/required/1' + - '#/components/schemas/EbpfAttachment/allOf/1/then/required/0' + - '#/components/schemas/EbpfAttachment/allOf/2/then/required/0' + - '#/components/schemas/ProbeRequest/allOf/0/then/not/required/0' + - '#/components/schemas/FlowDetail/allOf/1/not/required/0' + - '#/components/schemas/RoutingTraceResponse/not/anyOf/0/required/0' + - '#/components/schemas/RoutingTraceResponse/not/anyOf/1/required/0' + - '#/components/schemas/RoutingTraceResponse/not/anyOf/2/required/0' + - '#/components/schemas/GeoDataDownloadPatch/then/required/0' + - '#/components/schemas/GeoDataDownloadPatch/else/not/required/0' + - '#/components/schemas/StreamReadyEvent/not/anyOf/0/required/0' + - '#/components/schemas/StreamReadyEvent/not/anyOf/1/required/0' + - '#/components/schemas/StreamReadyEvent/not/anyOf/2/required/0' + - '#/components/schemas/StreamReadyEvent/not/anyOf/3/required/0' + - '#/components/schemas/StreamReadyEvent/not/anyOf/4/required/0' + - '#/components/schemas/StreamReadyEvent/not/anyOf/5/required/0' + - '#/components/schemas/StreamReadyEvent/not/anyOf/6/required/0' + no-unused-components: + - '#/components/schemas/LogRecord' + - '#/components/schemas/StreamReadyEvent' + - '#/components/schemas/RuntimeUpdatedEvent' + - '#/components/schemas/FlowUpdatedEvent' + - '#/components/schemas/FlowGapEvent' + - '#/components/schemas/OperationUpdatedEvent' + - '#/components/schemas/GenerationChangedEvent' + - '#/components/examples/FlowUpdated' diff --git a/api/auth.yaml b/api/auth.yaml index f390291..a6b1d4c 100644 --- a/api/auth.yaml +++ b/api/auth.yaml @@ -2,6 +2,7 @@ paths: /api/v1/auth/setup: post: operationId: setupAdministrator + tags: [ Authentication ] summary: Create the administrator account and open a session description: >- Available only while discovery reports `auth.mode: password` with @@ -66,6 +67,7 @@ paths: /api/v1/auth/login: post: operationId: login + tags: [ Authentication ] summary: Exchange the administrator credentials for a session description: >- Available only in password mode after setup. Checks run in this order: @@ -129,6 +131,7 @@ paths: /api/v1/auth/logout: post: operationId: logout + tags: [ Authentication ] summary: End the session that authenticates this request description: Requires a password-session bearer; a configured bearer cannot log out. security: diff --git a/api/common.yaml b/api/common.yaml index 5d48db7..dd95349 100644 --- a/api/common.yaml +++ b/api/common.yaml @@ -24,7 +24,7 @@ headers: type: string const: nosniff ETag: - description: Quoted group configuration revision. + description: Strong entity tag of the representation, the value a later If-Match compares. required: true schema: type: string @@ -106,9 +106,16 @@ parameters: name: If-Match in: header required: true + description: Evaluated as RFC 9110 §13.1.1 defines; see Conditional requests in the error contract. schema: - type: string - minLength: 1 + $ref: ./openapi.yaml#/components/schemas/EntityTagList + example: '"d1f62f00c6da9ec33956e66b8cc3b4670f164556fc12453193904af23451dec1"' + IfMatchOptional: + name: If-Match + in: header + description: Evaluated as RFC 9110 §13.1.1 defines; see Conditional requests in the error contract. A new group-configuration PATCH without If-Match returns 428 precondition_required; a retained idempotent replay may omit it. + schema: + $ref: ./openapi.yaml#/components/schemas/EntityTagList example: '"17"' IdempotencyKey: name: Idempotency-Key @@ -196,6 +203,12 @@ responses: Unauthorized: description: Credentials are missing or invalid headers: + WWW-Authenticate: + description: The authentication challenge (RFC 9110 §15.5.2); the native API uses the Bearer scheme. + required: true + schema: + type: string + pattern: ^Bearer(\s|$) Cache-Control: $ref: ./openapi.yaml#/components/headers/NoStore X-Content-Type-Options: @@ -214,6 +227,7 @@ responses: request_id: request-01HZX4K8W6 x-headers: Content-Type: application/json + WWW-Authenticate: Bearer Cache-Control: no-store X-Content-Type-Options: nosniff Forbidden: @@ -322,29 +336,6 @@ responses: Content-Type: application/json Cache-Control: no-store X-Content-Type-Options: nosniff - PreconditionFailed: - description: If-Match does not equal the current configuration revision or stored source content hash - headers: - Cache-Control: - $ref: ./openapi.yaml#/components/headers/NoStore - X-Content-Type-Options: - $ref: ./openapi.yaml#/components/headers/NoSniff - content: - application/json: - schema: - $ref: ./openapi.yaml#/components/schemas/ErrorResponse - examples: - stale_revision: - value: - error: - code: stale_revision - message: The resource changed; read it again. - details: null - request_id: request-01HZX4K8W6 - x-headers: - Content-Type: application/json - Cache-Control: no-store - X-Content-Type-Options: nosniff TooLarge: description: Request size or advertised fan-out limit exceeded headers: @@ -548,6 +539,7 @@ schemas: - permission_denied - resource_not_found - capability_not_supported + - method_not_allowed - state_conflict - idempotency_conflict - event_cursor_expired @@ -660,8 +652,13 @@ schemas: IpVersion: type: string enum: [ ipv4, ipv6 ] + EntityTagList: + type: string + description: "`*` or a comma-separated list of entity tags, strong or weak (W/ prefix), as RFC 9110 §8.8.3 and §13.1.1 define. An entity tag contains no space, tab or inner double quote. Empty list elements are ignored, as §5.6.1.2 requires of recipients." + pattern: '^(\*|((W/)?"[!#-~\u0080-\u00ff]*")?([ \t]*,[ \t]*((W/)?"[!#-~\u0080-\u00ff]*")?)*)$' IpAddress: - oneOf: + description: An IPv4 or IPv6 address. Validators must enforce the ipv4 and ipv6 formats; without format assertion, either branch accepts any string. + anyOf: - type: string format: ipv4 - type: string diff --git a/api/config.yaml b/api/config.yaml index 3b4b461..7ad6c8d 100644 --- a/api/config.yaml +++ b/api/config.yaml @@ -2,6 +2,7 @@ paths: /api/v1/config: get: operationId: getConfig + tags: [ Configuration ] summary: Read the effective configuration description: | Requires resources.config.available. Returns one coherent snapshot of the @@ -79,25 +80,7 @@ paths: "400": $ref: ./openapi.yaml#/components/responses/BadRequest "401": - description: Credentials are missing or invalid - headers: - Cache-Control: - $ref: ./openapi.yaml#/components/headers/NoStore - X-Content-Type-Options: - $ref: ./openapi.yaml#/components/headers/NoSniff - content: - application/json: - schema: - $ref: ./openapi.yaml#/components/schemas/ErrorResponse - examples: - authentication_required: - value: - error: { code: authentication_required, message: Credentials are required. } - request_id: request-config-1 - x-headers: - Content-Type: application/json - Cache-Control: no-store - X-Content-Type-Options: nosniff + $ref: ./openapi.yaml#/components/responses/Unauthorized "403": description: Authenticated caller lacks observe permission headers: @@ -170,6 +153,7 @@ paths: /api/v1/config/sources: post: operationId: createConfigSource + tags: [ Configuration ] summary: Create one configuration source and reload description: | Requires control, resources.config.available, resources.config.writable, @@ -417,11 +401,14 @@ paths: example: source-main get: operationId: getConfigSource + tags: [ Configuration ] summary: Read one accepted configuration source description: | - Requires resources.config.available. Returns the same ConfigSource as - GET /config, not a fresh read of the store, with its content; mask listener - secrets as in GET /config. + Requires resources.config.available. Returns the source's identity and + content from the same snapshot as GET /config, not a fresh read of the + store; mask listener secrets as in GET /config. The body carries no field + that can change while the source bytes stay the same: writable and + loaded_at are only in the GET /config source list. Unknown source IDs return 404 resource_not_found. Redacted text must never be saved as a replacement; compare its UTF-8 SHA-256 with content_sha256 before using returned content as an editing representation. @@ -433,6 +420,11 @@ paths: "200": description: Accepted source; content remains subject to visibility policy headers: + ETag: + schema: + type: string + minLength: 1 + description: The source's content_sha256 in double quotes, the value PUT compares in If-Match. Sent only when content is complete, that is, when no listener-secret value was masked; a masked body has no ETag. Cache-Control: $ref: ./openapi.yaml#/components/headers/NoStore X-Content-Type-Options: @@ -440,7 +432,7 @@ paths: content: application/json: schema: - $ref: ./openapi.yaml#/components/schemas/ConfigSource + $ref: ./openapi.yaml#/components/schemas/ConfigSourceContent examples: editable: summary: Unredacted dae text. @@ -450,12 +442,11 @@ paths: kind: main content_sha256: d1f62f00c6da9ec33956e66b8cc3b4670f164556fc12453193904af23451dec1 bytes: 31 - writable: true - loaded_at: 2026-08-15T09:30:00Z content: "routing {\n fallback: direct\n}\n" line_count: 3 x-headers: Content-Type: application/json + ETag: '"d1f62f00c6da9ec33956e66b8cc3b4670f164556fc12453193904af23451dec1"' Cache-Control: no-store X-Content-Type-Options: nosniff redacted: @@ -465,8 +456,6 @@ paths: kind: main content_sha256: "0000000000000000000000000000000000000000000000000000000000000000" bytes: 90 - writable: false - loaded_at: 2026-08-15T09:30:00Z content: "global {\n log_level: info\n}\nexperimental {\n native_api {\n secret: ''\n }\n}\n" line_count: 8 x-headers: @@ -513,6 +502,7 @@ paths: $ref: ./openapi.yaml#/components/responses/RateLimited put: operationId: replaceConfigSource + tags: [ Configuration ] summary: Replace one configuration source and reload description: | Requires control, resources.config.available, resources.config.writable, @@ -569,18 +559,14 @@ paths: - bearerAuth: [] - {} parameters: - - name: If-Match - in: header - required: true + - $ref: ./openapi.yaml#/components/parameters/IfMatch description: | - One strong entity tag containing the source's content_sha256 from - GET /config, enclosed in double quotes. Compare the digest with the - source's current bytes in the configuration store, not the snapshot revision. Wildcards, weak - tags, and tag lists are not accepted. - schema: - type: string - pattern: '^"[0-9a-f]{64}"$' - example: '"d1f62f00c6da9ec33956e66b8cc3b4670f164556fc12453193904af23451dec1"' + The source's content_sha256, from GET /config or the ETag of + GET /config/sources/{source_id}, enclosed in double quotes. The server + compares it with the source's current bytes in the configuration store, + not the snapshot revision, as RFC 9110 §13.1.1 defines: `*` matches the + existing source, a list matches when any strong tag in it matches, and a + weak tag never matches. - $ref: ./openapi.yaml#/components/parameters/IdempotencyKey requestBody: required: true @@ -759,6 +745,7 @@ paths: /api/v1/config/validate: post: operationId: validateConfig + tags: [ Configuration ] summary: Validate candidate configuration without applying it description: | Requires resources.config_validate.available and control because the body @@ -861,25 +848,7 @@ paths: Cache-Control: no-store X-Content-Type-Options: nosniff "401": - description: Credentials are missing or invalid - headers: - Cache-Control: - $ref: ./openapi.yaml#/components/headers/NoStore - X-Content-Type-Options: - $ref: ./openapi.yaml#/components/headers/NoSniff - content: - application/json: - schema: - $ref: ./openapi.yaml#/components/schemas/ErrorResponse - examples: - authentication_required: - value: - error: { code: authentication_required, message: Credentials are required. } - request_id: request-validate-2 - x-headers: - Content-Type: application/json - Cache-Control: no-store - X-Content-Type-Options: nosniff + $ref: ./openapi.yaml#/components/responses/Unauthorized "403": description: Authenticated caller lacks control permission headers: @@ -1042,9 +1011,10 @@ schemas: secrets_redacted: type: boolean description: True when a listener-secret value (the deployment secret of an API listener, such as honk's native_api.secret and clash_api.secret) was masked somewhere in this response. Content, paths and diagnostics are otherwise returned in the clear to an admitted request. - ConfigSource: + ConfigSourceContent: type: object - required: [ id, path, kind, content_sha256, bytes, writable, loaded_at, content, line_count ] + description: One accepted source's identity and content, the representation GET /config/sources/{source_id} returns. Every field stays the same while the source bytes do, so content_sha256 can serve as its entity tag. + required: [ id, path, kind, content_sha256, bytes, content, line_count ] properties: id: type: string @@ -1070,32 +1040,39 @@ schemas: bytes: $ref: ./openapi.yaml#/components/schemas/SafeUInt description: Accepted source size in bytes before redaction. - writable: - type: boolean - description: | - True only when server-wide editing is enabled and this source permits - replacement by a control caller. False for engine-written includes, - generated sources, and subscriptions, for a source that holds API - listener settings or secrets, and while the configuration store - cannot accept writes; observe alone never grants writes. - loaded_at: - $ref: ./openapi.yaml#/components/schemas/Timestamp - description: Time these source bytes were accepted, not the current file modification time. content: type: string description: Accepted dae text with listener-secret values masked. Use for editing only if its UTF-8 SHA-256 matches content_sha256. line_count: $ref: ./openapi.yaml#/components/schemas/SafeUInt description: Lines in the accepted source before redaction; empty text has zero lines, and a final newline does not add an empty line. - if: - properties: - kind: - enum: [ subscription, generated ] - required: [ kind ] - then: - properties: - writable: - const: false + ConfigSource: + description: A source entry of GET /config, ConfigSourceContent plus the fields that can change without the bytes changing. + allOf: + - $ref: ./openapi.yaml#/components/schemas/ConfigSourceContent + - type: object + required: [ writable, loaded_at ] + properties: + writable: + type: boolean + description: | + True only when server-wide editing is enabled and this source permits + replacement by a control caller. False for engine-written includes, + generated sources, and subscriptions, for a source that holds API + listener settings or secrets, and while the configuration store + cannot accept writes; observe alone never grants writes. + loaded_at: + $ref: ./openapi.yaml#/components/schemas/Timestamp + description: Time these source bytes were accepted, not the current file modification time. + if: + properties: + kind: + enum: [ subscription, generated ] + required: [ kind ] + then: + properties: + writable: + const: false ConfigSourceCreate: type: object additionalProperties: false diff --git a/api/discovery.yaml b/api/discovery.yaml index 155b5ab..2e86f0f 100644 --- a/api/discovery.yaml +++ b/api/discovery.yaml @@ -2,6 +2,7 @@ paths: /api: get: operationId: getDiscovery + tags: [ Discovery ] summary: Discover the native API description: >- Public in every auth mode, so a client can choose how to sign in. A @@ -84,6 +85,7 @@ paths: /api/v1/version: get: operationId: getVersion + tags: [ Discovery ] summary: Read native and engine version identity description: Requires bearer authentication when the listener has a deployment secret or runs in password mode. Anonymous access is permitted only on an explicitly secretless loopback listener. No resource permission is required. security: @@ -130,6 +132,7 @@ paths: /api/v1/capabilities: get: operationId: getCapabilities + tags: [ Discovery ] summary: Negotiate resources, visibility, and limits description: Requires bearer authentication when the listener has a deployment secret or runs in password mode. Anonymous access is permitted only on an explicitly secretless loopback listener. No resource permission is required. security: @@ -209,7 +212,6 @@ paths: available: true targets: [ node, group ] kinds: [ tcp_connect, http, dns ] - purposes: [ data, dns ] transports: [ tcp, udp ] ip_versions: [ ipv4, ipv6 ] limits: @@ -227,7 +229,7 @@ paths: max_bulk_close: 1000 flows: available: true - recording: on_demand + recording: auto scopes: [ userspace_tcp, userspace_udp ] min_flows: 64 max_flows: 10000 @@ -365,7 +367,7 @@ schemas: type: boolean AdmittedDiscovery: type: object - required: [ name, status, api_major, base_path, links ] + required: [ name, status, api_major, base_path, links, auth ] properties: name: type: string @@ -444,7 +446,6 @@ schemas: description: Present in password mode, null otherwise. auth: $ref: ./openapi.yaml#/components/schemas/AuthDiscovery - description: Absent from servers that predate password login; clients then assume a configured bearer. Version: type: object required: [ api, engine ] @@ -751,7 +752,7 @@ schemas: type: boolean config_patch: type: boolean - description: PATCH /groups/{group_id} is implemented. A group patch is a configuration write, so true requires resources.config.writable. + description: PATCH /groups/{group_id}/config is implemented. A group patch is a configuration write, so true requires resources.config.writable. selection: type: boolean max_patch_operations: @@ -765,7 +766,7 @@ schemas: available: const: true then: - required: [ targets, kinds, purposes, transports, ip_versions, limits ] + required: [ targets, kinds, transports, ip_versions, limits ] properties: available: type: boolean @@ -780,12 +781,6 @@ schemas: uniqueItems: true items: $ref: ./openapi.yaml#/components/schemas/ProbeKind - purposes: - type: array - uniqueItems: true - items: - type: string - enum: [ data, dns ] transports: type: array uniqueItems: true @@ -831,8 +826,8 @@ schemas: type: boolean recording: type: string - enum: [ off, on, sampled, on_demand ] - description: The engine's flow recording policy, not whether the recorder is capturing now. off records none, because the configuration does not permit the flow recorder or its mode is off; on records every flow in scopes (mode on); sampled records a subset; on_demand is mode auto, recording while clients create demand, whether or not any client does now. recording.flows.active in GET /runtime/settings reports whether the recorder is capturing. + enum: [ "off", "on", auto, sampled ] + description: The engine's flow recording policy, not whether the recorder is capturing now. off records none, because the configuration does not permit the flow recorder or its mode is off; on records every flow in scopes (mode on); auto enables recording on client demand; sampled records a subset. recording.flows.active in GET /runtime/settings reports whether capture is active now. scopes: type: array uniqueItems: true diff --git a/api/dns.yaml b/api/dns.yaml index ebd08dd..f3c9a76 100644 --- a/api/dns.yaml +++ b/api/dns.yaml @@ -2,6 +2,7 @@ paths: /api/v1/dns/query: post: operationId: queryDns + tags: [ DNS ] summary: Execute a routed diagnostic DNS query description: >- POST because the query has side effects: it sends live DNS traffic and, @@ -101,6 +102,7 @@ paths: /api/v1/dns/cache: get: operationId: listDnsCache + tags: [ DNS ] summary: Read a paginated DNS cache snapshot description: | A page holds at most limit entries and may hold fewer while @@ -207,6 +209,7 @@ paths: $ref: ./openapi.yaml#/components/responses/SnapshotUnavailable delete: operationId: deleteDnsCacheByName + tags: [ DNS ] summary: Delete cache entries for one exact name x-permission: control security: @@ -267,6 +270,7 @@ paths: /api/v1/dns/cache/{entry_id}: delete: operationId: deleteDnsCacheEntry + tags: [ DNS ] summary: Delete one opaque DNS cache entry x-permission: control security: @@ -309,6 +313,7 @@ paths: /api/v1/dns/cache/flush: post: operationId: flushDnsCache + tags: [ DNS ] summary: Flush the complete runtime DNS cache x-permission: control security: @@ -358,6 +363,7 @@ paths: /api/v1/dns/log: get: operationId: listDnsLog + tags: [ DNS ] summary: Read the bounded ring of recent resolutions description: | Requires resources.dns_log.available. Records client DNS resolutions, @@ -465,6 +471,7 @@ paths: /api/v1/dns/rules: get: operationId: listDnsRules + tags: [ DNS ] summary: Read the running generation's DNS routing rules description: | Requires resources.dns_rules.available. Return the DNS routing rules of the diff --git a/api/events.yaml b/api/events.yaml index f9a87be..1e1f41a 100644 --- a/api/events.yaml +++ b/api/events.yaml @@ -2,6 +2,7 @@ paths: /api/v1/events: get: operationId: streamEvents + tags: [ Events ] summary: Follow bounded resumable invalidation events description: | Send stream.ready first on every connection, including a valid resume. diff --git a/api/flow-steps.yaml b/api/flow-steps.yaml index 26eee14..e1f2203 100644 --- a/api/flow-steps.yaml +++ b/api/flow-steps.yaml @@ -149,7 +149,8 @@ schemas: enum: [ upstream, asis, accept, reject, requery, null ] allOf: - oneOf: - - properties: + - required: [ chain ] + properties: chain: enum: [ traffic, dns_upstream ] input: @@ -158,7 +159,8 @@ schemas: - $ref: ./openapi.yaml#/components/schemas/TrafficRoutingInput dns_action: type: "null" - - properties: + - required: [ chain ] + properties: chain: const: dns_request input: @@ -171,7 +173,8 @@ schemas: type: "null" mark: type: "null" - - properties: + - required: [ chain ] + properties: chain: const: dns_response input: @@ -351,7 +354,7 @@ schemas: $ref: ./openapi.yaml#/components/schemas/OutboundStepData OutboundStepData: type: object - required: [ attempt_id, parent_attempt_id, kind, evaluation_id, routing_source, routed_outbound, effective_outbound, mode_override, selection_path, leaf_node_id, leaf_node_name, target, target_kind, dial_ip, server_addr, resolution_location, status, error ] + required: [ attempt_id, parent_attempt_id, kind, evaluation_id, routing_source, routed_outbound, effective_outbound, selection_path, leaf_node_id, leaf_node_name, target, target_kind, dial_ip, server_addr, resolution_location, status, error ] properties: attempt_id: type: string @@ -371,10 +374,6 @@ schemas: type: [ string, "null" ] effective_outbound: type: [ string, "null" ] - mode_override: - type: string - enum: [ none, direct, global, unknown ] - description: Engine mode override observed for this outbound attempt, separate from the configured dial mode. This field does not expose a native runtime-mode setting. none means no override was applied; unknown means the recorder could not determine it. selection_path: type: array items: diff --git a/api/flows.yaml b/api/flows.yaml index a392eab..58711a6 100644 --- a/api/flows.yaml +++ b/api/flows.yaml @@ -2,6 +2,7 @@ paths: /api/v1/connections: get: operationId: listConnections + tags: [ Connections ] summary: Read a scope-labelled live connection snapshot x-permission: observe security: @@ -103,6 +104,7 @@ paths: $ref: ./openapi.yaml#/components/responses/TooLarge delete: operationId: closeConnections + tags: [ Connections ] summary: Close matching userspace-owned connections description: | Requires control permission and resources.connections.available with @@ -273,6 +275,7 @@ paths: /api/v1/connections/{connection_id}: delete: operationId: closeConnection + tags: [ Connections ] summary: Close one userspace-owned connection description: | Requires control permission and resources.connections.available with @@ -384,6 +387,7 @@ paths: /api/v1/flows: get: operationId: listFlows + tags: [ Flows ] summary: List active and retained terminal flow decisions x-permission: observe security: @@ -507,6 +511,7 @@ paths: /api/v1/flows/{flow_id}: get: operationId: getFlow + tags: [ Flows ] summary: Read one recorded causal flow trace x-permission: observe security: @@ -620,7 +625,6 @@ paths: routing_source: evaluation routed_outbound: proxy effective_outbound: proxy - mode_override: none selection_path: - group_id: group-proxy member_id: node-hk-01 @@ -805,7 +809,6 @@ paths: routing_source: evaluation routed_outbound: proxy effective_outbound: proxy - mode_override: none selection_path: [] leaf_node_id: node-hk-01 leaf_node_name: hk-01 @@ -880,7 +883,6 @@ paths: routing_source: evaluation routed_outbound: proxy effective_outbound: proxy - mode_override: none selection_path: [] leaf_node_id: node-hk-01 leaf_node_name: hk-01 @@ -1168,21 +1170,24 @@ schemas: not: required: [ mode ] - oneOf: - - properties: + - required: [ trace_status ] + properties: trace_status: const: complete trace: properties: status: const: complete - - properties: + - required: [ trace_status ] + properties: trace_status: const: partial trace: properties: status: const: partial - - properties: + - required: [ trace_status ] + properties: trace_status: const: disabled trace: diff --git a/api/geodata.yaml b/api/geodata.yaml index cb982fb..07ff8ef 100644 --- a/api/geodata.yaml +++ b/api/geodata.yaml @@ -2,6 +2,7 @@ paths: /api/v1/geodata: get: operationId: getGeoData + tags: [ Geodata ] summary: Read the geosite and geoip assets the engine loaded description: | Requires resources.geodata.available; otherwise returns 404 @@ -110,6 +111,7 @@ paths: /api/v1/geodata/update: post: operationId: updateGeoData + tags: [ Geodata ] summary: Queue a download of the geosite and geoip assets description: | Requires resources.geodata.can_update; otherwise returns 404 diff --git a/api/logs.yaml b/api/logs.yaml index ab3112e..1fd0429 100644 --- a/api/logs.yaml +++ b/api/logs.yaml @@ -2,6 +2,7 @@ paths: /api/v1/logs: get: operationId: streamLogs + tags: [ Logs ] summary: Follow bounded resumable engine logs description: | Requires resources.logs.available. Emit stream.ready first on every connection, diff --git a/api/nodes-groups.yaml b/api/nodes-groups.yaml index f8cceae..4964b9f 100644 --- a/api/nodes-groups.yaml +++ b/api/nodes-groups.yaml @@ -2,6 +2,7 @@ paths: /api/v1/nodes: get: operationId: listNodes + tags: [ Nodes ] summary: List nodes and latest typed health samples x-permission: observe security: @@ -73,6 +74,7 @@ paths: $ref: ./openapi.yaml#/components/responses/SnapshotUnavailable post: operationId: createNode + tags: [ Nodes ] summary: Add an inline node from a share link description: | Requires resources.nodes.can_manage; otherwise returns 404 @@ -82,7 +84,8 @@ paths: generation.changed. The response does not echo the link; the source text returns it as written (see Visibility in API Configuration). A link the engine cannot parse returns 422 unsupported_value with a sanitized reason in - error.message. A name already in use returns 409 state_conflict. The node belongs to the + error.message. A name already in use, or a configuration change while the + create is being admitted, returns 409 state_conflict. The node belongs to the inline provider and to every group whose filter matches it after reload. Returns 201 with the node once the change is active, or 202 with a node_create operation whose result is the created node. A failed @@ -181,7 +184,7 @@ paths: "404": $ref: ./openapi.yaml#/components/responses/NotFound "409": - description: A node with this name exists + description: A node with this name exists, or the configuration changed while the create was being admitted (state_conflict) headers: Cache-Control: $ref: ./openapi.yaml#/components/headers/NoStore @@ -279,6 +282,7 @@ paths: - $ref: ./openapi.yaml#/components/parameters/NodeId get: operationId: getNode + tags: [ Nodes ] summary: Read one node and its latest typed health samples description: | Returns the same projection as one entry of GET /api/v1/nodes. An unknown id @@ -325,6 +329,7 @@ paths: $ref: ./openapi.yaml#/components/responses/TooLarge delete: operationId: deleteNode + tags: [ Nodes ] summary: Remove an inline node from the managed configuration description: | Requires resources.nodes.can_manage; otherwise returns 404 @@ -407,10 +412,7 @@ paths: $ref: ./openapi.yaml#/components/responses/NotFound "409": $ref: ./openapi.yaml#/components/responses/Conflict - description: A group or the routing final outbound still names the node (state_conflict); nothing is removed. Filter-matched membership is not a reference. - "412": - $ref: ./openapi.yaml#/components/responses/PreconditionFailed - description: The configuration changed while the delete was being admitted (stale_revision); nothing is removed. Read the node back and retry. + description: A group or the routing final outbound still names the node, or the configuration changed while the delete was being admitted (state_conflict); nothing is removed. Filter-matched membership is not a reference. After a concurrent change, read the node back and retry. "413": $ref: ./openapi.yaml#/components/responses/TooLarge "503": @@ -418,6 +420,7 @@ paths: /api/v1/groups: get: operationId: listGroups + tags: [ Groups ] summary: List group summaries x-permission: observe security: @@ -468,7 +471,12 @@ paths: - $ref: ./openapi.yaml#/components/parameters/GroupId get: operationId: getGroup + tags: [ Groups ] summary: Read a complete group resource + description: | + Returns the group with its configuration, runtime selection and health. + The response has no ETag: selection and health change without a + configuration change. Conditional writes use GET /groups/{group_id}/config. x-permission: observe security: - bearerAuth: [] @@ -477,8 +485,6 @@ paths: "200": description: Current group headers: - ETag: - $ref: ./openapi.yaml#/components/headers/ETag Cache-Control: $ref: ./openapi.yaml#/components/headers/NoStore X-Content-Type-Options: @@ -547,6 +553,65 @@ paths: supports_nested_groups: true mutable_config: [ policy, default_member_id, final_outbound, check_url, check_interval, tolerance, idle_timeout, interrupt_connections ] probe_transports: [ tcp, udp ] + x-headers: + Content-Type: application/json + Cache-Control: no-store + X-Content-Type-Options: nosniff + "400": + $ref: ./openapi.yaml#/components/responses/BadRequest + "401": + $ref: ./openapi.yaml#/components/responses/Unauthorized + "403": + $ref: ./openapi.yaml#/components/responses/Forbidden + "404": + $ref: ./openapi.yaml#/components/responses/NotFound + "413": + $ref: ./openapi.yaml#/components/responses/TooLarge + /api/v1/groups/{group_id}/config: + parameters: + - $ref: ./openapi.yaml#/components/parameters/GroupId + get: + operationId: getGroupConfig + tags: [ Groups ] + summary: Read the group's policy and configured options + description: | + Returns the group's policy and config, the same values GET + /groups/{group_id} embeds, as the target document of PATCH. The ETag is + the configuration-wide revision in double quotes; it changes only with + an accepted configuration change. + x-permission: observe + security: + - bearerAuth: [] + - {} + responses: + "200": + description: Current group configuration + headers: + ETag: + $ref: ./openapi.yaml#/components/headers/ETag + description: The configuration-wide revision in double quotes, the value PATCH compares in If-Match. + Cache-Control: + $ref: ./openapi.yaml#/components/headers/NoStore + X-Content-Type-Options: + $ref: ./openapi.yaml#/components/headers/NoSniff + content: + application/json: + schema: + $ref: ./openapi.yaml#/components/schemas/GroupConfigDocument + examples: + current: + value: + policy: + kind: urltest + native: urltest + config: + default_member_id: null + final_outbound: direct + check_url: null + check_interval: 30 + tolerance: 50 + idle_timeout: null + interrupt_connections: false x-headers: Content-Type: application/json ETag: '"17"' @@ -563,12 +628,13 @@ paths: "413": $ref: ./openapi.yaml#/components/responses/TooLarge patch: - operationId: patchGroup + operationId: patchGroupConfig + tags: [ Groups ] summary: Patch mutable group configuration description: | - RFC 6902 JSON Patch whose target document is the group's policy and - config as GET returns them. Operations apply in order to that document; - see groups, Patch semantics, for remove, null, copy, move and test. + RFC 6902 JSON Patch whose target document is GroupConfigDocument, the + body GET /groups/{group_id}/config returns. Operations apply in order to + that document; see groups, Patch semantics, for remove, null, copy, move and test. Only fields in capabilities.mutable_config may change, judged by the effective write against the policy after the patch: one patch may set policy to urltest and add tolerance. When the resulting policy is not @@ -576,14 +642,14 @@ paths: A change to a field absent from mutable_config, such as interrupt_connections on an engine without that option, returns 422 unsupported_value. Requires resources.groups.config_patch. If-Match carries the - configuration-wide revision, so an accepted change anywhere in the - configuration makes an older revision fail with 412. + configuration-wide revision from the ETag of GET, so an accepted change + anywhere in the configuration makes an older revision fail with 412. x-permission: control security: - bearerAuth: [] - {} parameters: - - $ref: ./openapi.yaml#/components/parameters/IfMatch + - $ref: ./openapi.yaml#/components/parameters/IfMatchOptional - $ref: ./openapi.yaml#/components/parameters/IdempotencyKey requestBody: required: true @@ -600,12 +666,16 @@ paths: - op: replace path: /config/interrupt_connections value: true + x-headers: + Content-Type: application/json-patch+json + If-Match: '"17"' responses: "200": - description: Updated group + description: Updated group configuration headers: ETag: $ref: ./openapi.yaml#/components/headers/ETag + description: The new configuration revision in double quotes. Cache-Control: $ref: ./openapi.yaml#/components/headers/NoStore X-Content-Type-Options: @@ -613,7 +683,7 @@ paths: content: application/json: schema: - $ref: ./openapi.yaml#/components/schemas/Group + $ref: ./openapi.yaml#/components/schemas/GroupConfigDocument "202": description: Operation accepted headers: @@ -700,6 +770,7 @@ paths: - $ref: ./openapi.yaml#/components/parameters/GroupId put: operationId: selectGroupMember + tags: [ Groups ] summary: Replace a selector's choice, or pin a member on an automatic group x-permission: control security: @@ -763,6 +834,7 @@ paths: description: The runtime control owner is unavailable or did not confirm the transition (temporarily_unavailable). The selection may have changed; read the group back before retrying after Retry-After. delete: operationId: clearGroupOverride + tags: [ Groups ] summary: Clear a pinned member so the automatic policy chooses again x-permission: control security: @@ -991,7 +1063,7 @@ schemas: enum: [ node, group ] GroupConfig: type: object - description: The group's own configured options, the target of PATCH. For an option the engine supports, null means the group sets no value of its own and the engine's inheritance and defaults apply. Engine-only options go in a nested x- member, for example config["x-dae"].check_addresses; GET reports these members and they are not patch targets. Other unlisted keys are not allowed. + description: The group's own configured options, patched through PATCH /groups/{group_id}/config. For an option the engine supports, null means the group sets no value of its own and the engine's inheritance and defaults apply. Engine-only options go in a nested x- member, for example config["x-dae"].check_addresses; GET reports these members and they are not patch targets. Other unlisted keys are not allowed. required: [ default_member_id, final_outbound, check_url, check_interval, tolerance, idle_timeout, interrupt_connections ] additionalProperties: false patternProperties: @@ -1023,6 +1095,15 @@ schemas: interrupt_connections: type: [ boolean, "null" ] description: Whether changing the selection closes connections through the previous member. Null means the group sets no value of its own, or the engine has no such option. + GroupConfigDocument: + type: object + description: The group's policy and configured options, the target document of PATCH /groups/{group_id}/config. + required: [ policy, config ] + properties: + policy: + $ref: ./openapi.yaml#/components/schemas/GroupPolicy + config: + $ref: ./openapi.yaml#/components/schemas/GroupConfig SafeHttpUrl: type: string format: uri @@ -1194,10 +1275,9 @@ schemas: - $ref: ./openapi.yaml#/components/schemas/InterruptPatch - $ref: ./openapi.yaml#/components/schemas/RemovePatch - $ref: ./openapi.yaml#/components/schemas/CopyMovePatch - description: RFC 6902 operations over the group's policy and config, applied in order. Bounded by resources.groups.max_patch_operations. + description: RFC 6902 operations over the group's policy and config, applied in order. Bounded by resources.groups.max_patch_operations. Members an operation object does not define are ignored (RFC 6902 §4). PolicyPatch: type: object - additionalProperties: false required: [ op, path, value ] properties: op: @@ -1210,7 +1290,6 @@ schemas: $ref: "./openapi.yaml#/components/schemas/GroupPolicyRequest" MemberIdPatch: type: object - additionalProperties: false required: [ op, path, value ] properties: op: @@ -1223,7 +1302,6 @@ schemas: type: [ string, "null" ] OutboundPatch: type: object - additionalProperties: false required: [ op, path, value ] properties: op: @@ -1236,7 +1314,6 @@ schemas: type: [ string, "null" ] CheckUrlPatch: type: object - additionalProperties: false required: [ op, path, value ] properties: op: @@ -1251,7 +1328,6 @@ schemas: - $ref: "./openapi.yaml#/components/schemas/SafeHttpUrl" PositiveIntegerPatch: type: object - additionalProperties: false required: [ op, path, value ] properties: op: @@ -1265,7 +1341,6 @@ schemas: minimum: 1 TolerancePatch: type: object - additionalProperties: false required: [ op, path, value ] properties: op: @@ -1279,7 +1354,6 @@ schemas: minimum: 0 IdleTimeoutPatch: type: object - additionalProperties: false required: [ op, path, value ] properties: op: @@ -1293,7 +1367,6 @@ schemas: minimum: 0 InterruptPatch: type: object - additionalProperties: false required: [ op, path, value ] properties: op: @@ -1309,7 +1382,6 @@ schemas: enum: [ /policy, /config/default_member_id, /config/final_outbound, /config/check_url, /config/check_interval, /config/tolerance, /config/idle_timeout, /config/interrupt_connections ] RemovePatch: type: object - additionalProperties: false required: [ op, path ] properties: op: @@ -1318,7 +1390,6 @@ schemas: $ref: ./openapi.yaml#/components/schemas/MutableGroupPath CopyMovePatch: type: object - additionalProperties: false required: [ op, path, from ] properties: op: diff --git a/api/openapi.yaml b/api/openapi.yaml index 4a66f31..559517c 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -5,12 +5,50 @@ info: version: 0.1.0-draft description: | Normative wire contract for the native control-plane API. Unknown response - extension fields are permitted. Request objects reject unknown fields. + extension fields are permitted. Request objects reject unknown fields, + except JSON Patch operation objects, which ignore them as RFC 6902 requires. Published as a generated bundle at source/openapi.yaml; edit the api/ sources, not the published file. servers: - url: http://localhost:9527 security: [] +tags: + - name: Discovery + description: Entry point, version and capabilities. + - name: Authentication + description: Password-mode setup, login and logout. + - name: Configuration + description: Accepted configuration sources, validation and source writes. + - name: Runtime + description: Runtime status, memory, outbounds, history and datapath. + - name: "Runtime settings" + description: Runtime settings the engine can change without a reload. + - name: Nodes + description: Nodes and node management. + - name: Providers + description: Node providers and subscription refresh. + - name: Geodata + description: Geodata files, sources and updates. + - name: Groups + description: Groups, their configuration and runtime selection. + - name: Probes + description: Node and group health probes. + - name: Connections + description: Live connections. + - name: Flows + description: Recorded flows and their traces. + - name: Routing + description: Routing simulation. + - name: Rules + description: Traffic routing rules. + - name: DNS + description: DNS query, log, cache and rules. + - name: Events + description: Server-sent change events. + - name: Logs + description: Engine log records. + - name: Operations + description: Asynchronous operations (reload, suspend, resume) and their status. paths: /api: $ref: ./discovery.yaml#/paths/~1api @@ -62,6 +100,8 @@ paths: $ref: ./nodes-groups.yaml#/paths/~1api~1v1~1groups /api/v1/groups/{group_id}: $ref: ./nodes-groups.yaml#/paths/~1api~1v1~1groups~1{group_id} + /api/v1/groups/{group_id}/config: + $ref: ./nodes-groups.yaml#/paths/~1api~1v1~1groups~1{group_id}~1config /api/v1/groups/{group_id}/selection: $ref: ./nodes-groups.yaml#/paths/~1api~1v1~1groups~1{group_id}~1selection /api/v1/probes: @@ -134,6 +174,8 @@ components: $ref: ./common.yaml#/parameters/OperationId IfMatch: $ref: ./common.yaml#/parameters/IfMatch + IfMatchOptional: + $ref: ./common.yaml#/parameters/IfMatchOptional IdempotencyKey: $ref: ./common.yaml#/parameters/IdempotencyKey LastEventId: @@ -159,8 +201,6 @@ components: $ref: ./common.yaml#/responses/Gone SnapshotExpired: $ref: ./common.yaml#/responses/SnapshotExpired - PreconditionFailed: - $ref: ./common.yaml#/responses/PreconditionFailed TooLarge: $ref: ./common.yaml#/responses/TooLarge UnsupportedMediaType: @@ -222,6 +262,8 @@ components: $ref: ./discovery.yaml#/schemas/Capabilities EffectiveConfig: $ref: ./config.yaml#/schemas/EffectiveConfig + ConfigSourceContent: + $ref: ./config.yaml#/schemas/ConfigSourceContent ConfigSource: $ref: ./config.yaml#/schemas/ConfigSource ConfigSourceCreate: @@ -356,6 +398,8 @@ components: $ref: ./nodes-groups.yaml#/schemas/GroupPolicyRequest GroupMember: $ref: ./nodes-groups.yaml#/schemas/GroupMember + GroupConfigDocument: + $ref: ./nodes-groups.yaml#/schemas/GroupConfigDocument GroupConfig: $ref: ./nodes-groups.yaml#/schemas/GroupConfig SafeHttpUrl: @@ -506,6 +550,8 @@ components: $ref: ./flow-steps.yaml#/schemas/RuleCondition RuleEvaluation: $ref: ./flow-steps.yaml#/schemas/RuleEvaluation + EntityTagList: + $ref: ./common.yaml#/schemas/EntityTagList IpAddress: $ref: ./common.yaml#/schemas/IpAddress RoutingTraceInput: diff --git a/api/operations-probes.yaml b/api/operations-probes.yaml index b620f64..23cfd1a 100644 --- a/api/operations-probes.yaml +++ b/api/operations-probes.yaml @@ -2,6 +2,7 @@ paths: /api/v1/probes: post: operationId: createProbe + tags: [ Probes ] summary: Start a bounded typed probe operation x-permission: control security: @@ -22,7 +23,6 @@ paths: type: group group_id: group-proxy kind: dns - purpose: dns transport: [ udp ] ip_version: ipv4 members: [ node-hk-01, group-jp ] @@ -114,6 +114,7 @@ paths: /api/v1/operations/reload: post: operationId: startReload + tags: [ Operations ] summary: Start an asynchronous reload x-permission: control security: @@ -188,6 +189,7 @@ paths: /api/v1/operations/suspend: post: operationId: startSuspend + tags: [ Operations ] summary: Start an asynchronous suspend transition x-permission: control security: @@ -262,6 +264,7 @@ paths: /api/v1/operations/resume: post: operationId: startResume + tags: [ Operations ] summary: Start an asynchronous resume transition x-permission: control security: @@ -302,6 +305,7 @@ paths: /api/v1/operations/{operation_id}: get: operationId: getOperation + tags: [ Operations ] summary: Read an asynchronous operation x-permission: observe-owner-or-control security: @@ -510,19 +514,18 @@ schemas: ProbeRequest: type: object description: >- - The schema does not restrict kind, purpose and transport combinations. - tcp_connect and http take purpose data over tcp; dns takes purpose dns. - Any other combination parses and returns 422 unsupported_value. + The schema does not restrict kind and transport combinations. + tcp_connect and http run over tcp; dns runs over tcp, udp or both. + Any other combination parses and returns 422 unsupported_value. The + kind fixes the health purpose that results report: tcp_connect and http + test data, dns tests dns. additionalProperties: false - required: [ target, kind, purpose, transport, ip_version, warmth ] + required: [ target, kind, transport, ip_version, warmth ] properties: target: $ref: ./openapi.yaml#/components/schemas/ProbeTarget kind: $ref: ./openapi.yaml#/components/schemas/ProbeKind - purpose: - type: string - enum: [ data, dns ] transport: type: array minItems: 1 diff --git a/api/providers.yaml b/api/providers.yaml index 9d31dba..eeb6d73 100644 --- a/api/providers.yaml +++ b/api/providers.yaml @@ -2,6 +2,7 @@ paths: /api/v1/providers: get: operationId: listProviders + tags: [ Providers ] summary: List provider metadata description: | Read current subscription, file and inline provider metadata; never fetch a @@ -67,6 +68,7 @@ paths: $ref: ./openapi.yaml#/components/responses/SnapshotUnavailable post: operationId: createProvider + tags: [ Providers ] summary: Add a subscription provider to the managed configuration description: | Requires resources.providers.can_manage; otherwise returns 404 @@ -80,7 +82,8 @@ paths: POST /providers/{provider_id}/refresh to load it. Otherwise the backend may fetch the provider during activation and returns its actual state. The response carries the URL in url_redacted, as written apart from listener secrets. A name already in use - returns 409 state_conflict. A URL that is not HTTP(S) returns 422 unsupported_value. + returns 409 state_conflict, as does a configuration change while the create is + being admitted. A URL that is not HTTP(S) returns 422 unsupported_value. update_interval, user_agent and cache are accepted only when named in resources.providers.create_options; an omitted one takes the default listed there, and one the backend does not list returns 422 unsupported_value. @@ -194,7 +197,7 @@ paths: "404": $ref: ./openapi.yaml#/components/responses/NotFound "409": - description: A provider with this name exists + description: A provider with this name exists, or the configuration changed while the create was being admitted (state_conflict) headers: Cache-Control: $ref: ./openapi.yaml#/components/headers/NoStore @@ -292,6 +295,7 @@ paths: - $ref: ./openapi.yaml#/components/parameters/ProviderId get: operationId: getProvider + tags: [ Providers ] summary: Read one provider's current metadata x-permission: observe security: @@ -342,6 +346,7 @@ paths: $ref: ./openapi.yaml#/components/responses/TooLarge delete: operationId: deleteProvider + tags: [ Providers ] summary: Remove a subscription or file provider from the managed configuration description: | Requires resources.providers.can_manage; otherwise returns 404 @@ -424,10 +429,7 @@ paths: $ref: ./openapi.yaml#/components/responses/NotFound "409": $ref: ./openapi.yaml#/components/responses/Conflict - description: The configuration still names the provider in a reference the delete would break (state_conflict); nothing is removed. Filter-matched membership is not a reference. - "412": - $ref: ./openapi.yaml#/components/responses/PreconditionFailed - description: The configuration changed while the delete was being admitted (stale_revision); nothing is removed. Read the provider back and retry. + description: The configuration still names the provider in a reference the delete would break, or the configuration changed while the delete was being admitted (state_conflict); nothing is removed. Filter-matched membership is not a reference. After a concurrent change, read the provider back and retry. "413": $ref: ./openapi.yaml#/components/responses/TooLarge "503": @@ -437,6 +439,7 @@ paths: - $ref: ./openapi.yaml#/components/parameters/ProviderId post: operationId: refreshProvider + tags: [ Providers ] summary: Queue a provider refresh description: | Requires resources.providers.can_refresh. This action takes no request body diff --git a/api/routing.yaml b/api/routing.yaml index a9110c1..16f5488 100644 --- a/api/routing.yaml +++ b/api/routing.yaml @@ -2,6 +2,7 @@ paths: /api/v1/routing/trace: post: operationId: traceRouting + tags: [ Routing ] summary: Simulate bounded routing evaluation x-permission: control security: diff --git a/api/rules.yaml b/api/rules.yaml index 3c22708..018d01e 100644 --- a/api/rules.yaml +++ b/api/rules.yaml @@ -2,6 +2,7 @@ paths: /api/v1/rules: get: operationId: listRules + tags: [ Rules ] summary: Read the running generation's routing rules description: | Return one coherent, complete rule dictionary for the running routing generation, diff --git a/api/runtime.yaml b/api/runtime.yaml index a4db21a..b2f4977 100644 --- a/api/runtime.yaml +++ b/api/runtime.yaml @@ -2,6 +2,7 @@ paths: /api/v1/runtime: get: operationId: getRuntime + tags: [ Runtime ] summary: Read coherent runtime summary x-permission: observe security: @@ -90,6 +91,7 @@ paths: /api/v1/runtime/memory: get: operationId: getRuntimeMemory + tags: [ Runtime ] summary: Read lightweight process, cgroup, and eBPF memory x-permission: observe security: @@ -143,6 +145,7 @@ paths: /api/v1/runtime/outbounds: get: operationId: getRuntimeOutbounds + tags: [ Runtime ] summary: Read per-outbound cumulative counters description: | The engine keeps cumulative per-outbound counters for visible traffic, @@ -211,6 +214,7 @@ paths: /api/v1/runtime/traffic/history: get: operationId: getTrafficHistory + tags: [ Runtime ] summary: Read bounded traffic history description: | Reads a bounded in-memory ring of visible traffic samples without @@ -317,6 +321,7 @@ paths: /api/v1/runtime/memory/history: get: operationId: getMemoryHistory + tags: [ Runtime ] summary: Read bounded memory history description: | Requires resources.memory_history.available. Reads a bounded in-memory ring @@ -423,6 +428,7 @@ paths: /api/v1/datapath: get: operationId: getDatapath + tags: [ Runtime ] summary: Read detailed datapath state x-permission: observe security: diff --git a/api/settings.yaml b/api/settings.yaml index c5bc76b..b686c8c 100644 --- a/api/settings.yaml +++ b/api/settings.yaml @@ -2,6 +2,7 @@ paths: /api/v1/runtime/settings: get: operationId: getRuntimeSettings + tags: [ "Runtime settings" ] summary: Read the runtime-adjustable settings description: | Requires resources.runtime_settings.available. Returns the current @@ -99,6 +100,7 @@ paths: $ref: ./openapi.yaml#/components/responses/TooLarge patch: operationId: patchRuntimeSettings + tags: [ "Runtime settings" ] summary: Change runtime-adjustable settings without a reload description: | Requires control and resources.runtime_settings.available. The body is a @@ -312,11 +314,9 @@ schemas: type: string enum: [ log.level, log.buffered_records, dns_log.max_records, flows.max_flows, flows.retention_seconds, record_flows, record_logs, record_dns_log, geodata ] RecorderMode: - description: The value a PATCH sets for a recorder. true pins a permitted recorder on, false forces it off, and "auto" (the startup default) lets the engine record on demand. What counts as demand, and how long it lasts, is engine-defined. GET reports the result as the string RecorderState.mode, not as this value. - oneOf: - - type: boolean - - type: string - enum: [ auto ] + type: string + enum: [ "on", "off", auto ] + description: A recorder's mode. "on" keeps a permitted recorder on without clients, "off" forces it off, and auto (the startup default) lets the engine record on demand. What counts as demand, and how long it lasts, is engine-defined. PATCH sets it, GET reports it in RecorderState.mode, and resources.flows.recording reports the effective flow policy, which can be off when recording is disallowed or sampled when the engine samples. RecorderState: type: object required: [ allowed, mode, active ] @@ -325,9 +325,8 @@ schemas: type: boolean description: The configuration permits this recorder; false means never, whatever the mode. mode: - type: string - enum: [ auto, "on", "off" ] - description: The recorder's mode as reported by GET, always a string. "on" follows a PATCH of true, "off" a PATCH of false, and auto a PATCH of "auto" or no PATCH at all. + $ref: ./openapi.yaml#/components/schemas/RecorderMode + description: The mode last set by PATCH, or auto when none was set. active: type: boolean description: The recorder is capturing right now. diff --git a/redocly.yaml b/redocly.yaml index ee402d7..4e2239b 100644 --- a/redocly.yaml +++ b/redocly.yaml @@ -1,6 +1,18 @@ +# recommended-strict reports every recommended rule as an error, so any new +# lint finding fails CI. Deliberate exceptions are listed per location in +# .redocly.lint-ignore.yaml: +# - no-required-schema-properties-undefined: if/then and not branches require +# properties that the enclosing schema defines; the rule does not look there. +# - no-unused-components: SSE event payload schemas and the event example are bound by name through +# x-event-data-schemas on the text/event-stream media type, which the rule +# does not follow. tools/contract.mjs checks each binding resolves. +# - no-server-example.com: the server entry documents the local default listener. extends: - - spec + - recommended-strict rules: + # The repository has no license yet; add info.license when the maintainers + # choose one. + info-license: off no-invalid-media-type-examples: severity: error allowAdditionalProperties: true diff --git a/source/openapi.yaml b/source/openapi.yaml index fc54502..9b99657 100644 --- a/source/openapi.yaml +++ b/source/openapi.yaml @@ -4,17 +4,57 @@ info: version: 0.1.0-draft description: | Normative wire contract for the native control-plane API. Unknown response - extension fields are permitted. Request objects reject unknown fields. + extension fields are permitted. Request objects reject unknown fields, + except JSON Patch operation objects, which ignore them as RFC 6902 requires. Published as a generated bundle at source/openapi.yaml; edit the api/ sources, not the published file. jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: http://localhost:9527 security: [] +tags: + - name: Discovery + description: Entry point, version and capabilities. + - name: Authentication + description: Password-mode setup, login and logout. + - name: Configuration + description: Accepted configuration sources, validation and source writes. + - name: Runtime + description: Runtime status, memory, outbounds, history and datapath. + - name: Runtime settings + description: Runtime settings the engine can change without a reload. + - name: Nodes + description: Nodes and node management. + - name: Providers + description: Node providers and subscription refresh. + - name: Geodata + description: Geodata files, sources and updates. + - name: Groups + description: Groups, their configuration and runtime selection. + - name: Probes + description: Node and group health probes. + - name: Connections + description: Live connections. + - name: Flows + description: Recorded flows and their traces. + - name: Routing + description: Routing simulation. + - name: Rules + description: Traffic routing rules. + - name: DNS + description: DNS query, log, cache and rules. + - name: Events + description: Server-sent change events. + - name: Logs + description: Engine log records. + - name: Operations + description: Asynchronous operations (reload, suspend, resume) and their status. paths: /api: get: operationId: getDiscovery + tags: + - Discovery summary: Discover the native API description: 'Public in every auth mode, so a client can choose how to sign in. A caller the listener admits (a valid bearer or session, or no credential on an explicitly secretless loopback listener) receives the full view. Any other request without a credential receives the public view, which carries only the API name and major, the sign-in links, and the auth mode. A request that carries a credential is authenticated first: an invalid one gets 401, never the public view. No resource permission is required.' security: @@ -89,6 +129,8 @@ paths: /api/v1/version: get: operationId: getVersion + tags: + - Discovery summary: Read native and engine version identity description: Requires bearer authentication when the listener has a deployment secret or runs in password mode. Anonymous access is permitted only on an explicitly secretless loopback listener. No resource permission is required. security: @@ -135,6 +177,8 @@ paths: /api/v1/capabilities: get: operationId: getCapabilities + tags: + - Discovery summary: Negotiate resources, visibility, and limits description: Requires bearer authentication when the listener has a deployment secret or runs in password mode. Anonymous access is permitted only on an explicitly secretless loopback listener. No resource permission is required. security: @@ -231,9 +275,6 @@ paths: - tcp_connect - http - dns - purposes: - - data - - dns transports: - tcp - udp @@ -255,7 +296,7 @@ paths: max_bulk_close: 1000 flows: available: true - recording: on_demand + recording: auto scopes: - userspace_tcp - userspace_udp @@ -383,6 +424,8 @@ paths: /api/v1/auth/setup: post: operationId: setupAdministrator + tags: + - Authentication summary: Create the administrator account and open a session description: 'Available only while discovery reports `auth.mode: password` with `setup_required: true`. Checks run in this order: the Authorization header, the peer, the account state, the body. A header that does not carry a live session gets 401 `authentication_required`; before setup no session exists. The peer must be loopback, RFC 1918, RFC 4193 or link-local; any other peer is refused with `permission_denied` before the account state is read. After setup, the answer is 409 `setup_already_completed`, with or without a live session.' security: [] @@ -439,6 +482,8 @@ paths: /api/v1/auth/login: post: operationId: login + tags: + - Authentication summary: Exchange the administrator credentials for a session description: 'Available only in password mode after setup. Checks run in this order: the Authorization header, the account state, the body. A header that does not carry a live session gets 401 `authentication_required`; a live session does not replace the body, which is still validated. Before setup the answer is `setup_required`; a wrong username or password is `invalid_credentials`. The engine may end a session before `expires_at`, for example to stay within its session limit.' security: [] @@ -495,6 +540,8 @@ paths: /api/v1/auth/logout: post: operationId: logout + tags: + - Authentication summary: End the session that authenticates this request description: Requires a password-session bearer; a configured bearer cannot log out. security: @@ -524,6 +571,8 @@ paths: /api/v1/config: get: operationId: getConfig + tags: + - Configuration summary: Read the effective configuration description: | Requires resources.config.available. Returns one coherent snapshot of the @@ -612,27 +661,7 @@ paths: '400': $ref: '#/components/responses/BadRequest' '401': - description: Credentials are missing or invalid - headers: - Cache-Control: - $ref: '#/components/headers/NoStore' - X-Content-Type-Options: - $ref: '#/components/headers/NoSniff' - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - authentication_required: - value: - error: - code: authentication_required - message: Credentials are required. - request_id: request-config-1 - x-headers: - Content-Type: application/json - Cache-Control: no-store - X-Content-Type-Options: nosniff + $ref: '#/components/responses/Unauthorized' '403': description: Authenticated caller lacks observe permission headers: @@ -711,6 +740,8 @@ paths: /api/v1/config/validate: post: operationId: validateConfig + tags: + - Configuration summary: Validate candidate configuration without applying it description: | Requires resources.config_validate.available and control because the body @@ -823,27 +854,7 @@ paths: Cache-Control: no-store X-Content-Type-Options: nosniff '401': - description: Credentials are missing or invalid - headers: - Cache-Control: - $ref: '#/components/headers/NoStore' - X-Content-Type-Options: - $ref: '#/components/headers/NoSniff' - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - authentication_required: - value: - error: - code: authentication_required - message: Credentials are required. - request_id: request-validate-2 - x-headers: - Content-Type: application/json - Cache-Control: no-store - X-Content-Type-Options: nosniff + $ref: '#/components/responses/Unauthorized' '403': description: Authenticated caller lacks control permission headers: @@ -999,6 +1010,8 @@ paths: /api/v1/config/sources: post: operationId: createConfigSource + tags: + - Configuration summary: Create one configuration source and reload description: | Requires control, resources.config.available, resources.config.writable, @@ -1260,11 +1273,15 @@ paths: example: source-main get: operationId: getConfigSource + tags: + - Configuration summary: Read one accepted configuration source description: | - Requires resources.config.available. Returns the same ConfigSource as - GET /config, not a fresh read of the store, with its content; mask listener - secrets as in GET /config. + Requires resources.config.available. Returns the source's identity and + content from the same snapshot as GET /config, not a fresh read of the + store; mask listener secrets as in GET /config. The body carries no field + that can change while the source bytes stay the same: writable and + loaded_at are only in the GET /config source list. Unknown source IDs return 404 resource_not_found. Redacted text must never be saved as a replacement; compare its UTF-8 SHA-256 with content_sha256 before using returned content as an editing representation. @@ -1276,6 +1293,11 @@ paths: '200': description: Accepted source; content remains subject to visibility policy headers: + ETag: + schema: + type: string + minLength: 1 + description: The source's content_sha256 in double quotes, the value PUT compares in If-Match. Sent only when content is complete, that is, when no listener-secret value was masked; a masked body has no ETag. Cache-Control: $ref: '#/components/headers/NoStore' X-Content-Type-Options: @@ -1283,7 +1305,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ConfigSource' + $ref: '#/components/schemas/ConfigSourceContent' examples: editable: summary: Unredacted dae text. @@ -1293,8 +1315,6 @@ paths: kind: main content_sha256: d1f62f00c6da9ec33956e66b8cc3b4670f164556fc12453193904af23451dec1 bytes: 31 - writable: true - loaded_at: '2026-08-15T09:30:00Z' content: | routing { fallback: direct @@ -1302,6 +1322,7 @@ paths: line_count: 3 x-headers: Content-Type: application/json + ETag: '"d1f62f00c6da9ec33956e66b8cc3b4670f164556fc12453193904af23451dec1"' Cache-Control: no-store X-Content-Type-Options: nosniff redacted: @@ -1311,8 +1332,6 @@ paths: kind: main content_sha256: '0000000000000000000000000000000000000000000000000000000000000000' bytes: 90 - writable: false - loaded_at: '2026-08-15T09:30:00Z' content: | global { log_level: info @@ -1371,6 +1390,8 @@ paths: $ref: '#/components/responses/RateLimited' put: operationId: replaceConfigSource + tags: + - Configuration summary: Replace one configuration source and reload description: | Requires control, resources.config.available, resources.config.writable, @@ -1427,18 +1448,14 @@ paths: - bearerAuth: [] - {} parameters: - - name: If-Match - in: header - required: true + - $ref: '#/components/parameters/IfMatch' description: | - One strong entity tag containing the source's content_sha256 from - GET /config, enclosed in double quotes. Compare the digest with the - source's current bytes in the configuration store, not the snapshot revision. Wildcards, weak - tags, and tag lists are not accepted. - schema: - type: string - pattern: ^"[0-9a-f]{64}"$ - example: '"d1f62f00c6da9ec33956e66b8cc3b4670f164556fc12453193904af23451dec1"' + The source's content_sha256, from GET /config or the ETag of + GET /config/sources/{source_id}, enclosed in double quotes. The server + compares it with the source's current bytes in the configuration store, + not the snapshot revision, as RFC 9110 §13.1.1 defines: `*` matches the + existing source, a list matches when any strong tag in it matches, and a + weak tag never matches. - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true @@ -1632,6 +1649,8 @@ paths: /api/v1/runtime: get: operationId: getRuntime + tags: + - Runtime summary: Read coherent runtime summary x-permission: observe security: @@ -1720,6 +1739,8 @@ paths: /api/v1/runtime/memory: get: operationId: getRuntimeMemory + tags: + - Runtime summary: Read lightweight process, cgroup, and eBPF memory x-permission: observe security: @@ -1773,6 +1794,8 @@ paths: /api/v1/runtime/outbounds: get: operationId: getRuntimeOutbounds + tags: + - Runtime summary: Read per-outbound cumulative counters description: | The engine keeps cumulative per-outbound counters for visible traffic, @@ -1841,6 +1864,8 @@ paths: /api/v1/runtime/traffic/history: get: operationId: getTrafficHistory + tags: + - Runtime summary: Read bounded traffic history description: | Reads a bounded in-memory ring of visible traffic samples without @@ -1947,6 +1972,8 @@ paths: /api/v1/runtime/memory/history: get: operationId: getMemoryHistory + tags: + - Runtime summary: Read bounded memory history description: | Requires resources.memory_history.available. Reads a bounded in-memory ring @@ -2053,6 +2080,8 @@ paths: /api/v1/datapath: get: operationId: getDatapath + tags: + - Runtime summary: Read detailed datapath state x-permission: observe security: @@ -2128,6 +2157,8 @@ paths: /api/v1/nodes: get: operationId: listNodes + tags: + - Nodes summary: List nodes and latest typed health samples x-permission: observe security: @@ -2200,6 +2231,8 @@ paths: $ref: '#/components/responses/SnapshotUnavailable' post: operationId: createNode + tags: + - Nodes summary: Add an inline node from a share link description: | Requires resources.nodes.can_manage; otherwise returns 404 @@ -2209,7 +2242,8 @@ paths: generation.changed. The response does not echo the link; the source text returns it as written (see Visibility in API Configuration). A link the engine cannot parse returns 422 unsupported_value with a sanitized reason in - error.message. A name already in use returns 409 state_conflict. The node belongs to the + error.message. A name already in use, or a configuration change while the + create is being admitted, returns 409 state_conflict. The node belongs to the inline provider and to every group whose filter matches it after reload. Returns 201 with the node once the change is active, or 202 with a node_create operation whose result is the created node. A failed @@ -2309,7 +2343,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: A node with this name exists + description: A node with this name exists, or the configuration changed while the create was being admitted (state_conflict) headers: Cache-Control: $ref: '#/components/headers/NoStore' @@ -2405,6 +2439,8 @@ paths: /api/v1/providers: get: operationId: listProviders + tags: + - Providers summary: List provider metadata description: | Read current subscription, file and inline provider metadata; never fetch a @@ -2472,6 +2508,8 @@ paths: $ref: '#/components/responses/SnapshotUnavailable' post: operationId: createProvider + tags: + - Providers summary: Add a subscription provider to the managed configuration description: | Requires resources.providers.can_manage; otherwise returns 404 @@ -2485,7 +2523,8 @@ paths: POST /providers/{provider_id}/refresh to load it. Otherwise the backend may fetch the provider during activation and returns its actual state. The response carries the URL in url_redacted, as written apart from listener secrets. A name already in use - returns 409 state_conflict. A URL that is not HTTP(S) returns 422 unsupported_value. + returns 409 state_conflict, as does a configuration change while the create is + being admitted. A URL that is not HTTP(S) returns 422 unsupported_value. update_interval, user_agent and cache are accepted only when named in resources.providers.create_options; an omitted one takes the default listed there, and one the backend does not list returns 422 unsupported_value. @@ -2601,7 +2640,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: A provider with this name exists + description: A provider with this name exists, or the configuration changed while the create was being admitted (state_conflict) headers: Cache-Control: $ref: '#/components/headers/NoStore' @@ -2699,6 +2738,8 @@ paths: - $ref: '#/components/parameters/ProviderId' get: operationId: getProvider + tags: + - Providers summary: Read one provider's current metadata x-permission: observe security: @@ -2751,6 +2792,8 @@ paths: $ref: '#/components/responses/TooLarge' delete: operationId: deleteProvider + tags: + - Providers summary: Remove a subscription or file provider from the managed configuration description: | Requires resources.providers.can_manage; otherwise returns 404 @@ -2833,10 +2876,7 @@ paths: $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' - description: The configuration still names the provider in a reference the delete would break (state_conflict); nothing is removed. Filter-matched membership is not a reference. - '412': - $ref: '#/components/responses/PreconditionFailed' - description: The configuration changed while the delete was being admitted (stale_revision); nothing is removed. Read the provider back and retry. + description: The configuration still names the provider in a reference the delete would break, or the configuration changed while the delete was being admitted (state_conflict); nothing is removed. Filter-matched membership is not a reference. After a concurrent change, read the provider back and retry. '413': $ref: '#/components/responses/TooLarge' '503': @@ -2846,6 +2886,8 @@ paths: - $ref: '#/components/parameters/ProviderId' post: operationId: refreshProvider + tags: + - Providers summary: Queue a provider refresh description: | Requires resources.providers.can_refresh. This action takes no request body @@ -2985,6 +3027,8 @@ paths: - $ref: '#/components/parameters/NodeId' get: operationId: getNode + tags: + - Nodes summary: Read one node and its latest typed health samples description: | Returns the same projection as one entry of GET /api/v1/nodes. An unknown id @@ -3032,6 +3076,8 @@ paths: $ref: '#/components/responses/TooLarge' delete: operationId: deleteNode + tags: + - Nodes summary: Remove an inline node from the managed configuration description: | Requires resources.nodes.can_manage; otherwise returns 404 @@ -3114,10 +3160,7 @@ paths: $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' - description: A group or the routing final outbound still names the node (state_conflict); nothing is removed. Filter-matched membership is not a reference. - '412': - $ref: '#/components/responses/PreconditionFailed' - description: The configuration changed while the delete was being admitted (stale_revision); nothing is removed. Read the node back and retry. + description: A group or the routing final outbound still names the node, or the configuration changed while the delete was being admitted (state_conflict); nothing is removed. Filter-matched membership is not a reference. After a concurrent change, read the node back and retry. '413': $ref: '#/components/responses/TooLarge' '503': @@ -3125,6 +3168,8 @@ paths: /api/v1/geodata: get: operationId: getGeoData + tags: + - Geodata summary: Read the geosite and geoip assets the engine loaded description: | Requires resources.geodata.available; otherwise returns 404 @@ -3246,6 +3291,8 @@ paths: /api/v1/geodata/update: post: operationId: updateGeoData + tags: + - Geodata summary: Queue a download of the geosite and geoip assets description: | Requires resources.geodata.can_update; otherwise returns 404 @@ -3359,6 +3406,8 @@ paths: /api/v1/groups: get: operationId: listGroups + tags: + - Groups summary: List group summaries x-permission: observe security: @@ -3409,7 +3458,13 @@ paths: - $ref: '#/components/parameters/GroupId' get: operationId: getGroup + tags: + - Groups summary: Read a complete group resource + description: | + Returns the group with its configuration, runtime selection and health. + The response has no ETag: selection and health change without a + configuration change. Conditional writes use GET /groups/{group_id}/config. x-permission: observe security: - bearerAuth: [] @@ -3418,8 +3473,6 @@ paths: '200': description: Current group headers: - ETag: - $ref: '#/components/headers/ETag' Cache-Control: $ref: '#/components/headers/NoStore' X-Content-Type-Options: @@ -3498,6 +3551,66 @@ paths: probe_transports: - tcp - udp + x-headers: + Content-Type: application/json + Cache-Control: no-store + X-Content-Type-Options: nosniff + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '413': + $ref: '#/components/responses/TooLarge' + /api/v1/groups/{group_id}/config: + parameters: + - $ref: '#/components/parameters/GroupId' + get: + operationId: getGroupConfig + tags: + - Groups + summary: Read the group's policy and configured options + description: | + Returns the group's policy and config, the same values GET + /groups/{group_id} embeds, as the target document of PATCH. The ETag is + the configuration-wide revision in double quotes; it changes only with + an accepted configuration change. + x-permission: observe + security: + - bearerAuth: [] + - {} + responses: + '200': + description: Current group configuration + headers: + ETag: + $ref: '#/components/headers/ETag' + description: The configuration-wide revision in double quotes, the value PATCH compares in If-Match. + Cache-Control: + $ref: '#/components/headers/NoStore' + X-Content-Type-Options: + $ref: '#/components/headers/NoSniff' + content: + application/json: + schema: + $ref: '#/components/schemas/GroupConfigDocument' + examples: + current: + value: + policy: + kind: urltest + native: urltest + config: + default_member_id: null + final_outbound: direct + check_url: null + check_interval: 30 + tolerance: 50 + idle_timeout: null + interrupt_connections: false x-headers: Content-Type: application/json ETag: '"17"' @@ -3514,12 +3627,14 @@ paths: '413': $ref: '#/components/responses/TooLarge' patch: - operationId: patchGroup + operationId: patchGroupConfig + tags: + - Groups summary: Patch mutable group configuration description: | - RFC 6902 JSON Patch whose target document is the group's policy and - config as GET returns them. Operations apply in order to that document; - see groups, Patch semantics, for remove, null, copy, move and test. + RFC 6902 JSON Patch whose target document is GroupConfigDocument, the + body GET /groups/{group_id}/config returns. Operations apply in order to + that document; see groups, Patch semantics, for remove, null, copy, move and test. Only fields in capabilities.mutable_config may change, judged by the effective write against the policy after the patch: one patch may set policy to urltest and add tolerance. When the resulting policy is not @@ -3527,14 +3642,14 @@ paths: A change to a field absent from mutable_config, such as interrupt_connections on an engine without that option, returns 422 unsupported_value. Requires resources.groups.config_patch. If-Match carries the - configuration-wide revision, so an accepted change anywhere in the - configuration makes an older revision fail with 412. + configuration-wide revision from the ETag of GET, so an accepted change + anywhere in the configuration makes an older revision fail with 412. x-permission: control security: - bearerAuth: [] - {} parameters: - - $ref: '#/components/parameters/IfMatch' + - $ref: '#/components/parameters/IfMatchOptional' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true @@ -3551,12 +3666,16 @@ paths: - op: replace path: /config/interrupt_connections value: true + x-headers: + Content-Type: application/json-patch+json + If-Match: '"17"' responses: '200': - description: Updated group + description: Updated group configuration headers: ETag: $ref: '#/components/headers/ETag' + description: The new configuration revision in double quotes. Cache-Control: $ref: '#/components/headers/NoStore' X-Content-Type-Options: @@ -3564,7 +3683,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Group' + $ref: '#/components/schemas/GroupConfigDocument' '202': description: Operation accepted headers: @@ -3651,6 +3770,8 @@ paths: - $ref: '#/components/parameters/GroupId' put: operationId: selectGroupMember + tags: + - Groups summary: Replace a selector's choice, or pin a member on an automatic group x-permission: control security: @@ -3714,6 +3835,8 @@ paths: description: The runtime control owner is unavailable or did not confirm the transition (temporarily_unavailable). The selection may have changed; read the group back before retrying after Retry-After. delete: operationId: clearGroupOverride + tags: + - Groups summary: Clear a pinned member so the automatic policy chooses again x-permission: control security: @@ -3780,6 +3903,8 @@ paths: /api/v1/probes: post: operationId: createProbe + tags: + - Probes summary: Start a bounded typed probe operation x-permission: control security: @@ -3800,7 +3925,6 @@ paths: type: group group_id: group-proxy kind: dns - purpose: dns transport: - udp ip_version: ipv4 @@ -3895,6 +4019,8 @@ paths: /api/v1/connections: get: operationId: listConnections + tags: + - Connections summary: Read a scope-labelled live connection snapshot x-permission: observe security: @@ -4001,6 +4127,8 @@ paths: $ref: '#/components/responses/TooLarge' delete: operationId: closeConnections + tags: + - Connections summary: Close matching userspace-owned connections description: | Requires control permission and resources.connections.available with @@ -4172,6 +4300,8 @@ paths: /api/v1/connections/{connection_id}: delete: operationId: closeConnection + tags: + - Connections summary: Close one userspace-owned connection description: | Requires control permission and resources.connections.available with @@ -4283,6 +4413,8 @@ paths: /api/v1/flows: get: operationId: listFlows + tags: + - Flows summary: List active and retained terminal flow decisions x-permission: observe security: @@ -4411,6 +4543,8 @@ paths: /api/v1/flows/{flow_id}: get: operationId: getFlow + tags: + - Flows summary: Read one recorded causal flow trace x-permission: observe security: @@ -4527,7 +4661,6 @@ paths: routing_source: evaluation routed_outbound: proxy effective_outbound: proxy - mode_override: none selection_path: - group_id: group-proxy member_id: node-hk-01 @@ -4713,7 +4846,6 @@ paths: routing_source: evaluation routed_outbound: proxy effective_outbound: proxy - mode_override: none selection_path: [] leaf_node_id: node-hk-01 leaf_node_name: hk-01 @@ -4793,7 +4925,6 @@ paths: routing_source: evaluation routed_outbound: proxy effective_outbound: proxy - mode_override: none selection_path: [] leaf_node_id: node-hk-01 leaf_node_name: hk-01 @@ -4819,6 +4950,8 @@ paths: /api/v1/routing/trace: post: operationId: traceRouting + tags: + - Routing summary: Simulate bounded routing evaluation x-permission: control security: @@ -4961,6 +5094,8 @@ paths: /api/v1/rules: get: operationId: listRules + tags: + - Rules summary: Read the running generation's routing rules description: | Return one coherent, complete rule dictionary for the running routing generation, @@ -5076,6 +5211,8 @@ paths: /api/v1/events: get: operationId: streamEvents + tags: + - Events summary: Follow bounded resumable invalidation events description: | Send stream.ready first on every connection, including a valid resume. @@ -5141,6 +5278,8 @@ paths: /api/v1/logs: get: operationId: streamLogs + tags: + - Logs summary: Follow bounded resumable engine logs description: | Requires resources.logs.available. Emit stream.ready first on every connection, @@ -5253,6 +5392,8 @@ paths: /api/v1/runtime/settings: get: operationId: getRuntimeSettings + tags: + - Runtime settings summary: Read the runtime-adjustable settings description: | Requires resources.runtime_settings.available. Returns the current @@ -5371,6 +5512,8 @@ paths: $ref: '#/components/responses/TooLarge' patch: operationId: patchRuntimeSettings + tags: + - Runtime settings summary: Change runtime-adjustable settings without a reload description: | Requires control and resources.runtime_settings.available. The body is a @@ -5603,6 +5746,8 @@ paths: /api/v1/dns/query: post: operationId: queryDns + tags: + - DNS summary: Execute a routed diagnostic DNS query description: 'POST because the query has side effects: it sends live DNS traffic and, with cache_mode normal, writes the runtime cache. The name, types and upstream travel in the JSON body, not in the URL. Requires resources.dns_query.available; without it the request returns 404 capability_not_supported.' x-permission: control @@ -5699,6 +5844,8 @@ paths: /api/v1/dns/log: get: operationId: listDnsLog + tags: + - DNS summary: Read the bounded ring of recent resolutions description: | Requires resources.dns_log.available. Records client DNS resolutions, @@ -5818,6 +5965,8 @@ paths: /api/v1/dns/cache: get: operationId: listDnsCache + tags: + - DNS summary: Read a paginated DNS cache snapshot description: | A page holds at most limit entries and may hold fewer while @@ -5924,6 +6073,8 @@ paths: $ref: '#/components/responses/SnapshotUnavailable' delete: operationId: deleteDnsCacheByName + tags: + - DNS summary: Delete cache entries for one exact name x-permission: control security: @@ -5986,6 +6137,8 @@ paths: /api/v1/dns/cache/{entry_id}: delete: operationId: deleteDnsCacheEntry + tags: + - DNS summary: Delete one opaque DNS cache entry x-permission: control security: @@ -6028,6 +6181,8 @@ paths: /api/v1/dns/cache/flush: post: operationId: flushDnsCache + tags: + - DNS summary: Flush the complete runtime DNS cache x-permission: control security: @@ -6077,6 +6232,8 @@ paths: /api/v1/dns/rules: get: operationId: listDnsRules + tags: + - DNS summary: Read the running generation's DNS routing rules description: | Requires resources.dns_rules.available. Return the DNS routing rules of the @@ -6216,6 +6373,8 @@ paths: /api/v1/operations/reload: post: operationId: startReload + tags: + - Operations summary: Start an asynchronous reload x-permission: control security: @@ -6290,6 +6449,8 @@ paths: /api/v1/operations/suspend: post: operationId: startSuspend + tags: + - Operations summary: Start an asynchronous suspend transition x-permission: control security: @@ -6364,6 +6525,8 @@ paths: /api/v1/operations/resume: post: operationId: startResume + tags: + - Operations summary: Start an asynchronous resume transition x-permission: control security: @@ -6404,6 +6567,8 @@ paths: /api/v1/operations/{operation_id}: get: operationId: getOperation + tags: + - Operations summary: Read an asynchronous operation x-permission: observe-owner-or-control security: @@ -6565,7 +6730,7 @@ components: type: string const: nosniff ETag: - description: Quoted group configuration revision. + description: Strong entity tag of the representation, the value a later If-Match compares. required: true schema: type: string @@ -6666,9 +6831,16 @@ components: name: If-Match in: header required: true + description: Evaluated as RFC 9110 §13.1.1 defines; see Conditional requests in the error contract. schema: - type: string - minLength: 1 + $ref: '#/components/schemas/EntityTagList' + example: '"d1f62f00c6da9ec33956e66b8cc3b4670f164556fc12453193904af23451dec1"' + IfMatchOptional: + name: If-Match + in: header + description: Evaluated as RFC 9110 §13.1.1 defines; see Conditional requests in the error contract. A new group-configuration PATCH without If-Match returns 428 precondition_required; a retained idempotent replay may omit it. + schema: + $ref: '#/components/schemas/EntityTagList' example: '"17"' IdempotencyKey: name: Idempotency-Key @@ -6788,6 +6960,12 @@ components: Unauthorized: description: Credentials are missing or invalid headers: + WWW-Authenticate: + description: The authentication challenge (RFC 9110 §15.5.2); the native API uses the Bearer scheme. + required: true + schema: + type: string + pattern: ^Bearer(\s|$) Cache-Control: $ref: '#/components/headers/NoStore' X-Content-Type-Options: @@ -6806,6 +6984,7 @@ components: request_id: request-01HZX4K8W6 x-headers: Content-Type: application/json + WWW-Authenticate: Bearer Cache-Control: no-store X-Content-Type-Options: nosniff Forbidden: @@ -6914,29 +7093,6 @@ components: Content-Type: application/json Cache-Control: no-store X-Content-Type-Options: nosniff - PreconditionFailed: - description: If-Match does not equal the current configuration revision or stored source content hash - headers: - Cache-Control: - $ref: '#/components/headers/NoStore' - X-Content-Type-Options: - $ref: '#/components/headers/NoSniff' - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - stale_revision: - value: - error: - code: stale_revision - message: The resource changed; read it again. - details: null - request_id: request-01HZX4K8W6 - x-headers: - Content-Type: application/json - Cache-Control: no-store - X-Content-Type-Options: nosniff TooLarge: description: Request size or advertised fan-out limit exceeded headers: @@ -7135,6 +7291,7 @@ components: - permission_denied - resource_not_found - capability_not_supported + - method_not_allowed - state_conflict - idempotency_conflict - event_cursor_expired @@ -7201,6 +7358,7 @@ components: - api_major - base_path - links + - auth properties: name: type: string @@ -7304,7 +7462,6 @@ components: description: Present in password mode, null otherwise. auth: $ref: '#/components/schemas/AuthDiscovery' - description: Absent from servers that predate password login; clients then assume a configured bearer. PublicDiscovery: type: object description: What a client needs before it signs in. Status, base path, resource links, `auth_logout` and `auth.anonymous_loopback` are withheld. @@ -7772,7 +7929,7 @@ components: type: boolean config_patch: type: boolean - description: PATCH /groups/{group_id} is implemented. A group patch is a configuration write, so true requires resources.config.writable. + description: PATCH /groups/{group_id}/config is implemented. A group patch is a configuration write, so true requires resources.config.writable. selection: type: boolean max_patch_operations: @@ -7790,7 +7947,6 @@ components: required: - targets - kinds - - purposes - transports - ip_versions - limits @@ -7810,14 +7966,6 @@ components: uniqueItems: true items: $ref: '#/components/schemas/ProbeKind' - purposes: - type: array - uniqueItems: true - items: - type: string - enum: - - data - - dns transports: type: array uniqueItems: true @@ -7877,9 +8025,9 @@ components: enum: - 'off' - 'on' + - auto - sampled - - on_demand - description: The engine's flow recording policy, not whether the recorder is capturing now. off records none, because the configuration does not permit the flow recorder or its mode is off; on records every flow in scopes (mode on); sampled records a subset; on_demand is mode auto, recording while clients create demand, whether or not any client does now. recording.flows.active in GET /runtime/settings reports whether the recorder is capturing. + description: The engine's flow recording policy, not whether the recorder is capturing now. off records none, because the configuration does not permit the flow recorder or its mode is off; on records every flow in scopes (mode on); auto enables recording on client demand; sampled records a subset. recording.flows.active in GET /runtime/settings reports whether capture is active now. scopes: type: array uniqueItems: true @@ -8321,16 +8469,15 @@ components: secrets_redacted: type: boolean description: True when a listener-secret value (the deployment secret of an API listener, such as honk's native_api.secret and clash_api.secret) was masked somewhere in this response. Content, paths and diagnostics are otherwise returned in the clear to an admitted request. - ConfigSource: + ConfigSourceContent: type: object + description: One accepted source's identity and content, the representation GET /config/sources/{source_id} returns. Every field stays the same while the source bytes do, so content_sha256 can serve as its entity tag. required: - id - path - kind - content_sha256 - bytes - - writable - - loaded_at - content - line_count properties: @@ -8362,35 +8509,44 @@ components: bytes: $ref: '#/components/schemas/SafeUInt' description: Accepted source size in bytes before redaction. - writable: - type: boolean - description: | - True only when server-wide editing is enabled and this source permits - replacement by a control caller. False for engine-written includes, - generated sources, and subscriptions, for a source that holds API - listener settings or secrets, and while the configuration store - cannot accept writes; observe alone never grants writes. - loaded_at: - $ref: '#/components/schemas/Timestamp' - description: Time these source bytes were accepted, not the current file modification time. content: type: string description: Accepted dae text with listener-secret values masked. Use for editing only if its UTF-8 SHA-256 matches content_sha256. line_count: $ref: '#/components/schemas/SafeUInt' description: Lines in the accepted source before redaction; empty text has zero lines, and a final newline does not add an empty line. - if: - properties: - kind: - enum: - - subscription - - generated - required: - - kind - then: - properties: - writable: - const: false + ConfigSource: + description: A source entry of GET /config, ConfigSourceContent plus the fields that can change without the bytes changing. + allOf: + - $ref: '#/components/schemas/ConfigSourceContent' + - type: object + required: + - writable + - loaded_at + properties: + writable: + type: boolean + description: | + True only when server-wide editing is enabled and this source permits + replacement by a control caller. False for engine-written includes, + generated sources, and subscriptions, for a source that holds API + listener settings or secrets, and while the configuration store + cannot accept writes; observe alone never grants writes. + loaded_at: + $ref: '#/components/schemas/Timestamp' + description: Time these source bytes were accepted, not the current file modification time. + if: + properties: + kind: + enum: + - subscription + - generated + required: + - kind + then: + properties: + writable: + const: false ConfigSourceCreate: type: object additionalProperties: false @@ -10032,9 +10188,20 @@ components: enum: - node - group + GroupConfigDocument: + type: object + description: The group's policy and configured options, the target document of PATCH /groups/{group_id}/config. + required: + - policy + - config + properties: + policy: + $ref: '#/components/schemas/GroupPolicy' + config: + $ref: '#/components/schemas/GroupConfig' GroupConfig: type: object - description: The group's own configured options, the target of PATCH. For an option the engine supports, null means the group sets no value of its own and the engine's inheritance and defaults apply. Engine-only options go in a nested x- member, for example config["x-dae"].check_addresses; GET reports these members and they are not patch targets. Other unlisted keys are not allowed. + description: The group's own configured options, patched through PATCH /groups/{group_id}/config. For an option the engine supports, null means the group sets no value of its own and the engine's inheritance and defaults apply. Engine-only options go in a nested x- member, for example config["x-dae"].check_addresses; GET reports these members and they are not patch targets. Other unlisted keys are not allowed. required: - default_member_id - final_outbound @@ -10325,10 +10492,9 @@ components: - $ref: '#/components/schemas/InterruptPatch' - $ref: '#/components/schemas/RemovePatch' - $ref: '#/components/schemas/CopyMovePatch' - description: RFC 6902 operations over the group's policy and config, applied in order. Bounded by resources.groups.max_patch_operations. + description: RFC 6902 operations over the group's policy and config, applied in order. Bounded by resources.groups.max_patch_operations. Members an operation object does not define are ignored (RFC 6902 §4). PolicyPatch: type: object - additionalProperties: false required: - op - path @@ -10347,7 +10513,6 @@ components: $ref: '#/components/schemas/GroupPolicyRequest' MemberIdPatch: type: object - additionalProperties: false required: - op - path @@ -10368,7 +10533,6 @@ components: - 'null' OutboundPatch: type: object - additionalProperties: false required: - op - path @@ -10389,7 +10553,6 @@ components: - 'null' CheckUrlPatch: type: object - additionalProperties: false required: - op - path @@ -10410,7 +10573,6 @@ components: - $ref: '#/components/schemas/SafeHttpUrl' PositiveIntegerPatch: type: object - additionalProperties: false required: - op - path @@ -10433,7 +10595,6 @@ components: minimum: 1 TolerancePatch: type: object - additionalProperties: false required: - op - path @@ -10455,7 +10616,6 @@ components: minimum: 0 IdleTimeoutPatch: type: object - additionalProperties: false required: - op - path @@ -10477,7 +10637,6 @@ components: minimum: 0 InterruptPatch: type: object - additionalProperties: false required: - op - path @@ -10509,7 +10668,6 @@ components: - /config/interrupt_connections RemovePatch: type: object - additionalProperties: false required: - op - path @@ -10520,7 +10678,6 @@ components: $ref: '#/components/schemas/MutableGroupPath' CopyMovePatch: type: object - additionalProperties: false required: - op - path @@ -10679,12 +10836,11 @@ components: minLength: 1 ProbeRequest: type: object - description: The schema does not restrict kind, purpose and transport combinations. tcp_connect and http take purpose data over tcp; dns takes purpose dns. Any other combination parses and returns 422 unsupported_value. + description: 'The schema does not restrict kind and transport combinations. tcp_connect and http run over tcp; dns runs over tcp, udp or both. Any other combination parses and returns 422 unsupported_value. The kind fixes the health purpose that results report: tcp_connect and http test data, dns tests dns.' additionalProperties: false required: - target - kind - - purpose - transport - ip_version - warmth @@ -10693,11 +10849,6 @@ components: $ref: '#/components/schemas/ProbeTarget' kind: $ref: '#/components/schemas/ProbeKind' - purpose: - type: string - enum: - - data - - dns transport: type: array minItems: 1 @@ -11396,21 +11547,27 @@ components: required: - mode - oneOf: - - properties: + - required: + - trace_status + properties: trace_status: const: complete trace: properties: status: const: complete - - properties: + - required: + - trace_status + properties: trace_status: const: partial trace: properties: status: const: partial - - properties: + - required: + - trace_status + properties: trace_status: const: disabled trace: @@ -11610,7 +11767,9 @@ components: - null allOf: - oneOf: - - properties: + - required: + - chain + properties: chain: enum: - traffic @@ -11621,7 +11780,9 @@ components: - $ref: '#/components/schemas/TrafficRoutingInput' dns_action: type: 'null' - - properties: + - required: + - chain + properties: chain: const: dns_request input: @@ -11638,7 +11799,9 @@ components: type: 'null' mark: type: 'null' - - properties: + - required: + - chain + properties: chain: const: dns_response input: @@ -11935,7 +12098,6 @@ components: - routing_source - routed_outbound - effective_outbound - - mode_override - selection_path - leaf_node_id - leaf_node_name @@ -11979,14 +12141,6 @@ components: type: - string - 'null' - mode_override: - type: string - enum: - - none - - direct - - global - - unknown - description: Engine mode override observed for this outbound attempt, separate from the configured dial mode. This field does not expose a native runtime-mode setting. none means no override was applied; unknown means the recorder could not determine it. selection_path: type: array items: @@ -12260,8 +12414,13 @@ components: type: array items: $ref: '#/components/schemas/RuleCondition' + EntityTagList: + type: string + description: '`*` or a comma-separated list of entity tags, strong or weak (W/ prefix), as RFC 9110 §8.8.3 and §13.1.1 define. An entity tag contains no space, tab or inner double quote. Empty list elements are ignored, as §5.6.1.2 requires of recipients.' + pattern: ^(\*|((W/)?"[!#-~\u0080-\u00ff]*")?([ \t]*,[ \t]*((W/)?"[!#-~\u0080-\u00ff]*")?)*)$ IpAddress: - oneOf: + description: An IPv4 or IPv6 address. Validators must enforce the ipv4 and ipv6 formats; without format assertion, either branch accepts any string. + anyOf: - type: string format: ipv4 - type: string @@ -13373,12 +13532,12 @@ components: geodata: $ref: '#/components/schemas/GeoDataSettingsPatch' RecorderMode: - description: The value a PATCH sets for a recorder. true pins a permitted recorder on, false forces it off, and "auto" (the startup default) lets the engine record on demand. What counts as demand, and how long it lasts, is engine-defined. GET reports the result as the string RecorderState.mode, not as this value. - oneOf: - - type: boolean - - type: string - enum: - - auto + type: string + enum: + - 'on' + - 'off' + - auto + description: A recorder's mode. "on" keeps a permitted recorder on without clients, "off" forces it off, and auto (the startup default) lets the engine record on demand. What counts as demand, and how long it lasts, is engine-defined. PATCH sets it, GET reports it in RecorderState.mode, and resources.flows.recording reports the effective flow policy, which can be off when recording is disallowed or sampled when the engine samples. RecorderState: type: object required: @@ -13390,12 +13549,8 @@ components: type: boolean description: The configuration permits this recorder; false means never, whatever the mode. mode: - type: string - enum: - - auto - - 'on' - - 'off' - description: The recorder's mode as reported by GET, always a string. "on" follows a PATCH of true, "off" a PATCH of false, and auto a PATCH of "auto" or no PATCH at all. + $ref: '#/components/schemas/RecorderMode' + description: The mode last set by PATCH, or auto when none was set. active: type: boolean description: The recorder is capturing right now. diff --git a/source/v0.1.0/en/docs/api-config.md b/source/v0.1.0/en/docs/api-config.md index e8fe570..5b7ba40 100644 --- a/source/v0.1.0/en/docs/api-config.md +++ b/source/v0.1.0/en/docs/api-config.md @@ -16,7 +16,8 @@ the native contract because they make binding and authorization ambiguous. Configure the native listener under `experimental.native_api`. Its settings include `enabled`, `listen`, `secret`, `allow_origins`, and `ui`. Use an explicit -loopback address for local access. A non-loopback listener requires a secret. +loopback address for local access. A non-loopback listener requires a secret or +password mode. `experimental.clash_api.external_controller` configures the separate Clash-compatible listener. @@ -24,7 +25,8 @@ Clash-compatible listener. ## Listener and authentication rules - Omitting `listen` binds only to loopback. -- A non-loopback listener requires `secret`; otherwise startup fails closed. +- A non-loopback listener requires deployment-secret authentication (`secret`) + or password authentication; otherwise startup fails closed. - `secret` is opaque. Implementations may enforce a minimum entropy policy but must not require one specific textual encoding. - Authentication uses `Authorization: Bearer `. Secrets must not appear @@ -77,7 +79,8 @@ The native API defines two permissions: The deployment secret and a password session grant `control`. Implementations may support additional observe-only credentials, but must preserve these -permission names. Missing or invalid required credentials return `401`. An +permission names. Missing or invalid required credentials return `401` with +`WWW-Authenticate: Bearer`. An authenticated caller without the required permission normally receives `403 permission_denied`. Operation reads instead return `404 resource_not_found` for an operation the caller cannot see. diff --git a/source/v0.1.0/en/docs/check-nodes.md b/source/v0.1.0/en/docs/check-nodes.md index 257309f..a850f1f 100644 --- a/source/v0.1.0/en/docs/check-nodes.md +++ b/source/v0.1.0/en/docs/check-nodes.md @@ -16,18 +16,19 @@ title: Probes |-------|----------|----------| | target | yes | Exactly `{type: node, node_id}` or `{type: group, group_id}`. | | kind | yes | `tcp_connect`, `http`, or `dns`; supported kinds are advertised. | -| purpose | yes | `data` or `dns`; the health domain being tested, not inferred from UDP alone. | | transport | yes | Nonempty unique array of `tcp`/`udp`; `tcp_connect` and `http` take `[tcp]`. | | ip_version | yes | `ipv4`, `ipv6`, or `any`. `any` expands to advertised families. | | members | no | Group-only: `direct` (default), `leaves`, or nonempty unique direct-member IDs. | | warmth | yes | `cold` or `warm`. An unimplementable reuse constraint returns 422, not mislabeled results. | `tcp_connect` tests TCP reachability of the configured node server, not a -proxy handshake or application latency; it requires `transport: [tcp]` and -`purpose: data`. `http` tests the configured HTTP(S) check through the target -outbound, requires TCP/data, and measures through the response headers. -`dns` tests the configured DNS check through the target outbound and requires -`purpose: dns`; TCP and/or UDP describe that DNS query's transport. The IP +proxy handshake or application latency; it requires `transport: [tcp]`. +`http` tests the configured HTTP(S) check through the target outbound, requires +TCP, and measures through the response headers. `dns` tests the configured DNS +check through the target outbound; TCP and/or UDP describe that DNS query's +transport. The kind fixes the health `purpose` that results and health +observations report: `tcp_connect` and `http` report `data`, and `dns` reports +`dns`. A DNS probe keeps purpose `dns` when sent over UDP. The IP family refers to the check destination (node server for `tcp_connect`), not necessarily the tunnel's network. There is no arbitrary UDP echo or generic `latency` kind whose success criterion is unspecified. @@ -35,7 +36,7 @@ necessarily the tunnel's network. There is no arbitrary UDP echo or generic A group `direct` target preserves direct members and policy-authorized nested resolution; no eligible leaf produces `unavailable`, never an arbitrary sibling. `leaves` is an explicit diagnostic expansion. Deduplicate identical -leaf/kind/transport/purpose/family/warmth executions while retaining every +leaf/kind/transport/family/warmth executions while retaining every member-to-leaf association in the results. `cold` excludes reusable check connections; `warm` permits but does not require reuse. Result `warmth` states what actually happened (`cold`, `warm`, or `unknown`). Do not claim a cold @@ -43,8 +44,8 @@ physical tunnel merely because a new logical stream was opened. The request cannot specify arbitrary URLs, names, IPs or ports. Use administrator-configured check destinations under the -[outbound-request policy](api-config.html#Outbound-requests). `tcp_connect` and `http` take purpose `data` over -`tcp`; `dns` takes purpose `dns`. The request schema does not restrict these +[outbound-request policy](api-config.html#Outbound-requests). `tcp_connect` and `http` run over +`tcp`. The request schema does not restrict kind and transport combinations: a request that parses but pairs them otherwise, or asks for a target capability the backend lacks, returns `422 unsupported_value` before work. `400 invalid_request` is for a request that does not parse or has the diff --git a/source/v0.1.0/en/docs/configuration.md b/source/v0.1.0/en/docs/configuration.md index a1714f0..785f72d 100644 --- a/source/v0.1.0/en/docs/configuration.md +++ b/source/v0.1.0/en/docs/configuration.md @@ -50,9 +50,11 @@ Never save it over the source. ## GET /api/v1/config/sources/{source_id} -Requires `observe` and `resources.config.available`. Returns one `ConfigSource` -with the same fields and visibility rules as an entry in `GET /config`. It reads -the accepted snapshot, not the store's current contents. An unknown ID returns +Requires `observe` and `resources.config.available`. Returns one source's +identity and content (`ConfigSourceContent`) under the same visibility rules as +`GET /config`. It reads the accepted snapshot, not the store's current contents. +The body leaves out `writable` and `loaded_at`, which can change while the bytes +stay the same; read them from `GET /config`. An unknown ID returns `404 resource_not_found`; unavailable readback returns `404 capability_not_supported`. @@ -107,7 +109,11 @@ allowed length. Content that JSON escaping expands can still exceed the body limit, and that limit then applies. Exceeding either returns `413 request_too_large`. -`If-Match` accepts one quoted strong tag, not a wildcard, weak tag, or tag list. +`GET /config/sources/{source_id}` returns the quoted `content_sha256` in `ETag` +when its content is complete. A body with a masked listener-secret value has no +`ETag`: it is not the representation that `PUT` replaces. +`If-Match` is evaluated as [conditional requests](errors.html#Conditional-requests) +defines. The optional `Idempotency-Key` follows the [operation rules](operations.html): within the running instance's retention window, the same caller, method, path, key, and body return the original operation without another write or hash diff --git a/source/v0.1.0/en/docs/discovery.md b/source/v0.1.0/en/docs/discovery.md index 175c284..6ccd8d8 100644 --- a/source/v0.1.0/en/docs/discovery.md +++ b/source/v0.1.0/en/docs/discovery.md @@ -58,7 +58,7 @@ below. Other fields are withheld, not null. | links.auth_login | string or null | `/api/v1/auth/login` in password mode; null otherwise. | | links.auth_logout | string or null | `/api/v1/auth/logout` in password mode; null otherwise. | | links.x-<engine> | object, optional | The engine's [extension](capabilities.html#Engine-extensions) links, each under `/api/v1/x-/`, in the admitted view only; the public view's links stay closed. No other link member is allowed. | -| auth | object, optional | Authentication mode; absent on servers that predate password login. | +| auth | object | Authentication mode. | | auth.mode | string | `password` or `token`. | | auth.setup_required | boolean | Whether password-mode administrator setup is required. | | auth.anonymous_loopback | boolean | Whether loopback peers may enter without a credential in token mode. | @@ -71,8 +71,7 @@ Clash-compatible `/version` payload. In password mode, use setup when `auth.setup_required` is true and login when it is false. In token mode, use the configured bearer when the response is the public view or `auth.anonymous_loopback` is false, and no credential when it is -true. When `auth` is absent on an older server, or discovery answers `401`, use -the configured bearer. +true. When discovery answers `401`, use the configured bearer. ## Example diff --git a/source/v0.1.0/en/docs/errors.md b/source/v0.1.0/en/docs/errors.md index b114623..6290b84 100644 --- a/source/v0.1.0/en/docs/errors.md +++ b/source/v0.1.0/en/docs/errors.md @@ -6,7 +6,7 @@ title: Errors All native API errors use one JSON envelope: -{% api_example patchGroup 412 stale_revision %} +{% api_example patchGroupConfig 412 stale_revision %} | Field | Type | Description | |-------|------|-------------| @@ -32,14 +32,15 @@ for a failed configuration change listed under | 403 | `permission_denied` | The caller lacks the required permission, or listener security policy rejects the request. | | 404 | `resource_not_found` | The requested resource does not exist. | | 404 | `capability_not_supported` | The running adapter does not expose the resource or action. | -| 409 | `state_conflict` | The current state prevents the request: a name in use, a referenced object that is not current, or a transition the current state does not allow. | +| 405 | `method_not_allowed` | The path exists but does not support the request method; the response lists the supported methods in `Allow`. | +| 409 | `state_conflict` | The current state prevents the request: a name in use, a referenced object that is not current, a transition the current state does not allow, or a configuration change while a write without `If-Match` was being admitted. | | 409 | `idempotency_conflict` | An idempotency key was reused with a different request body. | | 409 | `event_cursor_expired` | Event or log SSE cursor cannot be replayed; open a fresh stream and establish a new baseline. | | 409 | `setup_required` | Password login was requested before an administrator was created. | | 409 | `setup_already_completed` | Administrator setup was requested after an administrator was created. | | 410 | `snapshot_expired` | A page cursor is no longer usable; restart the page walk. | | 410 | `flow_expired` | Flow evidence was evicted/expired and a tombstone still exists. | -| 412 | `stale_revision` | `If-Match` does not match the current resource revision or stored source content hash, or the configuration changed while a delete was being admitted. | +| 412 | `stale_revision` | `If-Match` does not match the current resource revision or stored source content hash. | | 413 | `request_too_large` | Request or requested fan-out exceeds an advertised limit. | | 415 | `unsupported_media_type` | Request `Content-Type` is unsupported. | | 422 | `unsupported_value` | The request is well-formed but the engine does not support its meaning, or full validation of a configuration candidate found error diagnostics. | @@ -57,8 +58,9 @@ every request, and a `GET` with a body is malformed. A server checks a request in this order and returns the status of the first check that fails: -1. Authentication, authorization and routing: `401`, `403`, and `404` for an - unknown route or an unadvertised capability. +1. Authentication, authorization and routing: `401`, `403`, `404` for an + unknown route or an unadvertised capability, and `405` with `Allow` for a + known path that does not support the method. 2. Request boundary: a missing required `If-Match` (`428`) or a malformed one (`400`), then `Content-Type` (`415`), body size (`413`), and parameter and body schema (`400`). @@ -73,19 +75,20 @@ check that fails: 6. Current state: `409`. A rate limit (`429`) or full shared capacity (`503`) is reported when the -request is admitted, after the checks it passed. Three exceptions: source -creation returns `409` for a `path` already in use before it validates the -content; a configuration write decides the listener-settings `403` during -validation; and a group patch checks `Content-Type` (`415`) first and reports a -missing (`428`) or malformed (`400`) `If-Match` after the replay lookup, so a -retained replay returns the original response without `If-Match`. The table below defines each status. Endpoint pages link here +request is admitted, after the checks it passed. Three exceptions apply. +Source creation returns `409` for a `path` already in use before it validates +the content. A configuration write decides the listener-settings `403` during +validation. For a group-configuration patch, check `Content-Type` (`415`) first. +Check a missing (`428`) or malformed (`400`) `If-Match` after replay lookup; a +retained replay can therefore omit the header. The table below defines each status. Endpoint pages link here instead of repeating the order; an endpoint page names only which of its own cases fall in which row. | Status | Code | The request fails because | |--------|------|---------------------------| -| 400 | `invalid_request` | It cannot be parsed, or a parameter or field is outside its schema: wrong type, a missing field, a field the schema does not define, a value outside the schema's enum, range or length, or a scalar value above a bound the capabilities advertise, such as a page `limit` above `max_page_size`. A page cursor sent with different filters or a different `limit` is also `400`. | +| 400 | `invalid_request` | It cannot be parsed, or a parameter or field is outside its schema: wrong type, a missing field, a field the schema does not define (JSON Patch operation objects ignore such members instead), a value outside the schema's enum, range or length, or a scalar value above a bound the capabilities advertise, such as a page `limit` above `max_page_size`. A page cursor sent with different filters or a different `limit` is also `400`. | | 413 | `request_too_large` | The payload, or the fan-out the request asks for, exceeds an advertised bound: the body size, the number of operations in a group patch (`max_patch_operations`), the matching live entries a bulk close selects, including non-closable ones (`max_bulk_close`), or the targets or results of a probe or trace. | +| 405 | `method_not_allowed` | The path exists, but not with this method ([RFC 9110 §15.5.6](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.6)). An unknown path is `404`. | | 428 | `precondition_required` | A required `If-Match` header is missing. | | 412 | `stale_revision` | `If-Match` names a revision or content hash that is no longer current. | | 422 | `unsupported_value` | It is well-formed and within every bound, but this engine does not support its meaning: an enum member or field the schema defines and the capabilities do not advertise, or a combination of fields or capabilities the engine does not implement. Error diagnostics from full validation of a configuration candidate are also `422`. | @@ -103,6 +106,29 @@ write the server could not confirm, such as a group selection, may still have taken effect; read the resource back before retrying. Error messages follow the [visibility rule](api-config.html#Visibility). +## Conditional requests + +Two resources carry a strong `ETag` that a write compares in `If-Match`: + +| Read | `ETag` | Conditional write | +|------|--------|-------------------| +| `GET /config/sources/{source_id}` | the source's `content_sha256`, sent only when no listener-secret value is masked | `PUT /config/sources/{source_id}` | +| `GET /groups/{group_id}/config` | the configuration-wide `revision` | `PATCH /groups/{group_id}/config` | + +The server evaluates `If-Match` as +[RFC 9110 §13.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.1) +defines. `*` matches when the resource exists. A comma-separated list matches +when any strong tag in it equals the current one. A weak tag (`W/"…"`) never +matches. A header that does not match returns `412 stale_revision`; a header +that is not `*` or a list of entity tags returns `400 invalid_request`. + +The server checks the request in the order under +[choosing the status](#Choosing-the-status): body parsing and schema checks come +before the `412` precondition. Unlike +[RFC 9110 §13.2.1](https://www.rfc-editor.org/rfc/rfc9110#section-13.2.1), this +contract validates the body before it compares `If-Match` with the current tag; +when both fail, the body error takes precedence. + ## Page cursors Every paged list (`GET /nodes`, `/providers`, `/flows`, `/dns/cache`, diff --git a/source/v0.1.0/en/docs/flows.md b/source/v0.1.0/en/docs/flows.md index d467ec1..cb6e840 100644 --- a/source/v0.1.0/en/docs/flows.md +++ b/source/v0.1.0/en/docs/flows.md @@ -327,10 +327,10 @@ Unobserved kernel-direct/blocked flows cannot be hidden behind a full userspace list. Kernel bypasses (multicast, own traffic, local services, closed admission) must be declared even where no connection exists. -Capabilities advertise `recording` (`off`, `on`, `sampled`, `on_demand`), +Capabilities advertise `recording` (`off`, `on`, `auto`, `sampled`), `scopes`, the optional `min_flows`, `max_flows`, `max_steps_per_flow`, `retention_seconds`, `snapshot_ttl_seconds`, and `max_page_size`. -`on_demand` follows the +`auto` follows the [runtime settings](runtime-status.html#GET-api-v1-runtime-settings) demand rules. `max_flows` and `retention_seconds` are supported maxima, not active values; that endpoint reports the active values. Retention is a **maximum age after termination**, not a @@ -354,7 +354,8 @@ slower approximation that re-executes routing during a read. An implementation claiming full transparency MUST demonstrate actual records for: TCP dial failure before tracker insertion; UDP no-reply expiry versus -reply-then-idle; kernel direct and block; must/block resisting mode override; +reply-then-idle; kernel direct and block; rules that an engine-wide outbound +mode does not override, where the engine has such a mode; each supported dial mode; accepted and rejected domain verification; DNS hit, stale hit, upstream failure and remote name resolution; nested group selection and cancelled/retried dials; interleaved A/AAAA evaluation/attempt references; diff --git a/source/v0.1.0/en/docs/groups.md b/source/v0.1.0/en/docs/groups.md index 139ea21..1875ed9 100644 --- a/source/v0.1.0/en/docs/groups.md +++ b/source/v0.1.0/en/docs/groups.md @@ -10,8 +10,9 @@ title: Groups The group API separates these responsibilities: -- `GET` reads the current group state. -- `PATCH` changes group configuration only. +- `GET /groups/{group_id}` reads the current group state. +- `GET /groups/{group_id}/config` reads the group's policy and configured + options with an `ETag`, and `PATCH` on the same path changes them. - `PUT` changes runtime selection when the policy supports manual selection, or pins a member on an automatic policy that allows an override. - `DELETE` clears such a pin so the automatic policy chooses again. - `POST /api/v1/probes` starts a typed probe job whose target can be a group. @@ -27,7 +28,7 @@ The group API separates these responsibilities: | id | string | Opaque stable group identifier. Do not derive API identity from `name`. | | name | string | Engine-visible group name. | | icon | string or null | Icon the configuration names for the group (absolute http(s) URL or data URI), shown beside the name. `null` when none is configured; a client may keep its own local override. | -| config_revision | string | Configuration-wide revision, the same value as `revision` in [GET /config](configuration.html); PATCH sends it in `If-Match`. Any accepted configuration change advances it, not only a change to this group. | +| config_revision | string | The same underlying revision as [`GET /config`](configuration.html)'s `revision`, quoted in the `ETag` of `GET /groups/{group_id}/config`. Any accepted configuration change advances it, not only a change to this group. | | policy.kind | string | Canonical behavior: `selector`, `urltest`, `loadbalance`, `fallback`, `random`, `score`, or `fixed`. A `fixed` group always uses its configured member, even when that member fails its check; dae's `fixed(index)` maps to it, with the index naming the member. | | policy.native | string | Effective engine policy, not a configuration alias that the runtime implements differently. | | members | array | Direct group members, in declaration order. | @@ -85,25 +86,40 @@ Use the detail endpoint for members and health observations. ## GET /api/v1/groups/{group_id} -Returns the complete current group resource described above. -The response includes an `ETag` whose value matches `config_revision`. +Returns the complete current group resource described above. The response +has no `ETag` because a tag based only on the configuration revision would not +track selection or health changes. Use `GET /groups/{group_id}/config` +for a conditional write. ### Request {% api_request getGroup %} -## PATCH /api/v1/groups/{group_id} +## GET /api/v1/groups/{group_id}/config + +Returns `{"policy": …, "config": …}`, the group's `policy` and `config` as +`GET /groups/{group_id}` reports them. The `ETag` is the configuration-wide +`config_revision` in double quotes and changes only with an accepted +configuration change. + +{% api_example getGroupConfig 200 current %} + +## PATCH /api/v1/groups/{group_id}/config Updates group configuration only. It does not change runtime selection. -Use RFC 6902 JSON Patch and send the revision returned by `GET` in -`If-Match`. Requires `resources.groups.config_patch`; without it the request +Use RFC 6902 JSON Patch and send the `ETag` of `GET /groups/{group_id}/config` +in `If-Match`, evaluated as [conditional requests](errors.html#Conditional-requests) +defines. A new group-configuration PATCH without `If-Match` returns `428`; a +retained idempotent replay may omit it. +Requires `resources.groups.config_patch`; without it the request returns `404 capability_not_supported`. A group patch is a configuration write, so `config_patch` is true only when `resources.config.writable` is. Because `config_revision` is configuration-wide, a patch sent after an -unrelated accepted change returns `412`; read the group again and retry. +unrelated accepted change returns `412`. Read `GET /groups/{group_id}/config` +again and retry with its current `ETag`. -{% api_example patchGroup request tolerance http %} +{% api_example patchGroupConfig request tolerance http %} Only fields listed in `capabilities.mutable_config` may be patched, judged by the effective write: the fields whose values the patch changes, checked against @@ -119,7 +135,8 @@ membership sources are intentionally not part of this operation: configuration reload and must not be silently changed at runtime. More operations than `resources.groups.max_patch_operations` returns `413 request_too_large` before any change is applied. A successful synchronous -update returns the new `ETag`; a rejected patch changes nothing. +update returns the updated document and its new `ETag`; a rejected patch +changes nothing. Mutable `check_url` values follow the [outbound-request policy](api-config.html#Outbound-requests). A group has one @@ -130,10 +147,11 @@ a patch changes `check_url`, after which it resolves the new host itself. ### Patch semantics -The patch target is the document `{"policy": …, "config": …}` built from the -group's `policy` and `config` as `GET` returns them. The server applies the +The patch target is the document `GET /groups/{group_id}/config` returns, so +paths are `/policy` and `/config/