Files
2026-07-15 09:43:14 +08:00

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.