@skrr-ai/cli 0.1.119 → 0.1.121
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/dist/base-command.d.ts +31 -0
- package/dist/base-command.js +117 -6
- package/dist/commands/agent/message/context.d.ts +1 -0
- package/dist/commands/agent/message/context.js +3 -2
- package/dist/commands/commands.js +11 -0
- package/dist/commands/commitments/action-proposal.d.ts +13 -0
- package/dist/commands/commitments/action-proposal.js +39 -0
- package/dist/commands/commitments/action-proposals/decide.js +8 -1
- package/dist/commands/commitments/action-proposals/execute.js +11 -1
- package/dist/commands/commitments/create.js +2 -2
- package/dist/commands/commitments/doctor.d.ts +8 -0
- package/dist/commands/commitments/doctor.js +22 -0
- package/dist/commands/commitments/execute-remediation.js +12 -1
- package/dist/commands/commitments/observations.js +2 -2
- package/dist/commands/commitments/pack/bind.d.ts +18 -1
- package/dist/commands/commitments/pack/bind.js +66 -8
- package/dist/commands/commitments/pack/plan.d.ts +4 -1
- package/dist/commands/commitments/pack/plan.js +30 -2
- package/dist/commands/commitments/update.js +8 -2
- package/dist/commands/completion/index.js +1 -1
- package/dist/commands/completion/values.js +1 -1
- package/dist/commands/computer-use/audio.d.ts +1 -0
- package/dist/commands/computer-use/audio.js +1 -0
- package/dist/commands/computer-use/browser.d.ts +1 -0
- package/dist/commands/computer-use/browser.js +1 -0
- package/dist/commands/computer-use/disable.d.ts +1 -0
- package/dist/commands/computer-use/disable.js +1 -0
- package/dist/commands/computer-use/enable.d.ts +1 -0
- package/dist/commands/computer-use/enable.js +1 -0
- package/dist/commands/computer-use/index.d.ts +1 -0
- package/dist/commands/computer-use/index.js +1 -0
- package/dist/commands/computer-use/install.d.ts +1 -0
- package/dist/commands/computer-use/install.js +1 -0
- package/dist/commands/computer-use/recover.d.ts +1 -0
- package/dist/commands/computer-use/recover.js +1 -0
- package/dist/commands/computer-use/status.d.ts +1 -0
- package/dist/commands/computer-use/status.js +1 -0
- package/dist/commands/computer-use/stop.d.ts +1 -0
- package/dist/commands/computer-use/stop.js +1 -0
- package/dist/commands/context.js +16 -11
- package/dist/commands/convos/recovery.js +1 -1
- package/dist/commands/daemon/diagnostics.d.ts +9 -0
- package/dist/commands/daemon/diagnostics.js +53 -0
- package/dist/commands/daemon/index.js +2 -0
- package/dist/commands/daemon/maintenance/attest.d.ts +15 -0
- package/dist/commands/daemon/maintenance/attest.js +52 -0
- package/dist/commands/daemon/maintenance/hold.d.ts +17 -0
- package/dist/commands/daemon/maintenance/hold.js +70 -0
- package/dist/commands/daemon/maintenance/index.d.ts +12 -0
- package/dist/commands/daemon/maintenance/index.js +30 -0
- package/dist/commands/daemon/maintenance/resume.d.ts +13 -0
- package/dist/commands/daemon/maintenance/resume.js +36 -0
- package/dist/commands/daemon/maintenance/status.d.ts +13 -0
- package/dist/commands/daemon/maintenance/status.js +36 -0
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +50 -6
- package/dist/commands/followups/plans/cancel-batch.js +6 -3
- package/dist/commands/followups/plans/capacity.js +7 -3
- package/dist/commands/followups/plans/observability.js +11 -3
- package/dist/commands/followups/plans/preferences/set.js +26 -8
- package/dist/commands/followups/plans/preferences.js +1 -1
- package/dist/commands/followups/plans/show.js +1 -1
- package/dist/commands/goals/key-results/assess.d.ts +36 -0
- package/dist/commands/goals/key-results/assess.js +110 -0
- package/dist/commands/goals/key-results/history.d.ts +30 -0
- package/dist/commands/goals/key-results/history.js +104 -0
- package/dist/commands/goals/key-results/list.d.ts +5 -4
- package/dist/commands/goals/key-results/list.js +12 -6
- package/dist/commands/goals/key-results/show.d.ts +4 -2
- package/dist/commands/goals/key-results/show.js +25 -4
- package/dist/commands/goals/key-results/update.d.ts +4 -0
- package/dist/commands/goals/key-results/update.js +18 -24
- package/dist/commands/goals/plan-now.js +12 -1
- package/dist/commands/goals/planner-compare.d.ts +30 -0
- package/dist/commands/goals/planner-compare.js +136 -0
- package/dist/commands/machines/dedicated/service/exec.d.ts +3 -0
- package/dist/commands/machines/dedicated/service/exec.js +70 -11
- package/dist/commands/machines/hosted/destroy.d.ts +1 -0
- package/dist/commands/machines/hosted/destroy.js +11 -1
- package/dist/commands/machines/hosted/exec.d.ts +1 -0
- package/dist/commands/machines/hosted/exec.js +12 -1
- package/dist/commands/machines/hosted/index.js +1 -0
- package/dist/commands/machines/hosted/operation.d.ts +14 -0
- package/dist/commands/machines/hosted/operation.js +51 -0
- package/dist/commands/machines/hosted/pull.js +1 -1
- package/dist/commands/machines/hosted/start.js +6 -2
- package/dist/commands/search/agents.js +5 -1
- package/dist/commands/search/all.js +74 -22
- package/dist/commands/search/skills.d.ts +1 -0
- package/dist/commands/search/skills.js +12 -1
- package/dist/commands/search/spaces.d.ts +8 -1
- package/dist/commands/search/spaces.js +50 -12
- package/dist/commands/search/tasks.d.ts +13 -2
- package/dist/commands/search/tasks.js +78 -14
- package/dist/commands/search/teams.js +5 -1
- package/dist/commands/search/workspace.d.ts +19 -0
- package/dist/commands/search/workspace.js +126 -0
- package/dist/commands/skills/browse.d.ts +1 -0
- package/dist/commands/skills/browse.js +8 -1
- package/dist/commands/skills/search.d.ts +1 -0
- package/dist/commands/skills/search.js +12 -1
- package/dist/commands/spaces/auto-executor/compare.d.ts +18 -0
- package/dist/commands/spaces/auto-executor/compare.js +139 -0
- package/dist/commands/spaces/auto-executor/runs.d.ts +3 -0
- package/dist/commands/spaces/auto-executor/runs.js +13 -1
- package/dist/commands/spaces/charter/create-from-description.d.ts +21 -0
- package/dist/commands/spaces/charter/create-from-description.js +77 -0
- package/dist/commands/spaces/charter/show.d.ts +22 -0
- package/dist/commands/spaces/charter/show.js +61 -0
- package/dist/commands/spaces/heartbeats/list.d.ts +1 -16
- package/dist/commands/spaces/heartbeats/list.js +13 -0
- package/dist/commands/spaces/install-template.js +3 -0
- package/dist/commands/spaces/list.d.ts +1 -0
- package/dist/commands/spaces/list.js +12 -3
- package/dist/commands/spaces/loops/migrate.d.ts +32 -0
- package/dist/commands/spaces/loops/migrate.js +103 -0
- package/dist/commands/spaces/loops/release.d.ts +14 -0
- package/dist/commands/spaces/loops/release.js +59 -0
- package/dist/commands/spaces/notebook.d.ts +25 -0
- package/dist/commands/spaces/notebook.js +75 -0
- package/dist/commands/spaces/proposals/discard.d.ts +17 -0
- package/dist/commands/spaces/proposals/discard.js +17 -0
- package/dist/commands/spaces/proposals/keep.d.ts +17 -0
- package/dist/commands/spaces/proposals/keep.js +17 -0
- package/dist/commands/spaces/proposals/list.d.ts +24 -0
- package/dist/commands/spaces/proposals/list.js +89 -0
- package/dist/commands/spaces/search.d.ts +1 -0
- package/dist/commands/spaces/search.js +9 -0
- package/dist/commands/spaces/skills/propose.d.ts +29 -0
- package/dist/commands/spaces/skills/propose.js +82 -0
- package/dist/commands/spaces/templates/fork.js +3 -0
- package/dist/commands/spaces/triggers/show.js +6 -0
- package/dist/commands/spaces/workflows/retry.d.ts +1 -0
- package/dist/commands/spaces/workflows/retry.js +8 -1
- package/dist/commands/spaces/workflows/triggers/create.js +3 -2
- package/dist/commands/spaces/workflows/triggers/fire.js +4 -0
- package/dist/commands/spaces/workflows/triggers/update.js +2 -1
- package/dist/commands/spaces/write-policy/set.d.ts +22 -0
- package/dist/commands/spaces/write-policy/set.js +82 -0
- package/dist/commands/spaces/write-policy/show.d.ts +19 -0
- package/dist/commands/spaces/write-policy/show.js +49 -0
- package/dist/commands/store/artifacts/receipt.d.ts +14 -0
- package/dist/commands/store/artifacts/receipt.js +34 -0
- package/dist/commands/store/artifacts/release.d.ts +2 -0
- package/dist/commands/store/artifacts/release.js +11 -0
- package/dist/commands/store/governance/publish-release.d.ts +2 -0
- package/dist/commands/store/governance/publish-release.js +30 -1
- package/dist/commands/store/governance/release-receipt.d.ts +12 -0
- package/dist/commands/store/governance/release-receipt.js +32 -0
- package/dist/commands/tasks/approvals/list.d.ts +11 -5
- package/dist/commands/tasks/approvals/list.js +13 -6
- package/dist/commands/tasks/attachments/facts.d.ts +12 -0
- package/dist/commands/tasks/attachments/facts.js +29 -0
- package/dist/commands/tasks/attachments/upload.d.ts +1 -0
- package/dist/commands/tasks/attachments/upload.js +16 -0
- package/dist/commands/tasks/bulk-update.js +35 -1
- package/dist/commands/tasks/context.d.ts +15 -0
- package/dist/commands/tasks/context.js +69 -0
- package/dist/commands/tasks/copy.d.ts +18 -0
- package/dist/commands/tasks/copy.js +85 -0
- package/dist/commands/tasks/create-tree.js +97 -23
- package/dist/commands/tasks/create.js +9 -1
- package/dist/commands/tasks/deliverable/add.js +20 -10
- package/dist/commands/tasks/move.js +8 -1
- package/dist/commands/tasks/operations/show.d.ts +16 -0
- package/dist/commands/tasks/operations/show.js +98 -0
- package/dist/commands/tasks/operations/wait.d.ts +22 -0
- package/dist/commands/tasks/operations/wait.js +114 -0
- package/dist/commands/tasks/preflight.d.ts +1 -0
- package/dist/commands/tasks/preflight.js +11 -1
- package/dist/commands/tasks/rearm-recurrence.js +4 -0
- package/dist/commands/tasks/result/show.js +22 -0
- package/dist/commands/tasks/result/submit.d.ts +1 -0
- package/dist/commands/tasks/result/submit.js +3 -0
- package/dist/commands/tasks/retry-receipt.d.ts +15 -0
- package/dist/commands/tasks/retry-receipt.js +89 -0
- package/dist/commands/tasks/retry-review.d.ts +16 -0
- package/dist/commands/tasks/retry-review.js +66 -0
- package/dist/commands/tasks/retry.d.ts +4 -8
- package/dist/commands/tasks/retry.js +34 -68
- package/dist/commands/tasks/runs.d.ts +21 -0
- package/dist/commands/tasks/runs.js +95 -0
- package/dist/commands/tasks/search.d.ts +1 -0
- package/dist/commands/tasks/search.js +9 -0
- package/dist/commands/tasks/show.d.ts +2 -0
- package/dist/commands/tasks/show.js +15 -1
- package/dist/commands/tasks/start-receipt.d.ts +15 -0
- package/dist/commands/tasks/start-receipt.js +95 -0
- package/dist/commands/tasks/start.d.ts +4 -0
- package/dist/commands/tasks/start.js +49 -3
- package/dist/commands/tasks/update.js +3 -0
- package/dist/commands/tasks/upsert.js +17 -3
- package/dist/commands/tasks/watch.d.ts +3 -0
- package/dist/commands/tasks/watch.js +85 -20
- package/dist/commands/wiki/history.d.ts +33 -0
- package/dist/commands/wiki/history.js +134 -0
- package/dist/commands/wiki/ls.d.ts +2 -0
- package/dist/commands/wiki/ls.js +8 -0
- package/dist/commands/wiki/update.d.ts +3 -1
- package/dist/commands/wiki/update.js +24 -2
- package/dist/commands/wiki/write.d.ts +22 -0
- package/dist/commands/wiki/write.js +144 -6
- package/dist/generated/agent-guides.json +81 -81
- package/dist/generated/managed-instruction-guides.json +1 -1
- package/dist/lib/agent-guides.d.ts +9 -0
- package/dist/lib/agent-guides.js +23 -1
- package/dist/lib/agentic-stream.d.ts +5 -0
- package/dist/lib/agentic-stream.js +122 -3
- package/dist/lib/api-error-reason.d.ts +2 -0
- package/dist/lib/api-error-reason.js +14 -0
- package/dist/lib/api-fetch.js +3 -0
- package/dist/lib/auth-diagnostics.d.ts +39 -0
- package/dist/lib/auth-diagnostics.js +71 -0
- package/dist/lib/cli-client-identity.d.ts +6 -0
- package/dist/lib/cli-client-identity.js +16 -0
- package/dist/lib/cli-installation-diagnostics.d.ts +52 -0
- package/dist/lib/cli-installation-diagnostics.js +254 -0
- package/dist/lib/command-hygiene.d.ts +45 -0
- package/dist/lib/command-hygiene.js +413 -0
- package/dist/lib/commitments.d.ts +64 -3
- package/dist/lib/commitments.js +34 -6
- package/dist/lib/completion-index.d.ts +16 -4
- package/dist/lib/completion-index.js +50 -14
- package/dist/lib/config.js +7 -1
- package/dist/lib/daemon-maintenance-command.d.ts +22 -0
- package/dist/lib/daemon-maintenance-command.js +39 -0
- package/dist/lib/daemon-maintenance.d.ts +275 -0
- package/dist/lib/daemon-maintenance.js +1035 -0
- package/dist/lib/dedicated-service.d.ts +3 -2
- package/dist/lib/dedicated-service.js +11 -3
- package/dist/lib/governed-store.d.ts +8 -0
- package/dist/lib/governed-store.js +5 -0
- package/dist/lib/hosted-machines.d.ts +33 -2
- package/dist/lib/hosted-machines.js +19 -1
- package/dist/lib/json-output.d.ts +1 -1
- package/dist/lib/machines.d.ts +9 -1
- package/dist/lib/machines.js +15 -1
- package/dist/lib/node-adapter.js +13 -0
- package/dist/lib/record-output.d.ts +22 -0
- package/dist/lib/record-output.js +78 -0
- package/dist/lib/roles.d.ts +3 -3
- package/dist/lib/scoped-memory-command.d.ts +18 -0
- package/dist/lib/scoped-memory-command.js +53 -2
- package/dist/lib/search-display.d.ts +7 -0
- package/dist/lib/search-display.js +31 -0
- package/dist/lib/skill-store-pagination.d.ts +8 -0
- package/dist/lib/skill-store-pagination.js +35 -0
- package/dist/lib/space-charter.d.ts +49 -0
- package/dist/lib/space-charter.js +78 -0
- package/dist/lib/space-loop-migration.d.ts +18 -0
- package/dist/lib/space-loop-migration.js +128 -0
- package/dist/lib/space-notebook.d.ts +22 -0
- package/dist/lib/space-notebook.js +140 -0
- package/dist/lib/space-proposal-decision-command.d.ts +36 -0
- package/dist/lib/space-proposal-decision-command.js +108 -0
- package/dist/lib/space-proposals.d.ts +44 -0
- package/dist/lib/space-proposals.js +186 -0
- package/dist/lib/space-templates.d.ts +8 -0
- package/dist/lib/space-templates.js +17 -0
- package/dist/lib/space-write-policy.d.ts +24 -0
- package/dist/lib/space-write-policy.js +103 -0
- package/dist/lib/task-agent-context-capabilities.d.ts +10 -0
- package/dist/lib/task-agent-context-capabilities.js +22 -0
- package/dist/lib/task-agent-context-compatibility.d.ts +9 -0
- package/dist/lib/task-agent-context-compatibility.js +30 -0
- package/dist/lib/task-asset-upload.d.ts +22 -0
- package/dist/lib/task-asset-upload.js +257 -29
- package/dist/lib/task-batch.d.ts +15 -1
- package/dist/lib/task-batch.js +35 -2
- package/dist/lib/task-operation-watch.d.ts +66 -0
- package/dist/lib/task-operation-watch.js +159 -0
- package/dist/lib/task-resolver.d.ts +1 -1
- package/dist/lib/task-resolver.js +4 -3
- package/dist/lib/task-work.d.ts +1 -1
- package/dist/lib/task-work.js +35 -1
- package/dist/lib/task-write-attribution.d.ts +14 -0
- package/dist/lib/task-write-attribution.js +35 -2
- package/dist/lib/triggers.d.ts +11 -0
- package/dist/lib/triggers.js +7 -0
- package/dist/lib/wiki-update.d.ts +1 -0
- package/dist/lib/wiki-update.js +10 -1
- package/dist/lib/wiki.js +4 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +31 -15
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +10 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonConsentProof.d.ts +20 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonConsentProof.js +72 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonWire.js +13 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/desktopDaemonProof.d.ts +21 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/desktopDaemonProof.js +96 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +2 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +102 -20
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/guidedUpdate.d.ts +370 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/guidedUpdate.js +315 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.d.ts +27 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +161 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineScopes.d.ts +26 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineScopes.js +41 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +20 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/maintenanceReportWire.d.ts +178 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/maintenanceReportWire.js +262 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/toolApprovalSignature.d.ts +51 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/toolApprovalSignature.js +106 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +32 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +10 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +172 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonConsentProof.d.ts +20 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonConsentProof.js +43 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonWire.js +33 -14
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/desktopDaemonProof.d.ts +21 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/desktopDaemonProof.js +62 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +2 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +133 -25
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/guidedUpdate.d.ts +370 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/guidedUpdate.js +404 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.d.ts +27 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +132 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineScopes.d.ts +26 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineScopes.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +28 -15
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/maintenanceReportWire.d.ts +178 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/maintenanceReportWire.js +342 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/toolApprovalSignature.d.ts +51 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/toolApprovalSignature.js +138 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +51 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +27181 -20293
- package/oclif.manifest.json +5745 -1813
- package/package.json +20 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure core of a GUIDED update (OSK-19107, Space OSK-S-524, tasks M51 to M53):
|
|
3
|
+
* hold the machine, wait for it to be quiet, update, verify, and give the machine
|
|
4
|
+
* back. The Desktop app and a standalone `skrr` install both run it, so what they
|
|
5
|
+
* decide on, and the words they use for what is in the way, cannot drift apart.
|
|
6
|
+
*
|
|
7
|
+
* This file is DECISIONS ONLY. It performs no I/O, reads no clock and holds no
|
|
8
|
+
* state, so every rule below is a table test. The orchestration (a persisted flow
|
|
9
|
+
* record, polling, the installer, the supervisor) lives with each caller, because
|
|
10
|
+
* that is the part that differs between a Desktop that quits mid-flow and a CLI that
|
|
11
|
+
* runs in the foreground.
|
|
12
|
+
*
|
|
13
|
+
* ## Why these rules (each names the failure it prevents)
|
|
14
|
+
*
|
|
15
|
+
* - **`prepare` is a probe that briefly refuses admitted work.** While it runs the
|
|
16
|
+
* daemon refuses admitted tool calls with an infra-class error, and a tool-proxy Run
|
|
17
|
+
* that sees one drops its tools to the Hosted Machine for good. So a guided flow
|
|
18
|
+
* calls it ONLY when the server's reading says there is nothing admitted to refuse
|
|
19
|
+
* ({@link prepareBasis}), never to find out whether there is.
|
|
20
|
+
* - **`unknown` is an answer.** A source nobody could read is never zero and never
|
|
21
|
+
* ready. {@link noServerVeto} is deliberately strict: every server source must have
|
|
22
|
+
* been read, and none may list active or unproven work.
|
|
23
|
+
* - **A server source can only veto.** Only the daemon's own positively announced
|
|
24
|
+
* report grants `ready`; the owner's attestation and the daemon's own `prepare`
|
|
25
|
+
* (labelled `no_veto`, D38) are the two other bases, and each is labelled as what
|
|
26
|
+
* it is wherever it is shown.
|
|
27
|
+
* - **Blockers are shown in the words of whoever knows them.** The daemon's blocker
|
|
28
|
+
* `reason` and the server's holder `label` are the vocabulary; nothing here
|
|
29
|
+
* re-spells them. They are bounded and never a raw path, because a diagnostic can
|
|
30
|
+
* carry one.
|
|
31
|
+
* - **A new PID, an exit code or a cloud connection is not a verified update.**
|
|
32
|
+
* {@link verifyReplacement} requires the daemon's own `verified`, from a process
|
|
33
|
+
* other than the one that was replaced, for the build that was asked for, and the
|
|
34
|
+
* same machine identity afterwards. A different identity is the reinstall case: the
|
|
35
|
+
* hold is left in place and shown, never resumed.
|
|
36
|
+
*
|
|
37
|
+
* The shapes are STRUCTURAL SUBSETS of `DaemonMaintenanceView` in
|
|
38
|
+
* `@skrr-ai/data-provider` (`daemonMaintenance.ts`), which is the one vocabulary.
|
|
39
|
+
* This package cannot import it (it sits below it), so `guidedUpdate.test.ts` reads
|
|
40
|
+
* that file and fails when a list mirrored here drifts from it.
|
|
41
|
+
*/
|
|
42
|
+
/** The server sources that can only veto. `daemon_report` is the only one that grants. */
|
|
43
|
+
export const GUIDED_SERVER_SOURCES = [
|
|
44
|
+
'claims',
|
|
45
|
+
'receipts',
|
|
46
|
+
'sessions',
|
|
47
|
+
'runs',
|
|
48
|
+
'leases',
|
|
49
|
+
'custody',
|
|
50
|
+
];
|
|
51
|
+
/** The refusals the owner API names, mirrored from `DAEMON_MAINTENANCE_REFUSALS`. */
|
|
52
|
+
export const GUIDED_REFUSALS = [
|
|
53
|
+
'managed_machine',
|
|
54
|
+
'no_stable_identity',
|
|
55
|
+
'not_found',
|
|
56
|
+
'ambiguous',
|
|
57
|
+
'generation_conflict',
|
|
58
|
+
'not_held',
|
|
59
|
+
'already_held',
|
|
60
|
+
'attestation_not_applicable',
|
|
61
|
+
'not_owner',
|
|
62
|
+
'held_by_other',
|
|
63
|
+
];
|
|
64
|
+
export function isGuidedRefusal(value) {
|
|
65
|
+
return typeof value === 'string' && GUIDED_REFUSALS.includes(value);
|
|
66
|
+
}
|
|
67
|
+
/** The daemon handoff phases, mirrored from `daemon/src/update/handoff.ts`. */
|
|
68
|
+
export const GUIDED_HANDOFF_PHASES = [
|
|
69
|
+
'ready',
|
|
70
|
+
'deferred',
|
|
71
|
+
'reconnecting',
|
|
72
|
+
'verified',
|
|
73
|
+
'failed',
|
|
74
|
+
'rolled-back',
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* Whether the server holds a reading that nothing vetoes, for a daemon that cannot
|
|
78
|
+
* report (D38). Strict on purpose: a source that is absent from the list, unreadable,
|
|
79
|
+
* truncated, or that names active or unproven work, is a veto. `daemon_report` must
|
|
80
|
+
* be `not_announced`: any other answer means the reading is not the
|
|
81
|
+
* `no_inventory_capability` case this is for.
|
|
82
|
+
*/
|
|
83
|
+
export function noServerVeto(sources) {
|
|
84
|
+
// `Array.isArray` does not narrow a readonly array, so the list is named explicitly.
|
|
85
|
+
if (!Array.isArray(sources))
|
|
86
|
+
return false;
|
|
87
|
+
const list = sources;
|
|
88
|
+
for (const name of GUIDED_SERVER_SOURCES) {
|
|
89
|
+
const source = list.find((s) => s && s.source === name);
|
|
90
|
+
if (!source || source.read !== 'ok' || source.truncated === true)
|
|
91
|
+
return false;
|
|
92
|
+
const holders = Array.isArray(source.holders)
|
|
93
|
+
? source.holders
|
|
94
|
+
: null;
|
|
95
|
+
if (!holders)
|
|
96
|
+
return false;
|
|
97
|
+
if (holders.some((h) => h && (h.class === 'active' || h.class === 'unproven')))
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const report = list.find((s) => s && s.source === 'daemon_report');
|
|
101
|
+
return report !== undefined && report.read === 'not_announced';
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Read the owner API's view for a flow that took `generation`.
|
|
105
|
+
*
|
|
106
|
+
* Order matters and is the rule set (each is a test):
|
|
107
|
+
* 1. No live hold: `not_held`. A different live generation: `generation_changed`.
|
|
108
|
+
* 2. The fence is not confirmed (`pausing`): nothing below it means anything.
|
|
109
|
+
* 3. The reading is `ready` (the daemon's own report): `ready` by evidence.
|
|
110
|
+
* 4. `draining`: work is running. Evidence, never overridden.
|
|
111
|
+
* 5. `unknown`: ready by attestation if the Journey says the owner vouched for this
|
|
112
|
+
* cause; `no_report` if the daemon cannot report and no source vetoes; else
|
|
113
|
+
* `unknown`. No reading at all is `unknown (source_unreadable)`, never ready.
|
|
114
|
+
*/
|
|
115
|
+
export function evaluateReadiness(view, expectedGeneration) {
|
|
116
|
+
const hold = view?.hold;
|
|
117
|
+
if (!view || !hold || hold.lifecycle !== 'held')
|
|
118
|
+
return { kind: 'not_held' };
|
|
119
|
+
if (expectedGeneration !== null && hold.generation !== expectedGeneration) {
|
|
120
|
+
return { kind: 'generation_changed', currentGeneration: hold.generation };
|
|
121
|
+
}
|
|
122
|
+
if (view.journey?.phase === 'pausing')
|
|
123
|
+
return { kind: 'pausing' };
|
|
124
|
+
const reading = view.reading;
|
|
125
|
+
if (!reading)
|
|
126
|
+
return { kind: 'unknown', cause: 'source_unreadable' };
|
|
127
|
+
if (reading.state === 'ready')
|
|
128
|
+
return { kind: 'ready', basis: 'evidence' };
|
|
129
|
+
if (reading.state === 'draining')
|
|
130
|
+
return { kind: 'draining' };
|
|
131
|
+
if (reading.state !== 'unknown')
|
|
132
|
+
return { kind: 'unknown', cause: 'source_unreadable' };
|
|
133
|
+
if (view.journey?.phase === 'ready' && view.journey.modifiers?.includes('attested')) {
|
|
134
|
+
return { kind: 'ready', basis: 'attested' };
|
|
135
|
+
}
|
|
136
|
+
if (reading.cause === 'no_inventory_capability' && noServerVeto(reading.sources)) {
|
|
137
|
+
return { kind: 'no_report' };
|
|
138
|
+
}
|
|
139
|
+
return { kind: 'unknown', cause: reading.cause ?? null };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Whether a guided flow may call the daemon's `prepare`, and on what basis. `null`
|
|
143
|
+
* means NO: `prepare` is never a way to find out. A legacy daemon never gets one:
|
|
144
|
+
* it has no endpoint, and its restart is the owner's attested decision, not a probe.
|
|
145
|
+
*/
|
|
146
|
+
export function prepareBasis(readiness, opts) {
|
|
147
|
+
if (opts.legacy)
|
|
148
|
+
return null;
|
|
149
|
+
if (readiness.kind === 'ready')
|
|
150
|
+
return readiness.basis;
|
|
151
|
+
if (readiness.kind === 'no_report')
|
|
152
|
+
return 'no_veto';
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether a legacy daemon (no handoff endpoint) may be restarted by the supervisor.
|
|
157
|
+
* Only on the owner's attestation: the daemon cannot report what it is doing and a
|
|
158
|
+
* SIGTERM ends its wrappers, so nothing but the owner's word can vouch for it.
|
|
159
|
+
*/
|
|
160
|
+
export function legacyRestartAllowed(readiness) {
|
|
161
|
+
return readiness.kind === 'ready' && readiness.basis === 'attested';
|
|
162
|
+
}
|
|
163
|
+
export const MAX_OWNER_BLOCKERS = 20;
|
|
164
|
+
const BLOCKER_TEXT_MAX = 200;
|
|
165
|
+
const ID_PATTERN = /^[A-Za-z0-9_-]{1,120}$/;
|
|
166
|
+
// A diagnostic can carry a local path or a URL. Showing one would leak the layout of
|
|
167
|
+
// someone's machine into a notice, so a reason that looks like one is dropped whole.
|
|
168
|
+
const PATHLIKE = /(?:^|[\s("'=])(?:~|\.{1,2})?[\\/](?:[\w.-]+[\\/])+|[A-Za-z]:[\\/]|\\\\|file:\/\/|\/(?:Users|home|var|tmp|private|opt|etc|usr)\//i;
|
|
169
|
+
/** A sentence safe to put in front of the owner, or `undefined` when it is not. */
|
|
170
|
+
export function sanitizeBlockerText(value, max = BLOCKER_TEXT_MAX) {
|
|
171
|
+
if (typeof value !== 'string')
|
|
172
|
+
return undefined;
|
|
173
|
+
const cleaned = Array.from(value, (ch) => {
|
|
174
|
+
const code = ch.charCodeAt(0);
|
|
175
|
+
return code < 0x20 || code === 0x7f ? ' ' : ch;
|
|
176
|
+
})
|
|
177
|
+
.join('')
|
|
178
|
+
.replace(/\s+/g, ' ')
|
|
179
|
+
.trim();
|
|
180
|
+
if (!cleaned || PATHLIKE.test(cleaned))
|
|
181
|
+
return undefined;
|
|
182
|
+
return cleaned.length > max ? `${cleaned.slice(0, max - 1)}…` : cleaned;
|
|
183
|
+
}
|
|
184
|
+
function boundedId(value) {
|
|
185
|
+
return typeof value === 'string' && ID_PATTERN.test(value) ? value : undefined;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Everything in the way, from the three places that know: the server's holders
|
|
189
|
+
* (active and unproven work: `pending_delivery` and `forensic` never block), the
|
|
190
|
+
* daemon's own refusal from `prepare`, and the work the Desktop app itself hosts and
|
|
191
|
+
* ends when it quits (`LocalSessionService` processes and agent browser windows,
|
|
192
|
+
* which the daemon's inventory cannot see).
|
|
193
|
+
*
|
|
194
|
+
* A daemon blocker whose id equals a server holder's ref is the same work seen twice:
|
|
195
|
+
* it is listed once, with the daemon's reason and the server's label. Whether the two
|
|
196
|
+
* ids are in fact comparable was never established (review §6.2 item 2), so a match
|
|
197
|
+
* only MERGES the wording and never hides a blocker that has no match.
|
|
198
|
+
*/
|
|
199
|
+
export function describeBlockers(input) {
|
|
200
|
+
const out = [];
|
|
201
|
+
const serverByRef = new Map();
|
|
202
|
+
const serverHolders = [];
|
|
203
|
+
for (const source of input.view?.reading?.sources ?? []) {
|
|
204
|
+
for (const holder of source?.holders ?? []) {
|
|
205
|
+
if (!holder || (holder.class !== 'active' && holder.class !== 'unproven'))
|
|
206
|
+
continue;
|
|
207
|
+
serverHolders.push(holder);
|
|
208
|
+
if (typeof holder.ref === 'string')
|
|
209
|
+
serverByRef.set(holder.ref, holder);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const merged = new Set();
|
|
213
|
+
for (const blocker of input.daemon ?? []) {
|
|
214
|
+
if (!blocker || typeof blocker.kind !== 'string')
|
|
215
|
+
continue;
|
|
216
|
+
const id = boundedId(blocker.id);
|
|
217
|
+
const holder = blocker.id ? serverByRef.get(blocker.id) : undefined;
|
|
218
|
+
if (holder)
|
|
219
|
+
merged.add(holder);
|
|
220
|
+
const label = holder ? sanitizeBlockerText(holder.label, 120) : undefined;
|
|
221
|
+
out.push({
|
|
222
|
+
source: 'daemon',
|
|
223
|
+
kind: blocker.kind.slice(0, 64),
|
|
224
|
+
text: sanitizeBlockerText(blocker.reason),
|
|
225
|
+
...(label ? { label } : {}),
|
|
226
|
+
...(id ? { id } : {}),
|
|
227
|
+
...(holder?.since !== undefined ? { since: holder.since } : {}),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
for (const holder of serverHolders) {
|
|
231
|
+
if (merged.has(holder))
|
|
232
|
+
continue;
|
|
233
|
+
const id = boundedId(holder.ref);
|
|
234
|
+
out.push({
|
|
235
|
+
source: 'server',
|
|
236
|
+
kind: String(holder.kind || 'work').slice(0, 64),
|
|
237
|
+
text: sanitizeBlockerText(holder.label, 120),
|
|
238
|
+
...(id ? { id } : {}),
|
|
239
|
+
...(holder.since !== undefined ? { since: holder.since } : {}),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const desktop = input.desktop;
|
|
243
|
+
if (desktop && desktop.localSessions > 0) {
|
|
244
|
+
out.push({ source: 'desktop', kind: 'desktop_local_sessions', count: desktop.localSessions });
|
|
245
|
+
}
|
|
246
|
+
if (desktop && desktop.agentBrowserWindows > 0) {
|
|
247
|
+
out.push({
|
|
248
|
+
source: 'desktop',
|
|
249
|
+
kind: 'desktop_agent_browser',
|
|
250
|
+
count: desktop.agentBrowserWindows,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return out.slice(0, MAX_OWNER_BLOCKERS);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* A string that changes whenever the set of things in the way changes. The "update
|
|
257
|
+
* now anyway" confirmation carries the one it was shown, and the action is refused
|
|
258
|
+
* when it no longer matches: a confirmation that names what WAS running is not a
|
|
259
|
+
* confirmation of what is.
|
|
260
|
+
*/
|
|
261
|
+
export function blockersFingerprint(blockers) {
|
|
262
|
+
return blockers
|
|
263
|
+
.map((b) => [b.source, b.kind, b.id ?? '', b.count ?? ''].join('|'))
|
|
264
|
+
.sort()
|
|
265
|
+
.join(';');
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The labels the owner is shown, as the server records them in the outcome
|
|
269
|
+
* (`acknowledged`): each bounded to the server's 160-character limit.
|
|
270
|
+
*/
|
|
271
|
+
export function acknowledgedLabels(blockers, max = 20) {
|
|
272
|
+
const labels = [];
|
|
273
|
+
for (const b of blockers) {
|
|
274
|
+
const detail = b.label ?? b.text;
|
|
275
|
+
const text = b.count ? `${b.kind} (${b.count})` : detail ? `${b.kind}: ${detail}` : b.kind;
|
|
276
|
+
labels.push(text.slice(0, 160));
|
|
277
|
+
if (labels.length >= max)
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
return labels;
|
|
281
|
+
}
|
|
282
|
+
function isHandoffStatus(value) {
|
|
283
|
+
if (!value || typeof value !== 'object')
|
|
284
|
+
return false;
|
|
285
|
+
const v = value;
|
|
286
|
+
return (v.protocolVersion === 1 &&
|
|
287
|
+
typeof v.handoffId === 'string' &&
|
|
288
|
+
typeof v.phase === 'string' &&
|
|
289
|
+
GUIDED_HANDOFF_PHASES.includes(v.phase) &&
|
|
290
|
+
typeof v.sourceProcessId === 'string' &&
|
|
291
|
+
!!v.target &&
|
|
292
|
+
typeof v.target.sha256 === 'string' &&
|
|
293
|
+
Array.isArray(v.blockers) &&
|
|
294
|
+
Array.isArray(v.expectedSessions));
|
|
295
|
+
}
|
|
296
|
+
export function classifyHandoffReply(input) {
|
|
297
|
+
const { statusCode, body } = input;
|
|
298
|
+
if (statusCode === null)
|
|
299
|
+
return { kind: 'unreachable' };
|
|
300
|
+
if (statusCode === 200) {
|
|
301
|
+
return isHandoffStatus(body) ? { kind: 'ok', status: body } : { kind: 'unreachable' };
|
|
302
|
+
}
|
|
303
|
+
if (statusCode === 404)
|
|
304
|
+
return { kind: 'absent' };
|
|
305
|
+
if (statusCode === 401 || statusCode === 403)
|
|
306
|
+
return { kind: 'unauthorized' };
|
|
307
|
+
if (statusCode === 400 || statusCode === 409 || statusCode === 413 || statusCode === 405) {
|
|
308
|
+
const code = body && typeof body === 'object' && typeof body.error === 'string'
|
|
309
|
+
? String(body.error).slice(0, 64)
|
|
310
|
+
: undefined;
|
|
311
|
+
return { kind: 'refused', ...(code ? { code } : {}) };
|
|
312
|
+
}
|
|
313
|
+
return { kind: 'unreachable' };
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Decide whether a committed replacement is complete. All of (1) and (2) must hold
|
|
317
|
+
* before anything else is asked of the server (3, {@link serverSawNewProcess}), and
|
|
318
|
+
* nothing here resumes: the verdict only says what was seen.
|
|
319
|
+
*
|
|
320
|
+
* An identity that is merely UNREADABLE (the bootstrap is mid-rotation, the machine
|
|
321
|
+
* payload could not be fetched) is `not_yet`, never a reinstall and never verified.
|
|
322
|
+
*/
|
|
323
|
+
export function verifyReplacement(evidence) {
|
|
324
|
+
const { handoff, expect, identity } = evidence;
|
|
325
|
+
if (!handoff)
|
|
326
|
+
return { kind: 'not_yet', missing: 'daemon_status' };
|
|
327
|
+
if (handoff.phase === 'failed' || handoff.phase === 'rolled-back') {
|
|
328
|
+
return {
|
|
329
|
+
kind: 'failed',
|
|
330
|
+
phase: handoff.phase,
|
|
331
|
+
reason: String(handoff.reason || handoff.phase).slice(0, 64),
|
|
332
|
+
// A failed handoff observed by the SAME process is a replacement that never
|
|
333
|
+
// happened: the old daemon is still the one running, and a retry is meaningful.
|
|
334
|
+
sameProcess: handoff.observer?.processId === expect.sourceProcessId,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (handoff.phase !== 'verified')
|
|
338
|
+
return { kind: 'not_yet', missing: 'verified' };
|
|
339
|
+
const by = handoff.verifiedBy;
|
|
340
|
+
if (!by ||
|
|
341
|
+
by.processId === expect.sourceProcessId ||
|
|
342
|
+
handoff.target.sha256 !== expect.targetSha256 ||
|
|
343
|
+
(expect.version !== null && by.version !== expect.version)) {
|
|
344
|
+
return { kind: 'not_yet', missing: 'verified' };
|
|
345
|
+
}
|
|
346
|
+
if (identity.bootstrapDaemonId === null || identity.runtimeKeyNow === null) {
|
|
347
|
+
return { kind: 'not_yet', missing: 'identity_unreadable' };
|
|
348
|
+
}
|
|
349
|
+
if (identity.bootstrapDaemonId !== identity.recordedDaemonId) {
|
|
350
|
+
return { kind: 'reinstall', what: 'daemon_id' };
|
|
351
|
+
}
|
|
352
|
+
if (identity.runtimeKeyNow !== identity.recordedRuntimeKey) {
|
|
353
|
+
return { kind: 'reinstall', what: 'runtime_key' };
|
|
354
|
+
}
|
|
355
|
+
return { kind: 'verified' };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The legacy counterpart: a daemon with no handoff cannot say `verified`, so the copy
|
|
359
|
+
* claims only what was seen ("restarted on version X"), never "adopted N sessions".
|
|
360
|
+
* A new process (a different start time and, where known, a different PID), the same
|
|
361
|
+
* identity, and the version it announces equal to the bundled one.
|
|
362
|
+
*/
|
|
363
|
+
export function verifyLegacyRestart(evidence) {
|
|
364
|
+
const { before, after, expect } = evidence;
|
|
365
|
+
if (after.daemonId === null || after.startedAt === null || evidence.runtimeKeyNow === null) {
|
|
366
|
+
return { kind: 'not_yet', missing: 'identity_unreadable' };
|
|
367
|
+
}
|
|
368
|
+
const newProcess = before.startedAt === null
|
|
369
|
+
? after.processId !== null && after.processId !== before.processId
|
|
370
|
+
: after.startedAt > before.startedAt &&
|
|
371
|
+
(after.processId === null ||
|
|
372
|
+
before.processId === null ||
|
|
373
|
+
after.processId !== before.processId);
|
|
374
|
+
if (!newProcess)
|
|
375
|
+
return { kind: 'not_yet', missing: 'verified' };
|
|
376
|
+
if (after.daemonId !== expect.daemonId)
|
|
377
|
+
return { kind: 'reinstall', what: 'daemon_id' };
|
|
378
|
+
if (evidence.runtimeKeyNow !== evidence.recordedRuntimeKey) {
|
|
379
|
+
return { kind: 'reinstall', what: 'runtime_key' };
|
|
380
|
+
}
|
|
381
|
+
if (expect.version !== null && after.version !== expect.version) {
|
|
382
|
+
return { kind: 'not_yet', missing: 'verified' };
|
|
383
|
+
}
|
|
384
|
+
return { kind: 'verified' };
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Whether the server independently saw a new process register under the held key
|
|
388
|
+
* (condition 3). The Journey says `back_held` for ANY process that registered since
|
|
389
|
+
* the fence, so it can only corroborate THIS replacement when it did not already say
|
|
390
|
+
* so before the commit. When it did, the answer is `not_independent`: the flow rests
|
|
391
|
+
* on (1) and (2) and says so.
|
|
392
|
+
*/
|
|
393
|
+
export function serverSawNewProcess(input) {
|
|
394
|
+
if (input.backHeldAtCommit)
|
|
395
|
+
return 'not_independent';
|
|
396
|
+
return input.journeyPhase === 'back_held' ? 'seen' : 'not_yet';
|
|
397
|
+
}
|
|
398
|
+
/** Whether an observed hold is the one this flow took, and still live. */
|
|
399
|
+
export function isOurLiveHold(view, generation) {
|
|
400
|
+
return (generation !== null &&
|
|
401
|
+
!!view?.hold &&
|
|
402
|
+
view.hold.lifecycle === 'held' &&
|
|
403
|
+
view.hold.generation === generation);
|
|
404
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The desktop reserves a new profile for every background authorization. */
|
|
2
|
+
export declare const INDEPENDENT_MACHINE_PROFILE_RE: RegExp;
|
|
3
|
+
export declare const INDEPENDENT_MACHINE_RESERVATION_FILE = "independent-machine-attempt.json";
|
|
4
|
+
export interface IndependentMachineReservation {
|
|
5
|
+
version: 2;
|
|
6
|
+
profile: string;
|
|
7
|
+
ownerId: string;
|
|
8
|
+
origin: string;
|
|
9
|
+
secretHash: string;
|
|
10
|
+
encryptedSecret: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}
|
|
13
|
+
/** Reserve a previously absent profile and persist the encrypted attempt secret.
|
|
14
|
+
* `encrypt` must use the platform secure store and throw if it is unavailable. */
|
|
15
|
+
export declare function reserveIndependentMachineProfile(options: {
|
|
16
|
+
root: string;
|
|
17
|
+
ownerId: string;
|
|
18
|
+
origin: string;
|
|
19
|
+
encrypt: (secret: string) => Buffer;
|
|
20
|
+
}): {
|
|
21
|
+
reservation: IndependentMachineReservation;
|
|
22
|
+
secret: string;
|
|
23
|
+
};
|
|
24
|
+
/** A saved reservation is a selection hint, never proof of machine ownership. */
|
|
25
|
+
export declare function readIndependentMachineReservation(root: string, profile: string): IndependentMachineReservation | null;
|
|
26
|
+
/** Confirm a one-shot signer is acting on the desktop's reserved attempt. */
|
|
27
|
+
export declare function verifyIndependentMachineReservation(root: string, profile: string, secret: unknown): boolean;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/** The desktop reserves a new profile for every background authorization. */
|
|
5
|
+
export const INDEPENDENT_MACHINE_PROFILE_RE = /^desktop-bg-[a-f0-9]{32}$/;
|
|
6
|
+
export const INDEPENDENT_MACHINE_RESERVATION_FILE = 'independent-machine-attempt.json';
|
|
7
|
+
function canonicalOrigin(input) {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(input);
|
|
10
|
+
const local = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
11
|
+
return !url.username && !url.password && url.origin === input &&
|
|
12
|
+
(url.protocol === 'https:' || (url.protocol === 'http:' && local));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function validSecret(secret) {
|
|
19
|
+
return typeof secret === 'string' && /^[A-Za-z0-9_-]{43}$/.test(secret) &&
|
|
20
|
+
Buffer.from(secret, 'base64url').length === 32 &&
|
|
21
|
+
Buffer.from(secret, 'base64url').toString('base64url') === secret;
|
|
22
|
+
}
|
|
23
|
+
function hashSecret(profile, ownerId, origin, secret) {
|
|
24
|
+
return crypto.createHash('sha256').update('independent-machine-reservation:v2\0')
|
|
25
|
+
.update(profile).update('\0').update(ownerId).update('\0').update(origin).update('\0').update(secret).digest();
|
|
26
|
+
}
|
|
27
|
+
function reservationPath(root, profile) {
|
|
28
|
+
if (!path.isAbsolute(root) || !INDEPENDENT_MACHINE_PROFILE_RE.test(profile)) {
|
|
29
|
+
throw new Error('Invalid independent machine profile');
|
|
30
|
+
}
|
|
31
|
+
return path.join(root, 'profiles', profile, INDEPENDENT_MACHINE_RESERVATION_FILE);
|
|
32
|
+
}
|
|
33
|
+
/** Reserve a previously absent profile and persist the encrypted attempt secret.
|
|
34
|
+
* `encrypt` must use the platform secure store and throw if it is unavailable. */
|
|
35
|
+
export function reserveIndependentMachineProfile(options) {
|
|
36
|
+
if (!path.isAbsolute(options.root) || !/^[a-fA-F0-9]{24}$/.test(options.ownerId) ||
|
|
37
|
+
!canonicalOrigin(options.origin))
|
|
38
|
+
throw new Error('Invalid background authorization context');
|
|
39
|
+
const secret = crypto.randomBytes(32).toString('base64url');
|
|
40
|
+
const encrypted = options.encrypt(secret);
|
|
41
|
+
if (!Buffer.isBuffer(encrypted) || encrypted.length === 0 || encrypted.length > 4096) {
|
|
42
|
+
throw new Error('Background authorization secure storage is unavailable');
|
|
43
|
+
}
|
|
44
|
+
const profiles = path.join(options.root, 'profiles');
|
|
45
|
+
const rootExisted = fs.existsSync(options.root);
|
|
46
|
+
const profilesExisted = fs.existsSync(profiles);
|
|
47
|
+
fs.mkdirSync(profiles, { recursive: true, mode: 0o700 });
|
|
48
|
+
for (let i = 0; i < 5; i += 1) {
|
|
49
|
+
const profile = `desktop-bg-${crypto.randomBytes(16).toString('hex')}`;
|
|
50
|
+
const dir = path.join(profiles, profile);
|
|
51
|
+
try {
|
|
52
|
+
fs.mkdirSync(dir, { mode: 0o700 });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === 'EEXIST')
|
|
56
|
+
continue;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const reservation = {
|
|
60
|
+
version: 2, profile, ownerId: options.ownerId.toLowerCase(), origin: options.origin,
|
|
61
|
+
secretHash: hashSecret(profile, options.ownerId.toLowerCase(), options.origin, secret).toString('hex'),
|
|
62
|
+
encryptedSecret: encrypted.toString('base64'),
|
|
63
|
+
createdAt: new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
const file = reservationPath(options.root, profile);
|
|
66
|
+
const fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL |
|
|
67
|
+
(fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
68
|
+
try {
|
|
69
|
+
fs.writeFileSync(fd, `${JSON.stringify(reservation)}\n`, 'utf8');
|
|
70
|
+
fs.fsyncSync(fd);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
fs.closeSync(fd);
|
|
74
|
+
}
|
|
75
|
+
const directories = [dir, profiles, ...(!profilesExisted ? [options.root] : []),
|
|
76
|
+
...(!rootExisted ? [path.dirname(options.root)] : [])];
|
|
77
|
+
for (const directory of directories) {
|
|
78
|
+
const directoryFd = fs.openSync(directory, 'r');
|
|
79
|
+
try {
|
|
80
|
+
fs.fsyncSync(directoryFd);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
fs.closeSync(directoryFd);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// A profile whose reservation cannot be written is never used. Keep any
|
|
87
|
+
// uncertain directory for recovery rather than deleting other state.
|
|
88
|
+
return { reservation, secret };
|
|
89
|
+
}
|
|
90
|
+
throw new Error('Could not reserve a new background machine profile');
|
|
91
|
+
}
|
|
92
|
+
/** A saved reservation is a selection hint, never proof of machine ownership. */
|
|
93
|
+
export function readIndependentMachineReservation(root, profile) {
|
|
94
|
+
try {
|
|
95
|
+
const file = reservationPath(root, profile);
|
|
96
|
+
const directory = fs.lstatSync(path.dirname(file));
|
|
97
|
+
if (!directory.isDirectory() || directory.isSymbolicLink())
|
|
98
|
+
return null;
|
|
99
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
const stat = fs.fstatSync(fd);
|
|
103
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 8192 || (stat.mode & 0o077) !== 0)
|
|
104
|
+
return null;
|
|
105
|
+
raw = fs.readFileSync(fd, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
fs.closeSync(fd);
|
|
109
|
+
}
|
|
110
|
+
const value = JSON.parse(raw);
|
|
111
|
+
if (value.version !== 2 || value.profile !== profile ||
|
|
112
|
+
typeof value.ownerId !== 'string' || !/^[a-f0-9]{24}$/.test(value.ownerId) ||
|
|
113
|
+
typeof value.origin !== 'string' || !canonicalOrigin(value.origin) ||
|
|
114
|
+
typeof value.secretHash !== 'string' || !/^[a-f0-9]{64}$/.test(value.secretHash) ||
|
|
115
|
+
typeof value.encryptedSecret !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value.encryptedSecret) ||
|
|
116
|
+
typeof value.createdAt !== 'string' || !Number.isFinite(Date.parse(value.createdAt)))
|
|
117
|
+
return null;
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Confirm a one-shot signer is acting on the desktop's reserved attempt. */
|
|
125
|
+
export function verifyIndependentMachineReservation(root, profile, secret) {
|
|
126
|
+
if (!validSecret(secret))
|
|
127
|
+
return false;
|
|
128
|
+
const saved = readIndependentMachineReservation(root, profile);
|
|
129
|
+
if (!saved)
|
|
130
|
+
return false;
|
|
131
|
+
return crypto.timingSafeEqual(Buffer.from(saved.secretHash, 'hex'), hashSecret(profile, saved.ownerId, saved.origin, secret));
|
|
132
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capabilities explained to a person before a separate background
|
|
3
|
+
* machine is authorized. This version is an immutable wire contract: adding
|
|
4
|
+
* access needs a new contract and a new authorization, not an edit to v1.
|
|
5
|
+
*/
|
|
6
|
+
export declare const INDEPENDENT_MACHINE_SCOPE_CONTRACT_V1: "desktop-background-v1";
|
|
7
|
+
export declare const INDEPENDENT_MACHINE_PERMISSIONS_V1: readonly [Readonly<{
|
|
8
|
+
scope: "daemon:tools:bash";
|
|
9
|
+
description: "Run terminal commands and local agents";
|
|
10
|
+
}>, Readonly<{
|
|
11
|
+
scope: "daemon:tools:fs:read";
|
|
12
|
+
description: "Read files in allowed folders";
|
|
13
|
+
}>, Readonly<{
|
|
14
|
+
scope: "daemon:tools:fs:write";
|
|
15
|
+
description: "Create and edit files in allowed folders";
|
|
16
|
+
}>, Readonly<{
|
|
17
|
+
scope: "daemon:browser:cdp";
|
|
18
|
+
description: "Control connected browser tabs";
|
|
19
|
+
}>, Readonly<{
|
|
20
|
+
scope: "daemon:session:replay";
|
|
21
|
+
description: "Replay results recorded while offline";
|
|
22
|
+
}>, Readonly<{
|
|
23
|
+
scope: "daemon:offline-queue";
|
|
24
|
+
description: "Send queued results after reconnecting";
|
|
25
|
+
}>];
|
|
26
|
+
export declare const INDEPENDENT_MACHINE_SCOPES_V1: readonly string[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capabilities explained to a person before a separate background
|
|
3
|
+
* machine is authorized. This version is an immutable wire contract: adding
|
|
4
|
+
* access needs a new contract and a new authorization, not an edit to v1.
|
|
5
|
+
*/
|
|
6
|
+
export const INDEPENDENT_MACHINE_SCOPE_CONTRACT_V1 = 'desktop-background-v1';
|
|
7
|
+
export const INDEPENDENT_MACHINE_PERMISSIONS_V1 = Object.freeze([
|
|
8
|
+
Object.freeze({ scope: 'daemon:tools:bash', description: 'Run terminal commands and local agents' }),
|
|
9
|
+
Object.freeze({ scope: 'daemon:tools:fs:read', description: 'Read files in allowed folders' }),
|
|
10
|
+
Object.freeze({ scope: 'daemon:tools:fs:write', description: 'Create and edit files in allowed folders' }),
|
|
11
|
+
Object.freeze({ scope: 'daemon:browser:cdp', description: 'Control connected browser tabs' }),
|
|
12
|
+
Object.freeze({ scope: 'daemon:session:replay', description: 'Replay results recorded while offline' }),
|
|
13
|
+
Object.freeze({ scope: 'daemon:offline-queue', description: 'Send queued results after reconnecting' }),
|
|
14
|
+
]);
|
|
15
|
+
export const INDEPENDENT_MACHINE_SCOPES_V1 = Object.freeze(INDEPENDENT_MACHINE_PERMISSIONS_V1.map((permission) => permission.scope));
|
|
@@ -28,7 +28,7 @@ export { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as cred
|
|
|
28
28
|
export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, KekUnavailableError, LinuxKek, MacosKek, registerZeroizeHook, WindowsKek, zeroizeKekCaches, type KekStrategy, } from './kek/index.js';
|
|
29
29
|
export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, normalizeHtu, publicJwkFromX, tokenHash, verifyProof as verifyDeviceProof, type DeviceKeyPair, type DeviceProofClaims, type DeviceProofHeader, type Ed25519PrivateJwk, type Ed25519PublicJwk, type VerifyFailureReason as DeviceProofVerifyFailureReason, type VerifyFailure as DeviceProofVerifyFailure, type VerifyOptions as DeviceProofVerifyOptions, type VerifySuccess as DeviceProofVerifySuccess, } from './deviceKey.js';
|
|
30
30
|
export { DPOP_HEADER, DPOP_MAX_AGE_SECONDS, DPOP_PROOF_INVALID, DPOP_PROOF_REQUIRED, DPOP_PROOF_TYP, DPOP_PROOF_VERIFY_UNAVAILABLE, accessTokenHash, createDpopKey, isAth, isJwkThumbprint, normalizeDpopHtu, verifyDpopProof, type DpopKey, type DpopProofClaims, type DpopProofRequest, type DpopVerifyFailureReason, type DpopVerifyOptions, type DpopVerifyResult, } from './dpop.js';
|
|
31
|
-
export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
31
|
+
export { initCredEnvelope, initExistingCredEnvelope, initNewIndependentCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
32
32
|
export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, describeDeviceKeySplit, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
|
|
33
33
|
export { getMachineUuid, getMachineUuidForProfile, describeMachineIdentity, type MachineIdentityDescription, type MachineIdentityReason, getHardwareMachineUuid, setMachineUuidProfile, getMachineUuidProfile, qualifyMachineUuid, MACHINE_IDENTITY_FILENAME, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
34
34
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
@@ -50,3 +50,4 @@ export * from './harnessPermissionControls.js';
|
|
|
50
50
|
export { compareReleaseVersions } from './releaseVersions.js';
|
|
51
51
|
export * from './computerUse.js';
|
|
52
52
|
export * from './desktopWire.js';
|
|
53
|
+
export { INDEPENDENT_MACHINE_PROFILE_RE, INDEPENDENT_MACHINE_RESERVATION_FILE, readIndependentMachineReservation, reserveIndependentMachineProfile, verifyIndependentMachineReservation, type IndependentMachineReservation, } from './independentMachineReservation.js';
|
|
@@ -112,7 +112,7 @@ export { DPOP_HEADER, DPOP_MAX_AGE_SECONDS, DPOP_PROOF_INVALID, DPOP_PROOF_REQUI
|
|
|
112
112
|
// Daemon and CLI consume these; the underlying primitives live above.
|
|
113
113
|
export {
|
|
114
114
|
// credEnvelope bridge
|
|
115
|
-
initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT,
|
|
115
|
+
initCredEnvelope, initExistingCredEnvelope, initNewIndependentCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT,
|
|
116
116
|
// Test seams — `__`-prefixed so consumers don't import them in
|
|
117
117
|
// production code paths. Re-exported here so CLI/daemon spec files
|
|
118
118
|
// can resolve the wrapped-DEK path against the same module instance.
|
|
@@ -186,3 +186,4 @@ export * from './harnessPermissionControls.js';
|
|
|
186
186
|
export { compareReleaseVersions } from './releaseVersions.js';
|
|
187
187
|
export * from './computerUse.js';
|
|
188
188
|
export * from './desktopWire.js';
|
|
189
|
+
export { INDEPENDENT_MACHINE_PROFILE_RE, INDEPENDENT_MACHINE_RESERVATION_FILE, readIndependentMachineReservation, reserveIndependentMachineProfile, verifyIndependentMachineReservation, } from './independentMachineReservation.js';
|