@miller-tech/uap 2.21.3 → 2.36.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -10
- package/config/model-profiles/qwen38-coder390.json +46 -0
- package/config/model-profiles/qwen38.json +3 -2
- package/config/model-registry.json +110 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/benchmarks/paired/types.d.ts +2 -2
- package/dist/bin/cli.js +315 -20
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/attention.d.ts +6 -0
- package/dist/cli/attention.d.ts.map +1 -0
- package/dist/cli/attention.js +115 -0
- package/dist/cli/attention.js.map +1 -0
- package/dist/cli/bench-report.d.ts +82 -0
- package/dist/cli/bench-report.d.ts.map +1 -0
- package/dist/cli/bench-report.js +237 -0
- package/dist/cli/bench-report.js.map +1 -0
- package/dist/cli/clients.d.ts +6 -0
- package/dist/cli/clients.d.ts.map +1 -0
- package/dist/cli/clients.js +142 -0
- package/dist/cli/clients.js.map +1 -0
- package/dist/cli/dashboard-grants.d.ts +26 -0
- package/dist/cli/dashboard-grants.d.ts.map +1 -0
- package/dist/cli/dashboard-grants.js +69 -0
- package/dist/cli/dashboard-grants.js.map +1 -0
- package/dist/cli/dashboard-pace.d.ts +31 -0
- package/dist/cli/dashboard-pace.d.ts.map +1 -0
- package/dist/cli/dashboard-pace.js +42 -0
- package/dist/cli/dashboard-pace.js.map +1 -0
- package/dist/cli/dashboard-session.d.ts +25 -0
- package/dist/cli/dashboard-session.d.ts.map +1 -0
- package/dist/cli/dashboard-session.js +814 -0
- package/dist/cli/dashboard-session.js.map +1 -0
- package/dist/cli/dashboard-types.d.ts +18 -0
- package/dist/cli/dashboard-types.d.ts.map +1 -0
- package/dist/cli/dashboard-types.js +9 -0
- package/dist/cli/dashboard-types.js.map +1 -0
- package/dist/cli/dashboard.d.ts +2 -18
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +43 -793
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/deliver.d.ts +35 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +93 -9
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/diff.d.ts +85 -0
- package/dist/cli/diff.d.ts.map +1 -0
- package/dist/cli/diff.js +293 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/eta.d.ts +35 -0
- package/dist/cli/eta.d.ts.map +1 -0
- package/dist/cli/eta.js +101 -0
- package/dist/cli/eta.js.map +1 -0
- package/dist/cli/handsfree.d.ts +7 -0
- package/dist/cli/handsfree.d.ts.map +1 -1
- package/dist/cli/handsfree.js +27 -0
- package/dist/cli/handsfree.js.map +1 -1
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +126 -5
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/loops.d.ts +24 -5
- package/dist/cli/loops.d.ts.map +1 -1
- package/dist/cli/loops.js +92 -12
- package/dist/cli/loops.js.map +1 -1
- package/dist/cli/models.d.ts +62 -0
- package/dist/cli/models.d.ts.map +1 -0
- package/dist/cli/models.js +567 -0
- package/dist/cli/models.js.map +1 -0
- package/dist/cli/plan.d.ts +9 -0
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +57 -4
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/proxy-relay.d.ts +55 -0
- package/dist/cli/proxy-relay.d.ts.map +1 -0
- package/dist/cli/proxy-relay.js +116 -0
- package/dist/cli/proxy-relay.js.map +1 -0
- package/dist/cli/proxy.d.ts +4 -0
- package/dist/cli/proxy.d.ts.map +1 -1
- package/dist/cli/proxy.js +26 -1
- package/dist/cli/proxy.js.map +1 -1
- package/dist/cli/sandbox.d.ts +101 -4
- package/dist/cli/sandbox.d.ts.map +1 -1
- package/dist/cli/sandbox.js +393 -44
- package/dist/cli/sandbox.js.map +1 -1
- package/dist/cli/self.d.ts +13 -0
- package/dist/cli/self.d.ts.map +1 -0
- package/dist/cli/self.js +94 -0
- package/dist/cli/self.js.map +1 -0
- package/dist/cli/systemd-services.d.ts +2 -8
- package/dist/cli/systemd-services.d.ts.map +1 -1
- package/dist/cli/systemd-services.js +5 -20
- package/dist/cli/systemd-services.js.map +1 -1
- package/dist/cli/task.js +1 -1
- package/dist/cli/task.js.map +1 -1
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/cli/wizard-config.js +31 -2
- package/dist/cli/wizard-config.js.map +1 -1
- package/dist/config/settings-registry.d.ts +1 -1
- package/dist/config/settings-registry.d.ts.map +1 -1
- package/dist/config/settings-registry.js +108 -0
- package/dist/config/settings-registry.js.map +1 -1
- package/dist/coordination/capability-router.js +2 -2
- package/dist/coordination/capability-router.js.map +1 -1
- package/dist/coordination/database.d.ts +13 -0
- package/dist/coordination/database.d.ts.map +1 -1
- package/dist/coordination/database.js +63 -7
- package/dist/coordination/database.js.map +1 -1
- package/dist/coordination/eta-inject.d.ts +32 -0
- package/dist/coordination/eta-inject.d.ts.map +1 -0
- package/dist/coordination/eta-inject.js +64 -0
- package/dist/coordination/eta-inject.js.map +1 -0
- package/dist/coordination/reactor.d.ts.map +1 -1
- package/dist/coordination/reactor.js +14 -0
- package/dist/coordination/reactor.js.map +1 -1
- package/dist/coordination/service.d.ts +18 -8
- package/dist/coordination/service.d.ts.map +1 -1
- package/dist/coordination/service.js +38 -28
- package/dist/coordination/service.js.map +1 -1
- package/dist/dashboard/client-registry.d.ts +163 -0
- package/dist/dashboard/client-registry.d.ts.map +1 -0
- package/dist/dashboard/client-registry.js +549 -0
- package/dist/dashboard/client-registry.js.map +1 -0
- package/dist/dashboard/controls.d.ts.map +1 -1
- package/dist/dashboard/controls.js +33 -4
- package/dist/dashboard/controls.js.map +1 -1
- package/dist/dashboard/data-seeder.d.ts +10 -7
- package/dist/dashboard/data-seeder.d.ts.map +1 -1
- package/dist/dashboard/data-seeder.js +31 -75
- package/dist/dashboard/data-seeder.js.map +1 -1
- package/dist/dashboard/data-service.d.ts +9 -0
- package/dist/dashboard/data-service.d.ts.map +1 -1
- package/dist/dashboard/data-service.js +32 -3
- package/dist/dashboard/data-service.js.map +1 -1
- package/dist/dashboard/placement-routes.d.ts +119 -0
- package/dist/dashboard/placement-routes.d.ts.map +1 -0
- package/dist/dashboard/placement-routes.js +331 -0
- package/dist/dashboard/placement-routes.js.map +1 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +408 -2
- package/dist/dashboard/server.js.map +1 -1
- package/dist/delivery/attention.d.ts +130 -0
- package/dist/delivery/attention.d.ts.map +1 -0
- package/dist/delivery/attention.js +341 -0
- package/dist/delivery/attention.js.map +1 -0
- package/dist/delivery/decompose.d.ts +18 -0
- package/dist/delivery/decompose.d.ts.map +1 -1
- package/dist/delivery/decompose.js +22 -1
- package/dist/delivery/decompose.js.map +1 -1
- package/dist/delivery/eta-estimate.d.ts +111 -0
- package/dist/delivery/eta-estimate.d.ts.map +1 -0
- package/dist/delivery/eta-estimate.js +171 -0
- package/dist/delivery/eta-estimate.js.map +1 -0
- package/dist/delivery/eta.d.ts +23 -0
- package/dist/delivery/eta.d.ts.map +1 -0
- package/dist/delivery/eta.js +59 -0
- package/dist/delivery/eta.js.map +1 -0
- package/dist/delivery/orchestrated-mission.d.ts +10 -0
- package/dist/delivery/orchestrated-mission.d.ts.map +1 -1
- package/dist/delivery/orchestrated-mission.js +15 -1
- package/dist/delivery/orchestrated-mission.js.map +1 -1
- package/dist/delivery/plan-check.d.ts +7 -1
- package/dist/delivery/plan-check.d.ts.map +1 -1
- package/dist/delivery/plan-check.js +23 -2
- package/dist/delivery/plan-check.js.map +1 -1
- package/dist/delivery/run-state.d.ts +1 -1
- package/dist/delivery/run-state.d.ts.map +1 -1
- package/dist/delivery/run-state.js +8 -1
- package/dist/delivery/run-state.js.map +1 -1
- package/dist/delivery/task-orchestrator.d.ts +8 -3
- package/dist/delivery/task-orchestrator.d.ts.map +1 -1
- package/dist/delivery/task-orchestrator.js +75 -43
- package/dist/delivery/task-orchestrator.js.map +1 -1
- package/dist/delivery/task-sync.d.ts +21 -0
- package/dist/delivery/task-sync.d.ts.map +1 -1
- package/dist/delivery/task-sync.js +44 -8
- package/dist/delivery/task-sync.js.map +1 -1
- package/dist/delivery/task-workspace.d.ts +3 -3
- package/dist/delivery/task-workspace.js +3 -3
- package/dist/generators/self-card.d.ts +65 -0
- package/dist/generators/self-card.d.ts.map +1 -0
- package/dist/generators/self-card.js +256 -0
- package/dist/generators/self-card.js.map +1 -0
- package/dist/inference/probe.js +1 -1
- package/dist/inference/probe.js.map +1 -1
- package/dist/memory/prepopulate.d.ts.map +1 -1
- package/dist/memory/prepopulate.js +2 -0
- package/dist/memory/prepopulate.js.map +1 -1
- package/dist/models/analytics.d.ts +32 -0
- package/dist/models/analytics.d.ts.map +1 -1
- package/dist/models/analytics.js +68 -5
- package/dist/models/analytics.js.map +1 -1
- package/dist/models/plan-readability.d.ts +55 -0
- package/dist/models/plan-readability.d.ts.map +1 -0
- package/dist/models/plan-readability.js +339 -0
- package/dist/models/plan-readability.js.map +1 -0
- package/dist/placement/admission.d.ts +59 -0
- package/dist/placement/admission.d.ts.map +1 -0
- package/dist/placement/admission.js +197 -0
- package/dist/placement/admission.js.map +1 -0
- package/dist/placement/auto.d.ts +44 -0
- package/dist/placement/auto.d.ts.map +1 -0
- package/dist/placement/auto.js +212 -0
- package/dist/placement/auto.js.map +1 -0
- package/dist/placement/enforce.d.ts +128 -0
- package/dist/placement/enforce.d.ts.map +1 -0
- package/dist/placement/enforce.js +655 -0
- package/dist/placement/enforce.js.map +1 -0
- package/dist/placement/idle.d.ts +50 -0
- package/dist/placement/idle.d.ts.map +1 -0
- package/dist/placement/idle.js +213 -0
- package/dist/placement/idle.js.map +1 -0
- package/dist/placement/ledger.d.ts +323 -0
- package/dist/placement/ledger.d.ts.map +1 -0
- package/dist/placement/ledger.js +263 -0
- package/dist/placement/ledger.js.map +1 -0
- package/dist/placement/preview.d.ts +51 -0
- package/dist/placement/preview.d.ts.map +1 -0
- package/dist/placement/preview.js +61 -0
- package/dist/placement/preview.js.map +1 -0
- package/dist/placement/probes.d.ts +47 -0
- package/dist/placement/probes.d.ts.map +1 -0
- package/dist/placement/probes.js +207 -0
- package/dist/placement/probes.js.map +1 -0
- package/dist/placement/proxy-env.d.ts +21 -0
- package/dist/placement/proxy-env.d.ts.map +1 -0
- package/dist/placement/proxy-env.js +42 -0
- package/dist/placement/proxy-env.js.map +1 -0
- package/dist/placement/registry.d.ts +1038 -0
- package/dist/placement/registry.d.ts.map +1 -0
- package/dist/placement/registry.js +274 -0
- package/dist/placement/registry.js.map +1 -0
- package/dist/policies/liveness.d.ts +6 -8
- package/dist/policies/liveness.d.ts.map +1 -1
- package/dist/policies/liveness.js +6 -25
- package/dist/policies/liveness.js.map +1 -1
- package/dist/policies/sandbox-anchor.d.ts +91 -0
- package/dist/policies/sandbox-anchor.d.ts.map +1 -0
- package/dist/policies/sandbox-anchor.js +187 -0
- package/dist/policies/sandbox-anchor.js.map +1 -0
- package/dist/policies/sandbox-grants.d.ts +131 -0
- package/dist/policies/sandbox-grants.d.ts.map +1 -0
- package/dist/policies/sandbox-grants.js +562 -0
- package/dist/policies/sandbox-grants.js.map +1 -0
- package/dist/state/manifest.d.ts +4 -2
- package/dist/state/manifest.d.ts.map +1 -1
- package/dist/state/manifest.js +32 -16
- package/dist/state/manifest.js.map +1 -1
- package/dist/utils/checkout-root.d.ts +2 -0
- package/dist/utils/checkout-root.d.ts.map +1 -0
- package/dist/utils/checkout-root.js +43 -0
- package/dist/utils/checkout-root.js.map +1 -0
- package/dist/utils/resolve-uap-bin.d.ts +5 -0
- package/dist/utils/resolve-uap-bin.d.ts.map +1 -0
- package/dist/utils/resolve-uap-bin.js +33 -0
- package/dist/utils/resolve-uap-bin.js.map +1 -0
- package/dist/utils/terminal-safe.d.ts +12 -0
- package/dist/utils/terminal-safe.d.ts.map +1 -0
- package/dist/utils/terminal-safe.js +53 -0
- package/dist/utils/terminal-safe.js.map +1 -0
- package/docs/ATTRIBUTION.md +1 -0
- package/docs/DASHBOARD_UPLIFT_SPEC.md +8 -8
- package/docs/INDEX.md +6 -5
- package/docs/architecture/OVERVIEW.md +15 -3
- package/docs/architecture/PROTOCOL.md +7 -1
- package/docs/architecture/adr/0006-operator-directed-model-placement.md +86 -0
- package/docs/architecture/adr/0007-auto-displacement-allowlist-as-standing-consent.md +79 -0
- package/docs/architecture/adr/0008-fleet-clients-link-out.md +92 -0
- package/docs/architecture/adr/0009-completion-triggered-dispatch-and-per-leaf-model-dials.md +71 -0
- package/docs/architecture/adr/0010-attention-queue-and-afk-auto-decider.md +159 -0
- package/docs/architecture/adr/0011-sandbox-grants-and-network-egress-boundary.md +219 -0
- package/docs/architecture/adr/0012-operator-legibility-surfaces.md +175 -0
- package/docs/getting-started/INSTALLATION.md +1 -1
- package/docs/guides/AUTOMATIC_FEATURES.md +8 -0
- package/docs/guides/LOCAL_MODELS.md +1 -1
- package/docs/guides/PROXY.md +2 -1
- package/docs/guides/SANDBOX.md +91 -4
- package/docs/integrations/MCP_ROUTER.md +1 -1
- package/docs/plans/model-performance-proof-options.md +176 -0
- package/docs/reference/API.md +1 -1
- package/docs/reference/CLI.md +169 -4
- package/docs/reference/COMPARISON.md +1 -1
- package/docs/reference/CONFIGURATION.md +12 -3
- package/docs/reference/CONFIGURATION_REFERENCE.md +269 -14
- package/docs/reference/DATABASE_SCHEMA.md +1 -1
- package/docs/reference/FEATURES.md +15 -3
- package/docs/reference/PATTERNS.md +1 -1
- package/docs/reference/PLATFORMS.md +14 -1
- package/docs/specs/operator-model-placement.md +1314 -0
- package/docs/specs/operator-model-placement.review.md +339 -0
- package/package.json +2 -2
- package/scripts/maintenance/publish-npm.sh +23 -11
- package/scripts/version-bump.sh +6 -51
- package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
- package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
- package/src/policies/schemas/policies/mandatory-testing-deployment.md +2 -5
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
- package/templates/hooks/session-start.sh +44 -0
- package/tools/agents/UAP/cli.py +14 -3
- package/tools/agents/UAP/paths.py +50 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +1420 -100
- package/tools/agents/scripts/toolcall_path_normalizer.py +46 -1
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +29 -6
- package/tools/agents/tests/test_doubling_break.py +64 -5
- package/tools/agents/tests/test_error_loop_break.py +10 -4
- package/tools/agents/tests/test_error_loop_ignores_correctives.py +13 -2
- package/tools/agents/tests/test_lockstep_escalation.py +280 -9
- package/tools/agents/tests/test_path_containment.py +94 -0
- package/tools/agents/tests/test_proxy_unix_socket.py +162 -0
- package/tools/agents/tests/test_signature_quality.py +400 -0
- package/tools/agents/tests/test_uap_compliance.py +11 -1
- package/web/dash/core.js +1 -0
- package/web/dash/tab-clients.js +196 -0
- package/web/dash/tab-models.js +387 -8
- package/web/dash/tab-policies.js +87 -4
- package/web/dash/tabs.js +77 -73
- package/web/dashboard.html +6 -0
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/web/dash/tab-agents.js +0 -21
- package/web/dash/tab-deliver.js +0 -12
- package/web/dash/tab-memory.js +0 -12
- package/web/dash/tab-orchestration.js +0 -21
- package/web/dash/tab-overview.js +0 -21
- package/web/dash/tab-tasks.js +0 -21
package/dist/dashboard/server.js
CHANGED
|
@@ -12,10 +12,17 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
12
12
|
import { join, dirname, sep } from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { getDashboardData, probeDatabaseHealth } from './data-service.js';
|
|
15
|
+
import { getPlacementState, getPlacementPending, getPlacementPreview, getPlacementAdmit } from './placement-routes.js';
|
|
16
|
+
import { resolvePendingPlacement, dismissPendingPlacement, loadPlacement, unloadPlacement } from '../placement/enforce.js';
|
|
17
|
+
import { autoPolicyPath, loadAutoPolicy, MIN_UNLOAD_IDLE_SECS, saveAutoPolicy } from '../placement/auto.js';
|
|
18
|
+
import { startIdleSweep } from '../placement/idle.js';
|
|
19
|
+
import { resolveAttentionBySource, sweepExpired } from '../delivery/attention.js';
|
|
15
20
|
import { seedDashboardData, cleanupSeeder } from './data-seeder.js';
|
|
16
21
|
import { getPolicyMemoryManager } from '../policies/policy-memory.js';
|
|
17
22
|
import { heuristicOrder, buildOrderPrompt, parseOrderResponse } from '../policies/policy-order.js';
|
|
18
23
|
import { readEventsSince, readRecentEvents } from '../utils/telemetry-store.js';
|
|
24
|
+
import { loadPolicy, runDoctor, worstHealth } from '../capacity/probe.js';
|
|
25
|
+
import { getClientSummaries, ensureClientDashboard, stopClientDashboard } from './client-registry.js';
|
|
19
26
|
/** First prose sentence of a policy's markdown (its description), fail-soft. */
|
|
20
27
|
function policyPromptDescription(md) {
|
|
21
28
|
if (!md)
|
|
@@ -147,6 +154,14 @@ export function startDashboardServer(options = {}) {
|
|
|
147
154
|
// Updated to the real bound port once the server is listening.
|
|
148
155
|
let boundPort = requestedPort;
|
|
149
156
|
const host = options.host || 'localhost';
|
|
157
|
+
// Idle-resident sweep handle (started in the listen callback once cwd's
|
|
158
|
+
// seeder has run; see §4.4.2). Stopped in close().
|
|
159
|
+
let idleSweeper = null;
|
|
160
|
+
// P3 attention decider handle — rides the SAME fleet-ownership guard as
|
|
161
|
+
// the idle sweeper (UAP_IDLE_SWEEP=0 marks a fleet-spawned CLIENT
|
|
162
|
+
// dashboard: exactly one sweeper may touch shared state). Stopped in
|
|
163
|
+
// close().
|
|
164
|
+
let attentionSweeper = null;
|
|
150
165
|
// Precedence: explicit option (--refresh) > UAP_DASH_REFRESH_MS > 2000.
|
|
151
166
|
// Floor 250ms — getDashboardData reads several DBs per snapshot, so a
|
|
152
167
|
// too-small interval would peg the event loop. Ceiling 1h — past 2^31-1ms,
|
|
@@ -156,6 +171,9 @@ export function startDashboardServer(options = {}) {
|
|
|
156
171
|
// Track SSE clients for live event streaming
|
|
157
172
|
const sseClients = new Set();
|
|
158
173
|
const cwd = process.cwd();
|
|
174
|
+
// In-flight /api/doctor run (probes block the loop; concurrent requests
|
|
175
|
+
// coalesce onto one sweep instead of serializing N of them).
|
|
176
|
+
let doctorInFlight = null;
|
|
159
177
|
// Mutation auth (security audit D1): the policy-mutation POST routes disable
|
|
160
178
|
// security controls (delivery-enforcement, self-protect) and persist the
|
|
161
179
|
// change to the DB. Without a gate, any LAN host (--host 0.0.0.0) or any web
|
|
@@ -230,7 +248,13 @@ export function startDashboardServer(options = {}) {
|
|
|
230
248
|
if (!isTokenBearingPage) {
|
|
231
249
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
232
250
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
233
|
-
|
|
251
|
+
// The mutation-token header is deliberately NOT advertised in the
|
|
252
|
+
// preflight allow-list: the same-origin UI needs no CORS permission
|
|
253
|
+
// for it, and a wildcard `Access-Control-Allow-Headers` would let any
|
|
254
|
+
// origin's scripted preflight ride the token header name (security
|
|
255
|
+
// review — the mutation surface now includes process termination via
|
|
256
|
+
// /api/clients/stop). Reads stay open; mutations stay same-origin.
|
|
257
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
234
258
|
}
|
|
235
259
|
if (req.method === 'OPTIONS') {
|
|
236
260
|
res.writeHead(204);
|
|
@@ -257,6 +281,146 @@ export function startDashboardServer(options = {}) {
|
|
|
257
281
|
res.end(JSON.stringify(data));
|
|
258
282
|
return;
|
|
259
283
|
}
|
|
284
|
+
// API: Fleet of client folders — registry + auto-scan, with per-client
|
|
285
|
+
// fail-soft summaries (deliver runs, queue depth, git, health probe).
|
|
286
|
+
// Read-only (no token): identical authority to /api/dashboard, one
|
|
287
|
+
// client at a time. Method-guarded so a future POST /api/clients
|
|
288
|
+
// mutation can't silently fall through to this read handler instead
|
|
289
|
+
// of the token-gated control chokepoint.
|
|
290
|
+
if (url === '/api/clients' && req.method === 'GET') {
|
|
291
|
+
try {
|
|
292
|
+
const clients = await getClientSummaries(cwd, boundPort);
|
|
293
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
294
|
+
res.end(JSON.stringify({ clients }));
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
298
|
+
res.end(JSON.stringify({ error: `client fleet unavailable: ${err.message}` }));
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// API: Model placement (spec §4.7) — READS ONLY in this phase; the
|
|
303
|
+
// load/unload/resolve mutation routes arrive with phase-3 enforcement
|
|
304
|
+
// and will go through mutationAuthorized(). `state` syncs the ledger
|
|
305
|
+
// (probes + unit reconcile) — heavier than the 2s dash refresh, so the
|
|
306
|
+
// tab throttles its own polling rather than riding the refresh tick.
|
|
307
|
+
if (url === '/api/placement/state') {
|
|
308
|
+
try {
|
|
309
|
+
const data = getPlacementState(cwd);
|
|
310
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
311
|
+
res.end(JSON.stringify(data));
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
315
|
+
res.end(JSON.stringify({ error: `placement state unavailable: ${err.message}` }));
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (url === '/api/placement/pending') {
|
|
320
|
+
const pending = getPlacementPending();
|
|
321
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
322
|
+
res.end(JSON.stringify({ pending }));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// API: Placement admission (spec §4.3) — the proxy gate's controller
|
|
326
|
+
// endpoint. POST, and LOOPBACK-ONLY rather than mutationToken-gated:
|
|
327
|
+
// the caller is the proxy (a local process that has no dashboard
|
|
328
|
+
// token), and its answer parks or forwards live model traffic, so it
|
|
329
|
+
// must never be reachable from off-host. Non-loopback callers get 403
|
|
330
|
+
// before the body is even read.
|
|
331
|
+
if (url === '/api/placement/admit' && req.method === 'POST') {
|
|
332
|
+
// Peer address captured from the live socket only: no '' fallback —
|
|
333
|
+
// a destroyed socket reports undefined and must FAIL CLOSED here,
|
|
334
|
+
// not coerce into the allowlist (this server is TCP-only, so an
|
|
335
|
+
// empty peer is never a unix socket).
|
|
336
|
+
const peer = req.socket.remoteAddress;
|
|
337
|
+
const loopback = peer === '127.0.0.1' || peer === '::1' || peer === '::ffff:127.0.0.1';
|
|
338
|
+
if (!loopback) {
|
|
339
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
340
|
+
res.end(JSON.stringify({ error: 'placement admit is loopback-only' }));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const body = await readBody(req);
|
|
344
|
+
const parsed = parseJsonBody(body);
|
|
345
|
+
const modelId = typeof parsed.model_id === 'string' ? parsed.model_id : '';
|
|
346
|
+
if (!modelId) {
|
|
347
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
348
|
+
res.end(JSON.stringify({ error: 'missing model_id' }));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const answer = getPlacementAdmit(cwd, {
|
|
352
|
+
model_id: modelId,
|
|
353
|
+
client: typeof parsed.client === 'string' ? parsed.client : undefined,
|
|
354
|
+
session: typeof parsed.session === 'string' ? parsed.session : undefined,
|
|
355
|
+
// The request's context need — spec §4.4 cost is per-request.
|
|
356
|
+
// getPlacementAdmit sanitizes the shape, the route just forwards.
|
|
357
|
+
cells: typeof parsed.cells === 'number' ? parsed.cells : undefined,
|
|
358
|
+
});
|
|
359
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
360
|
+
res.end(JSON.stringify(answer));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (url.startsWith('/api/placement/preview')) {
|
|
364
|
+
const urlObj = new URL(url, `http://${host}:${boundPort}`);
|
|
365
|
+
const model = urlObj.searchParams.get('model');
|
|
366
|
+
if (!model) {
|
|
367
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
368
|
+
res.end(JSON.stringify({ error: 'missing ?model=<id>' }));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const cells = Number(urlObj.searchParams.get('cells'));
|
|
372
|
+
try {
|
|
373
|
+
const preview = getPlacementPreview(cwd, model, {
|
|
374
|
+
cells: Number.isFinite(cells) && cells > 0 ? cells : undefined,
|
|
375
|
+
});
|
|
376
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
377
|
+
res.end(JSON.stringify(preview));
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
381
|
+
res.end(JSON.stringify({ error: `placement preview unavailable: ${err.message}` }));
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// API: Capacity doctor (`uap doctor` as a read route). Same shape the
|
|
386
|
+
// CLI prints with --json (reportVersion pins it). On-demand: the
|
|
387
|
+
// Overview tab fetches it once per visit (plus a Refresh button) — the
|
|
388
|
+
// probes are execFileSync-bounded (5s cap each), so it must NOT ride
|
|
389
|
+
// the 2s snapshot tick. Concurrent requests COALESCE onto the in-flight
|
|
390
|
+
// run (the probes block the event loop; two tabs must not serialize
|
|
391
|
+
// two full probe sweeps). Fail-open shape: policy-load errors surface
|
|
392
|
+
// as an `error` field, never a 500 crash of the panel.
|
|
393
|
+
if (url === '/api/doctor') {
|
|
394
|
+
try {
|
|
395
|
+
if (!doctorInFlight) {
|
|
396
|
+
doctorInFlight = (async () => {
|
|
397
|
+
try {
|
|
398
|
+
const loaded = loadPolicy(cwd);
|
|
399
|
+
const services = runDoctor(loaded.policy);
|
|
400
|
+
return {
|
|
401
|
+
reportVersion: 1,
|
|
402
|
+
policy: loaded.path,
|
|
403
|
+
worst: worstHealth(services),
|
|
404
|
+
services,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
return { reportVersion: 1, error: err.message };
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
doctorInFlight = null;
|
|
412
|
+
}
|
|
413
|
+
})();
|
|
414
|
+
}
|
|
415
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
416
|
+
res.end(JSON.stringify(await doctorInFlight));
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
420
|
+
res.end(JSON.stringify({ reportVersion: 1, error: err.message }));
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
260
424
|
// API: SSE event stream
|
|
261
425
|
if (url === '/api/events') {
|
|
262
426
|
res.writeHead(200, {
|
|
@@ -579,10 +743,54 @@ export function startDashboardServer(options = {}) {
|
|
|
579
743
|
return false;
|
|
580
744
|
}
|
|
581
745
|
};
|
|
746
|
+
// Bind failure is the ONE error every deployment hits eventually (two
|
|
747
|
+
// projects, one 3847). Without this handler the process dies with a raw
|
|
748
|
+
// EADDRINUSE stack and a Restart=on-failure unit crash-loops against a
|
|
749
|
+
// port squatter. With it: say WHO owns the port (the fix depends on it —
|
|
750
|
+
// another UAP dashboard is a deliberate operator state, anything else is
|
|
751
|
+
// a genuine failure) and exit with a status systemd can act on.
|
|
752
|
+
// Attached to BOTH the http server AND the WebSocketServer: when ws wraps
|
|
753
|
+
// an existing server, the bind error surfaces on the WebSocketServer
|
|
754
|
+
// instance ('Emitted error event on WebSocketServer instance' — verified
|
|
755
|
+
// live), so an http-only handler never fires.
|
|
756
|
+
const onBindError = (err) => {
|
|
757
|
+
const die = (message, code) => {
|
|
758
|
+
console.error(message);
|
|
759
|
+
process.exit(code);
|
|
760
|
+
};
|
|
761
|
+
if (err.code !== 'EADDRINUSE') {
|
|
762
|
+
die(`UAP Dashboard: server error: ${err.message} — exiting`, 1);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const probeUrl = `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${requestedPort}/api/health`;
|
|
766
|
+
fetch(probeUrl, { signal: AbortSignal.timeout(1500) })
|
|
767
|
+
.then(async (r) => (r.ok ? (await r.json()) : null))
|
|
768
|
+
.then((body) => {
|
|
769
|
+
if (body?.service === 'uap-dashboard') {
|
|
770
|
+
// Exit 0 on purpose: the port's owner is a functioning UAP
|
|
771
|
+
// dashboard, so restarting cannot help — the operator must decide
|
|
772
|
+
// which project owns the port. A on-failure unit must NOT loop.
|
|
773
|
+
die(`UAP Dashboard: port ${requestedPort} is already owned by another UAP dashboard` +
|
|
774
|
+
` (root: ${body.root ?? 'unknown'} — it serves THAT project's data, not this one: ${cwd}).`, 0);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
die(`UAP Dashboard: port ${requestedPort} is busy (not a UAP dashboard) — ` +
|
|
778
|
+
`free it or run with --port <n>. (${err.message})`, 1);
|
|
779
|
+
})
|
|
780
|
+
.catch(() => {
|
|
781
|
+
die(`UAP Dashboard: port ${requestedPort} is busy and did not answer a health probe — ` +
|
|
782
|
+
`free it or run with --port <n>. (${err.message})`, 1);
|
|
783
|
+
});
|
|
784
|
+
};
|
|
785
|
+
server.on('error', onBindError);
|
|
582
786
|
const wss = new WebSocketServer({
|
|
583
787
|
server,
|
|
584
788
|
verifyClient: ({ origin }) => originAllowed(origin),
|
|
585
789
|
});
|
|
790
|
+
// Bind errors surface HERE too (ws re-emits them on the WebSocketServer
|
|
791
|
+
// instance when it wraps the http server) — without this, a port conflict
|
|
792
|
+
// crashes with a raw stack instead of the owner report + exit contract.
|
|
793
|
+
wss.on('error', onBindError);
|
|
586
794
|
const pushInterval = setInterval(async () => {
|
|
587
795
|
if (wss.clients.size === 0 && sseClients.size === 0)
|
|
588
796
|
return;
|
|
@@ -677,6 +885,36 @@ export function startDashboardServer(options = {}) {
|
|
|
677
885
|
catch {
|
|
678
886
|
/* seeder failure is non-fatal */
|
|
679
887
|
}
|
|
888
|
+
// Idle-resident sweep (§4.4.2): this process already owns placement
|
|
889
|
+
// enforcement, so it owns the reverse too — unloading residents whose
|
|
890
|
+
// idle clock ran out. OFF unless the policy armed it (per-tick re-read:
|
|
891
|
+
// operator edits take effect without a restart). Fleet-spawned CLIENT
|
|
892
|
+
// dashboards set UAP_IDLE_SWEEP=0 so exactly one sweeper touches the
|
|
893
|
+
// shared ledger.
|
|
894
|
+
if (!['0', 'false', 'off'].includes((process.env.UAP_IDLE_SWEEP ?? '').toLowerCase())) {
|
|
895
|
+
idleSweeper = startIdleSweep(cwd, {
|
|
896
|
+
log: (m) => console.warn(m),
|
|
897
|
+
});
|
|
898
|
+
// P3: during a long unattended run — the exact AFK case this rail
|
|
899
|
+
// exists for — session starts are rare, and without a periodic
|
|
900
|
+
// sweep the decider would never fire between them while retry
|
|
901
|
+
// minted asks accumulate. 60s cadence: pending TTL is 120s and
|
|
902
|
+
// deadlines are pinned, so adoption lands within a minute of the
|
|
903
|
+
// real expiry. sweepExpired is fail-open and no-ops under
|
|
904
|
+
// UAP_ATTENTION_OFF.
|
|
905
|
+
attentionSweeper = setInterval(() => {
|
|
906
|
+
try {
|
|
907
|
+
const r = sweepExpired(cwd);
|
|
908
|
+
if (r.adopted + r.escalated > 0) {
|
|
909
|
+
console.warn(`[attention] sweep: ${r.adopted} auto-decided, ${r.escalated} escalated`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
/* the timer must never kill the dashboard */
|
|
914
|
+
}
|
|
915
|
+
}, 60_000);
|
|
916
|
+
attentionSweeper.unref();
|
|
917
|
+
}
|
|
680
918
|
// Signal readiness with the actually-bound port (essential for port: 0).
|
|
681
919
|
options.onListening?.({ port: boundPort, host });
|
|
682
920
|
});
|
|
@@ -688,6 +926,16 @@ export function startDashboardServer(options = {}) {
|
|
|
688
926
|
close: () => {
|
|
689
927
|
clearInterval(eventPoller);
|
|
690
928
|
clearInterval(pushInterval);
|
|
929
|
+
// Stop the idle-resident sweep before the rest of the teardown —
|
|
930
|
+
// a tick firing mid-close would race the server/ledger shutdown.
|
|
931
|
+
if (idleSweeper) {
|
|
932
|
+
idleSweeper.stop();
|
|
933
|
+
idleSweeper = null;
|
|
934
|
+
}
|
|
935
|
+
if (attentionSweeper) {
|
|
936
|
+
clearInterval(attentionSweeper);
|
|
937
|
+
attentionSweeper = null;
|
|
938
|
+
}
|
|
691
939
|
// Cleanup seeder (clear heartbeat, mark agent completed)
|
|
692
940
|
try {
|
|
693
941
|
cleanupSeeder(cwd);
|
|
@@ -733,7 +981,7 @@ function parseJsonBody(body) {
|
|
|
733
981
|
throw new Error('Invalid JSON in request body');
|
|
734
982
|
}
|
|
735
983
|
}
|
|
736
|
-
const CONTROL_PREFIXES = ['/api/tasks', '/api/ledger', '/api/orchestrator', '/api/agents', '/api/deliver'];
|
|
984
|
+
const CONTROL_PREFIXES = ['/api/tasks', '/api/ledger', '/api/orchestrator', '/api/agents', '/api/deliver', '/api/placement', '/api/clients'];
|
|
737
985
|
async function routeControl(url, cwd, body) {
|
|
738
986
|
const seg = url.split('/').filter(Boolean); // ['api','tasks','<id>','update']
|
|
739
987
|
const dec = (s) => decodeURIComponent(s);
|
|
@@ -768,6 +1016,164 @@ async function routeControl(url, cwd, body) {
|
|
|
768
1016
|
return handleDeliverCancel(cwd, dec(seg[2]));
|
|
769
1017
|
if (seg[1] === 'deliver' && seg.length === 4 && seg[3] === 'resume')
|
|
770
1018
|
return handleDeliverResume(cwd, dec(seg[2]));
|
|
1019
|
+
// Placement mutations (spec §4.7): token-gated like every control route.
|
|
1020
|
+
// The result object is returned whole (200) even on ok:false — the steps
|
|
1021
|
+
// and rollback detail are the operator's evidence, not an error string.
|
|
1022
|
+
if (url === '/api/placement/resolve')
|
|
1023
|
+
return handlePlacementResolve(cwd, body);
|
|
1024
|
+
if (url === '/api/placement/dismiss')
|
|
1025
|
+
return handlePlacementDismiss(cwd, body);
|
|
1026
|
+
if (url === '/api/placement/load')
|
|
1027
|
+
return handlePlacementLoad(cwd, body);
|
|
1028
|
+
if (url === '/api/placement/unload')
|
|
1029
|
+
return handlePlacementUnload(cwd, body);
|
|
1030
|
+
if (url === '/api/placement/auto')
|
|
1031
|
+
return handlePlacementAuto(body);
|
|
1032
|
+
// Fleet spawn-on-demand (Option B link-out): bring up the selected client's
|
|
1033
|
+
// OWN dashboard server — full control stays on that instance, token-gated
|
|
1034
|
+
// and loopback-bound exactly like this one. Registry-only: the path is
|
|
1035
|
+
// resolved against listClients() inside, never trusted from the wire.
|
|
1036
|
+
if (url === '/api/clients/serve')
|
|
1037
|
+
return handleClientsServe(cwd, body);
|
|
1038
|
+
// Fleet teardown: stops the client's own dashboard instance (pid recorded
|
|
1039
|
+
// at spawn, ownership-verified before SIGTERM — see stopClientDashboard).
|
|
1040
|
+
if (url === '/api/clients/stop')
|
|
1041
|
+
return handleClientsStop(cwd, body);
|
|
771
1042
|
return undefined;
|
|
772
1043
|
}
|
|
1044
|
+
async function handleClientsServe(cwd, body) {
|
|
1045
|
+
const clientPath = typeof body.path === 'string' ? body.path : '';
|
|
1046
|
+
if (!clientPath)
|
|
1047
|
+
throw new Error('path is required');
|
|
1048
|
+
return ensureClientDashboard(cwd, clientPath);
|
|
1049
|
+
}
|
|
1050
|
+
async function handleClientsStop(cwd, body) {
|
|
1051
|
+
const clientPath = typeof body.path === 'string' ? body.path : '';
|
|
1052
|
+
if (!clientPath)
|
|
1053
|
+
throw new Error('path is required');
|
|
1054
|
+
return stopClientDashboard(cwd, clientPath);
|
|
1055
|
+
}
|
|
1056
|
+
async function handlePlacementResolve(cwd, body) {
|
|
1057
|
+
const placementId = typeof body.placement_id === 'string' ? body.placement_id : '';
|
|
1058
|
+
const option = Number(body.option);
|
|
1059
|
+
if (!placementId)
|
|
1060
|
+
throw new Error('placement_id is required');
|
|
1061
|
+
if (!Number.isInteger(option) || option < 1)
|
|
1062
|
+
throw new Error('option must be a 1-based index (as `uap models pending` prints them)');
|
|
1063
|
+
// Approval signature (spec §4.5): the dashboard UI sends the victim list
|
|
1064
|
+
// the operator actually SAW in the preview; enforcement refuses if the
|
|
1065
|
+
// recomputed option's victims differ.
|
|
1066
|
+
const expectedVictims = Array.isArray(body.expected_victims)
|
|
1067
|
+
? body.expected_victims
|
|
1068
|
+
.filter((v) => typeof v === 'object' && v !== null &&
|
|
1069
|
+
typeof v.model === 'string' &&
|
|
1070
|
+
typeof v.config === 'string')
|
|
1071
|
+
.map((v) => ({ model: v.model, config: v.config }))
|
|
1072
|
+
: undefined;
|
|
1073
|
+
const result = await resolvePendingPlacement(cwd, placementId, option, expectedVictims ? { expectedVictims } : {});
|
|
1074
|
+
// P3: the operator answered through the PLACEMENT surface, not the queue —
|
|
1075
|
+
// close the attention ask by origin or a later sweep would record a false
|
|
1076
|
+
// AFK adoption over a decision that was actually made. Best-effort: no
|
|
1077
|
+
// open ask for this placement resolves nothing (idempotent).
|
|
1078
|
+
if (result.ok) {
|
|
1079
|
+
resolveAttentionBySource(cwd, `placement:${placementId}`, `operator applied option ${option} via the dashboard`);
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
function handlePlacementDismiss(cwd, body) {
|
|
1084
|
+
const placementId = typeof body.placement_id === 'string' ? body.placement_id : '';
|
|
1085
|
+
if (!placementId)
|
|
1086
|
+
throw new Error('placement_id is required');
|
|
1087
|
+
const { dismissed } = dismissPendingPlacement(placementId);
|
|
1088
|
+
if (dismissed) {
|
|
1089
|
+
resolveAttentionBySource(cwd, `placement:${placementId}`, 'operator dismissed the parked request via the dashboard');
|
|
1090
|
+
}
|
|
1091
|
+
return { dismissed };
|
|
1092
|
+
}
|
|
1093
|
+
async function handlePlacementLoad(cwd, body) {
|
|
1094
|
+
const model = typeof body.model === 'string' ? body.model : '';
|
|
1095
|
+
if (!model)
|
|
1096
|
+
throw new Error('model is required');
|
|
1097
|
+
return loadPlacement(cwd, model);
|
|
1098
|
+
}
|
|
1099
|
+
async function handlePlacementUnload(cwd, body) {
|
|
1100
|
+
const model = typeof body.model === 'string' ? body.model : '';
|
|
1101
|
+
if (!model)
|
|
1102
|
+
throw new Error('model is required');
|
|
1103
|
+
return unloadPlacement(cwd, model);
|
|
1104
|
+
}
|
|
1105
|
+
/** Phase-4 auto policy (§4.4.1) — the UI twin of `uap models auto`: status
|
|
1106
|
+
* read with no fields, enable/disable, and the displacement allowlist whose
|
|
1107
|
+
* additions require the same explicit `yes` consent the CLI's --yes demands
|
|
1108
|
+
* (what is being recorded is standing consent to evict unattended).
|
|
1109
|
+
* NO policy_path from the body (security review S1): an HTTP caller must not
|
|
1110
|
+
* redirect the policy write at an arbitrary filesystem path — the route always
|
|
1111
|
+
* writes the single authority (`autoPolicyPath()`, env-overridable for tests
|
|
1112
|
+
* via UAP_PLACEMENT_AUTO exactly like the CLI). */
|
|
1113
|
+
function handlePlacementAuto(body) {
|
|
1114
|
+
const stringList = (v) => Array.isArray(v) ? v.filter((m) => typeof m === 'string' && m.length > 0) : [];
|
|
1115
|
+
const enable = body.enable === true;
|
|
1116
|
+
const disable = body.disable === true;
|
|
1117
|
+
const allow = stringList(body.allow_displace);
|
|
1118
|
+
const disallow = stringList(body.disallow_displace);
|
|
1119
|
+
if (enable && disable)
|
|
1120
|
+
throw new Error('enable and disable are mutually exclusive');
|
|
1121
|
+
const path = autoPolicyPath();
|
|
1122
|
+
const allowUnload = stringList(body.allow_unload);
|
|
1123
|
+
const disallowUnload = stringList(body.disallow_unload);
|
|
1124
|
+
const idleSecs = body.unload_idle_after_secs;
|
|
1125
|
+
// `null` explicitly disarms the timer; a number arms/updates it. Anything
|
|
1126
|
+
// else in that field is a caller error, not a silent no-op.
|
|
1127
|
+
if (idleSecs !== undefined && idleSecs !== null && typeof idleSecs !== 'number') {
|
|
1128
|
+
throw new Error('unload_idle_after_secs must be a number of seconds, or null to disarm');
|
|
1129
|
+
}
|
|
1130
|
+
if (!enable && !disable && allow.length === 0 && disallow.length === 0 &&
|
|
1131
|
+
allowUnload.length === 0 && disallowUnload.length === 0 && idleSecs === undefined) {
|
|
1132
|
+
return loadAutoPolicy(path); // status: same shape the state payload serves
|
|
1133
|
+
}
|
|
1134
|
+
if (allow.length > 0 && body.yes !== true) {
|
|
1135
|
+
throw new Error('allow_displace records standing consent to UNLOAD the current resident(s) and load the requested ' +
|
|
1136
|
+
'model without an operator prompt — confirm with yes: true (the same gate as ' +
|
|
1137
|
+
'`uap models auto --allow-displace <model> --yes`)');
|
|
1138
|
+
}
|
|
1139
|
+
if (allowUnload.length > 0 && body.yes !== true) {
|
|
1140
|
+
throw new Error('allow_unload records standing consent to STOP this model\'s resident automatically once it ' +
|
|
1141
|
+
'goes idle — confirm with yes: true (the same gate as ' +
|
|
1142
|
+
'`uap models auto --allow-unload <model> --yes`)');
|
|
1143
|
+
}
|
|
1144
|
+
const prev = loadAutoPolicy(path);
|
|
1145
|
+
// Spread prev (NOT a literal rebuild): every field the route doesn't
|
|
1146
|
+
// know about must survive a save. The allowlists are cloned to fresh
|
|
1147
|
+
// arrays so the in-place filtering below never mutates `prev`.
|
|
1148
|
+
const allowDisplace = [...prev.allow_displace];
|
|
1149
|
+
const unloadAllow = [...(prev.unload_allow ?? [])];
|
|
1150
|
+
const touchesUnload = allowUnload.length > 0 || disallowUnload.length > 0 || prev.unload_allow !== undefined;
|
|
1151
|
+
// Old-shape policies keep their old-shape responses: unload_allow is only
|
|
1152
|
+
// (re)written when the caller touches it or it already exists on disk.
|
|
1153
|
+
const next = { ...prev, allow_displace: allowDisplace };
|
|
1154
|
+
if (enable)
|
|
1155
|
+
next.enabled = true;
|
|
1156
|
+
if (disable)
|
|
1157
|
+
next.enabled = false;
|
|
1158
|
+
for (const m of allow)
|
|
1159
|
+
if (!allowDisplace.includes(m))
|
|
1160
|
+
allowDisplace.push(m);
|
|
1161
|
+
next.allow_displace = allowDisplace.filter((m) => !disallow.includes(m));
|
|
1162
|
+
if (touchesUnload) {
|
|
1163
|
+
for (const m of allowUnload)
|
|
1164
|
+
if (!unloadAllow.includes(m))
|
|
1165
|
+
unloadAllow.push(m);
|
|
1166
|
+
next.unload_allow = unloadAllow.filter((m) => !disallowUnload.includes(m));
|
|
1167
|
+
}
|
|
1168
|
+
if (idleSecs === null)
|
|
1169
|
+
delete next.unload_idle_after_secs;
|
|
1170
|
+
else if (idleSecs !== undefined) {
|
|
1171
|
+
if (!Number.isFinite(idleSecs) || idleSecs < MIN_UNLOAD_IDLE_SECS) {
|
|
1172
|
+
throw new Error(`unload_idle_after_secs must be >= ${MIN_UNLOAD_IDLE_SECS} (under a minute the sweep would thrash)`);
|
|
1173
|
+
}
|
|
1174
|
+
next.unload_idle_after_secs = Math.floor(idleSecs);
|
|
1175
|
+
}
|
|
1176
|
+
saveAutoPolicy(next, path);
|
|
1177
|
+
return next;
|
|
1178
|
+
}
|
|
773
1179
|
//# sourceMappingURL=server.js.map
|