diff --git a/dockerfiles/hermes-hux-foundation/hux/__init__.py b/dockerfiles/hermes-hux-foundation/hux/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/hermes/scripts/hux_contracts.py b/dockerfiles/hermes-hux-foundation/hux/contracts.py similarity index 98% rename from services/hermes/scripts/hux_contracts.py rename to dockerfiles/hermes-hux-foundation/hux/contracts.py index 5c466594..a9a0005f 100644 --- a/services/hermes/scripts/hux_contracts.py +++ b/dockerfiles/hermes-hux-foundation/hux/contracts.py @@ -10,13 +10,18 @@ loudly rather than silently passing. from __future__ import annotations import json +import os import re from pathlib import Path from typing import Any -CONTRACT_DIR = Path(__file__).resolve().parents[1] / "contracts" / "hux" +CONTRACT_DIR = Path( + os.environ.get("HUX_CONTRACT_DIR") + or Path(__file__).resolve().parents[3] / "services" / "hermes" / "contracts" / "hux" +) SCHEMA_FILES = ( "common.schema.json", + "identity.schema.json", "event.schema.json", "memory.schema.json", "project.schema.json", diff --git a/services/hermes/scripts/hux_policy.py b/dockerfiles/hermes-hux-foundation/hux/rules.py similarity index 97% rename from services/hermes/scripts/hux_policy.py rename to dockerfiles/hermes-hux-foundation/hux/rules.py index 45a43f3d..5e427c42 100644 --- a/services/hermes/scripts/hux_policy.py +++ b/dockerfiles/hermes-hux-foundation/hux/rules.py @@ -13,7 +13,7 @@ import os from datetime import datetime, timedelta, timezone from typing import Any -from hux_contracts import load_flags +from hux.contracts import load_flags EFFORT_ORDER = ("low", "medium", "high", "xhigh") @@ -44,11 +44,12 @@ RELEASE_EVIDENCE: dict[str, tuple[str, ...]] = { CAPABILITIES = ( "read_files", "write_files", "shell", "network", "web_search", "send_message", - "memory_write", "artifact_write", "spend_tokens", "delegate", "deploy", + "memory_write", "artifact_write", "spend_tokens", "delegate", "deploy", "external_side_effect", ) _READ_ONLY = frozenset({"read_files", "web_search", "spend_tokens"}) _MUTATING = frozenset({"write_files", "shell", "send_message", "memory_write", "artifact_write", "delegate"}) _EXTERNAL = frozenset({"network", "deploy"}) +_ALWAYS_ASK = frozenset({"deploy", "external_side_effect"}) MODE_CATALOG: dict[str, dict[str, Any]] = { "fast": { @@ -140,12 +141,12 @@ def memory_policy_violations(entry: dict[str, Any]) -> list[str]: def default_capability_matrix() -> dict[str, dict[str, str]]: - """Autonomy level -> capability -> allow|ask|deny. Deploy always asks.""" + """Autonomy level -> capability -> allow|ask|deny. Deploy and external side effects always ask.""" matrix: dict[str, dict[str, str]] = {} for level in ("ask_first", "safe", "autonomous"): row: dict[str, str] = {} for capability in CAPABILITIES: - if capability == "deploy": + if capability in _ALWAYS_ASK: row[capability] = "ask" elif capability in _READ_ONLY: row[capability] = "allow" @@ -171,7 +172,7 @@ def effective_decision(policy: dict[str, Any], capability: str, now: datetime | if grant["decision"] == "deny": return "deny" decision = grant["decision"] - if capability == "deploy" and decision == "allow": + if capability in _ALWAYS_ASK and decision == "allow": return "ask" return decision diff --git a/docs/hermes_hux_contracts.md b/docs/hermes_hux_contracts.md index f318f5fa..b1dd638a 100644 --- a/docs/hermes_hux_contracts.md +++ b/docs/hermes_hux_contracts.md @@ -10,9 +10,12 @@ Schemas live in `services/hermes/contracts/hux/` as JSON Schema 2020-12 with one worked example per record under `examples/`. The rules a schema cannot express (state machines, capability matrix, mode catalog, privacy defaults, suggestion gating, flag dependencies) live in -`services/hermes/scripts/hux_policy.py`; `hux_contracts.py` validates records -without external packages. `testing/tests/test_hermes_hux_contracts.py` keeps -schemas, examples, rules and the live Switchyard catalog in agreement. +`dockerfiles/hermes-hux-foundation/hux/rules.py`; `hux/contracts.py` +validates records without external packages. +`testing/tests/test_hermes_hux_contract_schemas.py` keeps schemas, examples, +rules and the live Switchyard catalog in agreement. The contract is frozen at +`1.0.0` (see `docs/hux/ADR-0001-hux-v1-contract-freeze.md` for identity +headers, `If-Match`, idempotency and compatibility rules). ## Where the backend lives @@ -51,11 +54,12 @@ tenant. Responses are the records below, wrapped as `{"items": [...], | Area | Routes | Record | |---|---|---| +| Foundation (HUX-11) | `GET /hux/v1/capabilities`, `GET /hux/v1/manifest` | `hux.capabilities.v1`, `hux.manifest.v1`, errors as `hux.error.v1` | | Events (HUX-01) | `GET /hux/v1/conversations/{id}/events?after_seq=N` (JSON), `GET .../events/stream` (SSE, `id:` = seq) | `hux.event.v1` | | Memory (HUX-02) | `GET/POST /hux/v1/memory`, `POST /hux/v1/memory/{id}/{approve,reject,forget}`, `GET /hux/v1/memory/export` | `hux.memory.v1` | | Projects (HUX-03) | `GET/POST/PATCH /hux/v1/projects`, `GET/PATCH /hux/v1/conversations`, `POST /hux/v1/conversations/{id}/branch`, `GET /hux/v1/search?q=` | `hux.project.v1`, `hux.conversation.v1` | | Artifacts (HUX-04) | `GET/POST /hux/v1/artifacts`, `POST /hux/v1/artifacts/{id}/versions`, `GET .../versions/{n}/diff?from=`, `POST .../promote` | `hux.artifact.v1` | -| Autonomy (HUX-05) | `GET/PUT /hux/v1/policy?scope=`, `GET /hux/v1/approvals`, `POST /hux/v1/approvals/{id}` (`once|session|always|deny`, the gateway's own choices), `POST /hux/v1/runs/{id}/stop` returns the receipt | `hux.policy.v1`, `hux.approval.v1`, `hux.cancel_receipt.v1` | +| Autonomy (HUX-05) | `GET/PUT /hux/v1/policy?scope=`, `GET /hux/v1/approvals`, `POST /hux/v1/approvals/{id}` (`once|session|always|deny`, the gateway's own choices), `POST /hux/v1/runs/{id}/stop` returns the receipt, `GET /hux/v1/runs/{id}/budget` | `hux.policy.v1`, `hux.approval.v1`, `hux.cancel_receipt.v1`, `hux.budget_state.v1` | | Modes (HUX-06) | `GET /hux/v1/modes`, `PUT /hux/v1/conversations/{id}/mode` | `hux.mode.v1` | | Research (HUX-08) | `GET /hux/v1/messages/{id}/citations`, `GET /hux/v1/sources/{id}`, `GET/PATCH /hux/v1/notebooks/{id}` | `hux.source.v1`, `hux.passage.v1`, `hux.citation.v1`, `hux.research_notebook.v1` | | Onboarding (HUX-09) | `GET /hux/v1/suggestions?context=`, `POST /hux/v1/suggestions/{id}/{dismiss,never,acted}` | `hux.suggestion.v1`, `hux.suggestion_state.v1` | diff --git a/docs/hux/ADR-0001-hux-v1-contract-freeze.md b/docs/hux/ADR-0001-hux-v1-contract-freeze.md new file mode 100644 index 00000000..82329546 --- /dev/null +++ b/docs/hux/ADR-0001-hux-v1-contract-freeze.md @@ -0,0 +1,69 @@ +# ADR-0001: freeze the hux.v1 contract at 1.0.0 + +Status: accepted, 2026-08-24. Owners: Claude (backend), Codex (integration). + +## Decision + +`services/hermes/contracts/hux/` is frozen as contract version `1.0.0` +(`VERSION`, `flags.json.contract_version`). It is the only shape any HUX +surface (Chat, Worker, Telegram, voice) codes against. The Python reference +implementation lives in `dockerfiles/hermes-hux-foundation/hux/`: +`contracts.py` validates records, `rules.py` holds the governance rules the +schemas cannot express. + +Records carry their own version in `schema` (`hux.event.v1`). Wire fixtures +under `services/hermes/contracts/hux/examples/` are validated by +`testing/tests/test_hermes_hux_contract_schemas.py` and are the exchange +format with Codex: a change to a fixture is a change to the contract. + +## What 1.0.0 covers + +| Requirement | Where | +|---|---| +| Tenant and surface identity | `common.identity` (tenant_slot, hashed subject, surface, trust: router/relay/worker) — carried on every stored record that a caller can create | +| Capability negotiation, default-off flags | `identity.capabilities` (`GET /hux/v1/capabilities`), `flags.json`; a card is enabled only when its dependency chain is enabled (`rules.flag_enabled`) | +| Redacted activity events | `event`: `seq` (monotonic per conversation), `turn`, `correlation_id`, `idempotency_key`, replay by `after_seq`, kinds for decisions, delegation, failure/completion, cancellation, budget and side-effect gates; `detail` is allowlisted per kind and scrubbed | +| Memory | `memory`: provenance, `source`, `reason`, `sensitivity`, `topic`, `ttl`, `approval_mode` incl. `no_store`, `status` incl. `no_store`, edit via `supersedes`, delete via `forgotten`, retrieval removal via `retrievable=false`, revisioned | +| Projects, lineage, artifacts, versions, diffs, authorization | `project`, `conversation.branch`, `artifact.versions[]` immutable with `content_ref` hash, `diff_from`, `lineage`, `promotion`, `access`, `revision` | +| Autonomy | `permission.policy` (levels, grants, budgets: tokens/tool calls/wall clock/spend/subagents/delegations/scope), `approval` with `request.external`, `cancellation_receipt`, `budget_state` | +| Research | `citation.source/passage/citation/notebook` with classification, support verdict, notes, assumptions, unresolved questions, `dedupe_key` | +| Additive migrations, rollback readers | `identity.manifest` (`data_layout_version`, `min_reader_contract_version`); rules in "Compatibility" below | + +## Wire conventions + +- Identity is asserted by the trusted hop, never by the client body: the + chat router sets `X-Hermes-Tenant-Identity: slot-N`, `X-Hux-Subject: + usr_`, `X-Hux-Surface: chat`; the Telegram relay adds the relay key + and `trust: relay`; the Worker uses `trust: worker`. Requests without a + complete, well-formed identity are `401 unauthorized`. The service must + only be reachable from those hops (NetworkPolicy is Codex's to enforce). +- Mutations of revisioned records send `If-Match: `; a mismatch is + `409 conflict` with `hux.error.v1`. Creates accept `Idempotency-Key`; a + repeat returns the original record with `200` instead of a duplicate. +- Lists return `{"items": [...], "next": }`. Event pages use + `after_seq`; the SSE stream sets `id:` to `seq` so `Last-Event-ID` + resumes exactly. +- Errors are always `hux.error.v1`. A disabled card answers `404 flag_off` + so an old client and a flag-off server look identical. +- Every read and mutation writes a `common.audit_outcome`. + +## Compatibility rules + +1. Within `1.x`: only add optional fields, enum values or new record kinds. + Never rename, remove, change a type, or make an optional field required. +2. Readers ignore unknown fields and unknown enum values they do not need; + a reader that needs a new enum value must check `contract_version`. +3. On-disk data is append-only per family; `data_layout_version` bumps only + for additive layout changes, and `min_reader_contract_version` says the + oldest service that can still read it. Rolling the service back to any + version ≥ `min_reader_contract_version` leaves data readable. +4. Anything outside 1–3 is `hux.v2`, served beside v1 until every surface + has moved. + +## Consequences + +- Codex codes UI against the fixtures, not against the running service. +- Claude's implementation cards (HUX-01/02/03/04/05/08/10/11) may add + optional fields but must ship the fixture change in the same commit. +- If Codex needs a shared-interface change, it lands as a `1.(x+1).0` + revision of the schemas and fixtures here, never as an informal edit. diff --git a/services/hermes/contracts/hux/VERSION b/services/hermes/contracts/hux/VERSION new file mode 100644 index 00000000..3eefcb9d --- /dev/null +++ b/services/hermes/contracts/hux/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/services/hermes/contracts/hux/artifact.schema.json b/services/hermes/contracts/hux/artifact.schema.json index 8e1efae7..b30085b3 100644 --- a/services/hermes/contracts/hux/artifact.schema.json +++ b/services/hermes/contracts/hux/artifact.schema.json @@ -5,65 +5,192 @@ "description": "Typed, versioned output (HUX-04). Content lives behind content_ref; versions are immutable and diffable; lineage records what an artifact was derived from; promotion copies a version into a project.", "type": "object", "additionalProperties": false, - "required": ["schema", "id", "owner", "type", "title", "current_version", "versions", "sensitivity", "created_at", "updated_at"], + "required": [ + "schema", + "id", + "owner", + "type", + "title", + "current_version", + "versions", + "sensitivity", + "created_at", + "updated_at", + "revision", + "access" + ], "properties": { - "schema": {"const": "hux.artifact.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "owner": {"$ref": "common.schema.json#/$defs/user_ref"}, - "conversation_id": {"$ref": "common.schema.json#/$defs/id"}, - "project_id": {"$ref": "common.schema.json#/$defs/id"}, - "type": {"type": "string", "enum": ["markdown", "code", "html", "svg", "image", "json", "csv", "document", "audio"]}, - "language": {"type": "string", "maxLength": 40}, - "title": {"type": "string", "minLength": 1, "maxLength": 200}, - "current_version": {"type": "integer", "minimum": 1}, + "schema": { + "const": "hux.artifact.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "owner": { + "$ref": "common.schema.json#/$defs/user_ref" + }, + "conversation_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "project_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "type": { + "type": "string", + "enum": [ + "markdown", + "code", + "html", + "svg", + "image", + "json", + "csv", + "document", + "audio" + ] + }, + "language": { + "type": "string", + "maxLength": 40 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "current_version": { + "type": "integer", + "minimum": 1 + }, "versions": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, - "required": ["version", "created_at", "created_by", "content_ref"], + "required": [ + "version", + "created_at", + "created_by", + "content_ref" + ], "properties": { - "version": {"type": "integer", "minimum": 1}, - "created_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "created_by": {"$ref": "common.schema.json#/$defs/actor"}, - "message_id": {"type": "string", "maxLength": 120}, + "version": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "created_by": { + "$ref": "common.schema.json#/$defs/actor" + }, + "message_id": { + "type": "string", + "maxLength": 120 + }, "content_ref": { "type": "object", "additionalProperties": false, - "required": ["hash", "bytes", "mime"], + "required": [ + "hash", + "bytes", + "mime" + ], "properties": { - "hash": {"$ref": "common.schema.json#/$defs/sha256"}, - "bytes": {"type": "integer", "minimum": 0}, - "mime": {"type": "string", "maxLength": 120} + "hash": { + "$ref": "common.schema.json#/$defs/sha256" + }, + "bytes": { + "type": "integer", + "minimum": 0 + }, + "mime": { + "type": "string", + "maxLength": 120 + } } }, - "diff_from": {"type": "integer", "minimum": 1}, + "diff_from": { + "type": "integer", + "minimum": 1 + }, "lineage": { "type": "object", "additionalProperties": false, - "required": ["artifact_id", "version"], + "required": [ + "artifact_id", + "version" + ], "properties": { - "artifact_id": {"$ref": "common.schema.json#/$defs/id"}, - "version": {"type": "integer", "minimum": 1} + "artifact_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "type": "integer", + "minimum": 1 + } } }, - "note": {"type": "string", "maxLength": 200} + "note": { + "type": "string", + "maxLength": 200 + } } } }, "promotion": { "type": "object", "additionalProperties": false, - "required": ["project_id", "version", "at"], + "required": [ + "project_id", + "version", + "at" + ], "properties": { - "project_id": {"$ref": "common.schema.json#/$defs/id"}, - "version": {"type": "integer", "minimum": 1}, - "at": {"$ref": "common.schema.json#/$defs/timestamp"} + "project_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "type": "integer", + "minimum": 1 + }, + "at": { + "$ref": "common.schema.json#/$defs/timestamp" + } } }, - "sensitivity": {"$ref": "common.schema.json#/$defs/sensitivity"}, - "created_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "updated_at": {"$ref": "common.schema.json#/$defs/timestamp"} + "sensitivity": { + "$ref": "common.schema.json#/$defs/sensitivity" + }, + "created_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "updated_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + }, + "access": { + "type": "object", + "description": "Owner-only unless a share token is minted; shares are read-only and expire.", + "additionalProperties": false, + "required": [ + "mode" + ], + "properties": { + "mode": { + "type": "string", + "enum": [ + "owner", + "shared_readonly" + ] + }, + "share_expires_at": { + "$ref": "common.schema.json#/$defs/timestamp" + } + } + } } } diff --git a/services/hermes/contracts/hux/citation.schema.json b/services/hermes/contracts/hux/citation.schema.json index 0c21695d..4162e5e1 100644 --- a/services/hermes/contracts/hux/citation.schema.json +++ b/services/hermes/contracts/hux/citation.schema.json @@ -7,82 +7,320 @@ "source": { "type": "object", "additionalProperties": false, - "required": ["schema", "id", "kind", "title", "classification", "retrieved_at"], + "required": [ + "schema", + "id", + "kind", + "title", + "classification", + "retrieved_at" + ], "properties": { - "schema": {"const": "hux.source.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "kind": {"type": "string", "enum": ["web", "document", "artifact", "memory", "tool_output", "dataset"]}, - "uri": {"type": "string", "maxLength": 2000}, - "title": {"type": "string", "minLength": 1, "maxLength": 300}, - "publisher": {"type": "string", "maxLength": 200}, - "published_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "retrieved_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "classification": {"type": "string", "enum": ["primary", "secondary", "unknown"]}, - "content_hash": {"$ref": "common.schema.json#/$defs/sha256"}, - "provenance": {"$ref": "common.schema.json#/$defs/provenance"} + "schema": { + "const": "hux.source.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "kind": { + "type": "string", + "enum": [ + "web", + "document", + "artifact", + "memory", + "tool_output", + "dataset" + ] + }, + "uri": { + "type": "string", + "maxLength": 2000 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 300 + }, + "publisher": { + "type": "string", + "maxLength": 200 + }, + "published_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "retrieved_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "classification": { + "type": "string", + "enum": [ + "primary", + "secondary", + "unknown" + ] + }, + "content_hash": { + "$ref": "common.schema.json#/$defs/sha256" + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "dedupe_key": { + "type": "string", + "description": "Stable key used to merge duplicates: sha256 of normalised uri (source), of source_id+text hash (passage), of message_id+claim+passages (citation).", + "pattern": "^sha256:[0-9a-f]{64}$" + } } }, "passage": { "type": "object", "additionalProperties": false, - "required": ["schema", "id", "source_id", "text", "hash"], + "required": [ + "schema", + "id", + "source_id", + "text", + "hash" + ], "properties": { - "schema": {"const": "hux.passage.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "source_id": {"$ref": "common.schema.json#/$defs/id"}, - "text": {"type": "string", "minLength": 1, "maxLength": 4000}, - "hash": {"$ref": "common.schema.json#/$defs/sha256"}, + "schema": { + "const": "hux.passage.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "source_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "hash": { + "$ref": "common.schema.json#/$defs/sha256" + }, "locator": { "type": "object", "additionalProperties": false, "properties": { - "page": {"type": "integer", "minimum": 1}, - "selector": {"type": "string", "maxLength": 500}, - "line_start": {"type": "integer", "minimum": 1}, - "line_end": {"type": "integer", "minimum": 1}, - "char_start": {"type": "integer", "minimum": 0}, - "char_end": {"type": "integer", "minimum": 0} + "page": { + "type": "integer", + "minimum": 1 + }, + "selector": { + "type": "string", + "maxLength": 500 + }, + "line_start": { + "type": "integer", + "minimum": 1 + }, + "line_end": { + "type": "integer", + "minimum": 1 + }, + "char_start": { + "type": "integer", + "minimum": 0 + }, + "char_end": { + "type": "integer", + "minimum": 0 + } } + }, + "dedupe_key": { + "type": "string", + "description": "Stable key used to merge duplicates: sha256 of normalised uri (source), of source_id+text hash (passage), of message_id+claim+passages (citation).", + "pattern": "^sha256:[0-9a-f]{64}$" } } }, "citation": { "type": "object", "additionalProperties": false, - "required": ["schema", "id", "message_id", "claim", "passage_ids", "support"], + "required": [ + "schema", + "id", + "message_id", + "claim", + "passage_ids", + "support" + ], "properties": { - "schema": {"const": "hux.citation.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "message_id": {"type": "string", "minLength": 1, "maxLength": 120}, - "claim": {"type": "string", "minLength": 1, "maxLength": 1000}, - "passage_ids": {"type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": {"$ref": "common.schema.json#/$defs/id"}}, - "support": {"type": "string", "enum": ["supports", "partially_supports", "contradicts", "unverified"]}, - "note": {"type": "string", "maxLength": 500} + "schema": { + "const": "hux.citation.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "message_id": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "claim": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "passage_ids": { + "type": "array", + "minItems": 1, + "maxItems": 32, + "uniqueItems": true, + "items": { + "$ref": "common.schema.json#/$defs/id" + } + }, + "support": { + "type": "string", + "enum": [ + "supports", + "partially_supports", + "contradicts", + "unverified" + ] + }, + "note": { + "type": "string", + "maxLength": 500 + }, + "dedupe_key": { + "type": "string", + "description": "Stable key used to merge duplicates: sha256 of normalised uri (source), of source_id+text hash (passage), of message_id+claim+passages (citation).", + "pattern": "^sha256:[0-9a-f]{64}$" + } } }, "notebook": { "type": "object", "additionalProperties": false, - "required": ["schema", "id", "conversation_id", "question", "status", "source_ids", "passage_ids", "citation_ids", "assumptions", "unresolved_questions", "updated_at"], + "required": [ + "schema", + "id", + "conversation_id", + "question", + "status", + "source_ids", + "passage_ids", + "citation_ids", + "assumptions", + "unresolved_questions", + "updated_at", + "notes", + "revision" + ], "properties": { - "schema": {"const": "hux.research_notebook.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "conversation_id": {"$ref": "common.schema.json#/$defs/id"}, - "question": {"type": "string", "minLength": 1, "maxLength": 1000}, - "status": {"type": "string", "enum": ["open", "answered", "abandoned"]}, - "source_ids": {"type": "array", "uniqueItems": true, "items": {"$ref": "common.schema.json#/$defs/id"}}, - "passage_ids": {"type": "array", "uniqueItems": true, "items": {"$ref": "common.schema.json#/$defs/id"}}, - "citation_ids": {"type": "array", "uniqueItems": true, "items": {"$ref": "common.schema.json#/$defs/id"}}, - "assumptions": {"type": "array", "maxItems": 64, "items": {"type": "string", "minLength": 1, "maxLength": 500}}, - "unresolved_questions": {"type": "array", "maxItems": 64, "items": {"type": "string", "minLength": 1, "maxLength": 500}}, - "updated_at": {"$ref": "common.schema.json#/$defs/timestamp"} + "schema": { + "const": "hux.research_notebook.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "conversation_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "question": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "status": { + "type": "string", + "enum": [ + "open", + "answered", + "abandoned" + ] + }, + "source_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "common.schema.json#/$defs/id" + } + }, + "passage_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "common.schema.json#/$defs/id" + } + }, + "citation_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "common.schema.json#/$defs/id" + } + }, + "assumptions": { + "type": "array", + "maxItems": 64, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 500 + } + }, + "unresolved_questions": { + "type": "array", + "maxItems": 64, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 500 + } + }, + "updated_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "notes": { + "type": "array", + "maxItems": 128, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "at", + "text" + ], + "properties": { + "at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + }, + "source_id": { + "$ref": "common.schema.json#/$defs/id" + } + } + } + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + } } } }, "oneOf": [ - {"$ref": "#/$defs/source"}, - {"$ref": "#/$defs/passage"}, - {"$ref": "#/$defs/citation"}, - {"$ref": "#/$defs/notebook"} + { + "$ref": "#/$defs/source" + }, + { + "$ref": "#/$defs/passage" + }, + { + "$ref": "#/$defs/citation" + }, + { + "$ref": "#/$defs/notebook" + } ] } diff --git a/services/hermes/contracts/hux/common.schema.json b/services/hermes/contracts/hux/common.schema.json index 0ae1dd0f..02167526 100644 --- a/services/hermes/contracts/hux/common.schema.json +++ b/services/hermes/contracts/hux/common.schema.json @@ -25,98 +25,317 @@ }, "surface": { "type": "string", - "enum": ["chat", "worker", "telegram", "voice", "api"] + "enum": [ + "chat", + "worker", + "telegram", + "voice", + "api" + ] }, "provider": { "type": "string", "description": "Provider class, never a vendor model name. Switchyard resolves the class to a concrete target.", - "enum": ["codex", "claude", "local"] + "enum": [ + "codex", + "claude", + "local" + ] }, "effort": { "type": "string", - "enum": ["low", "medium", "high", "xhigh"] + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] }, "sensitivity": { "type": "string", "description": "public: safe to show anywhere; personal: user-owned but not sensitive; sensitive: health/finance/legal/relationships; restricted: credentials, minors, biometric, location traces.", - "enum": ["public", "personal", "sensitive", "restricted"] + "enum": [ + "public", + "personal", + "sensitive", + "restricted" + ] }, "redaction": { "type": "object", "additionalProperties": false, - "required": ["level"], + "required": [ + "level" + ], "properties": { - "level": {"type": "string", "enum": ["none", "partial", "full"]}, - "reason": {"type": "string", "maxLength": 200} + "level": { + "type": "string", + "enum": [ + "none", + "partial", + "full" + ] + }, + "reason": { + "type": "string", + "maxLength": 200 + } } }, "actor": { "type": "object", "additionalProperties": false, - "required": ["type", "id"], + "required": [ + "type", + "id" + ], "properties": { - "type": {"type": "string", "enum": ["user", "assistant", "tool", "system", "operator"]}, - "id": {"type": "string", "minLength": 1, "maxLength": 120}, - "display": {"type": "string", "maxLength": 120} + "type": { + "type": "string", + "enum": [ + "user", + "assistant", + "tool", + "system", + "operator" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "display": { + "type": "string", + "maxLength": 120 + } } }, "route": { "type": "object", "description": "What was asked of Switchyard and what it resolved. requested is a friendly mode or a route id; resolved_target is the Switchyard target name.", "additionalProperties": false, - "required": ["requested"], + "required": [ + "requested" + ], "properties": { - "requested": {"type": "string", "minLength": 1, "maxLength": 120}, - "resolved_target": {"type": "string", "maxLength": 120}, - "provider": {"$ref": "#/$defs/provider"}, - "effort": {"$ref": "#/$defs/effort"} + "requested": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "resolved_target": { + "type": "string", + "maxLength": 120 + }, + "provider": { + "$ref": "#/$defs/provider" + }, + "effort": { + "$ref": "#/$defs/effort" + } } }, "build": { "type": "object", "additionalProperties": false, "properties": { - "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, - "image_digest": {"$ref": "#/$defs/sha256"} + "commit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "image_digest": { + "$ref": "#/$defs/sha256" + } } }, "provenance": { "type": "object", "description": "Who produced a record, on which surface, under which session/run, through which route, from which build.", "additionalProperties": false, - "required": ["surface", "actor", "recorded_at"], + "required": [ + "surface", + "actor", + "recorded_at" + ], "properties": { - "surface": {"$ref": "#/$defs/surface"}, - "actor": {"$ref": "#/$defs/actor"}, - "recorded_at": {"$ref": "#/$defs/timestamp"}, - "session_id": {"type": "string", "maxLength": 120}, - "conversation_id": {"$ref": "#/$defs/id"}, - "message_id": {"type": "string", "maxLength": 120}, - "run_id": {"type": "string", "maxLength": 120}, - "route": {"$ref": "#/$defs/route"}, - "build": {"$ref": "#/$defs/build"} + "surface": { + "$ref": "#/$defs/surface" + }, + "actor": { + "$ref": "#/$defs/actor" + }, + "recorded_at": { + "$ref": "#/$defs/timestamp" + }, + "session_id": { + "type": "string", + "maxLength": 120 + }, + "conversation_id": { + "$ref": "#/$defs/id" + }, + "message_id": { + "type": "string", + "maxLength": 120 + }, + "run_id": { + "type": "string", + "maxLength": 120 + }, + "route": { + "$ref": "#/$defs/route" + }, + "build": { + "$ref": "#/$defs/build" + } } }, "evidence_ref": { "type": "object", "description": "Pointer to the thing that justifies a record. Never inline the payload here; the UI expands it through the owning API.", "additionalProperties": false, - "required": ["kind", "id"], + "required": [ + "kind", + "id" + ], "properties": { "kind": { "type": "string", - "enum": ["message", "tool_call", "tool_result", "artifact_version", "source", "passage", "memory", "approval", "run", "url", "file", "build", "flux", "pod"] + "enum": [ + "message", + "tool_call", + "tool_result", + "artifact_version", + "source", + "passage", + "memory", + "approval", + "run", + "url", + "file", + "build", + "flux", + "pod" + ] }, - "id": {"type": "string", "minLength": 1, "maxLength": 200}, - "uri": {"type": "string", "maxLength": 2000}, - "hash": {"$ref": "#/$defs/sha256"} + "id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "uri": { + "type": "string", + "maxLength": 2000 + }, + "hash": { + "$ref": "#/$defs/sha256" + } } }, "tags": { "type": "array", "maxItems": 32, "uniqueItems": true, - "items": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,39}$"} + "items": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]{0,39}$" + } + }, + "tenant_slot": { + "type": "string", + "description": "Router-assigned tenant process slot.", + "pattern": "^slot-[0-9]{1,3}$" + }, + "trust": { + "type": "string", + "description": "How the identity was asserted: router (browser via chat router), relay (Telegram relay key), worker (owner control plane).", + "enum": [ + "router", + "relay", + "worker" + ] + }, + "identity": { + "type": "object", + "description": "Resolved caller identity. Every record path and authorization check derives from tenant_slot + subject.", + "additionalProperties": false, + "required": [ + "tenant_slot", + "subject", + "surface", + "trust" + ], + "properties": { + "tenant_slot": { + "$ref": "#/$defs/tenant_slot" + }, + "subject": { + "$ref": "#/$defs/user_ref" + }, + "surface": { + "$ref": "#/$defs/surface" + }, + "trust": { + "$ref": "#/$defs/trust" + } + } + }, + "revision": { + "type": "integer", + "description": "Optimistic concurrency counter. Mutations send If-Match: ; a mismatch is 409.", + "minimum": 1 + }, + "idempotency_key": { + "type": "string", + "description": "Client-chosen key; a repeat with the same key returns the original record instead of creating a duplicate.", + "pattern": "^[A-Za-z0-9._:-]{8,120}$" + }, + "contract_version": { + "type": "string", + "pattern": "^1\\.[0-9]+\\.[0-9]+$" + }, + "audit_outcome": { + "type": "object", + "description": "Result of an authorization decision, written for every read and mutation.", + "additionalProperties": false, + "required": [ + "at", + "identity", + "action", + "resource", + "outcome" + ], + "properties": { + "at": { + "$ref": "#/$defs/timestamp" + }, + "identity": { + "$ref": "#/$defs/identity" + }, + "action": { + "type": "string", + "pattern": "^[a-z_]+\\.[a-z_]+$" + }, + "resource": { + "type": "string", + "maxLength": 200 + }, + "outcome": { + "type": "string", + "enum": [ + "allow", + "deny", + "not_found", + "conflict", + "flag_off" + ] + }, + "reason": { + "type": "string", + "maxLength": 200 + } + } } } } diff --git a/services/hermes/contracts/hux/event.schema.json b/services/hermes/contracts/hux/event.schema.json index 0a4a8788..fc5243c7 100644 --- a/services/hermes/contracts/hux/event.schema.json +++ b/services/hermes/contracts/hux/event.schema.json @@ -5,38 +5,120 @@ "description": "One entry in the per-conversation activity stream (HUX-01). seq is strictly monotonic per conversation; consumers resume with after_seq. Every other HUX record that changes state emits exactly one event.", "type": "object", "additionalProperties": false, - "required": ["schema", "id", "seq", "ts", "conversation_id", "kind", "summary", "provenance", "sensitivity", "redaction"], + "required": [ + "schema", + "id", + "seq", + "ts", + "conversation_id", + "kind", + "summary", + "provenance", + "sensitivity", + "redaction", + "turn", + "identity" + ], "properties": { - "schema": {"const": "hux.event.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "seq": {"type": "integer", "minimum": 0}, - "ts": {"$ref": "common.schema.json#/$defs/timestamp"}, - "conversation_id": {"$ref": "common.schema.json#/$defs/id"}, - "run_id": {"type": "string", "maxLength": 120}, - "parent_event_id": {"$ref": "common.schema.json#/$defs/id"}, - "correlation_id": {"type": "string", "maxLength": 120}, + "schema": { + "const": "hux.event.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "seq": { + "type": "integer", + "minimum": 0 + }, + "ts": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "conversation_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "run_id": { + "type": "string", + "maxLength": 120 + }, + "parent_event_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "correlation_id": { + "type": "string", + "maxLength": 120 + }, "kind": { "type": "string", "enum": [ - "message.user", "message.assistant", - "decision.route", "decision.plan", - "tool.call", "tool.result", - "approval.requested", "approval.resolved", - "memory.proposed", "memory.committed", "memory.forgotten", - "artifact.created", "artifact.version", "artifact.promoted", + "message.user", + "message.assistant", + "decision.route", + "decision.plan", + "tool.call", + "tool.result", + "approval.requested", + "approval.resolved", + "memory.proposed", + "memory.committed", + "memory.forgotten", + "artifact.created", + "artifact.version", + "artifact.promoted", "citation.attached", "mode.changed", - "run.started", "run.cancelled", "run.completed", "run.failed", + "run.started", + "run.cancelled", + "run.completed", + "run.failed", "privacy.notice", - "suggestion.shown", "suggestion.dismissed", - "release.transition" + "suggestion.shown", + "suggestion.dismissed", + "release.transition", + "delegation.started", + "delegation.completed", + "delegation.failed", + "memory.suppressed", + "memory.retrieval_removed", + "budget.exhausted", + "side_effect.blocked", + "side_effect.released" ] }, - "summary": {"type": "string", "minLength": 1, "maxLength": 280}, - "detail": {"type": "object", "description": "Kind-specific payload. Redacted according to redaction.level before leaving the tenant."}, - "evidence": {"type": "array", "maxItems": 64, "items": {"$ref": "common.schema.json#/$defs/evidence_ref"}}, - "provenance": {"$ref": "common.schema.json#/$defs/provenance"}, - "sensitivity": {"$ref": "common.schema.json#/$defs/sensitivity"}, - "redaction": {"$ref": "common.schema.json#/$defs/redaction"} + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 280 + }, + "detail": { + "type": "object", + "description": "Kind-specific payload restricted to the allowlisted keys for that kind. Raw tool arguments, logs, credentials and tokens are never stored here; the redaction pipeline drops unknown keys and scrubs secret patterns before the event is written." + }, + "evidence": { + "type": "array", + "maxItems": 64, + "items": { + "$ref": "common.schema.json#/$defs/evidence_ref" + } + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "sensitivity": { + "$ref": "common.schema.json#/$defs/sensitivity" + }, + "redaction": { + "$ref": "common.schema.json#/$defs/redaction" + }, + "turn": { + "type": "integer", + "description": "User-visible turn number within the conversation; several events share a turn.", + "minimum": 0 + }, + "idempotency_key": { + "$ref": "common.schema.json#/$defs/idempotency_key" + }, + "identity": { + "$ref": "common.schema.json#/$defs/identity" + } } } diff --git a/services/hermes/contracts/hux/examples/approval.json b/services/hermes/contracts/hux/examples/approval.json index 6da1c8b6..864ae50d 100644 --- a/services/hermes/contracts/hux/examples/approval.json +++ b/services/hermes/contracts/hux/examples/approval.json @@ -1 +1,30 @@ -{"schema":"hux.approval.v1","id":"apr_0001aaaa","run_id":"run_9f","conversation_id":"conv_0001abcd","capability":"write_files","request":{"summary":"Write supplier-comparison.md to the workspace","risk":"low","evidence":[{"kind":"tool_call","id":"call-7"}]},"status":"approved","decision":{"choice":"session","by":{"type":"user","id":"usr_0123456789abcdef"},"at":"2026-08-23T10:00:03Z"},"requested_at":"2026-08-23T10:00:02Z","expires_at":"2026-08-23T10:10:02Z"} +{ + "schema": "hux.approval.v1", + "id": "apr_0001aaaa", + "run_id": "run_9f", + "conversation_id": "conv_0001abcd", + "capability": "write_files", + "request": { + "summary": "Write supplier-comparison.md to the workspace", + "risk": "low", + "evidence": [ + { + "kind": "tool_call", + "id": "call-7" + } + ], + "external": false + }, + "status": "approved", + "decision": { + "choice": "session", + "by": { + "type": "user", + "id": "usr_0123456789abcdef" + }, + "at": "2026-08-23T10:00:03Z" + }, + "requested_at": "2026-08-23T10:00:02Z", + "expires_at": "2026-08-23T10:10:02Z", + "idempotency_key": "run_9f:approval:call-7" +} diff --git a/services/hermes/contracts/hux/examples/artifact.json b/services/hermes/contracts/hux/examples/artifact.json index 7ac57de8..6d409655 100644 --- a/services/hermes/contracts/hux/examples/artifact.json +++ b/services/hermes/contracts/hux/examples/artifact.json @@ -1 +1,53 @@ -{"schema":"hux.artifact.v1","id":"art_0001aaaa","owner":"usr_0123456789abcdef","conversation_id":"conv_0001abcd","project_id":"prj_0001aaaa","type":"markdown","title":"Supplier comparison","current_version":2,"versions":[{"version":1,"created_at":"2026-08-22T10:00:00Z","created_by":{"type":"assistant","id":"hermes"},"message_id":"msg-20","content_ref":{"hash":"sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881","bytes":2048,"mime":"text/markdown"}},{"version":2,"created_at":"2026-08-23T10:00:00Z","created_by":{"type":"user","id":"usr_0123456789abcdef"},"content_ref":{"hash":"sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881","bytes":2100,"mime":"text/markdown"},"diff_from":1,"note":"added delivery lead times"}],"promotion":{"project_id":"prj_0001aaaa","version":2,"at":"2026-08-23T10:00:05Z"},"sensitivity":"personal","created_at":"2026-08-22T10:00:00Z","updated_at":"2026-08-23T10:00:05Z"} +{ + "schema": "hux.artifact.v1", + "id": "art_0001aaaa", + "owner": "usr_0123456789abcdef", + "conversation_id": "conv_0001abcd", + "project_id": "prj_0001aaaa", + "type": "markdown", + "title": "Supplier comparison", + "current_version": 2, + "versions": [ + { + "version": 1, + "created_at": "2026-08-22T10:00:00Z", + "created_by": { + "type": "assistant", + "id": "hermes" + }, + "message_id": "msg-20", + "content_ref": { + "hash": "sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881", + "bytes": 2048, + "mime": "text/markdown" + } + }, + { + "version": 2, + "created_at": "2026-08-23T10:00:00Z", + "created_by": { + "type": "user", + "id": "usr_0123456789abcdef" + }, + "content_ref": { + "hash": "sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881", + "bytes": 2100, + "mime": "text/markdown" + }, + "diff_from": 1, + "note": "added delivery lead times" + } + ], + "promotion": { + "project_id": "prj_0001aaaa", + "version": 2, + "at": "2026-08-23T10:00:05Z" + }, + "sensitivity": "personal", + "created_at": "2026-08-22T10:00:00Z", + "updated_at": "2026-08-23T10:00:05Z", + "revision": 2, + "access": { + "mode": "owner" + } +} diff --git a/services/hermes/contracts/hux/examples/budget_state.json b/services/hermes/contracts/hux/examples/budget_state.json new file mode 100644 index 00000000..13a235f4 --- /dev/null +++ b/services/hermes/contracts/hux/examples/budget_state.json @@ -0,0 +1,20 @@ +{ + "schema": "hux.budget_state.v1", + "run_id": "run_9f", + "spent": { + "tokens": 120000, + "tool_calls": 12, + "wall_clock_seconds": 300, + "delegations": 1, + "spend_units": 10, + "subagents": 1 + }, + "limits": { + "tokens_per_run": 200000, + "tool_calls_per_run": 40, + "wall_clock_seconds": 900, + "spend_units": 50, + "subagents_per_run": 2 + }, + "exhausted": [] +} diff --git a/services/hermes/contracts/hux/examples/cancel_receipt.json b/services/hermes/contracts/hux/examples/cancel_receipt.json index 2fb50533..3eac4936 100644 --- a/services/hermes/contracts/hux/examples/cancel_receipt.json +++ b/services/hermes/contracts/hux/examples/cancel_receipt.json @@ -1 +1,24 @@ -{"schema":"hux.cancel_receipt.v1","id":"rcpt_0001aaaa","run_id":"run_9f","requested_by":{"type":"user","id":"usr_0123456789abcdef"},"requested_at":"2026-08-23T10:00:04Z","acknowledged_at":"2026-08-23T10:00:04Z","completed_at":"2026-08-23T10:00:05Z","outcome":"cancelled","side_effects":[{"description":"Partial file supplier-comparison.md left in workspace","reverted":true,"evidence":{"kind":"file","id":"supplier-comparison.md"}}]} +{ + "schema": "hux.cancel_receipt.v1", + "id": "rcpt_0001aaaa", + "run_id": "run_9f", + "requested_by": { + "type": "user", + "id": "usr_0123456789abcdef" + }, + "requested_at": "2026-08-23T10:00:04Z", + "acknowledged_at": "2026-08-23T10:00:04Z", + "completed_at": "2026-08-23T10:00:05Z", + "outcome": "cancelled", + "side_effects": [ + { + "description": "Partial file supplier-comparison.md left in workspace", + "reverted": true, + "evidence": { + "kind": "file", + "id": "supplier-comparison.md" + } + } + ], + "conversation_id": "conv_0001abcd" +} diff --git a/services/hermes/contracts/hux/examples/capabilities.json b/services/hermes/contracts/hux/examples/capabilities.json new file mode 100644 index 00000000..fe423c81 --- /dev/null +++ b/services/hermes/contracts/hux/examples/capabilities.json @@ -0,0 +1,33 @@ +{ + "schema": "hux.capabilities.v1", + "contract_version": "1.0.0", + "identity": { + "tenant_slot": "slot-3", + "subject": "usr_0123456789abcdef", + "surface": "chat", + "trust": "router" + }, + "cards": [ + { + "card": "HUX-11", + "flag": "hux.foundation", + "enabled": true, + "routes": [ + "/hux/v1/capabilities" + ] + }, + { + "card": "HUX-01", + "flag": "hux.activity_timeline", + "enabled": false, + "routes": [ + "/hux/v1/conversations/{id}/events", + "/hux/v1/conversations/{id}/events/stream" + ] + } + ], + "server": { + "commit": "d3cbeb06d3cbeb06d3cbeb06d3cbeb06d3cbeb06", + "image_digest": "sha256:4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd" + } +} diff --git a/services/hermes/contracts/hux/examples/citation.json b/services/hermes/contracts/hux/examples/citation.json index 8da220f2..1f5037cb 100644 --- a/services/hermes/contracts/hux/examples/citation.json +++ b/services/hermes/contracts/hux/examples/citation.json @@ -1 +1,11 @@ -{"schema":"hux.citation.v1","id":"cit_0001aaaa","message_id":"msg-44","claim":"Base cabinets from Example Co take three to four weeks.","passage_ids":["psg_0001aaaa"],"support":"supports"} +{ + "schema": "hux.citation.v1", + "id": "cit_0001aaaa", + "message_id": "msg-44", + "claim": "Base cabinets from Example Co take three to four weeks.", + "passage_ids": [ + "psg_0001aaaa" + ], + "support": "supports", + "dedupe_key": "sha256:abababababababababababababababababababababababababababababababab" +} diff --git a/services/hermes/contracts/hux/examples/conversation.json b/services/hermes/contracts/hux/examples/conversation.json index 8c02212d..81497487 100644 --- a/services/hermes/contracts/hux/examples/conversation.json +++ b/services/hermes/contracts/hux/examples/conversation.json @@ -1 +1,24 @@ -{"schema":"hux.conversation.v1","id":"conv_0001abcd","owner":"usr_0123456789abcdef","project_id":"prj_0001aaaa","title":"Compare cabinet suppliers","tags":["suppliers"],"pinned":false,"archived":false,"mode":"research","branch":{"parent_conversation_id":"conv_0000root","branch_point_message_id":"msg-12"},"artifact_ids":["art_0001aaaa"],"last_message_at":"2026-08-23T10:00:01Z","created_at":"2026-08-22T09:00:00Z","updated_at":"2026-08-23T10:00:01Z"} +{ + "schema": "hux.conversation.v1", + "id": "conv_0001abcd", + "owner": "usr_0123456789abcdef", + "project_id": "prj_0001aaaa", + "title": "Compare cabinet suppliers", + "tags": [ + "suppliers" + ], + "pinned": false, + "archived": false, + "mode": "research", + "branch": { + "parent_conversation_id": "conv_0000root", + "branch_point_message_id": "msg-12" + }, + "artifact_ids": [ + "art_0001aaaa" + ], + "last_message_at": "2026-08-23T10:00:01Z", + "created_at": "2026-08-22T09:00:00Z", + "updated_at": "2026-08-23T10:00:01Z", + "revision": 3 +} diff --git a/services/hermes/contracts/hux/examples/error.json b/services/hermes/contracts/hux/examples/error.json new file mode 100644 index 00000000..d3725d9e --- /dev/null +++ b/services/hermes/contracts/hux/examples/error.json @@ -0,0 +1,9 @@ +{ + "schema": "hux.error.v1", + "status": 409, + "code": "conflict", + "message": "revision 2 does not match current revision 3", + "details": [ + "If-Match: 2" + ] +} diff --git a/services/hermes/contracts/hux/examples/event.json b/services/hermes/contracts/hux/examples/event.json index f63fd30c..22c46d5d 100644 --- a/services/hermes/contracts/hux/examples/event.json +++ b/services/hermes/contracts/hux/examples/event.json @@ -1 +1,52 @@ -{"schema":"hux.event.v1","id":"evt_0001aaaa","seq":17,"ts":"2026-08-23T10:00:01Z","conversation_id":"conv_0001abcd","run_id":"run_9f","kind":"decision.route","summary":"Routed to a high-effort Claude target for a multi-step refactor","detail":{"candidates":3},"evidence":[{"kind":"run","id":"run_9f"}],"provenance":{"surface":"chat","actor":{"type":"assistant","id":"hermes"},"recorded_at":"2026-08-23T10:00:00Z","session_id":"sess-a1","conversation_id":"conv_0001abcd","run_id":"run_9f","route":{"requested":"thoughtful","resolved_target":"claude_sonnet_high","provider":"claude","effort":"high"},"build":{"commit":"d3cbeb06d3cbeb06d3cbeb06d3cbeb06d3cbeb06","image_digest":"sha256:4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd"}},"sensitivity":"personal","redaction":{"level":"none"}} +{ + "schema": "hux.event.v1", + "id": "evt_0001aaaa", + "seq": 17, + "ts": "2026-08-23T10:00:01Z", + "conversation_id": "conv_0001abcd", + "run_id": "run_9f", + "kind": "decision.route", + "summary": "Routed to a high-effort Claude target for a multi-step refactor", + "detail": { + "candidates": 3 + }, + "evidence": [ + { + "kind": "run", + "id": "run_9f" + } + ], + "provenance": { + "surface": "chat", + "actor": { + "type": "assistant", + "id": "hermes" + }, + "recorded_at": "2026-08-23T10:00:00Z", + "session_id": "sess-a1", + "conversation_id": "conv_0001abcd", + "run_id": "run_9f", + "route": { + "requested": "thoughtful", + "resolved_target": "claude_sonnet_high", + "provider": "claude", + "effort": "high" + }, + "build": { + "commit": "d3cbeb06d3cbeb06d3cbeb06d3cbeb06d3cbeb06", + "image_digest": "sha256:4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd" + } + }, + "sensitivity": "personal", + "redaction": { + "level": "none" + }, + "turn": 5, + "identity": { + "tenant_slot": "slot-3", + "subject": "usr_0123456789abcdef", + "surface": "chat", + "trust": "router" + }, + "idempotency_key": "run_9f:decision:17" +} diff --git a/services/hermes/contracts/hux/examples/manifest.json b/services/hermes/contracts/hux/examples/manifest.json new file mode 100644 index 00000000..5564f91f --- /dev/null +++ b/services/hermes/contracts/hux/examples/manifest.json @@ -0,0 +1,8 @@ +{ + "schema": "hux.manifest.v1", + "contract_version": "1.0.0", + "data_layout_version": 1, + "min_reader_contract_version": "1.0.0", + "created_at": "2026-08-24T00:00:00Z", + "updated_at": "2026-08-24T00:00:00Z" +} diff --git a/services/hermes/contracts/hux/examples/memory.json b/services/hermes/contracts/hux/examples/memory.json index 6ed79d61..21dbf02a 100644 --- a/services/hermes/contracts/hux/examples/memory.json +++ b/services/hermes/contracts/hux/examples/memory.json @@ -1 +1,65 @@ -{"schema":"hux.memory.v1","id":"mem_0001aaaa","owner":"usr_0123456789abcdef","scope":{"level":"project","scope_id":"prj_0001aaaa"},"kind":"preference","content":"Prefers terse answers with code first.","status":"proposed","approval_mode":"ask","sensitivity":"personal","topic":"general","ttl":{"policy":"decay","decay_days":180},"source":{"kind":"message","id":"msg-42"},"provenance":{"surface":"chat","actor":{"type":"assistant","id":"hermes"},"recorded_at":"2026-08-23T10:00:00Z","session_id":"sess-a1","conversation_id":"conv_0001abcd","run_id":"run_9f","route":{"requested":"thoughtful","resolved_target":"claude_sonnet_high","provider":"claude","effort":"high"},"build":{"commit":"d3cbeb06d3cbeb06d3cbeb06d3cbeb06d3cbeb06","image_digest":"sha256:4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd"}},"created_at":"2026-08-23T10:00:00Z","updated_at":"2026-08-23T10:00:00Z","audit":[{"at":"2026-08-23T10:00:00Z","action":"proposed","actor":{"type":"assistant","id":"hermes"}}]} +{ + "schema": "hux.memory.v1", + "id": "mem_0001aaaa", + "owner": "usr_0123456789abcdef", + "scope": { + "level": "project", + "scope_id": "prj_0001aaaa" + }, + "kind": "preference", + "content": "Prefers terse answers with code first.", + "status": "proposed", + "approval_mode": "ask", + "sensitivity": "personal", + "topic": "general", + "ttl": { + "policy": "decay", + "decay_days": 180 + }, + "source": { + "kind": "message", + "id": "msg-42" + }, + "provenance": { + "surface": "chat", + "actor": { + "type": "assistant", + "id": "hermes" + }, + "recorded_at": "2026-08-23T10:00:00Z", + "session_id": "sess-a1", + "conversation_id": "conv_0001abcd", + "run_id": "run_9f", + "route": { + "requested": "thoughtful", + "resolved_target": "claude_sonnet_high", + "provider": "claude", + "effort": "high" + }, + "build": { + "commit": "d3cbeb06d3cbeb06d3cbeb06d3cbeb06d3cbeb06", + "image_digest": "sha256:4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd" + } + }, + "created_at": "2026-08-23T10:00:00Z", + "updated_at": "2026-08-23T10:00:00Z", + "audit": [ + { + "at": "2026-08-23T10:00:00Z", + "action": "proposed", + "actor": { + "type": "assistant", + "id": "hermes" + } + } + ], + "reason": "You asked for code-first answers twice in this project.", + "retrievable": false, + "revision": 1, + "identity": { + "tenant_slot": "slot-3", + "subject": "usr_0123456789abcdef", + "surface": "chat", + "trust": "router" + } +} diff --git a/services/hermes/contracts/hux/examples/notebook.json b/services/hermes/contracts/hux/examples/notebook.json index cc2795c2..4b85a42a 100644 --- a/services/hermes/contracts/hux/examples/notebook.json +++ b/services/hermes/contracts/hux/examples/notebook.json @@ -1 +1,31 @@ -{"schema":"hux.research_notebook.v1","id":"nb_0001aaaa","conversation_id":"conv_0001abcd","question":"Which supplier delivers fastest under budget?","status":"open","source_ids":["src_0001aaaa"],"passage_ids":["psg_0001aaaa"],"citation_ids":["cit_0001aaaa"],"assumptions":["Budget cap is unchanged at 12k"],"unresolved_questions":["Does the lead time include installation?"],"updated_at":"2026-08-23T10:00:01Z"} +{ + "schema": "hux.research_notebook.v1", + "id": "nb_0001aaaa", + "conversation_id": "conv_0001abcd", + "question": "Which supplier delivers fastest under budget?", + "status": "open", + "source_ids": [ + "src_0001aaaa" + ], + "passage_ids": [ + "psg_0001aaaa" + ], + "citation_ids": [ + "cit_0001aaaa" + ], + "assumptions": [ + "Budget cap is unchanged at 12k" + ], + "unresolved_questions": [ + "Does the lead time include installation?" + ], + "updated_at": "2026-08-23T10:00:01Z", + "notes": [ + { + "at": "2026-08-23T10:00:00Z", + "text": "Lead times exclude installation per supplier FAQ.", + "source_id": "src_0001aaaa" + } + ], + "revision": 4 +} diff --git a/services/hermes/contracts/hux/examples/passage.json b/services/hermes/contracts/hux/examples/passage.json index 4d738beb..8d0be9fa 100644 --- a/services/hermes/contracts/hux/examples/passage.json +++ b/services/hermes/contracts/hux/examples/passage.json @@ -1 +1,13 @@ -{"schema":"hux.passage.v1","id":"psg_0001aaaa","source_id":"src_0001aaaa","text":"Standard base cabinets ship in 3-4 weeks.","hash":"sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881","locator":{"selector":"#lead-times","char_start":120,"char_end":160}} +{ + "schema": "hux.passage.v1", + "id": "psg_0001aaaa", + "source_id": "src_0001aaaa", + "text": "Standard base cabinets ship in 3-4 weeks.", + "hash": "sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881", + "locator": { + "selector": "#lead-times", + "char_start": 120, + "char_end": 160 + }, + "dedupe_key": "sha256:abababababababababababababababababababababababababababababababab" +} diff --git a/services/hermes/contracts/hux/examples/policy.json b/services/hermes/contracts/hux/examples/policy.json index 7a5bda96..1200d2cd 100644 --- a/services/hermes/contracts/hux/examples/policy.json +++ b/services/hermes/contracts/hux/examples/policy.json @@ -1 +1,56 @@ -{"schema":"hux.policy.v1","id":"pol_0001aaaa","owner":"usr_0123456789abcdef","scope":{"level":"conversation","scope_id":"conv_0001abcd"},"autonomy":"safe","grants":[{"capability":"network","decision":"ask","expires_at":"2026-08-24T10:00:00Z","granted_by":{"type":"user","id":"usr_0123456789abcdef"}}],"budgets":{"tokens_per_run":200000,"tool_calls_per_run":40,"wall_clock_seconds":900},"provenance":{"surface":"chat","actor":{"type":"assistant","id":"hermes"},"recorded_at":"2026-08-23T10:00:00Z","session_id":"sess-a1","conversation_id":"conv_0001abcd","run_id":"run_9f","route":{"requested":"thoughtful","resolved_target":"claude_sonnet_high","provider":"claude","effort":"high"},"build":{"commit":"d3cbeb06d3cbeb06d3cbeb06d3cbeb06d3cbeb06","image_digest":"sha256:4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd"}},"updated_at":"2026-08-23T10:00:00Z"} +{ + "schema": "hux.policy.v1", + "id": "pol_0001aaaa", + "owner": "usr_0123456789abcdef", + "scope": { + "level": "conversation", + "scope_id": "conv_0001abcd" + }, + "autonomy": "safe", + "grants": [ + { + "capability": "network", + "decision": "ask", + "expires_at": "2026-08-24T10:00:00Z", + "granted_by": { + "type": "user", + "id": "usr_0123456789abcdef" + } + } + ], + "budgets": { + "tokens_per_run": 200000, + "tool_calls_per_run": 40, + "wall_clock_seconds": 900, + "spend_units": 50, + "subagents_per_run": 2, + "scope": { + "conversations": [ + "conv_0001abcd" + ] + } + }, + "provenance": { + "surface": "chat", + "actor": { + "type": "assistant", + "id": "hermes" + }, + "recorded_at": "2026-08-23T10:00:00Z", + "session_id": "sess-a1", + "conversation_id": "conv_0001abcd", + "run_id": "run_9f", + "route": { + "requested": "thoughtful", + "resolved_target": "claude_sonnet_high", + "provider": "claude", + "effort": "high" + }, + "build": { + "commit": "d3cbeb06d3cbeb06d3cbeb06d3cbeb06d3cbeb06", + "image_digest": "sha256:4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd" + } + }, + "updated_at": "2026-08-23T10:00:00Z", + "revision": 1 +} diff --git a/services/hermes/contracts/hux/examples/project.json b/services/hermes/contracts/hux/examples/project.json index f22d2ef7..9387a87c 100644 --- a/services/hermes/contracts/hux/examples/project.json +++ b/services/hermes/contracts/hux/examples/project.json @@ -1 +1,17 @@ -{"schema":"hux.project.v1","id":"prj_0001aaaa","owner":"usr_0123456789abcdef","name":"Kitchen renovation","description":"Quotes, plans and supplier research.","tags":["home","2026"],"pinned":true,"archived":false,"default_mode":"research","created_at":"2026-08-20T09:00:00Z","updated_at":"2026-08-23T10:00:00Z"} +{ + "schema": "hux.project.v1", + "id": "prj_0001aaaa", + "owner": "usr_0123456789abcdef", + "name": "Kitchen renovation", + "description": "Quotes, plans and supplier research.", + "tags": [ + "home", + "2026" + ], + "pinned": true, + "archived": false, + "default_mode": "research", + "created_at": "2026-08-20T09:00:00Z", + "updated_at": "2026-08-23T10:00:00Z", + "revision": 3 +} diff --git a/services/hermes/contracts/hux/examples/source.json b/services/hermes/contracts/hux/examples/source.json index 8bd0ecdb..f784cc27 100644 --- a/services/hermes/contracts/hux/examples/source.json +++ b/services/hermes/contracts/hux/examples/source.json @@ -1 +1,33 @@ -{"schema":"hux.source.v1","id":"src_0001aaaa","kind":"web","uri":"https://example.com/cabinets","title":"Cabinet pricing 2026","publisher":"Example Co","retrieved_at":"2026-08-23T09:59:00Z","classification":"primary","content_hash":"sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881","provenance":{"surface":"chat","actor":{"type":"assistant","id":"hermes"},"recorded_at":"2026-08-23T10:00:00Z","session_id":"sess-a1","conversation_id":"conv_0001abcd","run_id":"run_9f","route":{"requested":"thoughtful","resolved_target":"claude_sonnet_high","provider":"claude","effort":"high"},"build":{"commit":"d3cbeb06d3cbeb06d3cbeb06d3cbeb06d3cbeb06","image_digest":"sha256:4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd"}}} +{ + "schema": "hux.source.v1", + "id": "src_0001aaaa", + "kind": "web", + "uri": "https://example.com/cabinets", + "title": "Cabinet pricing 2026", + "publisher": "Example Co", + "retrieved_at": "2026-08-23T09:59:00Z", + "classification": "primary", + "content_hash": "sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881", + "provenance": { + "surface": "chat", + "actor": { + "type": "assistant", + "id": "hermes" + }, + "recorded_at": "2026-08-23T10:00:00Z", + "session_id": "sess-a1", + "conversation_id": "conv_0001abcd", + "run_id": "run_9f", + "route": { + "requested": "thoughtful", + "resolved_target": "claude_sonnet_high", + "provider": "claude", + "effort": "high" + }, + "build": { + "commit": "d3cbeb06d3cbeb06d3cbeb06d3cbeb06d3cbeb06", + "image_digest": "sha256:4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd4a385fbd" + } + }, + "dedupe_key": "sha256:abababababababababababababababababababababababababababababababab" +} diff --git a/services/hermes/contracts/hux/flags.json b/services/hermes/contracts/hux/flags.json index 85b8b654..61dbdae5 100644 --- a/services/hermes/contracts/hux/flags.json +++ b/services/hermes/contracts/hux/flags.json @@ -2,17 +2,202 @@ "schema": "hux.flags.v1", "env_var": "HUX_FLAGS", "cards": [ - {"card": "HUX-01", "flag": "hux.activity_timeline", "title": "Activity timeline", "wave": "A", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["event.schema.json"], "depends_on": ["HUX-11"], "default": false, "rollback": "disable flag; events keep accumulating on the tenant PVC and are not user-visible"}, - {"card": "HUX-02", "flag": "hux.memory_control", "title": "Memory control", "wave": "A", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["memory.schema.json"], "depends_on": ["HUX-11", "HUX-10"], "default": false, "rollback": "disable flag; ledger stays read-only, upstream memory tool unchanged"}, - {"card": "HUX-03", "flag": "hux.projects", "title": "Projects and chats", "wave": "B", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["project.schema.json"], "depends_on": ["HUX-11"], "default": false, "rollback": "disable flag; upstream project list remains authoritative"}, - {"card": "HUX-04", "flag": "hux.artifacts", "title": "Artifact workspace", "wave": "B", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["artifact.schema.json"], "depends_on": ["HUX-11", "HUX-03"], "default": false, "rollback": "disable flag; artifacts remain downloadable files in the tenant workspace"}, - {"card": "HUX-05", "flag": "hux.autonomy", "title": "Autonomy controls", "wave": "A", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["permission.schema.json"], "depends_on": ["HUX-11", "HUX-01"], "default": false, "rollback": "disable flag; gateway approval mode reverts to the tenant default"}, - {"card": "HUX-06", "flag": "hux.friendly_modes", "title": "Friendly modes", "wave": "B", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["mode.schema.json"], "depends_on": ["HUX-11"], "default": false, "rollback": "disable flag; routing-priority chips remain"}, - {"card": "HUX-07", "flag": "hux.multimodal", "title": "Multimodal", "wave": "C", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["artifact.schema.json", "event.schema.json"], "depends_on": ["HUX-04", "HUX-05"], "default": false, "rollback": "disable flag; upstream upload path remains"}, - {"card": "HUX-08", "flag": "hux.research", "title": "Research and citations", "wave": "B", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["citation.schema.json"], "depends_on": ["HUX-11", "HUX-06"], "default": false, "rollback": "disable flag; prose citations remain"}, - {"card": "HUX-09", "flag": "hux.onboarding", "title": "Onboarding", "wave": "C", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["suggestion.schema.json"], "depends_on": ["HUX-03", "HUX-06"], "default": false, "rollback": "disable flag; no suggestions shown"}, - {"card": "HUX-10", "flag": "hux.privacy", "title": "Privacy behaviour", "wave": "A", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["privacy.schema.json"], "depends_on": ["HUX-11"], "default": false, "rollback": "disable flag; tenant isolation remains the privacy model"}, - {"card": "HUX-11", "flag": "hux.foundation", "title": "Shared foundation", "wave": "A", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["common.schema.json"], "depends_on": [], "default": false, "rollback": "disable flag; /hux/v1 returns 404 from the router"}, - {"card": "HUX-12", "flag": "hux.release_followthrough", "title": "Deployment follow-through", "wave": "A", "backend_owner": "claude", "frontend_owner": "codex", "contracts": ["release.schema.json"], "depends_on": [], "default": false, "rollback": "disable flag; release lane evidence archive remains the source of truth"} - ] + { + "card": "HUX-01", + "flag": "hux.activity_timeline", + "title": "Activity timeline", + "wave": "A", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "event.schema.json" + ], + "depends_on": [ + "HUX-11" + ], + "default": false, + "rollback": "disable flag; events keep accumulating on the tenant PVC and are not user-visible" + }, + { + "card": "HUX-02", + "flag": "hux.memory_control", + "title": "Memory control", + "wave": "A", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "memory.schema.json" + ], + "depends_on": [ + "HUX-11", + "HUX-10" + ], + "default": false, + "rollback": "disable flag; ledger stays read-only, upstream memory tool unchanged" + }, + { + "card": "HUX-03", + "flag": "hux.projects", + "title": "Projects and chats", + "wave": "B", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "project.schema.json" + ], + "depends_on": [ + "HUX-11" + ], + "default": false, + "rollback": "disable flag; upstream project list remains authoritative" + }, + { + "card": "HUX-04", + "flag": "hux.artifacts", + "title": "Artifact workspace", + "wave": "B", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "artifact.schema.json" + ], + "depends_on": [ + "HUX-11", + "HUX-03" + ], + "default": false, + "rollback": "disable flag; artifacts remain downloadable files in the tenant workspace" + }, + { + "card": "HUX-05", + "flag": "hux.autonomy", + "title": "Autonomy controls", + "wave": "A", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "permission.schema.json" + ], + "depends_on": [ + "HUX-11", + "HUX-01" + ], + "default": false, + "rollback": "disable flag; gateway approval mode reverts to the tenant default" + }, + { + "card": "HUX-06", + "flag": "hux.friendly_modes", + "title": "Friendly modes", + "wave": "B", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "mode.schema.json" + ], + "depends_on": [ + "HUX-11" + ], + "default": false, + "rollback": "disable flag; routing-priority chips remain" + }, + { + "card": "HUX-07", + "flag": "hux.multimodal", + "title": "Multimodal", + "wave": "C", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "artifact.schema.json", + "event.schema.json" + ], + "depends_on": [ + "HUX-04", + "HUX-05" + ], + "default": false, + "rollback": "disable flag; upstream upload path remains" + }, + { + "card": "HUX-08", + "flag": "hux.research", + "title": "Research and citations", + "wave": "B", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "citation.schema.json" + ], + "depends_on": [ + "HUX-11", + "HUX-06" + ], + "default": false, + "rollback": "disable flag; prose citations remain" + }, + { + "card": "HUX-09", + "flag": "hux.onboarding", + "title": "Onboarding", + "wave": "C", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "suggestion.schema.json" + ], + "depends_on": [ + "HUX-03", + "HUX-06" + ], + "default": false, + "rollback": "disable flag; no suggestions shown" + }, + { + "card": "HUX-10", + "flag": "hux.privacy", + "title": "Privacy behaviour", + "wave": "A", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "privacy.schema.json" + ], + "depends_on": [ + "HUX-11" + ], + "default": false, + "rollback": "disable flag; tenant isolation remains the privacy model" + }, + { + "card": "HUX-11", + "flag": "hux.foundation", + "title": "Shared foundation", + "wave": "A", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "common.schema.json", + "identity.schema.json" + ], + "depends_on": [], + "default": false, + "rollback": "disable flag; /hux/v1 returns 404 from the router" + }, + { + "card": "HUX-12", + "flag": "hux.release_followthrough", + "title": "Deployment follow-through", + "wave": "A", + "backend_owner": "claude", + "frontend_owner": "codex", + "contracts": [ + "release.schema.json" + ], + "depends_on": [], + "default": false, + "rollback": "disable flag; release lane evidence archive remains the source of truth" + } + ], + "contract_version": "1.0.0" } diff --git a/services/hermes/contracts/hux/identity.schema.json b/services/hermes/contracts/hux/identity.schema.json new file mode 100644 index 00000000..6dc4a4ac --- /dev/null +++ b/services/hermes/contracts/hux/identity.schema.json @@ -0,0 +1,157 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hermes.bstein.dev/contracts/hux/v1/identity.schema.json", + "title": "HUX identity, capability negotiation and data manifest", + "description": "HUX-11 foundation records. capabilities is the first call every client makes: it learns the contract version, which cards are on for this tenant (a card is on only when its whole dependency chain is on), and the server build. manifest is stored on the tenant PVC and tells a rolled-back reader whether it can read the data.", + "$defs": { + "capabilities": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "contract_version", + "identity", + "cards", + "server" + ], + "properties": { + "schema": { + "const": "hux.capabilities.v1" + }, + "contract_version": { + "$ref": "common.schema.json#/$defs/contract_version" + }, + "identity": { + "$ref": "common.schema.json#/$defs/identity" + }, + "cards": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "card", + "flag", + "enabled", + "routes" + ], + "properties": { + "card": { + "type": "string", + "pattern": "^HUX-[0-9]{2}$" + }, + "flag": { + "type": "string", + "pattern": "^hux\\.[a-z_]+$" + }, + "enabled": { + "type": "boolean" + }, + "routes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^/hux/v1/" + } + } + } + } + }, + "server": { + "$ref": "common.schema.json#/$defs/build" + } + } + }, + "manifest": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "contract_version", + "data_layout_version", + "min_reader_contract_version", + "created_at", + "updated_at" + ], + "properties": { + "schema": { + "const": "hux.manifest.v1" + }, + "contract_version": { + "$ref": "common.schema.json#/$defs/contract_version" + }, + "data_layout_version": { + "type": "integer", + "minimum": 1 + }, + "min_reader_contract_version": { + "$ref": "common.schema.json#/$defs/contract_version" + }, + "created_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "updated_at": { + "$ref": "common.schema.json#/$defs/timestamp" + } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "status", + "code", + "message" + ], + "properties": { + "schema": { + "const": "hux.error.v1" + }, + "status": { + "type": "integer", + "minimum": 400, + "maximum": 599 + }, + "code": { + "type": "string", + "enum": [ + "unauthorized", + "forbidden", + "not_found", + "conflict", + "flag_off", + "invalid", + "too_large", + "rate_limited", + "approval_required", + "budget_exhausted" + ] + }, + "message": { + "type": "string", + "maxLength": 280 + }, + "details": { + "type": "array", + "maxItems": 32, + "items": { + "type": "string", + "maxLength": 280 + } + } + } + } + }, + "oneOf": [ + { + "$ref": "#/$defs/capabilities" + }, + { + "$ref": "#/$defs/manifest" + }, + { + "$ref": "#/$defs/error" + } + ] +} diff --git a/services/hermes/contracts/hux/memory.schema.json b/services/hermes/contracts/hux/memory.schema.json index dfc39c48..83689fef 100644 --- a/services/hermes/contracts/hux/memory.schema.json +++ b/services/hermes/contracts/hux/memory.schema.json @@ -5,54 +5,201 @@ "description": "A single remembered item (HUX-02). Memory is a ledger: entries are appended, approved, expired or forgotten, never edited in place. Forgotten entries keep their audit trail but drop content.", "type": "object", "additionalProperties": false, - "required": ["schema", "id", "owner", "scope", "kind", "content", "status", "approval_mode", "sensitivity", "ttl", "source", "provenance", "created_at", "updated_at", "audit"], + "required": [ + "schema", + "id", + "owner", + "scope", + "kind", + "content", + "status", + "approval_mode", + "sensitivity", + "ttl", + "source", + "provenance", + "created_at", + "updated_at", + "audit", + "reason", + "retrievable", + "revision" + ], "properties": { - "schema": {"const": "hux.memory.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "owner": {"$ref": "common.schema.json#/$defs/user_ref"}, + "schema": { + "const": "hux.memory.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "owner": { + "$ref": "common.schema.json#/$defs/user_ref" + }, "scope": { "type": "object", "additionalProperties": false, - "required": ["level"], + "required": [ + "level" + ], "properties": { - "level": {"type": "string", "enum": ["global", "project", "conversation"]}, - "scope_id": {"$ref": "common.schema.json#/$defs/id"} + "level": { + "type": "string", + "enum": [ + "global", + "project", + "conversation" + ] + }, + "scope_id": { + "$ref": "common.schema.json#/$defs/id" + } } }, - "kind": {"type": "string", "enum": ["preference", "fact", "instruction", "context"]}, - "content": {"type": "string", "maxLength": 2000}, - "status": {"type": "string", "enum": ["proposed", "active", "rejected", "expired", "forgotten"]}, - "approval_mode": {"type": "string", "enum": ["automatic", "ask"]}, - "sensitivity": {"$ref": "common.schema.json#/$defs/sensitivity"}, - "topic": {"type": "string", "enum": ["general", "health", "finance", "legal", "relationships", "credentials", "minors", "location", "biometric"]}, + "kind": { + "type": "string", + "enum": [ + "preference", + "fact", + "instruction", + "context" + ] + }, + "content": { + "type": "string", + "maxLength": 2000 + }, + "status": { + "type": "string", + "enum": [ + "proposed", + "active", + "rejected", + "expired", + "forgotten", + "no_store" + ] + }, + "approval_mode": { + "type": "string", + "enum": [ + "automatic", + "ask", + "no_store" + ] + }, + "sensitivity": { + "$ref": "common.schema.json#/$defs/sensitivity" + }, + "topic": { + "type": "string", + "enum": [ + "general", + "health", + "finance", + "legal", + "relationships", + "credentials", + "minors", + "location", + "biometric" + ] + }, "ttl": { "type": "object", "additionalProperties": false, - "required": ["policy"], + "required": [ + "policy" + ], "properties": { - "policy": {"type": "string", "enum": ["never", "expires_at", "decay"]}, - "expires_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "decay_days": {"type": "integer", "minimum": 1, "maximum": 3650} + "policy": { + "type": "string", + "enum": [ + "never", + "expires_at", + "decay" + ] + }, + "expires_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "decay_days": { + "type": "integer", + "minimum": 1, + "maximum": 3650 + } } }, - "source": {"$ref": "common.schema.json#/$defs/evidence_ref"}, - "provenance": {"$ref": "common.schema.json#/$defs/provenance"}, - "created_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "updated_at": {"$ref": "common.schema.json#/$defs/timestamp"}, + "source": { + "$ref": "common.schema.json#/$defs/evidence_ref" + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "created_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "updated_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, "audit": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, - "required": ["at", "action", "actor"], + "required": [ + "at", + "action", + "actor" + ], "properties": { - "at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "action": {"type": "string", "enum": ["proposed", "approved", "rejected", "expired", "forgotten", "exported"]}, - "actor": {"$ref": "common.schema.json#/$defs/actor"}, - "note": {"type": "string", "maxLength": 200} + "at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "action": { + "type": "string", + "enum": [ + "proposed", + "approved", + "rejected", + "expired", + "forgotten", + "exported", + "no_store", + "superseded", + "retrieval_removed", + "edited" + ] + }, + "actor": { + "$ref": "common.schema.json#/$defs/actor" + }, + "note": { + "type": "string", + "maxLength": 200 + } } } + }, + "reason": { + "type": "string", + "description": "Why the assistant proposed remembering this, in the user's terms.", + "minLength": 1, + "maxLength": 280 + }, + "supersedes": { + "$ref": "common.schema.json#/$defs/id", + "description": "An edit is a new entry that supersedes the old one; the old entry moves to forgotten." + }, + "retrievable": { + "type": "boolean", + "description": "False removes the entry from retrieval without deleting audit. Forgotten, rejected, expired and no_store entries are never retrievable." + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + }, + "identity": { + "$ref": "common.schema.json#/$defs/identity" } } } diff --git a/services/hermes/contracts/hux/permission.schema.json b/services/hermes/contracts/hux/permission.schema.json index 2d87eab4..b383b2b9 100644 --- a/services/hermes/contracts/hux/permission.schema.json +++ b/services/hermes/contracts/hux/permission.schema.json @@ -6,39 +6,105 @@ "$defs": { "capability": { "type": "string", - "enum": ["read_files", "write_files", "shell", "network", "web_search", "send_message", "memory_write", "artifact_write", "spend_tokens", "delegate", "deploy"] + "enum": [ + "read_files", + "write_files", + "shell", + "network", + "web_search", + "send_message", + "memory_write", + "artifact_write", + "spend_tokens", + "delegate", + "deploy", + "external_side_effect" + ] + }, + "decision": { + "type": "string", + "enum": [ + "allow", + "ask", + "deny" + ] }, - "decision": {"type": "string", "enum": ["allow", "ask", "deny"]}, "policy": { "type": "object", "additionalProperties": false, - "required": ["schema", "id", "owner", "scope", "autonomy", "grants", "budgets", "provenance", "updated_at"], + "required": [ + "schema", + "id", + "owner", + "scope", + "autonomy", + "grants", + "budgets", + "provenance", + "updated_at", + "revision" + ], "properties": { - "schema": {"const": "hux.policy.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "owner": {"$ref": "common.schema.json#/$defs/user_ref"}, + "schema": { + "const": "hux.policy.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "owner": { + "$ref": "common.schema.json#/$defs/user_ref" + }, "scope": { "type": "object", "additionalProperties": false, - "required": ["level"], + "required": [ + "level" + ], "properties": { - "level": {"type": "string", "enum": ["global", "project", "conversation"]}, - "scope_id": {"$ref": "common.schema.json#/$defs/id"} + "level": { + "type": "string", + "enum": [ + "global", + "project", + "conversation" + ] + }, + "scope_id": { + "$ref": "common.schema.json#/$defs/id" + } } }, - "autonomy": {"type": "string", "enum": ["ask_first", "safe", "autonomous"]}, + "autonomy": { + "type": "string", + "enum": [ + "ask_first", + "safe", + "autonomous" + ] + }, "grants": { "type": "array", "maxItems": 64, "items": { "type": "object", "additionalProperties": false, - "required": ["capability", "decision"], + "required": [ + "capability", + "decision" + ], "properties": { - "capability": {"$ref": "#/$defs/capability"}, - "decision": {"$ref": "#/$defs/decision"}, - "expires_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "granted_by": {"$ref": "common.schema.json#/$defs/actor"} + "capability": { + "$ref": "#/$defs/capability" + }, + "decision": { + "$ref": "#/$defs/decision" + }, + "expires_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "granted_by": { + "$ref": "common.schema.json#/$defs/actor" + } } } }, @@ -46,85 +112,337 @@ "type": "object", "additionalProperties": false, "properties": { - "tokens_per_run": {"type": "integer", "minimum": 0}, - "tool_calls_per_run": {"type": "integer", "minimum": 0}, - "wall_clock_seconds": {"type": "integer", "minimum": 0}, - "delegations_per_run": {"type": "integer", "minimum": 0} + "tokens_per_run": { + "type": "integer", + "minimum": 0 + }, + "tool_calls_per_run": { + "type": "integer", + "minimum": 0 + }, + "wall_clock_seconds": { + "type": "integer", + "minimum": 0 + }, + "delegations_per_run": { + "type": "integer", + "minimum": 0 + }, + "spend_units": { + "type": "integer", + "minimum": 0 + }, + "subagents_per_run": { + "type": "integer", + "minimum": 0 + }, + "scope": { + "type": "object", + "additionalProperties": false, + "properties": { + "conversations": { + "type": "array", + "maxItems": 64, + "items": { + "$ref": "common.schema.json#/$defs/id" + } + }, + "paths": { + "type": "array", + "maxItems": 64, + "items": { + "type": "string", + "maxLength": 300 + } + } + } + } } }, - "provenance": {"$ref": "common.schema.json#/$defs/provenance"}, - "updated_at": {"$ref": "common.schema.json#/$defs/timestamp"} + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "updated_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + } } }, "approval": { "type": "object", "additionalProperties": false, - "required": ["schema", "id", "run_id", "conversation_id", "capability", "request", "status", "requested_at", "expires_at"], + "required": [ + "schema", + "id", + "run_id", + "conversation_id", + "capability", + "request", + "status", + "requested_at", + "expires_at" + ], "properties": { - "schema": {"const": "hux.approval.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "run_id": {"type": "string", "minLength": 1, "maxLength": 120}, - "conversation_id": {"$ref": "common.schema.json#/$defs/id"}, - "capability": {"$ref": "#/$defs/capability"}, + "schema": { + "const": "hux.approval.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "run_id": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "conversation_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "capability": { + "$ref": "#/$defs/capability" + }, "request": { "type": "object", "additionalProperties": false, - "required": ["summary", "risk"], + "required": [ + "summary", + "risk", + "external" + ], "properties": { - "summary": {"type": "string", "minLength": 1, "maxLength": 280}, - "detail": {"type": "string", "maxLength": 4000}, - "risk": {"type": "string", "enum": ["low", "medium", "high"]}, - "evidence": {"type": "array", "maxItems": 16, "items": {"$ref": "common.schema.json#/$defs/evidence_ref"}} + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 280 + }, + "detail": { + "type": "string", + "maxLength": 4000 + }, + "risk": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "evidence": { + "type": "array", + "maxItems": 16, + "items": { + "$ref": "common.schema.json#/$defs/evidence_ref" + } + }, + "external": { + "type": "boolean", + "description": "True when the action leaves the tenant (message, network call, deploy). External side effects always need an approval record regardless of autonomy level." + } } }, - "status": {"type": "string", "enum": ["pending", "approved", "denied", "expired", "cancelled"]}, + "status": { + "type": "string", + "enum": [ + "pending", + "approved", + "denied", + "expired", + "cancelled" + ] + }, "decision": { "type": "object", "additionalProperties": false, - "required": ["choice", "by", "at"], + "required": [ + "choice", + "by", + "at" + ], "properties": { - "choice": {"type": "string", "enum": ["once", "session", "always", "deny"]}, - "by": {"$ref": "common.schema.json#/$defs/actor"}, - "at": {"$ref": "common.schema.json#/$defs/timestamp"} + "choice": { + "type": "string", + "enum": [ + "once", + "session", + "always", + "deny" + ] + }, + "by": { + "$ref": "common.schema.json#/$defs/actor" + }, + "at": { + "$ref": "common.schema.json#/$defs/timestamp" + } } }, - "requested_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "expires_at": {"$ref": "common.schema.json#/$defs/timestamp"} + "requested_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "expires_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "idempotency_key": { + "$ref": "common.schema.json#/$defs/idempotency_key" + } } }, "cancellation_receipt": { "type": "object", "additionalProperties": false, - "required": ["schema", "id", "run_id", "requested_by", "requested_at", "outcome", "side_effects"], + "required": [ + "schema", + "id", + "run_id", + "requested_by", + "requested_at", + "outcome", + "side_effects" + ], "properties": { - "schema": {"const": "hux.cancel_receipt.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "run_id": {"type": "string", "minLength": 1, "maxLength": 120}, - "requested_by": {"$ref": "common.schema.json#/$defs/actor"}, - "requested_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "acknowledged_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "completed_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "outcome": {"type": "string", "enum": ["cancelled", "already_complete", "failed_to_cancel"]}, + "schema": { + "const": "hux.cancel_receipt.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "run_id": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "requested_by": { + "$ref": "common.schema.json#/$defs/actor" + }, + "requested_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "acknowledged_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "completed_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "outcome": { + "type": "string", + "enum": [ + "cancelled", + "already_complete", + "failed_to_cancel" + ] + }, "side_effects": { "type": "array", "maxItems": 64, "items": { "type": "object", "additionalProperties": false, - "required": ["description", "reverted"], + "required": [ + "description", + "reverted" + ], "properties": { - "description": {"type": "string", "minLength": 1, "maxLength": 280}, - "reverted": {"type": "boolean"}, - "evidence": {"$ref": "common.schema.json#/$defs/evidence_ref"} + "description": { + "type": "string", + "minLength": 1, + "maxLength": 280 + }, + "reverted": { + "type": "boolean" + }, + "evidence": { + "$ref": "common.schema.json#/$defs/evidence_ref" + } } } + }, + "conversation_id": { + "$ref": "common.schema.json#/$defs/id" + } + } + }, + "budget_state": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "run_id", + "spent", + "limits", + "exhausted" + ], + "properties": { + "schema": { + "const": "hux.budget_state.v1" + }, + "run_id": { + "type": "string", + "maxLength": 120 + }, + "spent": { + "type": "object", + "additionalProperties": false, + "properties": { + "tokens": { + "type": "integer", + "minimum": 0 + }, + "tool_calls": { + "type": "integer", + "minimum": 0 + }, + "wall_clock_seconds": { + "type": "integer", + "minimum": 0 + }, + "delegations": { + "type": "integer", + "minimum": 0 + }, + "spend_units": { + "type": "integer", + "minimum": 0 + }, + "subagents": { + "type": "integer", + "minimum": 0 + } + } + }, + "limits": { + "$ref": "#/$defs/policy/properties/budgets" + }, + "exhausted": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "tokens_per_run", + "tool_calls_per_run", + "wall_clock_seconds", + "delegations_per_run", + "spend_units", + "subagents_per_run" + ] + } } } } }, "oneOf": [ - {"$ref": "#/$defs/policy"}, - {"$ref": "#/$defs/approval"}, - {"$ref": "#/$defs/cancellation_receipt"} + { + "$ref": "#/$defs/policy" + }, + { + "$ref": "#/$defs/approval" + }, + { + "$ref": "#/$defs/cancellation_receipt" + }, + { + "$ref": "#/$defs/budget_state" + } ] } diff --git a/services/hermes/contracts/hux/project.schema.json b/services/hermes/contracts/hux/project.schema.json index db41955c..7e0b9a9c 100644 --- a/services/hermes/contracts/hux/project.schema.json +++ b/services/hermes/contracts/hux/project.schema.json @@ -7,63 +7,193 @@ "project": { "type": "object", "additionalProperties": false, - "required": ["schema", "id", "owner", "name", "tags", "pinned", "archived", "created_at", "updated_at"], + "required": [ + "schema", + "id", + "owner", + "name", + "tags", + "pinned", + "archived", + "created_at", + "updated_at", + "revision" + ], "properties": { - "schema": {"const": "hux.project.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "owner": {"$ref": "common.schema.json#/$defs/user_ref"}, - "name": {"type": "string", "minLength": 1, "maxLength": 120}, - "description": {"type": "string", "maxLength": 2000}, - "tags": {"$ref": "common.schema.json#/$defs/tags"}, - "pinned": {"type": "boolean"}, - "archived": {"type": "boolean"}, - "default_mode": {"type": "string", "enum": ["fast", "thoughtful", "research", "create", "private"]}, - "memory_scope_id": {"$ref": "common.schema.json#/$defs/id"}, - "created_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "updated_at": {"$ref": "common.schema.json#/$defs/timestamp"} + "schema": { + "const": "hux.project.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "owner": { + "$ref": "common.schema.json#/$defs/user_ref" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "description": { + "type": "string", + "maxLength": 2000 + }, + "tags": { + "$ref": "common.schema.json#/$defs/tags" + }, + "pinned": { + "type": "boolean" + }, + "archived": { + "type": "boolean" + }, + "default_mode": { + "type": "string", + "enum": [ + "fast", + "thoughtful", + "research", + "create", + "private" + ] + }, + "memory_scope_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "created_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "updated_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + } } }, "conversation": { "type": "object", "additionalProperties": false, - "required": ["schema", "id", "owner", "title", "tags", "pinned", "archived", "artifact_ids", "created_at", "updated_at"], + "required": [ + "schema", + "id", + "owner", + "title", + "tags", + "pinned", + "archived", + "artifact_ids", + "created_at", + "updated_at", + "revision" + ], "properties": { - "schema": {"const": "hux.conversation.v1"}, - "id": {"$ref": "common.schema.json#/$defs/id"}, - "owner": {"$ref": "common.schema.json#/$defs/user_ref"}, - "project_id": {"$ref": "common.schema.json#/$defs/id"}, - "title": {"type": "string", "minLength": 1, "maxLength": 200}, - "tags": {"$ref": "common.schema.json#/$defs/tags"}, - "pinned": {"type": "boolean"}, - "archived": {"type": "boolean"}, - "mode": {"type": "string", "enum": ["fast", "thoughtful", "research", "create", "private"]}, + "schema": { + "const": "hux.conversation.v1" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "owner": { + "$ref": "common.schema.json#/$defs/user_ref" + }, + "project_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "tags": { + "$ref": "common.schema.json#/$defs/tags" + }, + "pinned": { + "type": "boolean" + }, + "archived": { + "type": "boolean" + }, + "mode": { + "type": "string", + "enum": [ + "fast", + "thoughtful", + "research", + "create", + "private" + ] + }, "branch": { "type": "object", "additionalProperties": false, - "required": ["parent_conversation_id", "branch_point_message_id"], + "required": [ + "parent_conversation_id", + "branch_point_message_id" + ], "properties": { - "parent_conversation_id": {"$ref": "common.schema.json#/$defs/id"}, - "branch_point_message_id": {"type": "string", "minLength": 1, "maxLength": 120} + "parent_conversation_id": { + "$ref": "common.schema.json#/$defs/id" + }, + "branch_point_message_id": { + "type": "string", + "minLength": 1, + "maxLength": 120 + } } }, - "artifact_ids": {"type": "array", "maxItems": 500, "uniqueItems": true, "items": {"$ref": "common.schema.json#/$defs/id"}}, - "last_message_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "created_at": {"$ref": "common.schema.json#/$defs/timestamp"}, - "updated_at": {"$ref": "common.schema.json#/$defs/timestamp"} + "artifact_ids": { + "type": "array", + "maxItems": 500, + "uniqueItems": true, + "items": { + "$ref": "common.schema.json#/$defs/id" + } + }, + "last_message_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "created_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "updated_at": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + } } }, "search_index": { "type": "object", "description": "Fields the tenant indexes; the UI may not assume anything else is searchable.", "additionalProperties": false, - "required": ["conversation"], + "required": [ + "conversation" + ], "properties": { - "conversation": {"type": "array", "items": {"type": "string", "enum": ["title", "tags", "message_text", "artifact_titles", "project_name"]}} + "conversation": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "title", + "tags", + "message_text", + "artifact_titles", + "project_name" + ] + } + } } } }, "oneOf": [ - {"$ref": "#/$defs/project"}, - {"$ref": "#/$defs/conversation"} + { + "$ref": "#/$defs/project" + }, + { + "$ref": "#/$defs/conversation" + } ] } diff --git a/testing/tests/test_hermes_hux_contracts.py b/testing/tests/test_hermes_hux_contract_schemas.py similarity index 97% rename from testing/tests/test_hermes_hux_contracts.py rename to testing/tests/test_hermes_hux_contract_schemas.py index b989505d..cd781ac7 100644 --- a/testing/tests/test_hermes_hux_contracts.py +++ b/testing/tests/test_hermes_hux_contract_schemas.py @@ -9,7 +9,6 @@ refuse the transitions that would let merged/built/deployed be confused. from __future__ import annotations import copy -import importlib.util import json import sys import tomllib @@ -20,21 +19,13 @@ import pytest import yaml ROOT = Path(__file__).resolve().parents[2] -SCRIPTS = ROOT / "services" / "hermes" / "scripts" +FOUNDATION = ROOT / "dockerfiles" / "hermes-hux-foundation" CONTRACTS = ROOT / "services" / "hermes" / "contracts" / "hux" -def _load(name: str): - spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py") - assert spec and spec.loader - module = importlib.util.module_from_spec(spec) - sys.modules[name] = module - spec.loader.exec_module(module) - return module - - -contracts = _load("hux_contracts") -policy = _load("hux_policy") +if str(FOUNDATION) not in sys.path: + sys.path.insert(0, str(FOUNDATION)) +from hux import contracts, rules as policy # noqa: E402 SCHEMAS = contracts.load_all() EXAMPLES = {path.stem: json.loads(path.read_text()) for path in sorted((CONTRACTS / "examples").glob("*.json"))} NOW = datetime(2026, 8, 23, 12, 0, tzinfo=timezone.utc) @@ -166,6 +157,7 @@ def test_capability_matrix_shape(): for level, row in matrix.items(): assert set(row) == set(policy.CAPABILITIES) assert row["deploy"] == "ask", level + assert row["external_side_effect"] == "ask", level assert row["read_files"] == "allow" assert matrix["ask_first"]["shell"] == "ask" assert matrix["safe"]["network"] == "deny"