# Native policy core

Policy version `1.2.0-draft.1`, reused unchanged by the historical draft-2 and current draft-3 service profiles. These sections preserve the tested policy encoding, URL, evaluation and obligation rules. Section numbering is retained for cross-references. Current service, receipt and evidence behavior is defined by the [consolidated specification](../SPECIFICATION.md); [the draft-2 wire profile](WIRE-PROFILE.md) remains historical.

## 2. Encoding, discovery and policy contract

JSON MUST be UTF-8 without a byte-order mark, duplicate members, non-finite numbers or comments. Integer fields require lexical JSON integer tokens within 0–9,007,199,254,740,991 unless a narrower bound is stated: decimal digits only, no minus, decimal point or exponent. Negative zero (`-0`) is rejected, including for zero-allowed fields. Booleans are not integers; `1.0`, `1e0` and precision-rounded fractions are rejected. The semantic parser must enforce this even when a generic JSON Schema validator considers an integral decimal an integer. Required strings MUST be nonempty. Unknown object members are rejected in this draft unless the object explicitly permits them. Core identifiers, actions, purposes and enumerations are case-sensitive. Timestamps are valid calendar timestamps in the exact form `YYYY-MM-DDTHH:MM:SSZ`; leap-second value 60 is not accepted in this draft.

A publisher MUST serve the current policy at `https://<origin>/odexa.json`, status 200 and `Content-Type: application/json`. This is an Odexa draft publication convention chosen by the origin owner, not an IANA-registered well-known endpoint. Discovery fetches require no credential. No redirect is accepted for this authoritative endpoint in draft 1. An HTTP `Link` using the extension relation `https://odexa.io/relations/policy` MAY additionally point to that same URL; that identifier is proposed by this draft, not a claim that documentation has been published there.

For initial website setup, the informative [discovery guide](DISCOVERY.md) provides optional `robots.txt` comments and `/llms.txt` links to the actual policy, for example `https://example.com/odexa.json`. These hints leave existing crawl rules unchanged and supply neither permission nor agreement acceptance. They do not replace the canonical same-origin policy or authorize a provider. Advertise `/odexa-service.json` only when the corresponding service is implemented and its authority document is published. These examples add no policy field, wire requirement or standard robots directive.

Core policy fields, all required:

| Field | Type and meaning |
|---|---|
| `protocol_version` | Exactly `1.2.0-draft.1` |
| `policy_id` | Absolute HTTPS identifier on the policy's own origin for a stable policy lineage; no query or fragment |
| `revision` | Positive integer, increasing whenever any published policy bytes change |
| `origin` | Canonical HTTPS origin as defined in section 3 |
| `issued_at`, `expires_at` | UTC timestamps; `issued_at < expires_at` |
| `default` | Exactly `no_grant` |
| `resources` | 1–256 resource objects with unique `id` values |
| `rules` | 1–512 rule objects with unique `id` values |

A resource is `{id, selectors}`. `id` matches `[A-Za-z0-9][A-Za-z0-9._:-]{0,127}`. `selectors` contains 1–32 distinct objects `{type, path, query}`. `type` is `exact` or `directory_prefix`; `query` is `absent` or `any`. Paths follow section 3. A directory prefix MUST end in `/`. These IDs name policy scopes, not immutable delivered assets or asset versions. A selector covers the whole selected resource representation; partial-page CSS exclusions are outside this draft. Publish a deliberately licensed clean representation when a mixed-rights page cannot be licensed as a whole.

A rule is `{id, resource_ids, actions, purposes, effect, obligations}`. Its `id` follows the resource-token grammar. All three reference/vocabulary arrays are nonempty and contain no duplicates; each resource reference MUST exist. `effect` is `permit`, `prohibit` or `require_agreement`. `obligations` has 0–64 objects defined in section 5. Prohibition rules MUST have an empty obligations array.

