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

11 KiB

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.