Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ out the pixels. Coordinates are the thing that breaks when a window moves.
| `ArtifactRefV1` | A path-free reference to an immutable process artifact |
| `CodeCapabilityManifestV1` | Exact Python, locked dependencies, typed I/O, permissions, and verifier bindings |
| `ProcessEvidenceReceiptV1` | One signed root over child receipts, human receipts, and the artifact graph |
| `ProductionAdmissionRegistryStateV1` | One signed current state for active and revoked Production admissions |
| `ProductionLifecycleAdmissionBindingV2` | The target, release, artifact, digest, authority, and validity fields from one verified admission |
| `RewardEvidenceReceiptV1` | One verified terminal effect for a training episode. Not an Execute Seal |
| `AuthenticationTaskContractV1` | A value-free login requirement bound to an existing attended task |
| `AuthoringObserveV1` | PHI-safe authoring observe tree for the hosted MCP wire |
Expand Down Expand Up @@ -104,7 +106,7 @@ print(json.dumps(ComputerState.model_json_schema(), indent=2))
```

The same schemas ship as JSON under `openadapt_types/schemas/` for TypeScript,
Rust, and anything else that isn't Python. Thirty-four files, including
Rust, and anything else that isn't Python. Thirty-eight files, including
`execute-v1-openapi.json`, the public OpenAdapt Execute contract.

## Converting from the older formats
Expand Down
23 changes: 23 additions & 0 deletions docs/CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ free-form notes, and live record values stay on the customer runner. An
accepted answer only selects a compiled branch. The next action must still pass
its live-state, identity, policy, and effect contracts.

## Production admission registry

`ProductionAdmissionRegistryStateV1` is the one current signed registry state.
Each admission reference is either `active` or `revoked`. The registry does not
add a second permit, lease, authority file, or revocation history.

A consumer first verifies the registry signature and checks its saved minimum
registry revision. It then hashes and parses the exact referenced admission
bytes. The consumer checks the target, claim, repository, release kind,
artifact set, artifact digests, and artifact authorities against the exact
policy target. It saves the newest verified registry revision. This check
rejects an older active registry after a later signed revocation.

`expires_at: null` means that the admission stays active until the signed
registry revokes it. When an expiry is present, it must follow `not_before`,
and the consumer enforces it at read time. The policy does not cap this expiry.

## OpenAdapt Execute v1

OpenAdapt Execute is the public asynchronous contract for a qualified
Expand Down Expand Up @@ -162,6 +179,12 @@ Compile returns `needs_human_admit`, never `VERIFIED`. Bind tokens are
`oab_` plus 43 unreserved characters. Lease secrets are `oals_` plus 64
hex characters. Cloud `oar_` and pairing `oap_` are refused.

Command ids are `cmd_` plus one canonical Crockford ULID. Command times use
RFC 3339 with seconds and an offset. A command can live for at most 900
seconds. `parse_authoring_command` checks the full closed envelope and refuses
it at or after `expires_at`. `client_display` stays in the closed bind result
and bind status. It is not a command-envelope field.

## Clinic job inbox and MCP tools

`ClinicInboxV1`, `ClinicOutboxV1`, and `ClinicToolResultV1` are the public
Expand Down
45 changes: 45 additions & 0 deletions openadapt_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@
BIND_TOKEN_PATTERN,
LEASE_SECRET_PATTERN,
parse_authoring_bind_token,
parse_authoring_command,
parse_authoring_lease_secret,
parse_authoring_runner_uri,
require_authoring_command_active,
)
from openadapt_types.clinic_job import (
ACTION_TO_TOOL,
Expand Down Expand Up @@ -324,6 +326,27 @@
CodeRuntimeKind,
ProcessEvidenceReceiptV1,
)
from openadapt_types.production_lifecycle import (
PRODUCTION_ADMISSION_REGISTRY_STATE_SCHEMA,
PRODUCTION_EVIDENCE_OBJECT_REFERENCE_SCHEMA,
PRODUCTION_LIFECYCLE_ADMISSION_BINDING_SCHEMA,
PRODUCTION_LIFECYCLE_TARGET_SCHEMA,
ProductionAdmissionRegistryEntryV1,
ProductionAdmissionRegistryStateV1,
ProductionAdmissionStateV1,
ProductionArtifactAuthoritiesV2,
ProductionArtifactAuthorityV2,
ProductionArtifactKindV2,
ProductionLifecycleAdmissionBindingV2,
ProductionLifecycleTargetV2,
ProductionReleaseArtifactBindingV2,
ProductionReleaseKindV2,
ProductionTargetIdV2,
QualificationReleaseReferenceV2,
project_production_lifecycle_target_v3,
production_registry_signing_payload,
validate_production_admission,
)
from openadapt_types.reward import (
DEFAULT_REWARD_SCORING,
REWARD_CERTIFICATE_SCHEMA,
Expand Down Expand Up @@ -433,8 +456,10 @@
"BIND_TOKEN_PATTERN",
"LEASE_SECRET_PATTERN",
"parse_authoring_bind_token",
"parse_authoring_command",
"parse_authoring_lease_secret",
"parse_authoring_runner_uri",
"require_authoring_command_active",
# clinic job inbox / outbox / MCP
"ACTION_TO_TOOL",
"CLINIC_INBOX_SCHEMA",
Expand Down Expand Up @@ -639,6 +664,26 @@
"CodePermissionContractV1",
"CodeRuntimeKind",
"ProcessEvidenceReceiptV1",
# Production admission registry
"PRODUCTION_ADMISSION_REGISTRY_STATE_SCHEMA",
"PRODUCTION_EVIDENCE_OBJECT_REFERENCE_SCHEMA",
"PRODUCTION_LIFECYCLE_ADMISSION_BINDING_SCHEMA",
"PRODUCTION_LIFECYCLE_TARGET_SCHEMA",
"ProductionAdmissionRegistryEntryV1",
"ProductionAdmissionRegistryStateV1",
"ProductionAdmissionStateV1",
"ProductionArtifactAuthoritiesV2",
"ProductionArtifactAuthorityV2",
"ProductionArtifactKindV2",
"ProductionLifecycleAdmissionBindingV2",
"ProductionLifecycleTargetV2",
"ProductionReleaseArtifactBindingV2",
"ProductionReleaseKindV2",
"ProductionTargetIdV2",
"QualificationReleaseReferenceV2",
"project_production_lifecycle_target_v3",
"production_registry_signing_payload",
"validate_production_admission",
# reward contracts (not an Execute Seal)
"DEFAULT_REWARD_SCORING",
"REWARD_CERTIFICATE_SCHEMA",
Expand Down
58 changes: 56 additions & 2 deletions openadapt_types/authoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import json
import re
from datetime import datetime, timedelta, timezone
from enum import Enum
from math import isfinite
from typing import Any, Literal
Expand Down Expand Up @@ -72,7 +73,7 @@
_LEASE_BASE64URL_BODY_RE = re.compile(_LEASE_BASE64URL_BODY_PATTERN)

_NODE_ID_PATTERN = r"^n_[0-9a-f]{8}$"
_COMMAND_ID_PATTERN = r"^cmd_[0-9A-HJKMNP-TV-Z]{26}$"
_COMMAND_ID_PATTERN = r"^cmd_[0-7][0-9A-HJKMNP-TV-Z]{25}$"
_WORKFLOW_ID_PATTERN = r"^wf_[A-Za-z0-9_-]{8,64}$"
_PACK_ID_PATTERN = r"^(p\.[A-Za-z0-9_-]{12}|v1\.[A-Za-z0-9_-]{38,512})$"
_PROCESS_NAME_PATTERN = r"^[A-Za-z0-9 ._-]{1,64}$"
Expand Down Expand Up @@ -304,6 +305,30 @@ def _finite_unit(value: object) -> float:
return number


def _parse_rfc3339(value: str, field_name: str) -> datetime:
"""Parse the closed authoring timestamp profile as an absolute instant."""

if re.fullmatch(_TIMESTAMP_PATTERN, value) is None:
raise ValueError(f"{field_name} must be an RFC 3339 timestamp")
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise ValueError(f"{field_name} must be an RFC 3339 timestamp") from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise ValueError(f"{field_name} must include an offset")
return parsed


def _coerce_check_time(value: str | datetime | None) -> datetime:
if value is None:
return datetime.now(timezone.utc)
if isinstance(value, str):
return _parse_rfc3339(value, "at")
if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None:
raise ValueError("at must be an offset-aware datetime or RFC 3339 timestamp")
return value


class AuthoringNormalizedBoundsV1(_StrictContract):
"""Viewport-normalized overlay coordinates. Not backend pixels."""

Expand Down Expand Up @@ -633,8 +658,12 @@ def _status_and_result(self) -> "AuthoringCommandV1":
and self.args.pack_id != self.pack_id
):
raise ValueError("bind_pack args pack_id must match the envelope")
if self.expires_at <= self.enqueued_at:
enqueued_at = _parse_rfc3339(self.enqueued_at, "enqueued_at")
expires_at = _parse_rfc3339(self.expires_at, "expires_at")
if expires_at <= enqueued_at:
raise ValueError("expires_at must be after enqueued_at")
if expires_at - enqueued_at > timedelta(seconds=AUTHORING_LEASE_S):
raise ValueError("expires_at must be no more than 900 seconds after enqueued_at")
if self.status is AuthoringCommandStatusV1.ERROR:
if not isinstance(self.result, AuthoringErrorResultV1):
raise ValueError("error status requires an error result")
Expand All @@ -657,6 +686,31 @@ def _status_and_result(self) -> "AuthoringCommandV1":
return self


def require_authoring_command_active(
command: AuthoringCommandV1,
*,
at: str | datetime | None = None,
) -> AuthoringCommandV1:
"""Refuse a mailbox command at or after its expiry instant."""

check_time = _coerce_check_time(at)
expires_at = _parse_rfc3339(command.expires_at, "expires_at")
if check_time >= expires_at:
raise ValueError("authoring command has expired")
return command


def parse_authoring_command(
value: object,
*,
at: str | datetime | None = None,
) -> AuthoringCommandV1:
"""Strictly parse one command and enforce its current-time expiry."""

command = AuthoringCommandV1.model_validate(value)
return require_authoring_command_active(command, at=at)


class AuthoringCommandLookupV1(_StrictContract):
"""Non-blocking ``get_command_result`` body. No tree unless observe is done."""

Expand Down
Loading
Loading