feat(cloud): add targeted task governance foundation
Tests / Test passed: 659

This commit is contained in:
2026-07-13 22:21:12 +08:00
parent a3ba94be04
commit 2cd314b183
41 changed files with 2099 additions and 15 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-13
@@ -0,0 +1,212 @@
## Context
The Cloud Control Plane already persists scheduled tasks, pooled devices,
enrolled Hosts, user accounts, and user sessions. Public task submission has
only driver and capability-tag constraints, while the Console is primarily a
read dashboard. The Host Agent uses only outbound internal routes and can
heartbeat, claim, renew, and report work; it cannot submit a task. No durable
policy or AI usage ledger exists.
`cloud-planner-proxy` is the prerequisite for enforceable AI budgets. It
places every planner call from a Host using `AI_PLANNER_TRANSPORT=cloud` behind
an authenticated Cloud endpoint and leaves direct provider calls supported.
This change is deliberately scheduled after that change: a direct provider
call cannot be authoritatively measured or stopped by the Cloud.
The Cloud Platform remains the durable application layer. Public/internal
HTTP adapters, the Vue Console, and the Host Agent are outer adapters; no
HTTP, Cloud, or LLM dependency is introduced into `core`, `driver`, `device`,
or `tools`.
## Goals / Non-Goals
**Goals:**
- Let authorized Console users create goal or workflow tasks for an explicit
Host and optionally an explicit Device, without a fallback target.
- Make human task-submission permissions and target restrictions durable,
auditable, and API-enforced.
- Let an authenticated Host receive its effective versioned policy and create
goal tasks that are irrevocably constrained to itself.
- Account for provider-reported token use and enforce hard per-Host budgets
for Cloud-proxied planner calls, including concurrent requests.
- Display effective restrictions, budget status, and non-secret usage in the
Console.
**Non-Goals:**
- Custom roles, tenant isolation, task-level read ACLs, invitations, or an
external identity provider. Existing fixed roles remain the coarse access
control layer.
- General remote Host control, inbound connections, or allowing one Host to
enqueue work for another Host.
- Charging, invoice generation, provider price catalogues, prompt/screenshot
retention, or an attempt to hard-limit direct-to-provider planner calls.
- Cloud workflow-definition distribution. Host-self submission is goal-only
because a workflow definition is still stored locally on each Host.
## Decisions
### D1: Use explicit target selectors inside task constraints
`TaskConstraints` gains an optional target object with `host_id` and optional
`device_id`. A device target is invalid without its Host; a Host-only target
means any eligible device on that Host; an absent target preserves broad pool
scheduling. The existing serialized constraints field stores this additive
shape, while API status/list responses expose it directly.
The scheduler applies target filtering before the named assignment strategy,
in addition to driver type and capability tags. A missing, stale, busy, or
temporarily incompatible selected device leaves the task queued. It never
falls back to another target. The submission service validates a named Host
and Device against the known pool/enrollment ownership at creation time.
Using only a globally unique `device_id` was rejected because repository and
pool ownership are Host/device pairs; retaining the Host in the target makes
authorization and audit unambiguous. Duplicating target columns outside the
existing constraints payload was rejected for v1 because matching is already
performed after task retrieval and there is no target-indexed query path.
### D2: Compose fixed role scopes with a separate restrictive user policy
Keep `viewer`, `operator`, and `admin` roles and their existing scope mapping.
Add an optional `UserSubmissionPolicy` keyed by user id with a version,
`submission_enabled`, and tri-state Host/Device allow-lists (`null` means
unrestricted; an empty stored list means no permitted targets). A restrictive
policy requires an explicit target within its allow-list, preventing an
un-targeted task from escaping a user's permitted Hosts.
Public task submission first requires `tasks:submit`, then evaluates the
authenticated human principal's policy. The same policy applies to an
administrator's ordinary task submission; an administrator can change policy
through governance administration but does not silently bypass it. Policies
do not change task-read visibility in this increment.
Storing arbitrary policy JSON was rejected: it makes safe validation,
migration, and Console editing ambiguous. Adding a custom role/permission
model was rejected because fixed roles plus a narrow restrictive policy cover
the requested near-term controls without lockout complexity.
### D3: Store versioned Host policies and synchronize them in heartbeat responses
`HostGovernancePolicy` is keyed by Host id and has a monotonic revision. V1
contains an optional maximum active-task count and an optional UTC-day token
budget. The Cloud scheduler enforces active-task limits; the budget is
enforced only by the planner proxy. No policy record means no new limit,
preserving existing deployments.
Heartbeat requests carry the last policy revision applied by the Host. A
heartbeat response includes the effective complete policy when the revision is
different, otherwise only confirms its current revision. The Host persists a
safe cached policy snapshot for local status visibility, but the Cloud remains
the authority for task assignment and AI spend. This pull-on-existing-
heartbeat design keeps the outbound-only Host model and avoids a parallel
long-poll channel.
Pushing policy through a new inbound Host listener was rejected because it
breaks NAT deployments. Requiring a policy fetch before every planner call
was rejected because the proxy already performs the authoritative budget check
and would add avoidable latency.
### D4: Meter Cloud-proxy usage with reservation then settlement
After `cloud-planner-proxy` is complete, provider clients return a
non-secret usage object together with the existing tool-call decision. It
contains provider/model identity and provider-reported input, output, and
total token counts when available; `AIPlanner` continues to consume only the
decision.
Before the proxy invokes a provider for a Host with a token budget, it atomically
creates a budget reservation for a conservative per-decision ceiling within the
Host's UTC-day bucket. The Cloud configuration also applies a matching
provider output ceiling. If used plus reserved tokens would exceed the
budget, the endpoint rejects before invoking the provider. On a provider
response, the reservation is settled to actual reported usage and an immutable
usage event is recorded. If the transport outcome leaves usage unknown, the
reservation remains until a bounded expiry cleanup rather than being released
optimistically.
The proxy request carries Host execution context (task id, attempt, and lease
when one exists). A Host-Agent-local context adapter supplies that metadata to
the cloud client without changing `AIPlanner` or introducing Host imports into
`runtime`. Locally initiated/non-assignment calls record Host-level usage with
the assignment fields absent.
Post-hoc usage-only accounting was rejected because concurrent calls can
overspend a limit before aggregation. Client-side budget checks were rejected
because the Host is not the trust boundary. Direct transport is represented
as `unmetered`, not as zero usage or a compliant hard-budget path.
### D5: Give Hosts a narrow self-submission operation
Add `POST /internal/v1/hosts/{host_id}/tasks` under existing host-scoped
authentication. The route requires the authenticated Host to equal the URL
Host, accepts a goal and optional local Device target, and constructs the
Cloud-side target itself. A supplied Device must be enrolled/owned by that
Host. Host policy can disable self-submission; human user policies do not
apply because there is no human principal.
The endpoint accepts no workflow reference and no arbitrary Host target.
Giving an enrolled Host the public `tasks:submit` scope was rejected because
it would allow cross-Host task submission and blur service credentials with
human authorization.
### D6: Keep governance administration explicit in the Console and SDK
Add public, scope-protected governance routes for reading/updating user
submission policy, Host policy, and non-secret Host AI usage summaries/events.
`governance:read` and `governance:admin` are new scopes; fixed user roles need
no mapping change because administrators retain `*`, while scoped bearer
automation can receive just these scopes. Console task creation renders only
when `tasks:submit` is present. Policy and usage administration render only
for `governance:admin`; backend scope and policy checks are authoritative.
Every policy change records a safe audit event with actor, target, old/new
revision, and non-secret values. The Console clears any password inputs from
the existing Users flow and never displays provider credentials, prompts,
screenshots, cookies, or lease secrets.
## Risks / Trade-offs
- [Planner proxy is incomplete or a Host remains on direct transport] →
Token limits are not activated for that Host; the Console labels it
unmetered and the implementation gate prevents treating it as compliant.
- [Provider usage is unavailable after a transport failure] → Retain the
conservative reservation until expiry; this favors cost safety over
temporary under-utilization.
- [Policy migration or stale Console state changes a user's permitted target]
→ Enforce against current Cloud policy at every submission and return a
clear authorization failure; UI state is only advisory.
- [Targeted Hosts/devices are offline] → Keep the task queued and visible as
targeted rather than rerouting it; operators can edit or cancel only when a
later lifecycle capability supplies those operations.
- [Policy tables increase schema and operational complexity] → Use one normal
forward migration, transactional repository operations, and the existing
SQLite/PostgreSQL contract.
## Migration Plan
1. Do not start implementation until `cloud-planner-proxy` has completed its
tasks, passed strict validation and its runtime tests, and the active
Cloud Console user-authentication change has been reconciled and verified.
2. Add the forward schema migration for policies, audit records, usage events,
and reservations. Existing tasks remain valid because no target and no
policy continue to mean unrestricted behavior.
3. Deploy the Cloud API with policy/target routes and scheduler checks, then
deploy compatible Hosts. Hosts learn policies on their next heartbeat;
older Hosts continue normal assignment but cannot self-submit.
4. Enable Cloud planner transport on a pilot Host, configure a budget, and
verify proxy reservations and settlement before assigning budgets across
the fleet. Direct Hosts remain visibly unmetered.
5. Deploy the Console after the API. Rollback removes policy assignments or
returns affected Hosts to direct transport only when local provider
credentials are available; preserve governance and usage rows for a
forward fix rather than destructively downgrading production data.
## Open Questions
- V1 uses UTC-day token budgets. A later change can add calendar-month or
rolling windows without weakening the reservation/settlement contract.
- Token cost is intentionally excluded because provider pricing and cached
token semantics vary; usage events retain provider/model fields for a later
pricing layer.
@@ -0,0 +1,79 @@
## Why
The Cloud Console can currently inspect distributed work but cannot direct
that work to a selected Host or Device, govern who may submit it, or explain
and control the AI spend generated by each Host. Now that
`cloud-planner-proxy` is planned to centralize the LLM call path, the next
control-plane increment can make those operational decisions enforceable at
the Cloud boundary instead of trusting every edge process to self-govern.
## What Changes
- Add Console task creation for authorized operators, with explicit target
selectors for a Host and optionally one of that Host's Devices. A selected
target is a hard scheduling constraint and is never silently rerouted to a
different Host or Device.
- Add persisted Cloud governance policies: user submission restrictions
(submission enablement and allowed Host/Device targets) and versioned
per-Host operational/AI-budget policies. The API remains authoritative;
Console visibility is not an authorization boundary.
- Let a Host retrieve its current policy through the authenticated outbound
protocol, with version-aware heartbeat synchronization and a safe cached
representation for local visibility.
- Add a Host-scoped internal task-submission operation. An authenticated
Host can create a task only for itself and, when specified, one of its own
enrolled Devices; it never receives general public `tasks:submit` power.
- After `cloud-planner-proxy` is implemented and a Host uses its `cloud`
planner transport, record provider-reported token usage per Host/task/
attempt and enforce configured token budgets with atomic reservation and
settlement. Direct-to-provider Hosts remain explicitly unmetered and
cannot be represented as hard-budget-enforced.
- Extend the Console with task-creation, user-policy, Host-policy, and
per-Host AI-usage/budget views. Existing user lifecycle management remains
role-gated and gains policy editing rather than a new custom-role system.
## Capabilities
### New Capabilities
- `cloud-governance`: Durable, versioned user and Host policies that restrict
task submission/targets and define Host operational and AI-budget limits.
- `host-scoped-task-submission`: Authenticated internal operation that lets a
Host submit tasks constrained to itself.
- `cloud-ai-usage-governance`: Durable AI usage metering, budget reservations,
settlement, and non-secret reporting for Cloud-proxied planner calls.
### Modified Capabilities
- `cloud-console-ui`: add governed task creation and operational governance
views to the existing Cloud Console experience. This capability is still
defined by active Console changes and will be reconciled before archive.
- `platform-sdk`: extend public task submission/status and `CloudClient`
parity with explicit task targets and safe governance-facing responses.
- `task-scheduler`: make explicit Host/Device target selectors part of task
constraints and enforce them during matching.
- `host-agent-protocol`: return versioned Host policy through the outbound
protocol and add Host-self task submission while retaining host binding.
- `cloud-planner-proxy`: enforce Host budget reservations and emit
provider-reported usage only after the proxy change is implemented. This
capability is currently an active, unarchived change.
- `agent-runtime`: carry provider-reported non-secret token usage alongside a
tool-call decision so the Cloud proxy can meter the call without changing
`AIPlanner` decision semantics. This capability is also active and
unarchived.
## Impact
- `packages/cloud-platform/cloud`: governance and usage domain models,
repository port/SQLAlchemy implementation, Alembic migration, scheduler
matching, public/internal Pydantic contracts, policy-aware authorization,
and proxy metering.
- `apps/cloud-api`: router composition and configuration only; all durable
policy and accounting behavior remains in the Cloud Platform package.
- `apps/device-host-agent`: policy-aware client/heartbeat state and
self-targeted task submission; no inbound Cloud connection is introduced.
- `cloud-console/`: task composer plus user/Host policy and AI-usage views.
- Dependencies and rollout: implementation is blocked until
`cloud-planner-proxy` is completed, its migration is deployed, and affected
Hosts are using `AI_PLANNER_TRANSPORT=cloud`; it also follows completion of
the active Cloud Console user-authentication work.
@@ -0,0 +1,50 @@
## MODIFIED Requirements
### Requirement: Pluggable dual-provider tool-calling abstraction
The system SHALL support at least two interchangeable LLM providers
(Anthropic native tool use and OpenAI function calling) for the AI Planner's
decision calls, selectable via configuration, with both providers constrained
to return exactly one tool call per request. Independently of provider
selection, the system SHALL support at least two transports for making that
decision call -- direct-to-provider (the tool-calling client calls the
provider's SDK itself, using locally configured credentials) and cloud-proxy
(the tool-calling client calls the Cloud Control Plane's planner-decision
endpoint, which calls the provider using cloud-held credentials) -- selectable
via configuration without requiring any change to `AIPlanner`'s own decision
logic. A provider decision result SHALL also carry available non-secret
provider token-usage metadata so outer Cloud adapters can meter calls without
changing that decision logic.
#### Scenario: Provider selected via configuration
- **WHEN** the AI Planner is configured with a given provider identifier
- **THEN** it constructs and uses the tool-calling client for that provider
without requiring any change to `AIPlanner`'s own decision logic
#### Scenario: Provider response resolves to a single decision
- **WHEN** either supported provider returns a response to a tool-calling
request
- **THEN** the response is parsed into exactly one tool name and one arguments
object, regardless of which provider produced it
#### Scenario: Transport selected via configuration
- **WHEN** the Host Agent is configured with a given transport (direct or
cloud-proxy)
- **THEN** `AIPlanner` is constructed with the tool-calling client for that
transport, and its own decision logic is unchanged regardless of which
transport is in effect
#### Scenario: Direct transport remains available and default
- **WHEN** no transport is explicitly configured
- **THEN** the AI Planner uses the direct-to-provider transport, matching its
behavior before the cloud-proxy transport existed
#### Scenario: Cloud-proxy transport resolves a decision without a local provider client
- **WHEN** the Host Agent is configured with the cloud-proxy transport
- **THEN** its tool-calling client sends the decision request to the Cloud
Control Plane's planner-decision endpoint instead of constructing a local
Anthropic or OpenAI SDK client
#### Scenario: Provider reports token usage
- **WHEN** a provider response includes input or output token counts
- **THEN** the tool-calling result makes those non-secret counts available to
its outer adapter while `AIPlanner` selects the same single decision
@@ -0,0 +1,52 @@
## ADDED Requirements
### Requirement: Cloud-proxied planner calls produce durable non-secret usage events
After the Cloud planner proxy resolves a provider call, the system SHALL record
a durable usage event with Host, provider/model, provider-reported token
counts, request outcome, and known task-attempt context, while excluding raw
prompts, screenshots, provider credentials, cookies, and lease secrets.
#### Scenario: Provider returns token usage
- **WHEN** a Cloud-proxied planner call completes with provider-reported token
counts
- **THEN** the system stores those counts against the authenticated Host and
returns the planner decision without exposing secret request content
#### Scenario: Assignment context is available
- **WHEN** the Host makes a proxied planner call while executing a claimed
assignment
- **THEN** the recorded event includes that task id and attempt identifier
### Requirement: Host token budgets use atomic reservation and settlement
The Cloud planner proxy SHALL atomically reserve a conservative per-call token
amount before a provider invocation for every Host with an effective AI token
budget using Cloud planner transport, SHALL reject over-budget calls before
invoking the provider, and SHALL settle the reservation to provider-reported
usage when available.
#### Scenario: Remaining budget permits a call
- **WHEN** used tokens plus active reservations and the next conservative
reservation are within the Host's UTC-day budget
- **THEN** the proxy reserves budget, invokes the provider, and settles the
reservation after the provider response
#### Scenario: Remaining budget is insufficient
- **WHEN** the next reservation would exceed the Host's effective budget
- **THEN** the proxy rejects the planner call without invoking the provider
#### Scenario: Provider outcome has unknown usage
- **WHEN** a transport failure prevents the proxy from determining actual
provider usage after reservation
- **THEN** the reservation remains active until bounded expiry cleanup rather
than being released optimistically
### Requirement: Direct planner transport is explicitly unmetered
The system SHALL represent a Host using direct-to-provider planner transport as
unmetered for Cloud token accounting and SHALL not report it as complying with
a Cloud-enforced token budget.
#### Scenario: Console inspects a direct-transport Host
- **WHEN** an authorized operator views AI usage for a Host not using Cloud
planner transport
- **THEN** the system reports the Host as unmetered rather than zero usage or
hard-budget-enforced
@@ -0,0 +1,50 @@
## ADDED Requirements
### Requirement: Console composes explicitly targeted tasks
The Console SHALL provide a task-creation flow to principals with
`tasks:submit`, exposing goal/workflow input, Host selection, Device selection
scoped to the selected Host, and visible driver/capability constraints.
#### Scenario: Operator creates a Device-targeted task
- **WHEN** an authorized operator selects a Host and one of its Devices and
submits a valid task
- **THEN** the Console sends the explicit target to the public task API and
displays the resulting queued task with that target
#### Scenario: Restricted operator has no permitted target
- **WHEN** the Cloud API reports that the operator's policy disallows the
selected target or targetless submission
- **THEN** the Console shows the authorization error and does not imply that
the task was accepted or rerouted
### Requirement: Console administers governance policy within effective scopes
The Console SHALL expose user-submission and Host-policy views only to a
principal with `governance:admin`, and SHALL display effective non-secret
policy revisions and mutations without treating hidden controls as security.
#### Scenario: Administrator changes a Host budget
- **WHEN** a governance administrator saves a valid Host policy containing an
AI budget or active-task limit
- **THEN** the Console displays the returned newer policy revision and its
effective values
#### Scenario: Non-governance user opens the Console
- **WHEN** the current principal lacks `governance:admin`
- **THEN** the Console hides governance administration navigation while the
backend remains responsible for rejecting unauthorized requests
### Requirement: Console reports Host AI budget state without sensitive prompts
The Console SHALL render per-Host budget state and non-secret token usage for
authorized governance users, including an explicit unmetered state for Hosts
using direct planner transport.
#### Scenario: Administrator views a Cloud-proxied Host
- **WHEN** a governance administrator selects a Host with recorded proxy usage
- **THEN** the Console displays configured budget, used/reserved/remaining
tokens, provider/model, and non-secret event metadata
#### Scenario: Administrator views a direct-transport Host
- **WHEN** a governance administrator selects a Host that does not use Cloud
planner transport
- **THEN** the Console labels the Host unmetered and does not display zero as
a budget-compliant usage value
@@ -0,0 +1,53 @@
## ADDED Requirements
### Requirement: User task-submission policy is durable and authoritative
The system SHALL persist an optional versioned submission policy for each
human user, containing submission enablement and explicit Host/Device target
allow-lists, and SHALL enforce that policy after scope authentication on every
public task-submission request.
#### Scenario: Restricted user submits to an allowed Device
- **WHEN** a principal with `tasks:submit` has a policy allowing a Host/Device
target and submits a task for that exact target
- **THEN** the system accepts the task subject to normal validation and records
the authenticated principal as its submitter
#### Scenario: Restricted user omits or exceeds a target allow-list
- **WHEN** a principal with a restrictive policy submits without a target or
names a Host/Device outside its allow-list
- **THEN** the system rejects the request before creating a task
#### Scenario: Submission is disabled
- **WHEN** a principal with `tasks:submit` has a policy with submission
disabled
- **THEN** the system rejects task submission without changing the queue
### Requirement: Host governance policy is versioned and enforceable
The system SHALL persist a monotonic revisioned policy for each Host with an
optional active-task limit and optional UTC-day AI token budget, and SHALL
enforce active-task limits during Cloud scheduling.
#### Scenario: Host active-task limit is reached
- **WHEN** a Host already has its configured maximum number of assigned or
dispatched attempts and another queued task targets that Host
- **THEN** the scheduler leaves the later task queued until capacity becomes
available
#### Scenario: Host policy is updated
- **WHEN** an authorized governance administrator updates a Host policy
- **THEN** the system stores a strictly newer revision and later Cloud
authorization, scheduling, and proxy operations use the new policy
### Requirement: Governance mutations and reads use dedicated scopes
The system SHALL protect governance policy and AI-usage operations with
`governance:read` or `governance:admin` as appropriate, and SHALL record safe
audits for policy mutations.
#### Scenario: Administrator changes a policy
- **WHEN** a principal with `governance:admin` changes a user or Host policy
- **THEN** the system records actor, target, old/new revision, timestamp, and
non-secret policy metadata without recording credentials or request secrets
#### Scenario: Non-governance principal attempts a policy mutation
- **WHEN** a principal lacking `governance:admin` updates a governance policy
- **THEN** the system rejects the request before changing durable policy state
@@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: Planner proxy enforces effective Host token budgets before provider invocation
The Cloud planner-decision endpoint SHALL atomically reserve a conservative
bounded token amount before invoking its provider for an authenticated Host
using Cloud planner transport with an effective token budget, and SHALL reject
the request without invoking the provider when the reservation would exceed
the remaining budget.
#### Scenario: Budget permits a planner decision
- **WHEN** the Host has sufficient budget after used and reserved tokens are
considered
- **THEN** the endpoint creates a reservation and invokes the configured
provider exactly once
#### Scenario: Budget is exhausted
- **WHEN** a next planner decision would exceed the Host's effective budget
- **THEN** the endpoint returns a structured failure without invoking the
configured provider
### Requirement: Planner proxy settles provider-reported token usage without persisting prompts
The Cloud planner-decision endpoint SHALL settle its reservation to the
provider-reported token usage when available and SHALL retain only non-secret
metering metadata, never the raw prompt, screenshot, provider credentials, or
session/lease secret.
#### Scenario: Provider response includes usage
- **WHEN** the configured provider returns a valid tool-call decision and
token-usage metadata
- **THEN** the endpoint records and returns the decision, settles the Host's
reservation to the reported usage, and does not durably store request text
or screenshot bytes
#### Scenario: Usage is indeterminate after failure
- **WHEN** a reservation exists but the endpoint cannot determine provider
usage after a transport failure
- **THEN** the reservation remains until bounded expiry cleanup rather than
being released as unused
@@ -0,0 +1,61 @@
## MODIFIED Requirements
### Requirement: Host identity is authenticated and bound to one host id
The internal Host Agent API SHALL require a host-scoped bearer principal and
SHALL reject any request that attempts to act for a `host_id` different from
the authenticated principal's bound host.
#### Scenario: Host authenticates as itself
- **WHEN** a Host Agent presents valid credentials bound to its requested
`host_id`
- **THEN** the internal API authorizes permitted heartbeat, claim, renewal,
result, policy retrieval, and self-submission operations
#### Scenario: Host attempts to impersonate another host
- **WHEN** valid credentials bound to host A are used on a request for host B
- **THEN** the internal API rejects the request without reading or modifying
host B's state
### Requirement: Host Agent synchronizes heartbeat and complete device snapshots
The Host Agent SHALL periodically submit its complete local device snapshot
and last applied policy revision to the control plane, and the control plane
SHALL atomically refresh the host heartbeat, replace only that host's
pooled-device records, and return the effective Host policy whenever its
revision differs.
#### Scenario: Host reports devices
- **WHEN** a Host Agent submits a valid heartbeat containing its current devices
- **THEN** the control plane updates the host's last-seen time and exposes the
submitted devices through the aggregated pool
#### Scenario: Host policy revision changed
- **WHEN** a Host heartbeat presents a revision older than the effective
Cloud-host policy
- **THEN** the response includes the complete newer policy and revision for the
Host to cache
#### Scenario: Host policy revision is current
- **WHEN** a Host heartbeat presents the current effective policy revision
- **THEN** the response confirms that revision without resending an unrelated
policy representation
#### Scenario: Host reports no devices
- **WHEN** a previously populated host submits an empty device snapshot
- **THEN** only that host's prior device records are removed while devices
owned by other hosts remain unchanged
## ADDED Requirements
### Requirement: Host can submit a self-targeted goal through the outbound protocol
The Host Agent SHALL be able to submit a goal-only task through its existing
outbound authenticated protocol, and the control plane SHALL derive its target
from the authenticated Host rather than accepting another Host identifier.
#### Scenario: Host submits a local goal
- **WHEN** an authenticated Host submits a valid goal through its internal
self-submission operation
- **THEN** the control plane returns a queued task targeted to that Host
#### Scenario: Host submits an ineligible local Device
- **WHEN** a Host names a Device that is not owned by its authenticated Host
- **THEN** the control plane rejects the request without creating a task
@@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: Host can submit a goal task only for itself
The internal Host Agent API SHALL allow an authenticated Host to submit a
goal-based task that the Cloud Control Plane irrevocably targets to that same
Host, with an optional Device target owned by that Host.
#### Scenario: Host submits a goal for one of its Devices
- **WHEN** Host A presents valid Host A credentials and submits a valid goal
naming one of Host A's enrolled Devices
- **THEN** the control plane creates a queued task targeted to Host A and that
Device without granting Host A public task-submission authority
#### Scenario: Host omits a Device target
- **WHEN** an authenticated Host submits a valid goal without a Device target
- **THEN** the control plane creates a queued task targeted to that Host and
lets the scheduler select only an eligible Device owned by it
### Requirement: Host self-submission preserves host isolation
The internal Host task-submission operation SHALL reject a foreign Host,
foreign Device, workflow reference, or disabled self-submission policy before
creating any task.
#### Scenario: Host attempts a foreign target
- **WHEN** Host A submits a request naming Host B or a Device not owned by A
- **THEN** the control plane rejects the request and does not create a task
#### Scenario: Host self-submission is disabled
- **WHEN** a Host policy disables Host self-submission
- **THEN** the control plane rejects that Host's self-submission request
@@ -0,0 +1,84 @@
## MODIFIED Requirements
### Requirement: Task submission and status via the SDK
The system SHALL allow an external integrator to submit a task (goal or
workflow reference plus constraints and an optional explicit Host/Device
target) through the platform SDK's API, and to query that task's current
status by id, backed by the `task-scheduler` capability. A Device target MUST
include its owning Host, and the status representation SHALL expose any
explicit target separately from the eventual assignment.
#### Scenario: Submit a task via the API
- **WHEN** an integrator calls the task-submission endpoint with a valid goal
and optional constraints
- **THEN** the API returns a task id that can be used to poll status, and the
underlying `task-scheduler` records a new `queued` `ScheduledTask`
#### Scenario: Submit a task for one Host and Device
- **WHEN** a `tasks:submit` principal submits a valid target containing an
eligible Host and Device it is permitted to use
- **THEN** the API records that exact target and the scheduler cannot assign
the task outside it
#### Scenario: Device target lacks its Host
- **WHEN** an integrator submits a Device target without an owning Host
- **THEN** the API rejects the request before creating a task
#### Scenario: Query status of a known task
- **WHEN** an integrator requests status for a task id that exists
- **THEN** the API returns that task's current status (`queued`, `assigned`,
`dispatched`, `done`, or `failed`) and its explicit target when present
#### Scenario: Query status of an unknown task
- **WHEN** an integrator requests status for a task id that does not exist
- **THEN** the API returns a not-found response rather than an unhandled server
error
### Requirement: Python SDK client mirrors the REST API
The system SHALL provide a Python client (`CloudClient`) exposing methods
corresponding to every `/v1/...` resource, user-authentication,
user-administration, and governance route, including targeted task submission
and read-only Host AI-usage access, so integrators do not need to
hand-construct HTTP requests.
#### Scenario: Client submits a targeted task and retrieves status
- **WHEN** a caller uses `CloudClient` to submit a task with an explicit target
and then fetches status by the returned id
- **THEN** the client produces the same target and lifecycle result as direct
REST calls
#### Scenario: Client administers governance with a scoped bearer
- **WHEN** a caller configures `CloudClient` with `governance:admin` and
invokes a policy operation
- **THEN** the client sends that authentication and returns the corresponding
non-secret policy representation
### Requirement: Public API operations enforce scopes
The public platform API SHALL require operation-specific scopes for task
submission, task reading, pool reading, plugin reading, plugin
administration, user administration, governance reading, and governance
administration, regardless of whether the principal came from a bearer
credential or user session.
#### Scenario: Submit principal has task scope
- **WHEN** a bearer or user principal with `tasks:submit` calls the
task-submission endpoint
- **THEN** the request is authorized subject to normal task validation and
any effective user-submission policy
#### Scenario: Non-admin principal attempts plugin registration
- **WHEN** an authenticated principal without `plugins:admin` calls plugin
registration
- **THEN** the API rejects the request before resolving or loading the plugin
target
#### Scenario: Non-admin principal attempts user administration
- **WHEN** an authenticated principal without `users:admin` calls a
user-administration endpoint
- **THEN** the API rejects the request before reading or changing protected
user state
#### Scenario: Principal lacks governance administration scope
- **WHEN** an authenticated principal without `governance:admin` changes a
user or Host policy
- **THEN** the API rejects the request before changing policy or usage state
@@ -0,0 +1,58 @@
## MODIFIED Requirements
### Requirement: Task submission enqueues a scheduled task
The system SHALL allow a caller to submit a task (a goal string, or a
reference to a `WorkflowDefinition`, plus optional device constraints:
`driver_type`, required capability tags, and an explicit Host/Device target)
and SHALL enqueue it as a `ScheduledTask` with status `queued`, returning a
stable task id the caller can poll. A Device target MUST name its owning Host;
an explicit target remains attached to the task for its entire lifecycle.
#### Scenario: Successful submission
- **WHEN** a caller submits a task with a goal and no constraints
- **THEN** the scheduler creates a `ScheduledTask` with status `queued`,
assigns it a unique id, and returns that id to the caller without blocking
for a device to become available
#### Scenario: Targeted submission
- **WHEN** a caller submits a valid task targeted to a Host and one of its
Devices
- **THEN** the queued task retains that Host/Device target until it is assigned
or reaches a terminal lifecycle state
#### Scenario: Queue depth limit reached
- **WHEN** a caller submits a task while the queue already holds
`config.max_queue_depth` queued tasks
- **THEN** the scheduler rejects the submission with a clear error rather than
accepting an unbounded backlog
### Requirement: Assignment matches a queued task to an idle, constraint-matching device
The system SHALL assign a queued `ScheduledTask` to an idle `PooledDevice`
(as reported by the `device-pool` capability) whose explicit target, if any,
`driver_type`, and capability tags satisfy the task's constraints, using a
named, registrable `AssignmentStrategy`.
#### Scenario: Matching idle device available
- **WHEN** `assign()` runs and at least one idle `PooledDevice` matches the
head-of-queue task's constraints
- **THEN** the scheduler selects one such device via the configured
`AssignmentStrategy`, transitions the task to status `assigned`, and records
the chosen `device_id`/`host_id`
#### Scenario: Targeted Device is unavailable
- **WHEN** `assign()` runs and the only explicitly targeted Device is busy,
stale, absent, or otherwise ineligible
- **THEN** the task remains `queued` and the scheduler does not assign a
different Device or Host
#### Scenario: No matching device available
- **WHEN** `assign()` runs and no idle `PooledDevice` matches the head-of-queue
task's constraints
- **THEN** the task remains `queued` (not failed), and `assign()` returns
without error, ready to be retried on a later call
#### Scenario: Unknown assignment strategy configured
- **WHEN** `TaskScheduler` is configured with an `AssignmentStrategy` name
that is not registered
- **THEN** the scheduler raises a clear configuration error at startup/first-
assign rather than silently falling back to a default strategy
@@ -0,0 +1,113 @@
## 1. Preconditions and dependency reconciliation
- [x] 1.1 Confirm `cloud-planner-proxy` is fully implemented, passes its
strict validation and relevant runtime tests, and its deployed schema/API
contract is the base for this change.
- [ ] 1.2 Complete and reconcile `cloud-console-user-authentication`, including
the documented Console Users experience and its remaining PostgreSQL and
HTTPS/Host-Agent verification tasks.
- [ ] 1.3 Rebase this change's delta specs against the then-current canonical
`platform-sdk`, `cloud-console-ui`, `cloud-planner-proxy`, and
`agent-runtime` specifications before implementation begins.
## 2. Governance domain, persistence, and migration
- [ ] 2.1 Define target-selector, user-submission-policy, Host-governance-
policy, token-usage-event, and token-reservation domain models with
non-secret representations and revision semantics.
- [ ] 2.2 Extend the Cloud repository port with transactional CRUD/query
operations for policies, active Host capacity, budget reservations,
settlement, expiry cleanup, and bounded usage summaries/events.
- [ ] 2.3 Add SQLAlchemy rows, indexes, conversion helpers, and concurrency-
safe PostgreSQL/SQLite implementations for governance policies, usage
events, reservations, and safe policy audits.
- [ ] 2.4 Add an Alembic forward/downgrade revision that preserves existing
users, Hosts, devices, tasks, and attempts; advance schema readiness
checks to the new head.
- [ ] 2.5 Add repository and migration tests for policy revisions, null versus
empty allow-list semantics, active-task capacity, reservation races,
settlement, expiry cleanup, audit redaction, and SQLite/PostgreSQL
parity.
## 3. Targeted scheduling and public governance API
- [x] 3.1 Extend task constraints, SDK request/response models, persistence
serialization, status/list representations, and `CloudClient` for an
optional Host/Device target; reject a Device without its Host.
- [x] 3.2 Enforce target ownership/existence at public submission and filter
scheduler candidates by target before strategy selection; keep an
unavailable targeted task queued with no fallback assignment.
- [x] 3.3 Add `governance:read` and `governance:admin` scopes and policy-aware
public task authorization that combines `tasks:submit` with the
authenticated human user's effective submission policy.
- [ ] 3.4 Add bounded, non-secret public governance routes and `CloudClient`
methods for user policy, Host policy, Host AI-budget summaries, and
paginated usage events; audit every policy mutation.
- [ ] 3.5 Add public API/SDK tests for targeted submission, target-policy
denial, scope enforcement, status exposure, governance CRUD, and
absence of secret fields in responses/audits.
## 4. Host policy synchronization and self-submission
- [x] 4.1 Extend shared internal heartbeat request/response models with the
Host's last policy revision and a revision-aware effective-policy reply.
- [ ] 4.2 Update Host heartbeat synchronization to persist only safe cached
policy state and expose it through local status/history without adding an
inbound Cloud connection.
- [x] 4.3 Add the Host-scoped goal-only task-submission route and
`HostAgentClient` method; derive Host targeting from authenticated
credentials and validate any named local Device ownership.
- [ ] 4.4 Enforce the Host policy's self-submission and active-task limits in
the Cloud service/scheduler, not only in Host-local code.
- [ ] 4.5 Add Cloud API and Host Agent tests for revision convergence,
unchanged-policy replies, self-targeted task creation, foreign target
rejection, disabled self-submission, and outbound-only compatibility.
## 5. Cloud-proxy usage metering and hard budget enforcement
- [x] 5.1 Extend the dual-provider tool-calling result with optional
non-secret provider usage fields while preserving `AIPlanner`'s existing
single-decision behavior and direct transport compatibility.
- [ ] 5.2 Extend planner-proxy request context and Host-Agent-local context
binding so Cloud-proxied calls carry known task/attempt metadata without
importing Host or Cloud concerns into `runtime`.
- [ ] 5.3 Add Cloud proxy preflight reservation, configured conservative
per-call ceiling, provider invocation, actual-usage settlement, and
bounded unknown-usage reservation expiry.
- [ ] 5.4 Record non-secret usage events and expose accurate
used/reserved/remaining UTC-day budget summaries; explicitly report
direct transport as unmetered.
- [ ] 5.5 Add provider-fake, repository concurrency, Cloud API, and Host
Agent tests for usage parsing, over-budget rejection before provider
invocation, concurrent reservations, settlement, timeout uncertainty,
task-attempt attribution, and direct-transport labeling.
## 6. Cloud Console governance experience
- [x] 6.1 Add task composer API bindings and a scope-aware Console form for
goal/workflow submission, Host/Device target selection, and target/policy
validation errors.
- [ ] 6.2 Complete or reconcile the admin Users view, then add user-
submission-policy editing with safe refresh and conflict/error handling.
- [ ] 6.3 Add Host policy administration and AI-usage/budget views, including
revision display, unmetered direct Hosts, and no rendering of prompts,
screenshots, provider credentials, cookies, or lease secrets.
- [ ] 6.4 Add frontend tests for task composer scope/policy failures,
governance navigation, policy mutations, usage rendering, unmetered
status, CSRF writes, and retained session behavior on `403`.
## 7. Documentation, verification, and rollout
- [ ] 7.1 Update deployment and Console documentation with the dependency
order, migration/rollback sequence, policy semantics, Cloud transport
prerequisite, budget reservation behavior, direct-host limitation, and
safe administrator operations.
- [ ] 7.2 Run formatting, Ruff, compile checks, secret/redaction review, the
complete non-integration workspace test suite, Cloud Console unit tests,
type-check, and production build.
- [ ] 7.3 Run real PostgreSQL migration/concurrency coverage and a Compose HTTPS
manual flow covering user restriction, targeted scheduling, Host policy
delivery, Host self-submission, Cloud-proxy budget exhaustion, and a
direct Host labelled unmetered.
- [x] 7.4 Run `openspec validate cloud-console-governance --strict` and resolve
all proposal, design, specification, and task validation errors.