The `(policy_id, revision)` pair MUST identify immutable bytes. The publisher MUST retain past revisions at immutable URLs for outstanding evidence/agreements; the agreement service provides their URLs when needed. A policy file is limited to 128 KiB (131,072 bytes) of decoded body, with a maximum JSON nesting depth of 32. Oversize, malformed or unsupported policy is `invalid_policy`. A well-formed policy outside its time window yields `not_yet_valid` or `expired`; none supplies permission.

Evaluator input is `{url, actions, purposes}` with optional `supported_obligations` (default empty array), plus a trusted `now` timestamp. Arrays are nonempty except `supported_obligations`. Repeated vocabulary items are invalid input. Supported obligation names are the exact `type` values from section 5. A client MUST list every action and intended purpose of its operation; it MUST NOT omit storage or training to obtain a retrieval-only result. The local evaluator has no HTTP-method argument and cannot itself enforce method or delivery. Draft-1 gateways allow only GET/HEAD and check the method separately before evaluation/introspection.

Example policy:

```json
{
  "protocol_version": "1.2.0-draft.1",
  "policy_id": "https://publisher.example/policies/public",
  "revision": 1,
  "origin": "https://publisher.example",
  "issued_at": "2026-09-16T00:00:00Z",
  "expires_at": "2026-10-16T00:00:00Z",
  "default": "no_grant",
  "resources": [
    {"id": "public-guides", "selectors": [
      {"type": "directory_prefix", "path": "/guides/", "query": "absent"}
    ]}
  ],
  "rules": [
    {"id": "free-public-retrieval", "resource_ids": ["public-guides"],
     "actions": ["retrieve"], "purposes": ["public_retrieval"],
     "effect": "permit", "obligations": [
       {"type": "attribution", "name": "Example Originator", "url": "https://publisher.example/"}
     ]},
    {"id": "internal-use-agreement", "resource_ids": ["public-guides"],
     "actions": ["retrieve", "store", "transform"], "purposes": ["internal_knowledge"],
     "effect": "require_agreement", "obligations": [
       {"type": "retention", "max_seconds": 2592000}
     ]},
    {"id": "no-training", "resource_ids": ["public-guides"],
     "actions": ["retrieve", "store", "transform", "redistribute"],
     "purposes": ["model_training", "model_fine_tuning"],
     "effect": "prohibit", "obligations": []}
  ]
}
```

This example grants retrieval for a source-linked public answer. It does not silently grant storage, transformation or redistribution; those require their own matching permissions. An application wanting all those actions must publish/request them explicitly.

## 3. Exact URI handling

These rules are intentionally conservative and MUST be shared by publishers, agents and gateways. A gateway MUST also verify that its HTTP router has not introduced an unexamined alias. Unsupported URL forms are rejected rather than guessed.

