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
25 changes: 25 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- **Declared action scope and mutability (#92)** — `V2AgentCapabilities.actions` is now `list[V2ActionDefinition]` instead of an opaque `list[str]`. Each action declares `id`, `mutating` (does invoking it change agent-side state?), and `scope` (`workspace` / `mission` / `job`), so a consumer can authorize and group actions without pattern-matching the identifier. `id` is caller-supplied at invocation time, so deriving an authorization decision from it puts that decision in the caller's hands.

Plain strings are still accepted everywhere a list of actions is taken — in `build_v2_agent_registration(actions=...)` and when validating a registration payload — and coerce to `mutating=True, scope="job"`, the fail-closed reading that matches today's behaviour. Only the serialized form changes: `capabilities.actions` now emits objects. Consumers must accept both, because agents pinned to older Supervaizer releases keep sending bare strings. No protocol version discriminates the two shapes: `versions.a2ui_version` and `versions.a2a_version` cover the surface catalog and the A2A protocol, not the registration payload.

The builder derives the metadata from the definition each action comes from: resource actions take `V2ResourceDefinition.scope`, dataset queries are `mutating=False`, `V2JobSetupPolicy.preview_action` is `mutating=False`, and workspace binding actions are `workspace`-scoped. Resource operation ids are freeform, so operations stay `mutating=True` unless the agent declares otherwise.

Metadata precedence is explicit > derived > bare: an explicit `V2ActionDefinition` in `actions=` overrides the derived metadata — that is how a job-scoped `resource.invoice.reconcile` on a workspace-scoped resource is declared — while a bare id string declares nothing and defers to the definition it was derived from. Precedence is about metadata, not position: an id declared both ways within `actions=` keeps the explicit metadata regardless of which form comes first, and the earlier mention still fixes the order.

- **`V2DatasetDefinition.scope`** — Mirrors `V2ResourceDefinition.scope` (`workspace` / `mission` / `job`, defaulting to `workspace`) so dataset query actions carry a declared scope rather than an assumed one.

- **`V2AwaitingState.reopenable`** — Declares whether an already-answered awaiting step may be reopened and resubmitted with edited values. Defaults to `False`; previously consumers inferred this from a substring of `surface`.

### Tests

`just test`

| Status | Count |
| ---------- | ----- |
| ✅ Passed | 698 |
| 🤔 Skipped | 0 |
| 🔴 Failed | 0 |
| ⏱️ in | 67s |

## [1.5.0] - 2026-08-28

### Added
Expand Down
4 changes: 2 additions & 2 deletions docs/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "Supervaize API",
"description": "API version: v1 Controller version: 1.3.1\n\nAPI for controlling and managing Supervaize agents. \n\nMore information at [https://doc.supervaize.com](https://doc.supervaize.com)\n\n## Authentication\n\nSome endpoints require API key authentication. Protected endpoints expect the API key in the X-API-Key header.\n\n[Swagger](/docs)\n[Redoc](/redoc)\n[OpenAPI](/openapi.json)\n",
"description": "API version: v1 Controller version: 1.5.0\n\nAPI for controlling and managing Supervaize agents. \n\nMore information at [https://doc.supervaize.com](https://doc.supervaize.com)\n\n## Authentication\n\nSome endpoints require API key authentication. Protected endpoints expect the API key in the X-API-Key header.\n\n[Swagger](/docs)\n[Redoc](/redoc)\n[OpenAPI](/openapi.json)\n",
"termsOfService": "https://supervaize.com/terms/",
"contact": {
"name": "Support Team",
Expand Down Expand Up @@ -4187,7 +4187,7 @@
"type": "string",
"format": "date-time",
"title": "Timestamp",
"default": "2026-08-26T17:21:02.106370"
"default": "2026-08-29T19:33:56.088920"
},
"status_code": {
"type": "integer",
Expand Down
4 changes: 2 additions & 2 deletions docs/model_reference/model_core.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Model Reference Core

**Version:** 1.3.1
**Version:** 1.5.0

### `account.Account`

Expand Down Expand Up @@ -443,4 +443,4 @@ public_url: full url (including scheme and port) to use for outbound connections
```


*Uploaded on 2026-08-26 17:21:01*
*Uploaded on 2026-08-29 19:33:55*
29 changes: 25 additions & 4 deletions docs/model_reference/model_extra.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Model Reference extra

**Version:** 1.3.1
**Version:** 1.5.0

### `common.SvBaseModel`

Expand Down Expand Up @@ -226,6 +226,25 @@ _No additional fields beyond parent class._
| `dashboards` | `list[contracts.V2DashboardDefinition]` | — | |
| `workspace_binding` | `V2WorkspaceBindingDefinition` | `None` | |

### `contracts.V2ActionDefinition`

**Inherits from:** [`contracts.ContractModel`](#contractscontractmodel)

Declared metadata for one invokable agent action.

Consumers authorize, group and filter actions from these fields. They must
never be inferred from the identifier string: `id` is caller-supplied on
invocation, so pattern-matching it turns an authorization decision into
something the caller controls.

#### Model Fields

| Field | Type | Default | Description |
|---|---|---|---|
| `id` | `str` | **required** | Action identifier used to invoke the action. |
| `mutating` | `bool` | True | Whether invoking the action changes agent-side state. Defaults to True so an undeclared action requires write permission. |
| `scope` | `Literal['workspace', 'mission', 'job']` | 'job' | Context the action operates within. |

### `contracts.V2ActionRequest`

**Inherits from:** [`contracts.ContractModel`](#contractscontractmodel)
Expand Down Expand Up @@ -719,7 +738,7 @@ A2UI-shaped resource import surface consumed by Studio.
| Field | Type | Default | Description |
|---|---|---|---|
| `surfaces` | `list[str]` | — | |
| `actions` | `list[str]` | — | |
| `actions` | `list[contracts.V2ActionDefinition]` | — | |
| `case_lanes` | `list[contracts.V2CaseLaneDefinition]` | — | |
| `artifact_types` | `list[contracts.V2ArtifactTypeDefinition]` | — | |

Expand Down Expand Up @@ -786,6 +805,7 @@ A2UI-shaped resource import surface consumed by Studio.
| `surface` | `str` | **required** | |
| `action` | `str` | **required** | |
| `fields` | `list[contracts.V2AwaitingFieldDefinition]` | — | |
| `reopenable` | `bool` | False | Whether an already-answered step may be reopened and resubmitted with edited values. |

### `contracts.V2CaseLaneDefinition`

Expand Down Expand Up @@ -904,6 +924,7 @@ A2UI-shaped resource import surface consumed by Studio.
| `id` | `str` | **required** | |
| `label` | `str` | **required** | |
| `auto_surface` | `bool` | False | |
| `scope` | `Literal['workspace', 'mission', 'job']` | 'workspace' | |
| `display` | `V2ResourceDisplayDefinition` | `None` | |

### `contracts.V2Effect`
Expand Down Expand Up @@ -1562,7 +1583,7 @@ Standard error response model
| `error` | `str` | **required** | |
| `error_type` | `<enum 'ErrorType'>` | **required** | |
| `detail` | `str` | `None` | |
| `timestamp` | `datetime` | datetime.datetime(2026, 8, 26, 17, 21, 1, 586513) | |
| `timestamp` | `datetime` | datetime.datetime(2026, 8, 29, 19, 33, 55, 605372) | |
| `status_code` | `int` | **required** | |

### `routes.RegistrationRefreshRequest`
Expand Down Expand Up @@ -1630,4 +1651,4 @@ A base class for creating Pydantic models.
| `jti` | `str` | `None` | |


*Uploaded on 2026-08-26 17:21:01*
*Uploaded on 2026-08-29 19:33:55*
1 change: 1 addition & 0 deletions src/supervaizer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"V2A2UIResourceImportDocument",
),
"V2A2UISubmitDefinition": ("supervaizer.contracts", "V2A2UISubmitDefinition"),
"V2ActionDefinition": ("supervaizer.contracts", "V2ActionDefinition"),
"V2AgentCapabilities": ("supervaizer.contracts", "V2AgentCapabilities"),
"V2AgentIdentity": ("supervaizer.contracts", "V2AgentIdentity"),
"V2AgentMethod": ("supervaizer.contracts", "V2AgentMethod"),
Expand Down
16 changes: 10 additions & 6 deletions src/supervaizer/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from supervaizer.common import ApiSuccess, SvBaseModel, log
from supervaizer.contracts import (
SupervaizerV2AgentRegistrationContract,
V2ActionDefinition,
V2ActionRequest,
V2AgentMethod,
V2AgentMethods,
Expand Down Expand Up @@ -849,13 +850,16 @@ def _validate_supervaizer_v2_identity(self) -> None:
def _apply_v2_method_capabilities(self) -> None:
if self.supervaizer_v2_registration is None or self.v2_methods is None:
return
actions = [
*self.supervaizer_v2_registration.capabilities.actions,
*self.v2_methods.action_ids,
declared = self.supervaizer_v2_registration.capabilities.actions
known_ids = {action.id for action in declared}
self.supervaizer_v2_registration.capabilities.actions = [
*declared,
*(
V2ActionDefinition(id=action_id, mutating=True, scope="job")
for action_id in dict.fromkeys(self.v2_methods.action_ids)
if action_id not in known_ids
),
]
self.supervaizer_v2_registration.capabilities.actions = list(
dict.fromkeys(actions)
)

@property
def slug(self) -> str:
Expand Down
Loading
Loading