openapi: 3.0.3 info: title: Syntra version: 0.2.0 description: | HTTP API of the Syntra decision service. A capsule is one decision point: a decision spec (actions, exploration, learner, mode), one online learner, an optional Lycan feature program with its execution policy, and an event log of decisions and rewards. **Versioning.** Paths under `/v1` are canonical. Every `/v1` path is also served without the prefix (for example `GET /tenants`) as a deprecated alias with identical behavior. Alias responses carry `Deprecation: true` and `Link: ; rel="successor-version"`, except 401 and 429 responses, which are sent before routing. `/health`, `/ready`, `/metrics` and `/admin` are unversioned and need no credential. **Authentication.** Send `Authorization: Bearer ` or `Ocp-Apim-Subscription-Key: ` with the operator admin key or a scoped token. When an `Authorization` header is present it is the only one read. A server started with `--dev-mode` (no admin key, loopback only) treats every request as the operator and never answers 401 or 429. `x-scopes` on each operation lists the token scopes that may call it; `tenant_admin` and `read` tokens only reach their own tenant or capsule. **Errors.** Error bodies are `{"error": ""}`. Any request can be answered 413 (body over 4 MiB) or 408 (body not received within 30 s) before routing. Unknown routes answer 404 `no such route` (or `no such capsule route` under a capsule path). **Request ids.** Every response has an `x-request-id` header: the caller's `X-Request-Id` (1-128 printable ASCII characters) or a generated id. Feature-program failures record it in their `execution_denied` audit event. **Durability.** `decide` and `reward` answer once their record is queued; one writer thread commits the queue to `/syntra.db` in batches of at most 512 records or 2 ms. `durable: true` waits for the commit. Log reads wait up to 250 ms for the queue, so they include every decision and reward acknowledged before the read. **Local evaluation.** An SDK fetches the published model with `GET .../model?snapshot=true`, decides in-process, and uploads its decisions with `POST .../decisions:batch` (each is verified by replay before it is stored) and their rewards with `POST .../rewards:batch`. license: name: Apache-2.0 servers: - url: http://127.0.0.1:8787 description: Default `syntra serve` address. security: - bearerAuth: [] - subscriptionKey: [] tags: - name: infra description: Probes, metrics and the admin console. No credential. - name: auth description: Credential introspection and scoped tokens. - name: tenants description: Tenants and jobs. - name: capsules description: Capsule spec, mode, feature program and execution policy. - name: decisions description: Decide and reward. - name: logs description: Decision log, model and audit trail. - name: personalizer description: > Azure AI Personalizer v1.0-compatible rank, reward, activate and service configuration. The same operations answer at `/personalizer/v1.0/...` for a key scoped to one capsule (`Ocp-Apim-Subscription-Key` or Bearer), so a Personalizer client only changes its endpoint and key. Errors use Personalizer's shape, `{"error": {"code", "message"}}`. Multi-slot ranking is not supported. paths: # --- Infra (unversioned, no credential) ------------------------------------ /health: get: operationId: getHealth tags: [infra] summary: Liveness probe security: [] responses: "200": description: The process is serving requests. content: application/json: schema: $ref: "#/components/schemas/Health" /ready: get: operationId: getReady tags: [infra] summary: Readiness probe description: Writes and removes a probe file in the store root. security: [] responses: "200": description: The store is writable. content: application/json: schema: $ref: "#/components/schemas/Ready" "503": description: The store is not writable; `reason` says why. content: application/json: schema: $ref: "#/components/schemas/Ready" /metrics: get: operationId: getMetrics tags: [infra] summary: Prometheus metrics description: > Prometheus text format 0.0.4: requests by route label and status, server-side decide latency, event-log commits, backlog and losses, uploaded decisions accepted and rejected (`syntra_uploaded_decisions_accepted_total`, `syntra_uploaded_decisions_rejected_total`), default rewards applied (`syntra_default_rewards_total`), per-capsule model versions and uptime. It names every tenant, job and capsule, so it needs an admin credential unless the server runs with `--metrics-public` (or `SYNTRA_METRICS_PUBLIC=1`). x-scopes: [admin] responses: "200": description: Metrics. content: text/plain: schema: type: string "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/RateLimited" /admin: get: operationId: getAdminConsole tags: [infra] summary: Admin console description: > Static HTML page, also served at `/v1/admin`. It stores nothing; it calls the API with the key the operator enters. security: [] responses: "200": description: The console page. content: text/html: schema: type: string # --- Auth and tokens ------------------------------------------------------- /v1/auth/whoami: get: operationId: whoAmI tags: [auth] summary: Describe the presented credential x-scopes: [admin, tenant_admin, read] responses: "200": description: How the request authenticated and its scope. content: application/json: schema: $ref: "#/components/schemas/WhoAmI" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/RateLimited" /v1/capabilities: get: operationId: listCapabilities tags: [capsules] summary: Capabilities available to feature programs x-scopes: [admin, tenant_admin, read] responses: "200": description: The capability catalog. content: application/json: schema: type: array items: $ref: "#/components/schemas/Capability" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/RateLimited" /v1/admin/tokens: post: operationId: issueToken tags: [auth] summary: Issue a scoped token description: > The raw token appears only in this response; the server keeps its SHA-256 in `tokens.json`. x-scopes: [admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/IssueTokenRequest" responses: "200": description: The new token. content: application/json: schema: $ref: "#/components/schemas/IssueTokenResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" get: operationId: listTokens tags: [auth] summary: List unexpired tokens x-scopes: [admin] responses: "200": description: Token metadata; raw tokens are never returned. content: application/json: schema: $ref: "#/components/schemas/TokenList" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/RateLimited" /v1/admin/tokens/{tokenHash}: parameters: - $ref: "#/components/parameters/tokenHash" delete: operationId: revokeToken tags: [auth] summary: Revoke a token x-scopes: [admin] responses: "200": description: Revoked. content: application/json: schema: $ref: "#/components/schemas/Revoked" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/admin/capsules: get: operationId: listAllCapsules tags: [capsules] summary: List every capsule in the store x-scopes: [admin] responses: "200": description: Capsule addresses. content: application/json: schema: $ref: "#/components/schemas/CapsuleAddressList" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/RateLimited" # --- Tenants and jobs ------------------------------------------------------ /v1/tenants: get: operationId: listTenants tags: [tenants] summary: List the tenants the caller administers description: The operator sees every tenant, a `tenant_admin` token its own, a `read` token none. x-scopes: [admin, tenant_admin, read] responses: "200": description: Tenant names. content: application/json: schema: $ref: "#/components/schemas/TenantList" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/RateLimited" /v1/tenants/{tenant}: parameters: - $ref: "#/components/parameters/tenant" delete: operationId: deleteTenant tags: [tenants] summary: Delete a tenant description: > Erases every job and capsule of the tenant with their decisions, rewards and model snapshots. Audit events are kept, with a `capsule_deleted` event (`with: tenant`) for each capsule. x-scopes: [admin, tenant_admin] responses: "200": description: Deleted. content: application/json: schema: $ref: "#/components/schemas/Removed" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs: parameters: - $ref: "#/components/parameters/tenant" get: operationId: listJobs tags: [tenants] summary: List jobs description: An unknown tenant has no jobs (200, empty list). x-scopes: [admin, tenant_admin] responses: "200": description: Jobs with their capsule names. content: application/json: schema: $ref: "#/components/schemas/JobList" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/RateLimited" post: operationId: createJob tags: [tenants] summary: Create a job description: > Creates the tenant too. Creating an existing job changes nothing and answers 200. `PUT .../spec` also creates missing jobs. x-scopes: [admin, tenant_admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateJobRequest" responses: "200": description: The job already existed. content: application/json: schema: $ref: "#/components/schemas/CreateJobResponse" "201": description: Created. content: application/json: schema: $ref: "#/components/schemas/CreateJobResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" /v1/tenants/{tenant}/jobs/{job}: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" get: operationId: getJob tags: [tenants] summary: Get a job x-scopes: [admin, tenant_admin] responses: "200": description: The job. content: application/json: schema: $ref: "#/components/schemas/Job" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" delete: operationId: deleteJob tags: [tenants] summary: Delete a job description: > Erases every capsule of the job with its decisions, rewards and model snapshots. Audit events are kept, with a `capsule_deleted` event (`with: job`) for each capsule. x-scopes: [admin, tenant_admin] responses: "200": description: Deleted. content: application/json: schema: $ref: "#/components/schemas/Removed" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" get: operationId: listCapsules tags: [capsules] summary: List a job's capsules description: An unknown job has no capsules (200, empty list). x-scopes: [admin, tenant_admin] responses: "200": description: Capsule names. content: application/json: schema: $ref: "#/components/schemas/CapsuleNames" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/RateLimited" # --- Capsules -------------------------------------------------------------- /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" get: operationId: getCapsule tags: [capsules] summary: Get a capsule description: Spec, model version, installed program, policy status and event counts. x-scopes: [admin, tenant_admin, read] responses: "200": description: The capsule. content: application/json: schema: $ref: "#/components/schemas/Capsule" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/CapsuleLoadFailed" delete: operationId: deleteCapsule tags: [capsules] summary: Delete a capsule description: > Erases the capsule's files, decisions, rewards and model snapshots. Its audit events are kept and end with `capsule_deleted`. x-scopes: [admin, tenant_admin] responses: "200": description: Deleted; `removedRows` counts event rows. content: application/json: schema: $ref: "#/components/schemas/Removed" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/decide: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: decide tags: [decisions] summary: Choose an action description: > Runs the feature program (if installed) with `context` as `runtime.input`, builds the eligible set (request or spec actions, minus `excludedActions` and program `exclude.`, limited to program `only.` when any are published), and samples one action from the learner's PMF, or from the baseline PMF in `baselineExplore` mode. Every eligible action has probability at least `exploration.floor / K`. The decision is logged with its context, actions, PMF, probability, seed and model version. x-scopes: [admin, tenant_admin, read] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DecideRequest" responses: "200": description: > The decision. A repeated `eventId` with a byte-identical body returns the stored decision with `replayed: true`. content: application/json: schema: $ref: "#/components/schemas/DecideResponse" "400": description: > Invalid body or field, no actions, an unknown action id, no eligible action, or `baselineAction` missing in `baselineExplore` mode. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: The capsule does not exist. content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: The `eventId` was already used for a different request body. content: application/json: schema: $ref: "#/components/schemas/Error" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": description: > The feature program failed (`feature program failed: `: a policy denial, the execution budget or a runtime error; audited as `execution_denied`), or the capsule could not be loaded. content: application/json: schema: $ref: "#/components/schemas/Error" "503": $ref: "#/components/responses/Unavailable" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/reward: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: reward tags: [decisions] summary: Record a reward description: > Joins the decision (queued or committed), updates the model unless the capsule is frozen, and logs the reward. The reward is normalized to [0, 1] by `spec.reward.range` (clamped). A reward whose idempotency key was already used is a duplicate: 200 with `applied: false`. x-scopes: [admin, tenant_admin, read] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RewardRequest" responses: "200": $ref: "#/components/responses/RewardResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: The capsule or the decision does not exist. content: application/json: schema: $ref: "#/components/schemas/Error" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" "503": $ref: "#/components/responses/Unavailable" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/feedback: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: feedback tags: [decisions] summary: Record a reward (alias of `/reward`) deprecated: true x-scopes: [admin, tenant_admin, read] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RewardRequest" responses: "200": $ref: "#/components/responses/RewardResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" "503": $ref: "#/components/responses/Unavailable" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/spec: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" get: operationId: getSpec tags: [capsules] summary: Get the decision spec x-scopes: [admin, tenant_admin, read] responses: "200": description: The spec with every default filled in. content: application/json: schema: $ref: "#/components/schemas/DecisionSpec" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" put: operationId: putSpec tags: [capsules] summary: Create a capsule or patch its spec description: > An RFC 7396 merge patch over the current spec, or over the defaults when the capsule does not exist (which creates it, its job and tenant, and a deny-all policy). `null` removes a field, restoring its default; arrays such as `actions` are replaced whole. Unknown fields and out-of-range values are rejected. The learned model is kept unless `learner.bits` changes, in which case it is rebuilt from the reward log. With `replace=true` the patch applies to the defaults instead of the current spec, which also repairs a stored spec that no longer parses. Audited as `capsule_created`, `spec_updated` or `spec_replaced`. x-scopes: [admin, tenant_admin] parameters: - name: replace in: query description: Apply the body to the default spec rather than the current one. schema: type: boolean default: false requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DecisionSpec" responses: "200": description: Updated; the full spec. content: application/json: schema: $ref: "#/components/schemas/DecisionSpec" "201": description: Created; the full spec. content: application/json: schema: $ref: "#/components/schemas/DecisionSpec" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/mode: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: setMode tags: [capsules] summary: Switch the serving mode description: A spec patch limited to `mode` and `baselineEpsilon`. Audited as `mode_changed`. x-scopes: [admin, tenant_admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ModeRequest" responses: "200": description: The full spec. content: application/json: schema: $ref: "#/components/schemas/DecisionSpec" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/install: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: installProgram tags: [capsules] summary: Install a feature program description: > Uploads a compiled Lycan program (`.lyc`, from `lycan compile`). It must pass the verifier and contain no `choice`, `strategy` or `feedback` node. On each decide it runs with the request context as `runtime.input` under the capsule's policy and may publish `features.` (derived features), `exclude.` or `only.` (`true` to apply) and `reason` (a string echoed in the response). Creates the capsule with a default spec if it does not exist. Audited as `program_installed`. x-scopes: [admin, tenant_admin] requestBody: required: true content: application/octet-stream: schema: type: string format: binary responses: "200": description: Installed. content: application/json: schema: $ref: "#/components/schemas/InstallResponse" "400": description: Not a valid `.lyc` graph, rejected by the verifier, or it contains a learning node. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/program: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" delete: operationId: deleteProgram tags: [capsules] summary: Remove the feature program description: Audited as `program_removed` when a program was removed. x-scopes: [admin, tenant_admin] responses: "200": description: Done; `removed` is false when there was no program. content: application/json: schema: $ref: "#/components/schemas/ProgramRemoved" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/policy: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" get: operationId: getPolicy tags: [capsules] summary: Get the execution policy description: The stored document. A new capsule's policy denies stdout, stdin, file and network access. x-scopes: [admin, tenant_admin, read] responses: "200": description: The policy document. content: application/json: schema: $ref: "#/components/schemas/Policy" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" put: operationId: putPolicy tags: [capsules] summary: Replace the execution policy description: > Replaces the whole document (not a patch). Validated strictly: unknown keys, wrong types, an absolute or escaping `file_root`, malformed hosts and out-of-range budgets are rejected. Audited as `policy_updated`. x-scopes: [admin, tenant_admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Policy" responses: "200": description: Stored. content: application/json: schema: $ref: "#/components/schemas/Ok" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": description: > The scope does not allow this operation, or a credential other than a global admin (the operator key or an admin token) set `deny_private_networks` to false. content: application/json: schema: $ref: "#/components/schemas/Error" "404": $ref: "#/components/responses/NotFound" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" # --- Logs, model, audit ---------------------------------------------------- /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/logs: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" delete: operationId: purgeLogs tags: [logs] summary: Erase decisions and rewards description: > Keeps the spec, program, policy and learned model. Off-policy evaluation can no longer use the erased traffic. Audited as `logs_purged`. x-scopes: [admin, tenant_admin] responses: "200": description: Erased. content: application/json: schema: $ref: "#/components/schemas/PurgeResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/decisions: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" get: operationId: listDecisions tags: [logs] summary: List logged decisions description: > Decisions, including uploaded ones, oldest first, or newest first with `order=newest`. Page with `after` set to the previous page's `next`; the listing continues in the same order. x-scopes: [admin, tenant_admin, read] parameters: - name: since in: query description: Earliest decision time, in ms since the epoch (inclusive). schema: type: integer format: int64 - name: until in: query description: Latest decision time, in ms since the epoch (exclusive). schema: type: integer format: int64 - name: limit in: query description: Page size, clamped to [1, 1000]. schema: type: integer default: 100 - name: order in: query description: "`oldest` (the default) or `newest`." schema: type: string enum: [oldest, newest] default: oldest - name: after in: query description: Cursor; the `decisionId` of the last decision of the previous page. schema: type: string responses: "200": description: One page of decisions. content: application/json: schema: $ref: "#/components/schemas/DecisionList" "400": description: A query parameter is not an integer, an invalid name, or `after` names no decision. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/decisions/{decisionId}: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" - $ref: "#/components/parameters/decisionId" get: operationId: getDecision tags: [logs] summary: Get one decision and its rewards description: Queued decisions are found too. x-scopes: [admin, tenant_admin, read] responses: "200": description: The decision with `rewards`. content: application/json: schema: $ref: "#/components/schemas/Decision" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: The capsule or the decision does not exist. content: application/json: schema: $ref: "#/components/schemas/Error" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/model: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" get: operationId: getModel tags: [logs] summary: Get the model description: > The live spec, model version and reward watermark. With `snapshot=true`, the model published for local evaluation as well: its `decide` section (what SDKs rebuild the spec from), version, `modelTag` and serialized learner state. A change to the decide section publishes a new model at once; learning publishes at most once a second, and the 32 newest publications are kept for verifying uploads. x-scopes: [admin, tenant_admin, read] parameters: - name: snapshot in: query description: "`true` for the published model with its snapshot." schema: type: boolean default: false - name: If-None-Match in: header description: With `snapshot=true`, a quoted `modelTag`; answers 304 while it is still the published model. schema: type: string responses: "200": description: The model. headers: ETag: description: With `snapshot=true`, the `modelTag`, quoted. schema: type: string content: application/json: schema: $ref: "#/components/schemas/Model" "304": description: The `If-None-Match` tag is still the published model; empty body. headers: ETag: description: The `modelTag`, quoted. schema: type: string "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/CapsuleLoadFailed" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/decisions:batch: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: uploadDecisions tags: [decisions] summary: Upload locally made decisions description: > Each decision is replayed on the published model named by its `modelTag` with the same input and seed; it is stored only if the eligible set, the chosen action and every probability match (to 1e-9). A retried upload of a stored decision counts as accepted and as a duplicate. Refused items are listed in `rejected` and audited as `upload_rejected`; `retryable` ones (the log was backlogged) can be sent again. Not available for capsules with a feature program. x-scopes: [admin, tenant_admin, read] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DecisionUploadRequest" responses: "200": description: Per-item outcome. content: application/json: schema: $ref: "#/components/schemas/DecisionUploadResponse" "400": description: > The body is not `{"decisions": [...]}`, an invalid name, or the capsule has a feature program. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": description: More than 4096 items, or a body over 4 MiB. content: application/json: schema: $ref: "#/components/schemas/Error" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/CapsuleLoadFailed" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/rewards:batch: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: uploadRewards tags: [decisions] summary: Upload rewards description: > Applies each reward in order, exactly as `POST .../reward` would. Upload a decision before its rewards. A failed item does not stop the others. x-scopes: [admin, tenant_admin, read] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RewardUploadRequest" responses: "200": description: One result per item, in order. content: application/json: schema: $ref: "#/components/schemas/RewardUploadResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": description: More than 4096 items, or a body over 4 MiB. content: application/json: schema: $ref: "#/components/schemas/Error" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/CapsuleLoadFailed" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/evaluate: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: evaluate tags: [logs] summary: Off-policy evaluation on the capsule's log description: > Estimates what a policy would have earned on the logged decisions with rewards (IPS, SNIPS and cross-fitted DR with bootstrap intervals, DM as a point estimate whose interval fields are null, and each one's paired lift over the logged policy) and checks the gates. The policy is `policy` (`logged`, `greedy` or `constant:`) or `spec`, a candidate spec patch evaluated as it would serve: its exploration's probabilities over the predictions of its learner, cross-fitted on the logs (a `baselineExplore` candidate cannot be scored). Reads at most 2,000,000 decisions; narrow larger logs with `since` and `until`. x-scopes: [admin, tenant_admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EvaluateRequest" responses: "200": description: The report; a failed gate is reported, not an error. content: application/json: schema: $ref: "#/components/schemas/OpeReport" "400": description: > Invalid body, both or neither of `policy` and `spec`, an unknown policy, an invalid spec patch, a gate that does not parse, or a setting out of range. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: No logged decisions to evaluate, or too few with rewards. content: application/json: schema: $ref: "#/components/schemas/Error" "413": description: More than 2,000,000 logged decisions in the window, or a body over 4 MiB. content: application/json: schema: $ref: "#/components/schemas/Error" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/promote: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: promote tags: [capsules] summary: Apply a spec patch if it passes off-policy evaluation description: > Evaluates `spec` (a merge patch over the current spec) like `evaluate` and applies it only when every gate passes; at least one gate is required, `policy` is not allowed. Refuses to apply if the spec changed during the evaluation. Audited as `spec_promoted` or `promotion_refused`. x-scopes: [admin, tenant_admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EvaluateRequest" responses: "200": description: Every gate passed; the patch is applied. content: application/json: schema: $ref: "#/components/schemas/PromoteResponse" "400": description: No `spec`, `policy` given, no gate, or anything `evaluate` answers 400 for. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: > A gate failed (`promoted: false` with the report), the spec changed during the evaluation, or there is nothing to evaluate. content: application/json: schema: $ref: "#/components/schemas/PromoteConflict" "413": description: More than 2,000,000 logged decisions in the window, or a body over 4 MiB. content: application/json: schema: $ref: "#/components/schemas/Error" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/personalizer/v1.0/rank: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" post: operationId: personalizerRank tags: [personalizer] summary: Rank actions (Personalizer) description: > Personalizer's rank. `contextFeatures` and each action's `features` are arrays of objects merged into one object (their keys act as namespaces). The chosen action (`rewardActionId`) comes first, then the other eligible actions by probability, then excluded ones at 0. In Apprentice mode (`baselineExplore`) the first action is the baseline. With `deferActivation` the event is neither logged nor learned from until activated; rewards that arrive first are held. A repeated `eventId` with the same request returns the same answer. x-scopes: [admin, tenant_admin, read] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PersonalizerRankRequest" responses: "201": description: The ranking. content: application/json: schema: $ref: "#/components/schemas/PersonalizerRankResponse" "400": $ref: "#/components/responses/PersonalizerError" "413": $ref: "#/components/responses/PersonalizerError" "401": $ref: "#/components/responses/PersonalizerError" "403": $ref: "#/components/responses/PersonalizerError" "404": $ref: "#/components/responses/PersonalizerError" "409": $ref: "#/components/responses/PersonalizerError" "429": $ref: "#/components/responses/PersonalizerError" "503": $ref: "#/components/responses/PersonalizerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/personalizer/v1.0/events/{eventId}/reward: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" - $ref: "#/components/parameters/eventId" post: operationId: personalizerReward tags: [personalizer] summary: Report a reward (Personalizer) description: > `{"value": x}` for the event, like `POST .../reward` with the event id as the decision id. Held until activation for a deferred event. x-scopes: [admin, tenant_admin, read] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PersonalizerRewardRequest" responses: "204": description: Recorded. "400": $ref: "#/components/responses/PersonalizerError" "413": $ref: "#/components/responses/PersonalizerError" "401": $ref: "#/components/responses/PersonalizerError" "403": $ref: "#/components/responses/PersonalizerError" "404": $ref: "#/components/responses/PersonalizerError" "429": $ref: "#/components/responses/PersonalizerError" "503": $ref: "#/components/responses/PersonalizerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/personalizer/v1.0/events/{eventId}/activate: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" - $ref: "#/components/parameters/eventId" post: operationId: personalizerActivate tags: [personalizer] summary: Activate a deferred event (Personalizer) description: > Logs a deferred event and applies the rewards held for it. An event that is already active answers 204 too; an unknown one 404. Deferred events survive a graceful restart (not a crash) and are dropped after 24 hours if never activated. x-scopes: [admin, tenant_admin, read] responses: "204": description: Active. "400": $ref: "#/components/responses/PersonalizerError" "401": $ref: "#/components/responses/PersonalizerError" "403": $ref: "#/components/responses/PersonalizerError" "404": $ref: "#/components/responses/PersonalizerError" "429": $ref: "#/components/responses/PersonalizerError" "503": $ref: "#/components/responses/PersonalizerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/personalizer/v1.0/configurations/service: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" get: operationId: personalizerGetServiceConfiguration tags: [personalizer] summary: Service configuration (Personalizer) description: > The capsule's spec as Personalizer settings. For SquareCB exploration `explorationPercentage` reports the exploration floor. `defaultReward` is null when unset. x-scopes: [admin, tenant_admin, read] responses: "200": description: The configuration. content: application/json: schema: $ref: "#/components/schemas/PersonalizerServiceConfiguration" "400": $ref: "#/components/responses/PersonalizerError" "401": $ref: "#/components/responses/PersonalizerError" "403": $ref: "#/components/responses/PersonalizerError" "404": $ref: "#/components/responses/PersonalizerError" "429": $ref: "#/components/responses/PersonalizerError" put: operationId: personalizerPutServiceConfiguration tags: [personalizer] summary: Update the service configuration (Personalizer) description: > Maps `rewardWaitTime` (ISO 8601) to `reward.waitSeconds`, `defaultReward` to `reward.default`, `rewardAggregation` (`earliest` or `sum`) to `rewards`, `explorationPercentage` to epsilon-greedy exploration, and `learningMode` (`Online`, `Apprentice`, `Frozen`) to `mode`. Other fields Personalizer clients send are accepted and ignored. Audited as `spec_updated`. x-scopes: [admin, tenant_admin] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PersonalizerServiceConfiguration" responses: "200": description: The configuration after the update. content: application/json: schema: $ref: "#/components/schemas/PersonalizerServiceConfiguration" "400": $ref: "#/components/responses/PersonalizerError" "413": $ref: "#/components/responses/PersonalizerError" "401": $ref: "#/components/responses/PersonalizerError" "403": $ref: "#/components/responses/PersonalizerError" "404": $ref: "#/components/responses/PersonalizerError" "429": $ref: "#/components/responses/PersonalizerError" /v1/tenants/{tenant}/jobs/{job}/capsules/{capsule}/audits: parameters: - $ref: "#/components/parameters/tenant" - $ref: "#/components/parameters/job" - $ref: "#/components/parameters/capsule" get: operationId: listAudits tags: [logs] summary: List audit events description: > The newest `limit` events, oldest first. Audit events outlive the capsule: after a delete they still answer here, ending with `capsule_deleted`. 404 only when there are none and no capsule. x-scopes: [admin, tenant_admin, read] parameters: - name: limit in: query description: Number of events, clamped to [1, 1000]. schema: type: integer default: 100 responses: "200": description: Audit events. content: application/json: schema: $ref: "#/components/schemas/AuditList" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/ServerError" components: securitySchemes: bearerAuth: type: http scheme: bearer description: > The operator admin key (`--admin-key`, `SYNTRA_ADMIN_KEY` or `LYCAN_ADMIN_KEY`) or a scoped token from `POST /v1/admin/tokens`. The scheme word is case-insensitive. subscriptionKey: type: apiKey in: header name: Ocp-Apim-Subscription-Key description: The same credentials in the Personalizer-style header. parameters: tenant: name: tenant in: path required: true description: Tenant name. schema: $ref: "#/components/schemas/Name" job: name: job in: path required: true description: Job name. schema: $ref: "#/components/schemas/Name" capsule: name: capsule in: path required: true description: Capsule name. schema: $ref: "#/components/schemas/Name" decisionId: name: decisionId in: path required: true description: A `decisionId` returned by decide. schema: type: string eventId: name: eventId in: path required: true description: The `eventId` of a Personalizer rank call. schema: type: string tokenHash: name: tokenHash in: path required: true description: SHA-256 (hex) of the raw token, as listed by `GET /v1/admin/tokens`. schema: type: string headers: RetryAfter: description: Seconds to wait before retrying. schema: type: integer responses: PersonalizerError: description: An error in Personalizer's shape. content: application/json: schema: $ref: "#/components/schemas/PersonalizerErrorBody" BadRequest: description: Invalid request; the message names the problem. content: application/json: schema: $ref: "#/components/schemas/Error" Unauthorized: description: Missing or unknown credential. content: application/json: schema: $ref: "#/components/schemas/Error" Forbidden: description: The credential's scope does not allow this operation on this resource. content: application/json: schema: $ref: "#/components/schemas/Error" NotFound: description: The resource does not exist. content: application/json: schema: $ref: "#/components/schemas/Error" PayloadTooLarge: description: The request body is over 4 MiB. content: application/json: schema: $ref: "#/components/schemas/Error" RateLimited: description: > The credential exceeded its request rate (token bucket per credential; `SYNTRA_RATE_LIMIT_RPS` and `SYNTRA_RATE_LIMIT_BURST`). headers: Retry-After: $ref: "#/components/headers/RetryAfter" content: application/json: schema: $ref: "#/components/schemas/RateLimitError" Unavailable: description: > The event log queue is full, or a `durable` request's commit was not confirmed within 5 s. Retry after `Retry-After`; retrying a reward with the same idempotency key is safe. headers: Retry-After: $ref: "#/components/headers/RetryAfter" content: application/json: schema: $ref: "#/components/schemas/Error" ServerError: description: Internal error (storage failure). content: application/json: schema: $ref: "#/components/schemas/Error" CapsuleLoadFailed: description: The capsule's stored spec, program or event log could not be loaded. content: application/json: schema: $ref: "#/components/schemas/Error" RewardResult: description: Applied, or a duplicate of an earlier reward. content: application/json: schema: $ref: "#/components/schemas/RewardResponse" schemas: # --- Shared ---------------------------------------------------------------- Name: type: string description: 1-128 characters from `A-Z a-z 0-9 _ - .`, not starting with `.`. pattern: "^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,127}$" Error: type: object required: [error] properties: error: type: string RateLimitError: type: object required: [error, retryAfterSeconds] properties: error: type: string retryAfterSeconds: type: integer Ok: type: object required: [ok] properties: ok: type: boolean Removed: type: object required: [ok, removedRows] properties: ok: type: boolean removedRows: type: integer description: Decision, reward and model-snapshot rows erased (audit events are kept). # --- Infra ----------------------------------------------------------------- Health: type: object required: [ok, service] properties: ok: type: boolean service: type: string Ready: type: object required: [ok, service] properties: ok: type: boolean service: type: string reason: type: string description: Why the store is not writable; only when `ok` is false. Capability: type: object required: [name, version, package, summary, inputs, output, purity, deterministic, effects, cost, failure, safety] properties: name: type: string example: file.readText version: type: string package: type: string summary: type: string inputs: type: array items: type: string output: type: string purity: type: string enum: [pure, read_only_effect, effectful] deterministic: type: boolean effects: type: array description: Policy gates the capability needs, such as `file_read` or `network`. items: type: string cost: type: string failure: type: string safety: type: string # --- Auth ------------------------------------------------------------------ Scope: description: > `admin`: every route. `tenant_admin`: every tenant, job and capsule route of one tenant. `read`: decide, reward and the read routes of one capsule. oneOf: - title: admin type: object required: [kind] properties: kind: type: string enum: [admin] - title: tenant_admin type: object required: [kind, tenant] properties: kind: type: string enum: [tenant_admin] tenant: type: string - title: read type: object required: [kind, tenant, job, capsule] properties: kind: type: string enum: [read] tenant: type: string job: type: string capsule: type: string WhoAmI: type: object required: [ok, kind, principalId, scope] properties: ok: type: boolean kind: type: string enum: [dev_mode, legacy_admin, scoped_token] description: "`legacy_admin` is the operator admin key." principalId: type: string nullable: true description: "`operator`, the token hash, or null in dev mode." scope: $ref: "#/components/schemas/Scope" IssueTokenRequest: type: object required: [scope] properties: scope: $ref: "#/components/schemas/Scope" ttlSeconds: type: integer minimum: 0 description: Lifetime in seconds; omit for no expiry. label: type: string default: "" example: scope: {kind: read, tenant: acme, job: routing, capsule: router} ttlSeconds: 86400 label: checkout-api IssueTokenResponse: type: object required: [token, hash, scope, expiresAt] properties: token: type: string description: The raw token. Shown once. hash: type: string description: SHA-256 of the token (hex); use it to revoke. scope: $ref: "#/components/schemas/Scope" expiresAt: type: integer nullable: true description: Unix time in seconds, or null. TokenRecord: type: object required: [hash, scope, createdAt, expiresAt, lastUsedAt, label] properties: hash: type: string scope: $ref: "#/components/schemas/Scope" createdAt: type: integer description: Unix time in seconds. expiresAt: type: integer nullable: true lastUsedAt: type: integer nullable: true description: Last successful authentication, recorded at most once a minute. label: type: string TokenList: type: object required: [tokens] properties: tokens: type: array items: $ref: "#/components/schemas/TokenRecord" Revoked: type: object required: [ok, revoked] properties: ok: type: boolean revoked: type: boolean # --- Tenants and jobs ------------------------------------------------------ TenantList: type: object required: [tenants] properties: tenants: type: array items: type: string Job: type: object required: [id, capsules] properties: id: type: string name: type: string createdAtMs: type: integer capsules: type: array items: type: string JobList: type: object required: [jobs] properties: jobs: type: array items: $ref: "#/components/schemas/Job" CreateJobRequest: type: object additionalProperties: false required: [id] properties: id: $ref: "#/components/schemas/Name" name: type: string description: Display name; defaults to `id`. CreateJobResponse: type: object required: [ok, created, job] properties: ok: type: boolean created: type: boolean job: $ref: "#/components/schemas/Job" CapsuleNames: type: object required: [capsules] properties: capsules: type: array items: type: string CapsuleAddress: type: object required: [tenant, job, capsule] properties: tenant: type: string job: type: string capsule: type: string CapsuleAddressList: type: object required: [capsules] properties: capsules: type: array items: $ref: "#/components/schemas/CapsuleAddress" # --- Capsules -------------------------------------------------------------- Capsule: type: object required: [tenant, job, capsule, spec, modelVersion, program, policyError, stats] properties: tenant: type: string job: type: string capsule: type: string spec: $ref: "#/components/schemas/DecisionSpec" modelVersion: type: integer description: Learner updates applied. program: type: object nullable: true description: The installed feature program, or null. required: [programSha256, programBytes, installedAtMs] properties: programSha256: type: string programBytes: type: integer installedAtMs: type: integer policyError: type: string nullable: true description: Why the stored policy is unusable (the program then runs deny-all), or null. stats: type: object nullable: true description: Committed event counts, or null if the event store could not be read. required: [decisions, rewards, firstDecisionMs, lastDecisionMs, lastRewardSeq] properties: decisions: type: integer rewards: type: integer firstDecisionMs: type: integer nullable: true lastDecisionMs: type: integer nullable: true lastRewardSeq: type: integer nullable: true Action: type: object additionalProperties: false required: [id] properties: id: type: string minLength: 1 maxLength: 256 description: Unique within its action list. features: type: object additionalProperties: true description: Action features (namespace `a`), flattened like the context. example: id: small features: {cost: 0.2} DecisionSpec: type: object additionalProperties: false description: > Configuration of one capsule. Every field is optional and defaults as shown. As a `PUT .../spec` body it is a merge patch: `null` restores a field's default. properties: actions: type: array maxItems: 1024 default: [] description: Declared actions. May stay empty when every request supplies `actions`. items: $ref: "#/components/schemas/Action" reward: $ref: "#/components/schemas/RewardSpec" exploration: $ref: "#/components/schemas/ExplorationSpec" learner: $ref: "#/components/schemas/LearnerSpec" mode: type: string enum: [learner, baselineExplore, frozen] default: learner description: > `learner` serves and trains the learned policy. `baselineExplore` serves the request's `baselineAction` with probability `1 - baselineEpsilon` plus `baselineEpsilon / K` on every action, and keeps training. `frozen` serves the learned policy and records rewards without learning. baselineEpsilon: type: number minimum: 0 exclusiveMinimum: true maximum: 1 default: 0.1 seed: type: integer minimum: 0 maximum: 18446744073709551615 description: > Fixed base seed: decision seeds come from it and a per-process counter. Omit for OS randomness. snapshotEvery: type: integer minimum: 1 maximum: 1000000000 default: 1000 description: Persist a model snapshot every this many learner updates (and at shutdown). rewards: type: string enum: [first, sum] default: first description: > `first`: one reward per decision; later ones are duplicates. `sum`: every reward counts; a reward's `idempotencyKey` deduplicates retries. example: actions: - id: small features: {cost: 0.2} - id: large features: {cost: 1.0} reward: {range: [0, 1]} exploration: {floor: 0.05} RewardSpec: type: object additionalProperties: false properties: range: type: array minItems: 2 maxItems: 2 default: [0, 1] description: "`[lo, hi]`, finite, `lo < hi`. Rewards map to `(r - lo) / (hi - lo)`, clamped to [0, 1]." items: type: number default: type: number description: > Reward given to a decision that still has none `waitSeconds` after it was made (implicit feedback: report clicks, let misses default to 0). Omitted: unrewarded decisions stay unrewarded. Under `rewards: first` a real reward arriving after the default is a duplicate; under `sum` it still adds. waitSeconds: type: integer minimum: 1 maximum: 604800 default: 600 description: How long a decision waits for its reward before `default` applies. ExplorationSpec: type: object additionalProperties: false properties: kind: type: string enum: [squarecb, epsilonGreedy] default: squarecb description: "`squarecb`: inverse gap weighting with `gamma = gammaScale * n^gammaExponent` (n = learner updates)." gammaScale: type: number minimum: 0 maximum: 1000000 default: 10 gammaExponent: type: number minimum: 0 maximum: 1 default: 0.5 epsilon: type: number minimum: 0 maximum: 1 default: 0.05 description: Exploration rate of `epsilonGreedy`. floor: type: number minimum: 0 exclusiveMinimum: true maximum: 1 default: 0.05 description: Mixed in after exploration, `p = (1 - floor) * p + floor / K`, so every eligible action keeps probability `>= floor / K`. LearnerSpec: type: object additionalProperties: false properties: bits: type: integer minimum: 10 maximum: 24 default: 18 description: The model has `2^bits` hashed weight slots. Changing it rebuilds the model from the reward log. learningRate: type: number minimum: 0 exclusiveMinimum: true maximum: 10 default: 0.5 description: > Step size of the normalized adaptive updates. Lower (0.05 to 0.1) is steadier on noisy rewards with close actions; higher adapts faster when the best actions change. importance: type: string enum: [none, mtr] default: none description: "`mtr` weights each update by `min(1 / p, maxImportanceWeight)`." maxImportanceWeight: type: number minimum: 1 maximum: 1000000 default: 100 ModeRequest: type: object additionalProperties: false required: [mode] properties: mode: type: string enum: [learner, baselineExplore, frozen] baselineEpsilon: type: number minimum: 0 exclusiveMinimum: true maximum: 1 example: mode: baselineExplore baselineEpsilon: 0.2 InstallResponse: type: object required: [ok, programSha256, bytes] properties: ok: type: boolean programSha256: type: string bytes: type: integer ProgramRemoved: type: object required: [ok, removed] properties: ok: type: boolean removed: type: boolean Policy: type: object additionalProperties: false description: > What the feature program may do. Defaults apply to keys missing from the document. properties: allow_stdout: type: boolean default: true allow_stdin: type: boolean default: false allow_file_read: type: boolean default: false allow_file_write: type: boolean default: false allow_network: type: boolean default: false allow_insecure_http: type: boolean default: false description: Permit plain `http://`; otherwise the sandbox is https-only. allow_self_modify: type: boolean description: Accepted for compatibility; unused. file_root: type: string nullable: true maxLength: 255 description: > File sandbox root, relative to the capsule's `data/` directory. Absolute paths and `..` are rejected. Omit for `data/` itself. allowed_hosts: type: array default: [] description: Host names (`*.` wildcard prefix allowed) the program may call. Empty denies all. items: type: string deny_private_networks: type: boolean default: true description: Block loopback, private, link-local and metadata addresses. Only a global admin credential (the operator key or an admin token) may set false. max_execution_ms: type: integer minimum: 1 maximum: 60000 default: 30000 description: Wall-clock budget per run, checked every 64 node evaluations. max_memory_bytes: type: integer minimum: 0 description: Accepted; not enforced. example: allow_file_read: true file_root: "." allow_network: true allowed_hosts: [api.example.com] # --- Decide and reward ----------------------------------------------------- DecideRequest: type: object additionalProperties: false properties: context: type: object nullable: true additionalProperties: true description: > Request context (namespace `c` features); null or absent means `{}`. The feature program reads it as `runtime.input`. example: {task: code, promptTokens: 812, user: {tier: pro}} actions: type: array maxItems: 1024 description: > Actions for this request only, replacing the spec's. Ids in `excludedActions` and `baselineAction` then refer to this list. items: $ref: "#/components/schemas/Action" example: - id: small features: {cost: 0.2} - id: large features: {cost: 1.0} excludedActions: type: array default: [] description: Action ids removed from the eligible set. items: type: string example: [large] baselineAction: type: string description: The caller's incumbent action; required in `baselineExplore` mode. example: small eventId: type: string pattern: "^[A-Za-z0-9_.:-]{1,128}$" description: > Becomes the `decisionId`. Resending a byte-identical body returns the stored decision (`replayed: true`); a different body answers 409. example: req-7f3a durable: type: boolean default: false description: Answer only after the decision is committed. example: false contextKey: type: string deprecated: true description: v1 compatibility; stored as `context.contextKey`. example: checkout features: type: object deprecated: true additionalProperties: true description: v1 compatibility; merged into `context`, replacing keys. example: {region: eu} example: context: {task: code, promptTokens: 812, user: {tier: pro}} excludedActions: [large] eventId: req-7f3a RankedAction: type: object required: [id, probability] properties: id: type: string probability: type: number minimum: 0 maximum: 1 DecideResponse: type: object required: [decisionId, action, actionIndex, probability, ranking, mode, modelVersion] properties: decisionId: type: string description: The request's `eventId`, or a generated `dec_...` id that sorts by time. action: type: string description: The chosen action id. actionIndex: type: integer description: Index of the chosen action in the action list (request or spec). probability: type: number minimum: 0 maximum: 1 description: Probability the chosen action was sampled with. ranking: type: array description: Eligible actions, most probable first. items: $ref: "#/components/schemas/RankedAction" mode: type: string enum: [learner, baselineExplore, frozen] modelVersion: type: integer description: Learner updates applied when the decision was made. reason: type: string description: The feature program's `reason`, when it published one. replayed: type: boolean description: Present (true) when an `eventId` retry returned the stored decision. example: decisionId: dec_1a0cfaedd2f398a769c4d3e action: small actionIndex: 0 probability: 0.93 ranking: - {id: small, probability: 0.93} - {id: large, probability: 0.07} mode: learner modelVersion: 1234 RewardRequest: type: object additionalProperties: false required: [decisionId] description: Exactly one of `reward` and `value`. oneOf: - required: [reward] - required: [value] properties: decisionId: type: string example: dec_1a0cfaedd2f398a769c4d3e reward: type: number description: The raw reward, a finite number. example: 1.0 value: type: number deprecated: true description: Alias of `reward`. example: 1.0 idempotencyKey: type: string minLength: 1 maxLength: 256 description: > Under `rewards: sum`, deduplicates retries (at most 256 bytes, no NUL; default a fresh key, so every reward counts). Ignored under `rewards: first`, where the key is always the decision id. example: order-4411 detail: description: > Any JSON stored with the reward; a non-object is stored as `{"value": ...}`. The key `learned` is reserved. example: {latencyMs: 812, costUsd: 0.004} durable: type: boolean default: false description: Answer only after the reward is committed. example: true example: decisionId: dec_1a0cfaedd2f398a769c4d3e reward: 1.0 detail: {latencyMs: 812} RewardResponse: type: object required: [ok, applied, modelVersion] properties: ok: type: boolean applied: type: boolean description: False for a duplicate, or a reward held for a deferred event. learned: type: boolean description: Present when applied; false in `frozen` mode. duplicate: type: boolean description: Present (true) for a duplicate. held: type: boolean description: > Present (true) when the decision is a Personalizer event with deferred activation that is not activated yet: the reward is kept and applied on activation. modelVersion: type: integer # --- Logs ------------------------------------------------------------------ Decision: type: object required: [decisionId, tsMs, modelVersion, mode, context, derived, actions, eligible, pmf, chosenIndex, action, probability, seed, reason, requestSha256, programSha256] properties: decisionId: type: string tsMs: type: integer description: Decision time, ms since the epoch. modelVersion: type: integer mode: type: string enum: [learner, baselineExplore, frozen] context: type: object additionalProperties: true description: The context as featurized (after `contextKey` and `features` are merged in). derived: type: object nullable: true additionalProperties: true description: Features the program published (namespace `d`); null without a program. actions: type: array description: The action list decided over. items: $ref: "#/components/schemas/Action" eligible: type: array description: Indices into `actions` of the eligible actions, ascending. items: type: integer pmf: type: array nullable: true description: Probability of each eligible action, aligned with `eligible`. items: type: number chosenIndex: type: integer action: type: string probability: type: number nullable: true seed: type: string description: Sampling seed (a u64 in decimal); replaying it reproduces the draw. reason: type: string nullable: true requestSha256: type: string description: SHA-256 of the request body. programSha256: type: string nullable: true rewards: type: array description: Only on `GET .../decisions/{decisionId}`. items: $ref: "#/components/schemas/RewardEvent" RewardEvent: type: object required: [seq, tsMs, reward, rewardNormalized, idempotencyKey, detail] properties: seq: type: integer description: Reward sequence number; the model applies rewards in this order. tsMs: type: integer reward: type: number rewardNormalized: type: number idempotencyKey: type: string detail: type: object nullable: true additionalProperties: true description: The stored detail; `learned` is false for rewards recorded while frozen. DecisionList: type: object required: [decisions, next] properties: decisions: type: array items: $ref: "#/components/schemas/Decision" next: type: string nullable: true description: Cursor for the next page, or null when this page is not full. Model: type: object required: [spec, modelVersion, rewardWatermark, programSha256] properties: spec: $ref: "#/components/schemas/DecisionSpec" decide: $ref: "#/components/schemas/DecideSection" modelVersion: type: integer description: Learner updates applied (of the published model, with `snapshot=true`). modelTag: type: string description: > With `snapshot=true`: names the published (decide section, snapshot) pair (SHA-256 over the section as served and the snapshot checksum); uploaded decisions carry it. rewardWatermark: type: integer description: > Sequence number of the last reward covered by the latest snapshot or startup replay. programSha256: type: string nullable: true snapshotBytes: type: integer description: With `snapshot=true`. snapshot: type: string format: byte description: With `snapshot=true`; the learner state, base64. DecideSection: type: object description: > With `snapshot=true`: the part of the spec a local-evaluation SDK needs. Server-only settings are left out, so new ones never break deployed SDKs; SDKs ignore unknown keys and refuse a newer `version`. required: [version, actions, exploration, mode, baselineEpsilon, bits, rewards] properties: version: type: integer description: Changes only when a field that affects deciding is added or changes meaning. actions: type: array items: $ref: "#/components/schemas/Action" exploration: $ref: "#/components/schemas/ExplorationSpec" mode: type: string enum: [learner, baselineExplore, frozen] baselineEpsilon: type: number bits: type: integer description: The learner's hash bits. seed: type: integer format: int64 description: Present when the spec fixes a seed. rewards: type: string enum: [first, sum] UploadInput: type: object additionalProperties: false description: The decide input the decision was made from; fields as in DecideRequest. properties: context: type: object nullable: true additionalProperties: true example: {task: code} actions: type: array maxItems: 1024 items: $ref: "#/components/schemas/Action" example: - id: small - id: large excludedActions: type: array default: [] items: type: string example: [] baselineAction: type: string example: small UploadedDecision: type: object additionalProperties: false required: [decisionId, tsMs, modelTag, seed, input, chosenIndex, probability, pmf, eligible] description: A decision made in-process by an SDK. Items with unknown fields are rejected. properties: decisionId: type: string pattern: "^[A-Za-z0-9_.:-]{1,128}$" example: loc_1a0cfb2c4d80123456789abcdef tsMs: type: integer description: Decision time, ms since the epoch; at most 7 days old and 5 minutes ahead. example: 1790190738735 modelTag: type: string description: The `modelTag` of the published model the decision was made with. example: 3f2a9c0d41b7e8a5 modelVersion: type: integer description: Optional; must match the tag's version. example: 1234 seed: description: The sampling seed, a u64 (a decimal string keeps full precision in JavaScript). oneOf: - type: string pattern: "^[0-9]{1,20}$" - type: integer minimum: 0 example: "13505299653486734512" input: $ref: "#/components/schemas/UploadInput" chosenIndex: type: integer minimum: 0 example: 0 chosenId: type: string description: > The chosen action's id. Optional; when the model is retired it is checked against the action list before the item is stored unverified. probability: type: number minimum: 0 maximum: 1 example: 0.93 pmf: type: array items: type: number example: [0.93, 0.07] eligible: type: array items: type: integer example: [0, 1] DecisionUploadRequest: type: object required: [decisions] properties: decisions: type: array maxItems: 4096 items: $ref: "#/components/schemas/UploadedDecision" UploadRejection: type: object required: [index, decisionId, error, retryable] properties: index: type: integer description: Position of the item in `decisions`. decisionId: type: string description: The item's `decisionId`, or empty if it had none. error: type: string retryable: type: boolean description: True when the server could not take the item right now; upload it again later. DecisionUploadResponse: type: object required: [accepted, duplicates, unverified, rejected] properties: accepted: type: integer description: Items stored, including retried uploads of stored decisions. duplicates: type: integer description: Accepted items that were already stored. unverified: type: integer description: > Accepted items whose model the server no longer has (retired after a restart or a long outage): stored with mode `unverified`, rewards still train the model, off-policy evaluation leaves them out. rejected: type: array items: $ref: "#/components/schemas/UploadRejection" RewardUploadItem: type: object additionalProperties: false required: [decisionId] description: Fields as in RewardRequest, without `durable`. Exactly one of `reward` and `value`. oneOf: - required: [reward] - required: [value] properties: decisionId: type: string example: loc_1a0cfb2c4d80123456789abcdef reward: type: number example: 0.8 value: type: number deprecated: true description: Alias of `reward`. example: 0.8 idempotencyKey: type: string minLength: 1 maxLength: 256 example: order-4411 detail: description: Any JSON stored with the reward; the key `learned` is reserved. example: {latencyMs: 812} RewardUploadRequest: type: object required: [rewards] properties: rewards: type: array maxItems: 4096 items: $ref: "#/components/schemas/RewardUploadItem" RewardUploadFailure: type: object required: [ok, error] properties: ok: type: boolean enum: [false] decisionId: type: string description: Absent when the item could not be parsed. status: type: integer description: The status `POST .../reward` would have answered (for example 404). error: type: string RewardUploadResponse: type: object required: [results] properties: results: type: array items: oneOf: - $ref: "#/components/schemas/RewardResponse" - $ref: "#/components/schemas/RewardUploadFailure" AuditEvent: type: object additionalProperties: false required: [seq, tsMs, event, detail] properties: seq: type: integer tsMs: type: integer description: Event time, ms since the epoch. event: type: string description: > `capsule_created`, `spec_updated`, `mode_changed`, `program_installed`, `program_removed`, `policy_updated`, `logs_purged`, `execution_denied`, `upload_rejected`, `spec_promoted`, `promotion_refused`, `spec_replaced`, `spec_applied`, `upload_unverified` or `capsule_deleted`. detail: type: object description: > `execution_denied` carries `error` and `requestId`; spec events the patch and `specSha256`; promotion events the evaluation summary. PersonalizerRankRequest: type: object required: [actions] properties: contextFeatures: type: array items: type: object description: Feature objects, merged into one; their keys act as namespaces. actions: type: array minItems: 1 items: type: object required: [id] properties: id: type: string features: type: array items: type: object excludedActions: type: array items: type: string eventId: type: string description: "1-128 characters from `A-Z a-z 0-9 _ . : -`; generated when absent." deferActivation: type: boolean default: false PersonalizerRankResponse: type: object required: [ranking, eventId, rewardActionId] properties: ranking: type: array items: type: object required: [id, probability] properties: id: type: string probability: type: number eventId: type: string rewardActionId: type: string PersonalizerRewardRequest: type: object required: [value] properties: value: type: number PersonalizerServiceConfiguration: type: object properties: rewardWaitTime: type: string description: ISO 8601 duration, e.g. `PT10M`. defaultReward: type: number nullable: true rewardAggregation: type: string enum: [earliest, sum] explorationPercentage: type: number minimum: 0 maximum: 1 learningMode: type: string enum: [Online, Apprentice, Frozen] modelExportFrequency: type: string description: Always `PT1S` (models publish within a second). logRetentionDays: type: integer description: Always -1 (no automatic retention; see `DELETE .../logs`). PersonalizerErrorBody: type: object required: [error] properties: error: type: object required: [code, message] properties: code: type: string description: > `BadArgument`, `Unauthorized`, `Forbidden`, `ResourceNotFound`, `Conflict`, `TooManyRequests`, `ServiceUnavailable` or `InternalServerError`. message: type: string AuditList: type: object required: [audits] properties: audits: type: array items: $ref: "#/components/schemas/AuditEvent" EvaluateRequest: type: object additionalProperties: false properties: policy: type: string description: "`logged`, `greedy` (cross-fitted) or `constant:`; not with `spec`." example: greedy spec: type: object additionalProperties: true description: A candidate spec as a merge patch over the current spec; not with `policy`. example: {exploration: {floor: 0.1}} gates: type: array default: [] description: > Checks such as `lift.dr.lower >= 0.01`, `ess >= 200`, `coverage >= 0.5` or `n >= 1000` (metric, `>= > <= < ==`, a number or metric, optional `+`/`-` offset). items: type: string example: ["lift.dr.lower >= 0", "n >= 2"] since: type: integer description: Earliest decision time to evaluate, ms since the epoch. example: 1790000000000 until: type: integer description: Latest decision time to evaluate, ms since the epoch. example: 1890000000000 folds: type: integer minimum: 2 maximum: 100 default: 5 description: Cross-fitting folds of the reward model. example: 2 bootstrap: type: integer minimum: 0 maximum: 100000 default: 1000 description: Bootstrap resamples; 0 for normal intervals only, otherwise at least 100. example: 100 seed: type: integer minimum: 0 default: 7 description: Seed of the bootstrap resampling. example: 7 wMax: type: number minimum: 1 default: 100 description: Importance weights are clipped to at most this. example: 50 rewardRange: type: array minItems: 2 maxItems: 2 description: Raw reward range for the reward model; default the observed range. items: type: number example: [0, 1] OpeInterval: type: object description: A value with its standard error and 95% intervals; null where undefined. required: [se, lower, upper, normalLower, normalUpper] properties: estimate: type: number nullable: true mean: type: number nullable: true se: type: number nullable: true lower: type: number nullable: true upper: type: number nullable: true normalLower: type: number nullable: true normalUpper: type: number nullable: true OpeByEstimator: type: object required: [dm, ips, snips, dr] properties: dm: $ref: "#/components/schemas/OpeInterval" ips: $ref: "#/components/schemas/OpeInterval" snips: $ref: "#/components/schemas/OpeInterval" dr: $ref: "#/components/schemas/OpeInterval" GateResult: type: object required: [check, left, right, pass] properties: check: type: string left: type: number nullable: true right: type: number nullable: true description: The right-hand value, offset included. pass: type: boolean OpeReport: type: object required: [policy, data, logged, estimators, lift, diagnostics, settings, warnings, gates, gatesPassed, verdict] description: Values are in raw reward units. properties: policy: type: string description: The evaluated policy's label. data: type: object required: [rows, rowsWithoutPmf, rowsUnverified, rowsWithoutReward, rewardAggregation, aggregatedRewards, folds, rewardRange, rewardRangeSource, rewardsOutsideRange] properties: rows: type: integer description: Decisions read. rowsWithoutPmf: type: integer rowsUnverified: type: integer description: Uploaded decisions whose model was retired before they could be verified; left out. rowsWithoutReward: type: integer rewardAggregation: type: string enum: [first, sum] aggregatedRewards: type: integer folds: type: integer rewardRange: type: array items: type: number rewardRangeSource: type: string enum: [given, observed] rewardsOutsideRange: type: integer logged: $ref: "#/components/schemas/OpeInterval" estimators: $ref: "#/components/schemas/OpeByEstimator" lift: $ref: "#/components/schemas/OpeByEstimator" diagnostics: type: object required: [n, ess, coverage, clipRate, clippedRows, maxWeight, meanWeight, fallbackRows, fallbackRate, unsupportedMass] properties: n: type: integer description: Rows evaluated (rows with a reward). ess: type: number nullable: true coverage: type: number nullable: true clipRate: type: number nullable: true clippedRows: type: integer maxWeight: type: number nullable: true meanWeight: type: number nullable: true fallbackRows: type: integer fallbackRate: type: number nullable: true unsupportedMass: type: number nullable: true settings: type: object required: [bootstrap, seed, wMax, confidence, interval, rewardModel] properties: bootstrap: type: integer seed: type: integer wMax: type: number nullable: true confidence: type: number interval: type: string enum: [bootstrapPercentile, normal] rewardModel: $ref: "#/components/schemas/LearnerSpec" warnings: type: array items: type: string gates: type: array items: $ref: "#/components/schemas/GateResult" gatesPassed: type: boolean description: True when every gate passes, including when there are none. verdict: type: string PromoteResponse: type: object required: [promoted, spec, report] properties: promoted: type: boolean spec: $ref: "#/components/schemas/DecisionSpec" report: $ref: "#/components/schemas/OpeReport" PromoteConflict: type: object required: [error] properties: promoted: type: boolean description: False when a gate failed. error: type: string report: $ref: "#/components/schemas/OpeReport" PurgeResponse: type: object required: [ok, removed] properties: ok: type: boolean removed: type: object required: [removedDecisions, removedRewards] properties: removedDecisions: type: integer removedRewards: type: integer