@miller-tech/uap 2.21.3 → 2.36.1
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 +1386 -99
- 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 +349 -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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# ADR 0008: Dashboard fleet "Clients" tab ships as link-out, not multi-root
|
|
2
|
+
|
|
3
|
+
- **Status**: Accepted
|
|
4
|
+
- **Date**: 2026-10-10 (branch `feature/348-dashboard-fleet-clients`)
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The dashboard server (`uap dashboard serve`) is single-project: every route
|
|
9
|
+
reads `process.cwd()` captured at start. The operator runs many UAP-managed
|
|
10
|
+
client folders as siblings on disk and wants one surface that shows each
|
|
11
|
+
client's job queue (tasks) and delivery processes, with the ability to
|
|
12
|
+
switch to any of them.
|
|
13
|
+
|
|
14
|
+
Two shapes solve this:
|
|
15
|
+
|
|
16
|
+
- **Option A — multi-root host**: the host server accepts a root parameter
|
|
17
|
+
on every API call and re-scopes all panels in place. Largest payoff, but
|
|
18
|
+
it turns the host into a cross-project write surface: every existing
|
|
19
|
+
mutation route (`/api/deliver/launch`, task mutations, policy toggles)
|
|
20
|
+
would need a second authority dimension, and the mutation token would
|
|
21
|
+
suddenly grant control of every sibling project at once.
|
|
22
|
+
|
|
23
|
+
- **Option B — fleet overview + link-out**: the host stays single-project
|
|
24
|
+
and gains a read-only `GET /api/clients` that summarizes the fleet
|
|
25
|
+
(deliver-run states, queue depth, git, activity, health probe). Opening a
|
|
26
|
+
client spawns that client's OWN `uap dashboard serve` on a loopback port
|
|
27
|
+
(registry-only paths) and links out to it. Full control exists per
|
|
28
|
+
client, but only through each client's token-gated instance.
|
|
29
|
+
|
|
30
|
+
## Decision
|
|
31
|
+
|
|
32
|
+
Ship Option B. The host server gains no cross-project write authority; the
|
|
33
|
+
only wire-reachable cross-client action is `POST /api/clients/serve`
|
|
34
|
+
(token-gated, resolved strictly against the client registry — never a
|
|
35
|
+
raw path from the wire). Discovery is auto-scan of the scan root (default:
|
|
36
|
+
the host's parent directory, ignore-list filtered) merged with manual
|
|
37
|
+
entries in `~/.uap/clients.json` (`uap clients add/remove/list/scan`).
|
|
38
|
+
|
|
39
|
+
Consequences and guards:
|
|
40
|
+
|
|
41
|
+
- **Sticky ports**: spawned dashboards record their port in the registry
|
|
42
|
+
(`ports` map), and computed ports skip claimed ones — a fleet change
|
|
43
|
+
(new folder, rename) can never silently move a live dashboard's port,
|
|
44
|
+
which would orphan the old process and wedge the new assignment.
|
|
45
|
+
- **Root-matched probes**: liveness is true only when the answering
|
|
46
|
+
dashboard reports the expected project root, so a foreign listener on a
|
|
47
|
+
candidate port can never be link-mistaken for the client.
|
|
48
|
+
- **Spawn is registry-only and argv-fixed** (`dashboard serve --port N
|
|
49
|
+
--host 127.0.0.1`): no shell, no path from the wire.
|
|
50
|
+
- **Host never writes into client projects** — the earlier `.uap/dash.pid`
|
|
51
|
+
write was dropped; nothing reads it and it breached the invariant.
|
|
52
|
+
- **Disclosure scope** (recorded for the security review): `GET
|
|
53
|
+
/api/clients` is unauthenticated and CORS-open exactly like
|
|
54
|
+
`/api/dashboard`, but it widens exposure from one project to every
|
|
55
|
+
sibling (absolute paths, branches, activity). On `--host 0.0.0.0` binds
|
|
56
|
+
this is LAN-visible; operators choosing that bind accept the wider
|
|
57
|
+
read. If the CORS-open-read posture is ever tightened, this route
|
|
58
|
+
belongs in that sweep. No secrets, tokens, or pids are in the payload.
|
|
59
|
+
- **Environment**: spawned children inherit the operator's environment
|
|
60
|
+
(including `UAP_DASHBOARD_TOKEN`, deliberately kept so scripted fleet
|
|
61
|
+
control works) except the host's `UAP_CLIENTS_*` test overrides, which
|
|
62
|
+
are scrubbed so the child scans its own siblings.
|
|
63
|
+
|
|
64
|
+
## Alternatives rejected
|
|
65
|
+
|
|
66
|
+
- **Multi-root host (Option A)**: rejected for the write-surface blast
|
|
67
|
+
radius above. Note this change prepares rather than blocks it — the
|
|
68
|
+
per-project read surface (`getTaskData(cwd)`, `listRuns(root)`) was
|
|
69
|
+
already root-parameterized, and the registry's `ClientEntry` model is
|
|
70
|
+
reusable if in-place switching is ever wanted.
|
|
71
|
+
- **Federated static links (no spawn)**: rejected because the operator
|
|
72
|
+
wants one click from the fleet view to a live dashboard; requiring a
|
|
73
|
+
manual `uap dashboard serve` per client defeats the point.
|
|
74
|
+
|
|
75
|
+
## Follow-ups
|
|
76
|
+
|
|
77
|
+
Completed on this branch:
|
|
78
|
+
|
|
79
|
+
- **`uap clients stop` + tab Stop action** (spawned-instance teardown):
|
|
80
|
+
spawn-on-demand records the child pid in the registry (`pids` map —
|
|
81
|
+
host-owned bookkeeping in `~/.uap/clients.json`, never a write into the
|
|
82
|
+
client project). Stop requires BOTH a root-matched live probe and pid
|
|
83
|
+
ownership verification (`/proc/<pid>/cwd` must be the client root) before
|
|
84
|
+
SIGTERM — a recycled pid is never killed; on mismatch or missing evidence
|
|
85
|
+
it refuses and names the port for a manual stop. Stale pid records are
|
|
86
|
+
cleared opportunistically whenever a probe shows the dashboard gone.
|
|
87
|
+
- **`resolveUapBin` extracted** to `src/utils/resolve-uap-bin.ts`
|
|
88
|
+
(`controls.ts` and `client-registry.ts` share it; the util lives one level
|
|
89
|
+
under `dist/` so the same-install `<root>/dist/bin/cli.js` resolution is
|
|
90
|
+
unchanged).
|
|
91
|
+
|
|
92
|
+
None outstanding.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ADR 0009: Completion-triggered dispatch and per-leaf model dials
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-11
|
|
5
|
+
- Spec: completion-triggered scheduling (C1) and routing dials (C2), responding to the
|
|
6
|
+
"serial execution / no delegation" and "wrong worker on the wrong job"
|
|
7
|
+
gaps in mtlynch.io's *Why Are Coding Agents So Dumb?*
|
|
8
|
+
- Supersedes: none — replaces the wave-barrier scheduling note documented in
|
|
9
|
+
`deliver.parallelTasks` (CONFIGURATION.md) and the `OrchestratorConfig`
|
|
10
|
+
doc block; extends the `DeliveryPhase` plan schema additively.
|
|
11
|
+
|
|
12
|
+
## Context
|
|
13
|
+
|
|
14
|
+
Two independent costs lived in the task orchestrator:
|
|
15
|
+
|
|
16
|
+
1. **Wave-barrier idling.** The main loop dispatched a "wave" of up to
|
|
17
|
+
`concurrency` READY tasks and waited for ALL of them (`Promise.all`)
|
|
18
|
+
before recomputing the ready set. A 2-minute task in a wave with a
|
|
19
|
+
30-minute sibling left its slot idle for 28 minutes, and a dependent
|
|
20
|
+
whose only blocker finished early still waited for the whole wave.
|
|
21
|
+
2. **Mission-wide model binding.** One model per mission meant paying the
|
|
22
|
+
strong model's rate for mechanical fill/test leaves, or starving the
|
|
23
|
+
hard design leaf by running everything cheap. Phase plans are the natural
|
|
24
|
+
place to say which leaf needs which worker, but `DeliveryPhase` had no
|
|
25
|
+
dial and deliver bound the executor model once, at mission start.
|
|
26
|
+
|
|
27
|
+
## Decision
|
|
28
|
+
|
|
29
|
+
1. **C1 — completion-triggered dispatch.** The orchestrator keeps an
|
|
30
|
+
in-flight array, fills free slots from the READY set (graph order), races
|
|
31
|
+
on completion, and settles the winner sequentially before refilling.
|
|
32
|
+
Invariants:
|
|
33
|
+
- settlement is strictly one-at-a-time; graph mutations (blackboard,
|
|
34
|
+
skip cascade, repair chains, P5 re-planning) never interleave;
|
|
35
|
+
- WHICH flight settles first is timing-dependent: `outcomes` interleaving
|
|
36
|
+
varies run-to-run and the per-id contract is last-entry-wins. This is
|
|
37
|
+
a deliberate trade of run-to-run reproducibility for utilization;
|
|
38
|
+
- a throwing settle drains in-flight flights (`allSettled` in a
|
|
39
|
+
`finally`) so a seam-callback throw cannot abandon tasks mid-mutation
|
|
40
|
+
(reachable only under completion-triggered dispatch — the wave barrier
|
|
41
|
+
guaranteed an empty in-flight set on any throw).
|
|
42
|
+
2. **C2 — per-leaf model dials.** `DeliveryPhase.model?: string` is an
|
|
43
|
+
optional dial, planner-relayed (copy a key the MISSION text names
|
|
44
|
+
verbatim, never invent one) or hand-authored. Semantics:
|
|
45
|
+
- normalized once at parse (`normalizeModelDial`: lowercased to match
|
|
46
|
+
the preset table, control-character-stripped against log injection,
|
|
47
|
+
64-char cap); the run-state resume sanitizer applies the SAME rules;
|
|
48
|
+
- bound only where a per-task executor exists (isolated + agentic
|
|
49
|
+
tasks); the shared non-isolated executor stays mission-bound at start
|
|
50
|
+
because its write-allowlist refs are shared mutable state;
|
|
51
|
+
- fail-closed: unknown keys, non-isolated tasks, and the blind-executor
|
|
52
|
+
path all degrade to the mission model with a warning naming the actual
|
|
53
|
+
reason, plus a plan-time lint at mission start (not hours later at
|
|
54
|
+
bind time). The preset lookup uses `Object.hasOwn` — prototype-chain
|
|
55
|
+
keys (`constructor`, `__proto__`) are planner-reachable untrusted
|
|
56
|
+
text and must not bind an unpinned model.
|
|
57
|
+
3. **Known debt, out of scope here:** the production projection drops
|
|
58
|
+
`criteria` (pre-existing; the dial threading fixed `model` in the same
|
|
59
|
+
map). Filed for a separate change.
|
|
60
|
+
|
|
61
|
+
## Consequences
|
|
62
|
+
|
|
63
|
+
- Utilization: a slot refills the moment a task settles; dependents start
|
|
64
|
+
when their last dependency succeeds, not when the wave drains.
|
|
65
|
+
- The first C1 tests would literally deadlock under wave scheduling — they
|
|
66
|
+
pin the behavior by construction (a regression manifests as a timeout).
|
|
67
|
+
- Resume round-trips keep dials; the persisted `model` key is additive and
|
|
68
|
+
old readers strip it harmlessly (their allowlist predates the field).
|
|
69
|
+
- Blast radius: `task-orchestrator.ts` (loop), `decompose.ts` (schema +
|
|
70
|
+
prompt), `orchestrated-mission.ts` (threading + P5 feed), `deliver.ts`
|
|
71
|
+
(projection + binding), `run-state.ts` (round-trip), two reference docs.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# ADR 0010: Attention queue and the AFK auto-decider
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-11
|
|
5
|
+
- Spec: unattended autonomy (A1) and the operator attention queue (A2), responding to
|
|
6
|
+
the "blocks on the operator / no safe way to proceed unattended" gap in
|
|
7
|
+
mtlynch.io's *Why Are Coding Agents So Dumb?*
|
|
8
|
+
- Supersedes: none — extends the placement park path (ADR 0006's pending
|
|
9
|
+
approvals) and the session-start surface; adds a new module, no schema it
|
|
10
|
+
depends on changes.
|
|
11
|
+
|
|
12
|
+
## Context
|
|
13
|
+
|
|
14
|
+
When a decision is genuinely the operator's — a placement parked until a
|
|
15
|
+
swap is approved, a grant that exceeds standing consent — the system had
|
|
16
|
+
two bad options: stall the run waiting for a human, or silently drop the
|
|
17
|
+
ask and pick a default nobody signed off on. mtlynch's complaint applies
|
|
18
|
+
verbatim: the agent is either dumb-but-safe (idle) or autonomous-but-
|
|
19
|
+
unaccountable (silent).
|
|
20
|
+
|
|
21
|
+
The missing piece is not more automation; it is a **visible decision
|
|
22
|
+
queue with recorded consequences**:
|
|
23
|
+
|
|
24
|
+
- every ask carries its own context (what was requested, why it parked)
|
|
25
|
+
so the operator can answer it cold, minutes or hours later;
|
|
26
|
+
- every ask carries a **safe default** the asking surface itself vouches
|
|
27
|
+
for — an action that is correct *without further consent* (for a parked
|
|
28
|
+
placement: let the pending request expire on its own TTL — nothing is
|
|
29
|
+
loaded, nothing is evicted);
|
|
30
|
+
- an absent operator is expected, not exceptional: long unattended runs
|
|
31
|
+
are a first-class mode.
|
|
32
|
+
|
|
33
|
+
## Decision
|
|
34
|
+
|
|
35
|
+
1. **A2 — the attention queue** (`src/delivery/attention.ts`):
|
|
36
|
+
`.uap/attention.json` holds bounded, pending-or-resolved items
|
|
37
|
+
(`kind`, `title`, `detail`, `source`, optional `safeDefault{decision,
|
|
38
|
+
rationale}`, `graceDeadline`, `status`). Surfaces:
|
|
39
|
+
- **one queue per CHECKOUT, not per cwd**: the file lives in the MAIN
|
|
40
|
+
checkout's `.uap/` (resolved via `mainCheckoutRoot`, the same anchor
|
|
41
|
+
the policy-liveness fix established on 2026-07-13), because three
|
|
42
|
+
surfaces with three different cwds (dashboard server process, CLI,
|
|
43
|
+
session-start hook inside a worktree) would otherwise write and read
|
|
44
|
+
three different files and the rail would silently no-op — flagged
|
|
45
|
+
BLOCKER by the architecture review. `UAP_ATTENTION_QUEUE` pins an
|
|
46
|
+
explicit file for tests and odd layouts.
|
|
47
|
+
- **enqueue sites** vouch for their own safe default and may PIN the
|
|
48
|
+
item's deadline to the ask's own expiry (`expiresAt`): the placement
|
|
49
|
+
site pins to the pending entry's TTL, so the decider adopts at the
|
|
50
|
+
moment the pending actually expires — never advertising an actionable
|
|
51
|
+
swap for a dead pending, never recording an adoption before the
|
|
52
|
+
expiry it describes. Generic sites use the grace window. Only one
|
|
53
|
+
enqueue site ships with this ADR: the placement admit path (a park
|
|
54
|
+
that found no auto plan) enqueues a review-request sourced
|
|
55
|
+
`placement:<id>` whose safe default is the TTL expiry — the recorded
|
|
56
|
+
rationale says plainly that nothing is loaded or evicted by waiting.
|
|
57
|
+
A resolved ask (forward / auto-load) never enters the queue, and the
|
|
58
|
+
only park reason with options to apply (`not_resident`) gets the
|
|
59
|
+
"apply" instruction; the no-option reasons say so plainly instead of
|
|
60
|
+
inviting a decision that cannot be made.
|
|
61
|
+
- **the asking surface also CLOSES its asks**: applying or dismissing a
|
|
62
|
+
pending placement resolves the matching attention item by origin
|
|
63
|
+
(`resolveAttentionBySource`), so the queue learns of decisions made
|
|
64
|
+
through the placement surface itself — otherwise a later sweep would
|
|
65
|
+
record a false AFK adoption over an operator decision that was
|
|
66
|
+
actually made.
|
|
67
|
+
- **read surfaces**: `uap attention list/ack/decide` (CLI, new) and the
|
|
68
|
+
session-start hook, which sweeps, summarizes what was auto-decided
|
|
69
|
+
while away, and shows the top OPEN items so every session opens with
|
|
70
|
+
what is waiting on the operator. The default view is the open set
|
|
71
|
+
(pending + acknowledged + escalated) — an escalated ask is the
|
|
72
|
+
loudly-unresolved class and must not drop out of the casual view.
|
|
73
|
+
- **enqueue dedupes on open asks**: by `source` (the ledger's rule) AND
|
|
74
|
+
by (kind, title) — an expired-pending client retrying every TTL mints
|
|
75
|
+
a new placement id per retry, and without content dedupe an
|
|
76
|
+
unattended weekend would mint one never-pruned ask per retry. Each
|
|
77
|
+
item still self-resolves at its own pinned deadline.
|
|
78
|
+
- **sanitized twice**: at ingest AND on load. The file lives in
|
|
79
|
+
shared-write `.uap/` (writable by every agent on the project, and a
|
|
80
|
+
cloned third-party repo need not gitignore it), so the load path
|
|
81
|
+
re-applies the control-char/length sanitizer and validates
|
|
82
|
+
`kind`/`status` against their enums — a planted row with ANSI escapes
|
|
83
|
+
or a planted `safeDefault` must not reach operator terminals or ride
|
|
84
|
+
a sweep wearing its `[auto]` label (security review, F1).
|
|
85
|
+
- **fail-open everywhere**: the queue is an observability rail — a
|
|
86
|
+
corrupt or unwritable store logs a warning and never blocks the
|
|
87
|
+
asking surface (all enqueue call sites are best-effort; a failed
|
|
88
|
+
persist returns null instead of a phantom item). `source` is
|
|
89
|
+
server-generated, never client text, and the placement admit path
|
|
90
|
+
rejects model ids outside `[A-Za-z0-9._-]` before any queue write —
|
|
91
|
+
client text never becomes a durable prompt-injection channel. The
|
|
92
|
+
hook renders item text with an untrusted-data disclaimer.
|
|
93
|
+
2. **A1 — the AFK auto-decider** (`sweepExpired`, same module; invoked by
|
|
94
|
+
`uap attention sweep`, at session start, and on a 60s timer inside the
|
|
95
|
+
dashboard server — gated by the same fleet-ownership env as the idle
|
|
96
|
+
sweeper, because during a long unattended run, the exact case this rail
|
|
97
|
+
exists for, session starts are rare and nothing else would fire):
|
|
98
|
+
- an item whose grace window elapsed WITH a recorded safe default has
|
|
99
|
+
the decider's resolution recorded **on the item only** — `status:
|
|
100
|
+
"auto-decided"`, the adopted decision and rationale logged. The
|
|
101
|
+
decider never mutates the underlying system: it adopts what the
|
|
102
|
+
surface already promised would happen unattended (for placement,
|
|
103
|
+
the expiry is the system's own TTL doing the work; the sweep just
|
|
104
|
+
makes the record honest instead of leaving a stale pending row).
|
|
105
|
+
- an item with NO safe default escalates (`status: "escalated"`) — the
|
|
106
|
+
opposite failure mode of mtlynch's complaint is the one we refuse:
|
|
107
|
+
never silently expire an ask that mattered.
|
|
108
|
+
- **operator beats the decider**: any operator resolution (ack keeps
|
|
109
|
+
the item alive but the grace clock keeps running — seen ≠ answered;
|
|
110
|
+
decide closes it) wins over the sweep; sweeps are idempotent against
|
|
111
|
+
already-resolved items; and a decision made through the asking
|
|
112
|
+
surface itself (apply/dismiss) resolves the queue item by origin.
|
|
113
|
+
- **standing consent stays the boundary**: a safe default is only as
|
|
114
|
+
trustworthy as the surface that recorded it (cf. ADR 0007's
|
|
115
|
+
allowlist discipline). The decider adds no authority of its own — it
|
|
116
|
+
is a bookkeeper for promises surfaces already had the right to make.
|
|
117
|
+
3. **Grace window:** env `UAP_ATTENTION_GRACE_SECS` > config
|
|
118
|
+
`attention.graceSecs` > 300s, capped at 86,400 (24h); `0` adopts
|
|
119
|
+
immediately; a cleared/empty env value is treated as unset (fails safe
|
|
120
|
+
to config). The grace window is the DEFAULT deadline — a site that knows
|
|
121
|
+
its ask's real expiry pins it (see Decision §1). `UAP_ATTENTION_OFF=1`
|
|
122
|
+
is the kill switch: no enqueue, no sweep, list returns nothing, and no
|
|
123
|
+
session-start hint — the rail goes quiet end-to-end while the file is
|
|
124
|
+
left alone.
|
|
125
|
+
|
|
126
|
+
## Consequences
|
|
127
|
+
|
|
128
|
+
- A parked placement now has three fates instead of two: answered
|
|
129
|
+
(operator decides — through the queue OR through the placement surface,
|
|
130
|
+
which closes the ask by origin), adopted (default the surface vouched
|
|
131
|
+
for, logged, at the deadline the site pinned), or escalated (no safe
|
|
132
|
+
default — visible, not silent). Never a quiet stall.
|
|
133
|
+
- The session-start hint makes the queue self-advertising: the operator
|
|
134
|
+
does not need to know the command exists to encounter what is waiting —
|
|
135
|
+
and a mid-session sweep in the dashboard keeps the decider alive during
|
|
136
|
+
exactly the unattended runs it exists for.
|
|
137
|
+
- Bounded: open items are never pruned but content-dedupe collapses
|
|
138
|
+
retry-floods to one ask per question; resolved history prunes past 250.
|
|
139
|
+
Open items still only ever leave the queue via ack/decide/sweep — a
|
|
140
|
+
surface that enqueues an ask with NO lifecycle of its own must provide
|
|
141
|
+
its own resolution path or its asks linger by design (visible, not
|
|
142
|
+
silent).
|
|
143
|
+
- **Accepted v1 debt — lost-update race:** writers are unlocked
|
|
144
|
+
read-modify-write (last writer wins); a sweep racing an operator decide
|
|
145
|
+
in the same millisecond window can drop the operator's record. The
|
|
146
|
+
placement ledger solves this with `withLedger` advisory locking; the
|
|
147
|
+
queue inherits it only if the race is ever observed in practice (both
|
|
148
|
+
reviews rated it rideable for an advisory rail; the failure is a lost
|
|
149
|
+
audit row, never a wrong system action).
|
|
150
|
+
- The decider is deliberately weak. Extending it to actually DO things
|
|
151
|
+
(issue grants, trigger displacement) would move it from bookkeeping to
|
|
152
|
+
agency and must be its own ADR with its own consent analysis.
|
|
153
|
+
- Blast radius: new module + new CLI file + command registration; the
|
|
154
|
+
placement admit path gains one best-effort enqueue (with a charset gate
|
|
155
|
+
on model ids); apply/dismiss close their asks; the session-start
|
|
156
|
+
template gains a fail-open block; the dashboard server gains a 60s
|
|
157
|
+
fail-open sweep timer; the settings registry gains four keys. No
|
|
158
|
+
existing schema or contract changes; readers of `.uap/attention.json`
|
|
159
|
+
are only the new surface.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# ADR 0011: Per-session sandbox grants and the network egress boundary
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-11
|
|
5
|
+
- Spec: session authority (D2) + network egress boundary (D1), responding to
|
|
6
|
+
the "agents with too much authority" gap in mtlynch.io's *Why Are Coding
|
|
7
|
+
Agents So Dumb?*
|
|
8
|
+
- Supersedes: none — extends the bubblewrap sandbox (`uap sandbox`) and
|
|
9
|
+
the inference proxy; no existing schema changes.
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
`uap sandbox` (the base floor) is all-or-nothing authority: the workdir and
|
|
14
|
+
scratch are writable, everything else is EROFS, and the only escape hatch
|
|
15
|
+
was `UAP_SANDBOX_ALLOW=path:path` — an **unrecorded** env var: no session
|
|
16
|
+
scope, no expiry, no reason, invisible to the operator, surviving forever
|
|
17
|
+
in shell histories and hook templates. Meanwhile the sandbox shared the
|
|
18
|
+
host network by design (the agent must reach the proxy), so "too much
|
|
19
|
+
authority" had two open faces: unrecorded write holes and unrestricted
|
|
20
|
+
network egress.
|
|
21
|
+
|
|
22
|
+
The bar: **every new authority must state who consented.**
|
|
23
|
+
|
|
24
|
+
## Decision
|
|
25
|
+
|
|
26
|
+
### D2 — the consent ledger (`src/policies/sandbox-grants.ts`)
|
|
27
|
+
|
|
28
|
+
Every extra writable hole is a **grant**, recorded in
|
|
29
|
+
`.uap/sandbox-grants.json` at the main checkout root (the
|
|
30
|
+
`mainCheckoutRoot` anchor the attention queue established in ADR 0010 —
|
|
31
|
+
one ledger per checkout, shared by every surface; `UAP_SANDBOX_GRANTS`
|
|
32
|
+
pins an explicit file):
|
|
33
|
+
|
|
34
|
+
- `uap sandbox grant <path...> --session <id> --reason "..." [--ttl N]`
|
|
35
|
+
creates a grant `{id, sessionId, paths[], reason, grantedBy, grantedAt,
|
|
36
|
+
expiresAt}`. A grant is refused without a session, a stated reason, or a
|
|
37
|
+
grantable target (existing absolute path; `/`, `/home`, `$HOME` itself
|
|
38
|
+
are refused — an over-broad grant IS a sandbox escape). Default TTL 8h,
|
|
39
|
+
hard cap 7d: authority is bounded by construction. An identical open
|
|
40
|
+
grant is RENEWED, not duplicated.
|
|
41
|
+
- `uap sandbox --session <id> -- <cmd>` resolves the session's ACTIVE
|
|
42
|
+
grants and opens exactly those `--bind` holes; `uap sandbox grants` /
|
|
43
|
+
`revoke <id>` list and remove them. (The `--` separator is REQUIRED
|
|
44
|
+
before the command: without it, the sandbox's own flags — `--session`,
|
|
45
|
+
`--net`, `--ttl`, `--reason`, `--json` — are parsed as sandbox options,
|
|
46
|
+
not passed through to the session command.)
|
|
47
|
+
- Every run appends a line to `.uap/sandbox-audit.jsonl` — the consent
|
|
48
|
+
record: which grants were used, which network mode engaged, and any
|
|
49
|
+
legacy `UAP_SANDBOX_ALLOW` entries labeled `unrecordedAllow` (still
|
|
50
|
+
honored for compatibility, now named in the audit and the banner).
|
|
51
|
+
- Store rules inherited from ADR 0010's hardened queue: sanitize at ingest
|
|
52
|
+
AND on load (the ledger is shared-write `.uap/` and a planted row must
|
|
53
|
+
not become a kernel-writable hole), atomic writes with unique tmp
|
|
54
|
+
names, malformed rows dropped, and an unreadable ledger treated as
|
|
55
|
+
EMPTY — the safe direction for an authority store is least authority.
|
|
56
|
+
"Sanitize" for this ledger means more than text fields: the load path
|
|
57
|
+
RE-VALIDATES every path with the full grantable-path rule (absolute,
|
|
58
|
+
existing, symlink-resolved, and not `/`, `/home`, or `$HOME` — the P4
|
|
59
|
+
review caught that a string-prefix check alone would let a planted
|
|
60
|
+
well-formed row bind the whole root) and refuses expiries beyond the
|
|
61
|
+
hard TTL bound (a planted `expiresAt: "9999"` must never load).
|
|
62
|
+
`UAP_SANDBOX_GRANTS_OFF=1` resolves zero grants; the sandbox's kernel
|
|
63
|
+
boundary itself is never disabled by any of this.
|
|
64
|
+
- **Consent must come from the operator, not from inside the sandbox**
|
|
65
|
+
(the security review's forgeability finding, three parts):
|
|
66
|
+
1. `grant`/`revoke` are REFUSED when `UAP_SANDBOX_ACTIVE=1` — that env
|
|
67
|
+
is set by the sandbox itself on the untrusted side, so an agent
|
|
68
|
+
cannot write new authority or remove the operator's (listing stays
|
|
69
|
+
allowed: read-only).
|
|
70
|
+
2. The planner `--ro-bind`s the ledger and audit files over their host
|
|
71
|
+
paths in EVERY sandbox namespace (a missing file gets `/dev/null`
|
|
72
|
+
bound at the path). A bind-mounted file cannot be unlinked, replaced,
|
|
73
|
+
or written inside the namespace, so the sandboxed agent can neither
|
|
74
|
+
plant grant rows nor forge audit lines — the load-path re-validation
|
|
75
|
+
checks SHAPE, but shape cannot establish WHO consented; the kernel
|
|
76
|
+
mount does.
|
|
77
|
+
3. Granted paths are re-resolved (`realpath`) at PLAN time and the
|
|
78
|
+
resolved value is re-checked for breadth, closing the load→exec
|
|
79
|
+
TOCTOU window where a concurrent process could swap a granted path
|
|
80
|
+
for a symlink. (A microsecond window between this resolution and
|
|
81
|
+
bwrap's own remains — accepted, documented.)
|
|
82
|
+
- Fail-closed everywhere authority is parsed: a garbage or negative
|
|
83
|
+
`--ttl` is REFUSED (the old expression silently granted the 8h default
|
|
84
|
+
— the wrong direction); `--ttl 0` floors to 1s; an unknown
|
|
85
|
+
`UAP_SANDBOX_NET` value fails loud with exit 2 rather than silently
|
|
86
|
+
meaning FULL network; and every persist is followed by a reload
|
|
87
|
+
verification (a concurrent writer's lost update is retried once, then
|
|
88
|
+
reported as failure — never a silent success).
|
|
89
|
+
|
|
90
|
+
### D1 — the network egress boundary
|
|
91
|
+
|
|
92
|
+
**`--net off`** — `uap sandbox --net off` adds `--unshare-net`
|
|
93
|
+
with nothing brought up: no TCP, no DNS, not even loopback, at the kernel.
|
|
94
|
+
Any `connect()` fails. This is the complete boundary, intended for
|
|
95
|
+
untrusted code, evals, and data-only work; a harness that needs the model
|
|
96
|
+
fails loudly with connection refused, which is the boundary working. The
|
|
97
|
+
net mode defaults from `UAP_SANDBOX_NET=off`; every run's actual mode is
|
|
98
|
+
audit-logged (`netMode: shared|off`).
|
|
99
|
+
|
|
100
|
+
**The proxy's unix-socket surface** — `PROXY_SOCKET=<path>`
|
|
101
|
+
makes `anthropic_proxy.py` serve the SAME app on an AF_UNIX socket
|
|
102
|
+
(0600, parent dir 0700) alongside its TCP port: a permission-gated model
|
|
103
|
+
choke point for shared boxes. Stale-socket policy (hardened by the
|
|
104
|
+
security review): a live listener owns its path — and "live" includes a
|
|
105
|
+
socket that cannot be dialed (`EACCES`/`EPERM` is not proof of death, so
|
|
106
|
+
another user's socket is never evicted and impersonated); only a refused
|
|
107
|
+
or missing socket is a corpse. A non-socket file at the path is NEVER
|
|
108
|
+
unlinked (`PROXY_SOCKET` can arrive from the operator's env or the
|
|
109
|
+
sandbox-writable `.uap/proxy.env`, so a typo or planted value must not
|
|
110
|
+
become an arbitrary-file-delete primitive), and a group/other-writable
|
|
111
|
+
parent dir draws a warning (another local user could unlink or squat the
|
|
112
|
+
path). `uap proxy connect --socket <path> [--port N] --allow-anyone`
|
|
113
|
+
re-serves that socket on local TCP for clients that cannot speak
|
|
114
|
+
AF_UNIX — behind an explicit consent flag, because while the relay runs
|
|
115
|
+
it inverts the socket's gate: every local user can reach the model path
|
|
116
|
+
through the loopback port.
|
|
117
|
+
|
|
118
|
+
### D1's rejected design: the "only proxy" netns (and why)
|
|
119
|
+
|
|
120
|
+
The intended D1 was a netns cut with a SINGLE hole to the proxy: bwrap
|
|
121
|
+
`--unshare-net` plus a bind-mounted `PROXY_SOCKET`, with an in-namespace
|
|
122
|
+
relay re-serving the proxy on `127.0.0.1:4000`. **It cannot work on
|
|
123
|
+
rootless Linux**, and the failure is worth recording because the design
|
|
124
|
+
looks correct on paper:
|
|
125
|
+
|
|
126
|
+
1. **AF_UNIX pathname sockets are network-namespace-scoped.** The socket
|
|
127
|
+
file bind-mounts fine, but `connect()` looks the listener up in the
|
|
128
|
+
CONNECTING process's netns unix-socket table and finds nothing:
|
|
129
|
+
empirically `ECONNREFUSED` from inside the namespace while the same
|
|
130
|
+
socket answers on the host (verified on Linux 7.2, bwrap 0.11). The
|
|
131
|
+
hole through the wall is a file that carries no reachability.
|
|
132
|
+
2. **bwrap closes every inherited fd** (verified: an fd handed to the
|
|
133
|
+
child via `stdio` slot 3 arrives closed), so an SCM_RIGHTS broker that
|
|
134
|
+
passes pre-connected proxy sockets across the wall is impossible too.
|
|
135
|
+
3. **The userspace network stacks cannot express the policy.**
|
|
136
|
+
pasta/slirp4netns bridge a rootless netns to the host, but neither
|
|
137
|
+
offers an outbound port allowlist — attaching one would give the
|
|
138
|
+
sandbox FULL egress, the exact opposite of the boundary.
|
|
139
|
+
|
|
140
|
+
A boundary that requires a helper that isn't installed, or a mechanism
|
|
141
|
+
the kernel doesn't provide, would be a suggestion rather than enforcement.
|
|
142
|
+
So `--net proxy` was **cut, not faked**: the two REAL modes are `shared`
|
|
143
|
+
(no claim) and `off` (kernel-complete), and the model-path choke point
|
|
144
|
+
(`PROXY_SOCKET`, 0600) ships on the proxy side where permission gating
|
|
145
|
+
actually holds.
|
|
146
|
+
|
|
147
|
+
## Consequences
|
|
148
|
+
|
|
149
|
+
- Write authority beyond the workdir requires a recorded, bounded,
|
|
150
|
+
revocable consent. The legacy `UAP_SANDBOX_ALLOW` env still works for
|
|
151
|
+
compatibility — every use is audited and named as unrecorded — and its
|
|
152
|
+
entries get the same plan-time realpath re-check recorded grants get:
|
|
153
|
+
a symlink entry binds its resolved target, and an entry whose resolution
|
|
154
|
+
is `/`, `/home`, or `$HOME` is refused with an operator-visible note,
|
|
155
|
+
never bound.
|
|
156
|
+
- `--net off` gives untrusted-code runs a complete, kernel-enforced
|
|
157
|
+
egress boundary.
|
|
158
|
+
- Model-path gating becomes a socket permission problem (0600/0700) for
|
|
159
|
+
multi-user or sandboxed-adjacent setups, rather than a TCP port anyone
|
|
160
|
+
on the box can dial.
|
|
161
|
+
- The "agent session with ONLY model egress" use case remains open. The
|
|
162
|
+
honest paths to it: (a) upstream `pasta`-class outbound filtering, if it
|
|
163
|
+
ever grows port allowlists; (b) a setuid/privileged broker that can
|
|
164
|
+
join the sandbox netns; (c) rootful sandboxing (a future
|
|
165
|
+
`--net proxy,via:root` could revisit). None are rootless today, so none
|
|
166
|
+
are claimed.
|
|
167
|
+
- The audit JSONL is bounded: every line carries a monotonic `seq`, and
|
|
168
|
+
past 12,000 lines it compacts to the 10,000 newest event lines — plus
|
|
169
|
+
the triggering event and one `audit-compaction` marker recording the
|
|
170
|
+
drop, so the file lands at 10,002 — a pruned trail is an EXPECTED
|
|
171
|
+
state, never a silent one. Seq numbering is continuous across
|
|
172
|
+
compaction AND audit-file deletion: the trust anchor carries the
|
|
173
|
+
witnessed seq high-water (`seqHi`), and each line numbers from the max
|
|
174
|
+
of the file's last seq and the anchor's — a trail whose numbering can
|
|
175
|
+
restart is a forgeable trail. Honest limits: concurrent audit appends
|
|
176
|
+
can mint the same next-seq before either lands (a duplicate seq is
|
|
177
|
+
evidence to investigate, not proof of planting on its own), and an
|
|
178
|
+
older binary's next recorded mutation drops the optional `seqHi` field
|
|
179
|
+
— it is recomputed from the file at the next current-binary mutation,
|
|
180
|
+
which only matters if the audit file is deleted inside that
|
|
181
|
+
mixed-version window.
|
|
182
|
+
- The dashboard's session view lists the active authority per session —
|
|
183
|
+
id, session, remaining TTL, reason, paths, and the revoke command per
|
|
184
|
+
grant — plus the trust-anchor verdict (an INTEGRITY MISMATCH renders
|
|
185
|
+
red, rows listed-but-unverified, same contract as the CLI listing).
|
|
186
|
+
The per-checkout-root anchoring guarantees it reads the same ledger the
|
|
187
|
+
sandbox does.
|
|
188
|
+
- Tamper evidence (out-of-checkout anchor): every successful
|
|
189
|
+
grant/renew/revoke writes a 0600 anchor at
|
|
190
|
+
`$XDG_STATE_HOME/uap/sandbox-anchors/<hash-of-main-checkout-root>.json`
|
|
191
|
+
(`UAP_SANDBOX_ANCHOR_DIR` pins the dir for tests) recording the SHA-256
|
|
192
|
+
of the sanitized ledger as of that recorded operation. The key is the
|
|
193
|
+
MAIN checkout root (the same `git rev-parse --git-common-dir` root the
|
|
194
|
+
session-start hook and the python tools use), so a worktree session and
|
|
195
|
+
its main checkout share ONE anchor for the ONE ledger they share — a
|
|
196
|
+
worktree session is never silently unanchored, and a cross-cwd grant
|
|
197
|
+
never fabricates a mismatch. A granted path covering the anchor dir is
|
|
198
|
+
refused (a hole over the anchor would let the sandboxed side de-anchor
|
|
199
|
+
itself silently). Verification (`uap sandbox grants` states it; the
|
|
200
|
+
launch path enforces it) compares the on-disk ledger against the
|
|
201
|
+
anchor: a mismatch means something with the CHECKOUT's write reach
|
|
202
|
+
changed the ledger outside a recorded consent operation — new grants
|
|
203
|
+
are refused, and the session resolves ZERO grant-derived holes
|
|
204
|
+
(fail-closed; the kernel boundary is unaffected). A `revoke` on a
|
|
205
|
+
mismatched ledger still removes authority (the safe direction) but
|
|
206
|
+
re-anchors the remaining rows as the new baseline and names that at
|
|
207
|
+
the moment it stops being detectable.
|
|
208
|
+
What this remains honest about: it is tamper-EVIDENCE, not origin
|
|
209
|
+
proof. The anchor lives in the user's state dir, so a same-user process
|
|
210
|
+
OUTSIDE the checkout can still write it; a plant that predates the
|
|
211
|
+
first anchor is baselined into it (bootstrap cannot verify history);
|
|
212
|
+
and true origin proof (a different uid, a signed ledger, a hardware
|
|
213
|
+
key) stays open on purpose — the threat model closes the agent-side
|
|
214
|
+
vectors, and the anchor closes the checkout-write-reach side of the
|
|
215
|
+
operator residual.
|
|
216
|
+
- Residual, accepted: the microsecond TOCTOU between the planner's
|
|
217
|
+
`realpath` and bwrap's own bind (a concurrent process with write on a
|
|
218
|
+
granted path's parent could still race it), and the anchor's same-user
|
|
219
|
+
limit named above.
|