1. Parse an absolute ASCII URI of at most 4,096 characters. Scheme matching is case-insensitive but only HTTPS is accepted. Reject user information, backslashes, whitespace, controls and raw `<`, `>`, `"`, `{`, `}`, `|`, `^`, backtick anywhere in the URL. Internationalised hostnames must already use ASCII A-labels; do not run differing IDNA conversions inside the evaluator.
2. Hosts are lowercase DNS labels or canonical dotted-decimal IPv4. DNS labels contain ASCII letters/digits and interior hyphens, are 1–63 characters, and do not begin/end with a hyphen. Total host length is at most 253 characters. A trailing dot is rejected. IPv4 must be exactly four decimal octets, each 0–255, without leading zeroes except the single digit `0`. IPv6 literals and ambiguous numeric-host forms are unsupported in draft 1.
3. Port is absent or a decimal integer 1–65535 without leading zeroes. Effective port 443 is omitted from the canonical origin. A published `origin` MUST already equal `https://` plus the canonical host and optional nondefault port, with no path, slash suffix, query or fragment. Request scheme/host case and an explicit `:443` normalise to this form before comparison.
4. Only request origins equal to the policy origin after this normalisation can match resources. A different valid origin yields `no_grant`, not a syntax error. There is no subdomain, suffix or redirect inheritance. An empty request path normalises to `/`.
5. Raw paths contain only `/` and ASCII RFC 3986 `pchar`: unreserved characters, sub-delimiters, `:` and `@`, with well-formed percent escapes. Raw non-ASCII characters are rejected. Percent-encoded unreserved ASCII is decoded. **Every other percent-encoded ASCII octet is rejected**, including encoded slash, backslash, percent, question mark, fragment mark, spaces and controls. This prevents a second decoder from changing scope.
6. Percent-encoded octets at or above 128 are retained as uppercase `%HH` and MUST form well-formed UTF-8 when the path's escapes are decoded for validation. Reject invalid UTF-8, overlong sequences, surrogate code points and values beyond U+10FFFF. Do not apply Unicode normalisation. Encoded and unencoded reserved ASCII are not interchangeable in this profile.
7. Reject a path containing `//` or a segment equal to `.` or `..` after unreserved decoding. Do not remove dot segments or merge slashes. Path matching is case-sensitive.
8. Strip a request fragment before determining HTTP resource scope. A literal `?` before that fragment denotes a query even when its value is empty. `query: absent` matches only when no query delimiter exists. `query: any` deliberately covers all query values. Query content does not participate in path matching; it MUST still be ASCII, contain no whitespace/control/backslash and use valid percent escapes. No query decoding or sorting occurs.
9. Policy selector paths MUST already be in this canonical form, contain neither query nor fragment, and have a maximum length of 4,096 ASCII characters. `exact` uses byte-for-byte path equality. `directory_prefix` uses byte-prefix matching: `/guides/` matches itself and descendants, but not `/guides` or `/guides-old/`.

Examples: `/a/%62` normalises to `/a/b`; `/a/%2E%2E/b`, `/a//b`, `/a%2Fb` and `/a%252Fb` are rejected. `/guides/a?` fails an `absent` selector. `#section` changes no fetched resource. A restricted route with unavoidable aliases must be adapted at the origin before gateway conformance is claimed.

## 4. Actions, purposes and deterministic decisions

Actions:

| Action | Meaning |
|---|---|
| `retrieve` | Obtain a representation, including an authorised GET or HEAD |
| `store` | Retain a copy, cache, persistent embedding or index beyond the immediate retrieval operation |
| `transform` | Derive a representation, summary, embedding or model update from the asset |
| `redistribute` | Provide the asset or a substantial copy/derived representation to another principal |

Purposes:

| Purpose | Meaning |
|---|---|
| `search_indexing` | Build a public search discovery index; does not independently authorise full-content answers or distribution |
| `public_retrieval` | Source-linked retrieval/answering for public discovery; does not include a separately maintained internal knowledge product |
| `internal_knowledge` | Maintain or operate a private/organisational knowledge index or retrieval product |
| `model_training` | Train model parameters, excluding the separately classified fine-tuning use |
| `model_fine_tuning` | Adapt an existing model's parameters using the asset |
| `redistribution` | Supply content or derived asset collections as a product or onward distribution |

Purposes are not hierarchical. No vocabulary item implies another. “Commercial” versus “non-commercial” is not inferred from a purpose; price and additional agreed terms are separate. Unknown vocabulary makes input/policy invalid. An adapter to another standard must not silently broaden these meanings.

For every member of the Cartesian product `actions × purposes`:

1. Validate the policy and request. An invalid policy yields `invalid_policy`; otherwise an invalid request yields `invalid_request`. Before rule evaluation, `now < issued_at` yields `not_yet_valid`, and `now >= expires_at` yields `expired`.
2. Select resources with at least one matching selector, then ALL rules referring to a selected resource and containing the action and purpose. Rule order and selector specificity do not override another applicable rule.
3. Any matching `prohibit` yields `prohibit`.
4. No matching rule yields `no_grant`.
5. Combine obligations from all matching permit/agreement rules. If the client lacks support for any combined obligation type, yield `unsupported`.
6. Otherwise any `require_agreement` yields `require_agreement`; otherwise yield `permit`.

