Skip to content

fix(api): HTTP semantics, group config resource, simpler fields, strict lint - #42

Merged
Zakkaus merged 7 commits into
daeuniverse:honkfrom
Zakkaus:fix/contract-standards
Sep 28, 2026
Merged

Zakkaus merged 7 commits into
daeuniverse:honkfrom
Zakkaus:fix/contract-standards

Conversation

@Zakkaus

@Zakkaus Zakkaus commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Final-acceptance fixes, part F1: HTTP and JSON Patch semantics, a group configuration resource, fewer engine-specific fields, and a strict Redocly lint. Findings come from the two final audits (A = astra, O = opus).

Items

  1. IpAddress uses anyOf (A12, O3). Both branches are strings that differ only by format, and OAS 3.1 treats format as an annotation by default, so oneOf rejected every address. dae: none, the value is still an IPv4 or IPv6 string.
  2. 412 only for a failed If-Match (A9, O1, O2). Node and provider create and delete now report a configuration change during admission as 409 state_conflict; the 412 responses on the two deletes are gone. dae: return 409 when its write lock sees a newer revision.
  3. Conditional group patch on a configuration resource (A6, A11). GET /groups/{id} carries selection and health, so a strong ETag tied to the configuration revision did not describe it. New GET /groups/{id}/config returns {policy, config} with the revision as ETag; PATCH moves to the same path. The operation paths (/policy, /config/<option>) do not change, since the patch already targeted that document. If-Match is optional in the schema so a retained replay can omit it; a server that requires it returns 428. GET /groups/{id} keeps config and sends no ETag. dae: one extra read route over the group's configured options.
    • The spec named "the GroupConfig document", but GroupConfig has no policy, and policy is patchable. The document is therefore {policy, config}, which groups.md already defined as the patch target.
  4. JSON Patch operations ignore unknown members (A10, RFC 6902 §4). Patch operation schemas no longer set additionalProperties: false, and the rule that request objects reject unknown fields now excludes patch operations. dae: check the members each op needs and ignore the rest.
  5. If-Match per RFC 9110 §13.1.1 (A8, O8). * matches an existing resource, a list matches when any strong tag matches, and a weak tag never matches. A header that does not match returns 412; a header that is not entity-tag syntax returns 400. The new shared EntityTagList schema accepts the full syntax. errors.md has a new "Conditional requests" section. dae: standard entity-tag list parsing.
  6. GET /config/sources/{id} sends ETag (O8): the quoted content_sha256, the value PUT compares. dae: the hash it already computes.
  7. 405 with Allow (O8, RFC 9110 §15.5.6) for a known path with an unsupported method, as new ErrorCode method_not_allowed. Unknown paths stay 404. dae: Go's router reports method mismatches separately.
  8. Precondition order. honk decodes the source-PUT body before comparing the hash, so the contract keeps body checks before 412 and states this once, under Conditional requests, with the RFC 9110 §13.2.1 difference. honk's group patch checks operation members after the revision, which gets an HC row.
  9. WWW-Authenticate on every 401 (A7). The shared Unauthorized response requires WWW-Authenticate: Bearer and shows it in its example. The two inline 401 responses in config.yaml now use the shared response. honk already sends the header.
  10. Operations link template (A2): the contract keeps {operation_id}. No contract change; HC row for honk's {id}.
  11. mode_override leaves the shared outbound step (O4). It is honk's Clash mode, which dae does not have. honk-notes documents it as data["x-honk"].mode_override. flows.md now says "rules that an engine-wide outbound mode does not override, where the engine has such a mode" instead of "must/block resisting mode override".
  12. Probe request purpose removed (A15). The kind determines it: tcp_connect and http test data, and dns tests dns. The probes.purposes capability is gone. Results and health keep purpose. dae: derive it from the kind.
  13. One recorder mode spelling (O21): "on" | "off" | "auto" in the PATCH body, recording.*.mode and resources.flows.recording (which keeps sampled). true, false and on_demand are gone.
  14. Discovery auth required (O24). A draft has no older servers to allow for.
  15. Redocly (A14, O9). Every operation has one area tag, and the tags are declared with descriptions at the root. Lint extends recommended-strict, so CI (yarn check:contract) fails on any finding. Exceptions are listed by location in .redocly.lint-ignore.yaml, and redocly.yaml gives the reason for each: then/not branches that require properties the parent defines, SSE payload schemas bound through x-event-data-schemas, and the localhost server. The RouteStepData and FlowDetail oneOf branches now require their discriminator, and the unused PreconditionFailed response is removed.
  • info-license is off, not satisfied: the repository has no license, and choosing one is up to the maintainers.
  1. Listener rule (A3). A non-loopback listener requires deployment-secret or password authentication, plus TLS. The old text required a secret, which contradicted password mode.

