# harvested from https://hagglebee.com/governance/spectral.yml on 2026-10-05 — authored and served by the provider x-method: harvested x-source: https://hagglebee.com/governance/spectral.yml x-fetched: '2026-10-05' extends: - - spectral:oas - recommended formats: - oas3 documentationUrl: https://yawplet.com/governance/ rules: license-url: 'off' operation-operationid-camel-case: description: Every operation has an operationId in camelCase. MCP tools, the Postman collection and the API reference are all keyed on it. message: '{{path}} needs a camelCase operationId: {{error}}' severity: error given: - $.paths[*][get,put,post,delete,patch,options,head,trace] - $.webhooks[*][get,put,post,delete,patch,options,head,trace] then: - field: operationId function: truthy - field: operationId function: casing functionOptions: type: camel operation-summary-and-description: description: Every operation has both a summary (one line, used as the tool title) and a description (what it does, what it costs, what comes back). message: '{{path}} is missing a summary or a description.' severity: error given: - $.paths[*][get,put,post,delete,patch,options,head,trace] - $.webhooks[*][get,put,post,delete,patch,options,head,trace] then: - field: summary function: truthy - field: description function: truthy operation-single-declared-tag: description: Every operation has exactly one tag, and it is one of the tags declared at the root (operation-tag-defined from spectral:oas checks the declaration). One tag means one folder in the Postman collection and one section in the reference. message: '{{path}} must have exactly one declared tag.' severity: error given: - $.paths[*][get,put,post,delete,patch,options,head,trace] - $.webhooks[*][get,put,post,delete,patch,options,head,trace] then: field: tags function: schema functionOptions: schema: type: array minItems: 1 maxItems: 1 items: type: string tags-described: description: Every root tag has a description, so a reader knows what the group is for. message: Tag {{path}} has no description. severity: info given: $.tags[*] then: field: description function: truthy info-contact-complete: description: info.contact names the operator with a name, an email and a url. message: 'info.contact needs name, email and url: {{error}}' severity: error given: $.info then: field: contact function: schema functionOptions: schema: type: object required: - name - email - url info-terms-of-service: description: info.termsOfService is an https URL. Each site serves the same terms at /terms/. message: info.termsOfService must be an https URL. severity: warn given: $.info then: field: termsOfService function: pattern functionOptions: match: ^https:// servers-https-only: description: Every server URL is https. The sites are served only over TLS. message: Server {{value}} is not https. severity: error given: $.servers[*] then: field: url function: pattern functionOptions: match: ^https:// paths-versioned-lowercase: description: Every path starts with /v1 and is made of lowercase segments (a-z, 0-9, hyphen) or {snake_case} parameters, with no trailing slash and no query string. message: Path {{property}} is not /v1 plus lowercase segments without a trailing slash. severity: error given: $.paths then: field: '@key' function: pattern functionOptions: match: ^/v1(/([a-z][a-z0-9-]*|\{[a-z_]+\}))+$ error-responses-problem-json: description: Every 4xx and 5xx response offers application/problem+json (RFC 9457). Agents can rely on type, title, status, detail and a stable code. message: '{{path}} does not offer application/problem+json.' severity: error given: $.paths[*][*].responses[?(@property.match(/^[45]/))] then: field: content function: schema functionOptions: schema: type: object required: - application/problem+json problem-schema-is-error: description: Every application/problem+json body is the shared Error schema, by reference, so there is one problem shape across the API. message: '{{path}} must be $ref: #/components/schemas/Error' severity: error resolved: false given: - $.components.responses[*].content['application/problem+json'].schema - $.paths[*][*].responses[?(@property.match(/^[45]/))].content['application/problem+json'].schema then: field: $ref function: pattern functionOptions: match: ^#/components/schemas/Error$ success-response-example: description: Every 2xx response returns application/json with an example (or named examples). Agents learn the shape from the example before they spend anything. message: '{{path}} needs application/json with an example.' severity: error given: $.paths[*][*].responses[?(@property.match(/^2/))] then: field: content function: schema functionOptions: schema: type: object required: - application/json properties: application/json: anyOf: - required: - example - required: - examples request-body-example: description: Every JSON request body has an example (or named examples), and the Postman collection sends it as the default body. message: '{{path}} needs a request example.' severity: error given: $.paths[*][*].requestBody.content['application/json'] then: function: schema functionOptions: schema: anyOf: - required: - example - required: - examples create-post-safe-to-retry-and-try: description: createPost declares the Idempotency-Key header (a retry is never charged twice) and the dry_run query parameter (every check, nothing charged). Posting costs money, so both are part of the contract. message: createPost must declare Idempotency-Key (header) and dry_run (query). severity: error given: $.paths['/v1/posts'].post then: field: parameters function: schema functionOptions: schema: type: array allOf: - contains: type: object required: - name - in properties: name: const: Idempotency-Key in: const: header - contains: type: object required: - name - in properties: name: const: dry_run in: const: query no-credentials-in-query: description: No query parameter carries a credential. Keys travel in the Authorization header, never in a URL that ends up in logs. message: Query parameter {{value}} looks like a credential. severity: error given: - $.paths[*][*].parameters[?(@.in == 'query')] - $.paths[*].parameters[?(@.in == 'query')] - $.components.parameters[?(@.in == 'query')] then: field: name function: pattern functionOptions: notMatch: /^(api[_-]?key|key|token|access[_-]?token|secret|password|auth|authorization|session)$/i security-scheme-bearer-header: description: Every security scheme is HTTP bearer, so the API key is sent as Authorization Bearer and nowhere else. message: '{{path}} must be type http, scheme bearer.' severity: error given: $.components.securitySchemes[*] then: function: schema functionOptions: schema: type: object required: - type - scheme properties: type: const: http scheme: const: bearer webhooks-signed-delivery: description: 'The contract has a webhooks section, and every outbound delivery declares the Standard Webhooks headers: webhook-id, webhook-timestamp and webhook-signature, all required.' message: 'webhooks must exist and each delivery must declare required webhook-id, webhook-timestamp and webhook-signature headers: {{error}}' severity: error given: $ then: field: webhooks function: schema functionOptions: schema: type: object minProperties: 1 additionalProperties: type: object required: - post properties: post: type: object required: - parameters properties: parameters: type: array allOf: - contains: type: object required: - name - in - required properties: name: const: webhook-id in: const: header required: const: true - contains: type: object required: - name - in - required properties: name: const: webhook-timestamp in: const: header required: const: true - contains: type: object required: - name - in - required properties: name: const: webhook-signature in: const: header required: const: true agentic-access-declared: description: 'Every operation carries x-agentic-access: action-class (read, acting, connected), consequence (read, write, financial, irreversible), human-in-the-loop (none, recommended, required), reversible, and notes. Defined in x-agentic-access-schema.' message: '{{path}} needs a complete x-agentic-access: {{error}}' severity: error given: - $.paths[*][get,put,post,delete,patch,options,head,trace] - $.webhooks[*][get,put,post,delete,patch,options,head,trace] then: field: x-agentic-access function: schema functionOptions: schema: type: object additionalProperties: false required: - action-class - consequence - human-in-the-loop - reversible - notes properties: action-class: enum: - read - acting - connected consequence: enum: - read - write - financial - irreversible human-in-the-loop: enum: - none - recommended - required reversible: type: boolean notes: type: string minLength: 20 agentic-access-irreversible-not-reversible: description: An operation whose consequence is irreversible cannot also say reversible true. message: '{{path}} says irreversible but reversible is not false.' severity: error given: $.paths[*][?(@ && @['x-agentic-access'] && @['x-agentic-access'].consequence == 'irreversible')] then: field: x-agentic-access.reversible function: schema functionOptions: schema: const: false agentic-access-402-is-financial: description: An operation that can answer 402 (the owner must pay) moves money, so its x-agentic-access consequence is financial. message: '{{path}} can answer 402, so its consequence must be financial.' severity: error given: $.paths[*][?(@ && @.responses && @.responses['402'])] then: field: x-agentic-access.consequence function: pattern functionOptions: match: ^financial$ read-is-read: description: An operation whose action-class is read has no write consequence. It either changes nothing (read) or, like search past its free allowance, costs money (financial). message: '{{path}} is a read, so its consequence must be read or financial.' severity: error given: $.paths[*][?(@ && @['x-agentic-access'] && @['x-agentic-access']['action-class'] == 'read')] then: field: x-agentic-access.consequence function: pattern functionOptions: match: ^(read|financial)$ needs-human-carries-account-url: description: 'The NeedsHuman problem shows account_url and for_human: true, the hand-off every agent must recognise.' message: 'The NeedsHuman example must carry account_url and for_human: true.' severity: warn given: $.components.responses.NeedsHuman.content['application/problem+json'].example then: function: schema functionOptions: schema: type: object required: - account_url - for_human properties: for_human: const: true post-content-untrusted: description: 'A published Post declares content_trust: untrusted-user-content as a constant, so every reader is told not to follow what a post says.' message: Post.content_trust must be const untrusted-user-content. severity: error given: $.components.schemas.Post.properties.content_trust then: field: const function: pattern functionOptions: match: ^untrusted-user-content$ money-integer-micro-dollars: description: An integer money field (price, balance, amount, charged, refunded, penalty_if_abuse, threshold, price_each) says in its description that it is micro-dollars (1 USD = 1,000,000). message: '{{path}} is money: say micro-dollars in its description.' severity: warn given: $..properties[?(@property.match(/^(price|balance|amount|charged|refunded|penalty_if_abuse|threshold|price_each)$/) && @.type == 'integer')] then: field: description function: pattern functionOptions: match: micro-dollars parameters-described: description: Every parameter has a description. message: Parameter {{path}} has no description. severity: warn given: - $.paths[*][*].parameters[*] - $.paths[*].parameters[*] - $.webhooks[*][*].parameters[*] - $.components.parameters[*] then: field: description function: truthy headers-no-x-prefix: description: Header names do not use the X- prefix (RFC 6648); we use registered or draft names such as RateLimit and Idempotency-Key. message: Header {{property}} uses the X- prefix. severity: info given: - $.paths[*][*].responses[*].headers - $.components.headers then: field: '@key' function: pattern functionOptions: notMatch: ^[Xx]-