For the whole request, use this precedence: `prohibit` → `no_grant` → `unsupported` → `require_agreement` → `permit`. Return each tuple's outcome and matched rule IDs as well as the overall outcome and combined obligations. In particular, one permitted action cannot hide an ungranted or prohibited action.

An input action/purpose list asserts the full Cartesian set. If a workflow intends `(retrieve,public_retrieval)` and separately `(transform,model_training)`, evaluate those two operation requests separately rather than submitting two lists that accidentally assert four combinations. A workflow proceeds only where every actual operation has permission or the appropriate agreement. The returned `access_credential_verified` is always false for the local policy evaluator; agreement verification belongs to the service/gateway.

`permit` is conditional on performing the duties. It is not evidence that they were fulfilled. `no_grant` means Odexa supplies no affirmative permission for that tuple; it is not a statement that applicable law necessarily prohibits it. A broad prohibition cannot be overridden by a narrow permit. Publishers wanting exceptions must partition their declared scopes or deliberately revise the policy.

A service MUST NOT manufacture a grant for a prohibited/unlisted tuple. Its offers must remain within the policy's permit/agreement outcomes, and any delegate's narrower authority. A service can offer a recorded free agreement for a permitted tuple when requested; it cannot require that workflow retroactively as a condition of an otherwise unchanged static permit.

## 5. Obligations and free static use

The closed draft-1 obligation registry is:

| Object | Required behavior and combination |
|---|---|
| `{type:"attribution", name, url}` | `name` is a 1–256-character display string; `url` is an absolute HTTPS source URL without fragment. Credit both when presenting the resulting answer/copy. Combine distinct `(name,url)` pairs; deduplicate identical pairs. |
| `{type:"retention", max_seconds}` | Integer 0–9,007,199,254,740,991. Delete covered retained representations within this many elapsed seconds from acquisition. Copies/transformations do not reset the clock. Zero permits no persistence beyond the immediate operation. Combine by taking the minimum. |
| `{type:"report_usage", endpoint, deadline_seconds}` | HTTPS endpoint without query/fragment; integer deadline 1–9,007,199,254,740,991. Send a section-10 client report for each completed covered action or reporting interval within this many elapsed seconds of its completion. Combine each distinct endpoint; for repeated endpoints take the shortest deadline. |

No obligation removes another. Unsupported or contradictory duties do not become optional. Data subject to retention must not be licensed for irreversible training if the client cannot honour the specified deletion duty; it must decline or obtain revised terms that the originator is authorised to offer. Odexa does not claim that deleting a training input untrains a model.

For static operation, an agent fetches/revalidates policy, evaluates all proposed tuples, records the policy revision/digest and complies with resulting duties. It MUST NOT keep using cached policy after expiry or more than 300 seconds after successful validation, whichever is sooner. A new valid 304 response resets the validation time but not the policy expiry. A failed refresh supplies no new permission. Static permission requires no account, signature exchange, provider or payment. No acceptance receipt is created merely by reading a file.

Originators MAY observe HTTP delivery through their own server/CDN logs. Those observations must use section-10 labels if exported as Odexa evidence. A `report_usage` duty records a client's assertion, not independent observation of that downstream use.

For reporting, retrieval completes when the client finishes the response or receives HEAD metadata; a store write completes when its retained representation is committed; a transformation or redistribution completes when that operation ends. Continuous storage and operations lasting more than 86,400 seconds additionally produce a reporting checkpoint at each 86,400-second interval from operation start. A checkpoint reports `in_progress`, not completed model training or completed retention. Final completion/failure is reported separately. The deadline runs from that completion/checkpoint time. Failed operations are not counted as completed use. A compliance percentage is valid only for known expected triggers within declared coverage; a missing client report does not reveal an unknown action's completion time.
