This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user