Review fixes

  1. Source ETag (high). GET /config/sources/{source_id} returns ConfigSourceContent: id, path, absolute_path, kind, content_sha256, bytes, content and line_count, all fixed for given bytes. writable and loaded_at stay in the GET /config source list (ConfigSource = ConfigSourceContent plus those two), so one strong ETag never covers two bodies (RFC 9110 §8.8.1). A body with a masked listener-secret value is not what PUT replaces, so it has no ETag. PUT still compares the stored content hash. dae: serialize the source without the two fields and set the header only when nothing was masked.
  2. EntityTagList. The pattern follows RFC 9110 §5.6.1.2 and §8.8.3: empty list elements ("17", , "18") are accepted, spaces, tabs and inner quotes inside a tag are not, and only an uppercase W/ marks a weak tag. Tests cover each case. dae: split on commas, trim OWS, skip empty elements, and reject any element that is not W/"…" or "…" over etagc.
  3. Examples. The group patch example carries Content-Type: application/json-patch+json and If-Match: "17", so the replay test removes a header that is present. The shared IfMatch example (source PUT) is a quoted SHA-256; IfMatchOptional keeps "17".
  4. Descriptions. Node and provider create 409 also covers a configuration change during admission (state_conflict). Discovery drops the case of a server without auth, which is now required.
  5. Wording. A new group-configuration PATCH without If-Match returns 428, and a retained idempotent replay may omit it; the error contract, groups page and IfMatchOptional say the same. The body-before-precondition order is stated as a deviation from RFC 9110 §13.2.1. Recording policy, probe purpose, the group ETag rationale and the retry step are reworded without changing their meaning. dae: requires If-Match on group patches, as honk already does.
  6. License. info-license stays off. The maintainers (CODEOWNERS) choose the license; the rule comes back once the repository has one.

The drift ledger now also records: the single-source body and ETag change for honk (native_api/config.rs:577-578); group patch validation split into operation-shape checks before the 412 precondition and failed test (409) or unsupported values (422) after it; and no transition release for probe purpose: honk rejects it as an unknown field and doona stops sending it. doona does not read the single-source GET, so it needs no change for item 1.

Checks

  • yarn check:contract: bundle, strict lint with 0 problems (96 ignored by location), checker (575 examples, 222 schemas, 47 paths), 86/86 tests.
  • hexo clean && hexo generate: builds.

honk and doona

The drift ledger has an F1 row for each honk change: 412→409 on node/provider writes, the group config route, patch members, If-Match parsing, source ETag, 405, patch validation order, the operations link, x-honk.mode_override, probe purpose, recorder strings. It also has D3 items for doona: the group editor route, the recorder strings, dropping probe purpose, and reading mode_override from x-honk.

- IpAddress uses anyOf; the ipv4/ipv6 branches differ only by format.
- Node and provider create/delete report a configuration change during
  admission as 409 state_conflict; 412 is only for a failed If-Match.
- JSON Patch operation objects ignore members they do not define
  (RFC 6902 section 4).
- If-Match follows RFC 9110 section 13.1.1: `*`, tag lists and weak tags
  are well-formed and are compared, not rejected as malformed.
- GET /config/sources/{source_id} sends the content hash as ETag.
- A known path with an unsupported method returns 405 with Allow.
- errors.md states once that body checks precede the 412 precondition.
- Every 401 carries WWW-Authenticate: Bearer.
- A non-loopback listener needs a deployment secret or password mode.
GET /groups/{group_id} carries runtime selection and health, which change
without a configuration change, so one strong ETag tied to the
configuration revision cannot describe it. The group now sends no ETag.

GET /groups/{group_id}/config returns {policy, config}, the document the
patch already targeted, with the configuration revision as ETag. PATCH
moves to the same path; operation paths (/policy, /config/<option>) and
bodies are unchanged. If-Match is optional in the schema so a retained
replay can omit it; a server that requires it returns 428.
- mode_override is honk's Clash mode, which dae does not have. It leaves
  the shared outbound step and is documented as x-honk in the honk notes.
- A probe request no longer carries purpose: the kind determines it.
  Results and health observations keep purpose; the capability drops
  its purposes axis.
- Recorder mode has one spelling, "on" | "off" | "auto", in the PATCH
  body, GET recording.*.mode and resources.flows.recording.
- Discovery auth is required; a draft has no older servers to allow for.
Every operation carries one area tag, declared with a description at the
root. Lint now extends recommended-strict, so any finding fails CI.
Deliberate exceptions are listed per location in .redocly.lint-ignore.yaml
(conditional then/not branches, SSE payload schemas bound through
x-event-data-schemas, the local server), explained in redocly.yaml.
info-license stays off until the repository has a license.

The RouteStepData and FlowDetail oneOf branches now require their
discriminator, and the unused PreconditionFailed response is removed.
GET /config/sources/{id} tags its body with content_sha256, so the body now carries only identity and content; writable and loaded_at stay in the GET /config list. A masked body has no ETag. EntityTagList follows RFC 9110: empty list elements are ignored and an entity tag has no space or tab.
The group patch example carries its Content-Type and If-Match, so the replay test drops a header that is actually there. The source PUT If-Match example is a content hash. Node and provider create 409 also covers a configuration change during admission. Discovery no longer mentions servers without auth.
…obes

A new group-configuration PATCH without If-Match returns 428; a retained replay may omit it. The body-before-precondition order is stated as a deviation from RFC 9110 §13.2.1.
@Zakkaus
Zakkaus merged commit 87d44dd into daeuniverse:honk Sep 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant