215 lines
11 KiB
Markdown
215 lines
11 KiB
Markdown
## 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, defining planner
|
|
prompt/screenshot retention (owned by `task-execution-progress-visibility`),
|
|
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 governance views clear password inputs and
|
|
never display provider credentials, screenshots, cookies, or lease secrets;
|
|
the separate task-detail planner-history view is governed by
|
|
`task-execution-progress-visibility`.
|
|
|
|
## 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.
|