@intentic/sandbox-contract 1.331.0 → 1.333.0
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 +23 -1
- package/dist/chores/probes.d.ts.map +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/contracts/accounts.contract.d.ts +11 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +38 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +14 -3
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +29 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +3 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.d.ts +7 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +2 -0
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/logs.contract.d.ts +10 -0
- package/dist/contracts/logs.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +36 -1
- package/dist/contracts/needs.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.js +2 -2
- package/dist/contracts/needs.contract.js.map +1 -1
- package/dist/contracts/offload.contract.d.ts +70 -0
- package/dist/contracts/offload.contract.d.ts.map +1 -1
- package/dist/contracts/offload.contract.js +10 -1
- package/dist/contracts/offload.contract.js.map +1 -1
- package/dist/contracts/privacy.contract.d.ts +14 -0
- package/dist/contracts/privacy.contract.d.ts.map +1 -1
- package/dist/contracts/privacy.contract.js +11 -1
- package/dist/contracts/privacy.contract.js.map +1 -1
- package/dist/contracts/providers.contract.d.ts +3 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +103 -2
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.js +2 -2
- package/dist/contracts/runner.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +1 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +106 -94
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +19 -1
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +17 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +4 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +46 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +6 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-words.d.ts.map +1 -1
- package/dist/events/agent-words.js +1 -1
- package/dist/events/agent-words.js.map +1 -1
- package/dist/events/resume.d.ts +3 -0
- package/dist/events/resume.d.ts.map +1 -1
- package/dist/events/resume.js +17 -2
- package/dist/events/resume.js.map +1 -1
- package/dist/events/sandbox-notice.d.ts +18 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +27 -1
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +2 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +64 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +17 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/index.d.ts +427 -167
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/models/agent-catalog.d.ts.map +1 -1
- package/dist/models/agent-catalog.js +8 -7
- package/dist/models/agent-catalog.js.map +1 -1
- package/dist/models/agent-runtimes.d.ts +3 -1
- package/dist/models/agent-runtimes.d.ts.map +1 -1
- package/dist/models/agent-runtimes.js +10 -0
- package/dist/models/agent-runtimes.js.map +1 -1
- package/dist/models/model-order.d.ts +1 -0
- package/dist/models/model-order.d.ts.map +1 -1
- package/dist/models/model-order.js +9 -0
- package/dist/models/model-order.js.map +1 -1
- package/dist/models/model-pins.d.ts.map +1 -1
- package/dist/models/model-pins.js.map +1 -1
- package/dist/models/model-roles.d.ts +33 -18
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +31 -27
- package/dist/models/model-roles.js.map +1 -1
- package/dist/models/model-setups.d.ts +46 -0
- package/dist/models/model-setups.d.ts.map +1 -0
- package/dist/models/model-setups.js +86 -0
- package/dist/models/model-setups.js.map +1 -0
- package/dist/models/plan-pools.d.ts +3 -1
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +18 -6
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/models/provider-specs.d.ts +20 -0
- package/dist/models/provider-specs.d.ts.map +1 -1
- package/dist/models/provider-specs.js +13 -1
- package/dist/models/provider-specs.js.map +1 -1
- package/dist/policy/command-classes.d.ts.map +1 -1
- package/dist/policy/command-classes.js +26 -3
- package/dist/policy/command-classes.js.map +1 -1
- package/dist/policy/control-scopes.d.ts +47 -0
- package/dist/policy/control-scopes.d.ts.map +1 -1
- package/dist/policy/control-scopes.js +16 -0
- package/dist/policy/control-scopes.js.map +1 -1
- package/dist/policy/credential-material.d.ts +6 -0
- package/dist/policy/credential-material.d.ts.map +1 -1
- package/dist/policy/credential-material.js +37 -15
- package/dist/policy/credential-material.js.map +1 -1
- package/dist/policy/device-programs.d.ts +28 -0
- package/dist/policy/device-programs.d.ts.map +1 -0
- package/dist/policy/device-programs.js +79 -0
- package/dist/policy/device-programs.js.map +1 -0
- package/dist/policy/identity-assertion.d.ts +27 -0
- package/dist/policy/identity-assertion.d.ts.map +1 -0
- package/dist/policy/identity-assertion.js +77 -0
- package/dist/policy/identity-assertion.js.map +1 -0
- package/dist/policy/sandbox-power.d.ts +34 -0
- package/dist/policy/sandbox-power.d.ts.map +1 -0
- package/dist/policy/sandbox-power.js +146 -0
- package/dist/policy/sandbox-power.js.map +1 -0
- package/dist/policy/search-globs.d.ts +3 -0
- package/dist/policy/search-globs.d.ts.map +1 -1
- package/dist/policy/search-globs.js +118 -0
- package/dist/policy/search-globs.js.map +1 -1
- package/dist/policy/secret-hosts.d.ts +1 -0
- package/dist/policy/secret-hosts.d.ts.map +1 -1
- package/dist/policy/secret-hosts.js +1 -0
- package/dist/policy/secret-hosts.js.map +1 -1
- package/dist/policy/turned-away.d.ts.map +1 -1
- package/dist/policy/turned-away.js +1 -0
- package/dist/policy/turned-away.js.map +1 -1
- package/dist/protocol/doors/runner-protocol.d.ts +45 -0
- package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/runner-protocol.js +7 -0
- package/dist/protocol/doors/runner-protocol.js.map +1 -1
- package/dist/protocol/pairing-redeem.d.ts +23 -0
- package/dist/protocol/pairing-redeem.d.ts.map +1 -0
- package/dist/protocol/pairing-redeem.js +44 -0
- package/dist/protocol/pairing-redeem.js.map +1 -0
- package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
- package/dist/protocol/peer-mcp-server.js +1 -1
- package/dist/protocol/peer-mcp-server.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +855 -1
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-json-routes.js +61 -1
- package/dist/protocol/raw/raw-json-routes.js.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +17 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +5 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/protocol/sse.d.ts +20 -1
- package/dist/protocol/sse.d.ts.map +1 -1
- package/dist/protocol/sse.js +112 -17
- package/dist/protocol/sse.js.map +1 -1
- package/dist/schemas/agent.d.ts +35 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +12 -1
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/automations.d.ts +7 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +11 -3
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/device-swaps.d.ts +19 -0
- package/dist/schemas/device-swaps.d.ts.map +1 -0
- package/dist/schemas/device-swaps.js +11 -0
- package/dist/schemas/device-swaps.js.map +1 -0
- package/dist/schemas/devices.d.ts +17 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +2 -1
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/engines.d.ts +8 -1
- package/dist/schemas/engines.d.ts.map +1 -1
- package/dist/schemas/engines.js +1 -1
- package/dist/schemas/engines.js.map +1 -1
- package/dist/schemas/environment.d.ts +7 -0
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js +2 -0
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/git/git-scope.d.ts +20 -0
- package/dist/schemas/git/git-scope.d.ts.map +1 -0
- package/dist/schemas/git/git-scope.js +37 -0
- package/dist/schemas/git/git-scope.js.map +1 -0
- package/dist/schemas/git/git.d.ts +1 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +9 -1
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +42 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +3 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/keep-warm.d.ts +1 -0
- package/dist/schemas/keep-warm.d.ts.map +1 -1
- package/dist/schemas/keep-warm.js +1 -1
- package/dist/schemas/keep-warm.js.map +1 -1
- package/dist/schemas/logs.d.ts +21 -0
- package/dist/schemas/logs.d.ts.map +1 -1
- package/dist/schemas/logs.js +19 -0
- package/dist/schemas/logs.js.map +1 -1
- package/dist/schemas/members.d.ts +4 -0
- package/dist/schemas/members.d.ts.map +1 -1
- package/dist/schemas/members.js +5 -0
- package/dist/schemas/members.js.map +1 -1
- package/dist/schemas/metrics.d.ts +12 -0
- package/dist/schemas/metrics.d.ts.map +1 -1
- package/dist/schemas/metrics.js +26 -8
- package/dist/schemas/metrics.js.map +1 -1
- package/dist/schemas/needs.d.ts +97 -1
- package/dist/schemas/needs.d.ts.map +1 -1
- package/dist/schemas/needs.js +53 -4
- package/dist/schemas/needs.js.map +1 -1
- package/dist/schemas/offload.d.ts +154 -0
- package/dist/schemas/offload.d.ts.map +1 -1
- package/dist/schemas/offload.js +34 -0
- package/dist/schemas/offload.js.map +1 -1
- package/dist/schemas/privacy.d.ts +19 -0
- package/dist/schemas/privacy.d.ts.map +1 -1
- package/dist/schemas/privacy.js +17 -2
- package/dist/schemas/privacy.js.map +1 -1
- package/dist/schemas/project-bring-back.d.ts +172 -0
- package/dist/schemas/project-bring-back.d.ts.map +1 -0
- package/dist/schemas/project-bring-back.js +39 -0
- package/dist/schemas/project-bring-back.js.map +1 -0
- package/dist/schemas/providers/handoff.d.ts +77 -0
- package/dist/schemas/providers/handoff.d.ts.map +1 -0
- package/dist/schemas/providers/handoff.js +85 -0
- package/dist/schemas/providers/handoff.js.map +1 -0
- package/dist/schemas/providers/plan-limits.d.ts +27 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +8 -1
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +4 -0
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/usage.d.ts +3 -1
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +7 -2
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/restart-intent.d.ts +47 -0
- package/dist/schemas/restart-intent.d.ts.map +1 -0
- package/dist/schemas/restart-intent.js +19 -0
- package/dist/schemas/restart-intent.js.map +1 -0
- package/dist/schemas/run-targets.d.ts.map +1 -1
- package/dist/schemas/run-targets.js +5 -2
- package/dist/schemas/run-targets.js.map +1 -1
- package/dist/schemas/settings-history.d.ts +1 -1
- package/dist/schemas/settings-history.d.ts.map +1 -1
- package/dist/schemas/settings-history.js +21 -0
- package/dist/schemas/settings-history.js.map +1 -1
- package/dist/schemas/settings.d.ts +101 -100
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +43 -24
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/short-codes.d.ts +47 -0
- package/dist/schemas/short-codes.d.ts.map +1 -0
- package/dist/schemas/short-codes.js +30 -0
- package/dist/schemas/short-codes.js.map +1 -0
- package/dist/schemas/speech.d.ts +4 -0
- package/dist/schemas/speech.d.ts.map +1 -1
- package/dist/schemas/speech.js +1 -0
- package/dist/schemas/speech.js.map +1 -1
- package/dist/schemas/system.d.ts +176 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +55 -2
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/voice.d.ts +171 -0
- package/dist/schemas/voice.d.ts.map +1 -0
- package/dist/schemas/voice.js +99 -0
- package/dist/schemas/voice.js.map +1 -0
- package/dist/schemas/workspace/workspace-upload.d.ts +14 -0
- package/dist/schemas/workspace/workspace-upload.d.ts.map +1 -0
- package/dist/schemas/workspace/workspace-upload.js +6 -0
- package/dist/schemas/workspace/workspace-upload.js.map +1 -0
- package/dist/state/arrival.d.ts +3 -0
- package/dist/state/arrival.d.ts.map +1 -1
- package/dist/state/arrival.js +1 -0
- package/dist/state/arrival.js.map +1 -1
- package/dist/state/definition.d.ts +112 -40
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +3 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts +5 -2
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +3 -2
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/update-probation.d.ts +50 -0
- package/dist/state/update-probation.d.ts.map +1 -0
- package/dist/state/update-probation.js +81 -0
- package/dist/state/update-probation.js.map +1 -0
- package/dist/state/versions.d.ts +6 -0
- package/dist/state/versions.d.ts.map +1 -1
- package/dist/state/versions.js +47 -0
- package/dist/state/versions.js.map +1 -1
- package/dist/state/workspace-state.d.ts +1 -1
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +1 -1
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/embed.d.ts +2 -1
- package/dist/text/embed.d.ts.map +1 -1
- package/dist/text/embed.js +10 -5
- package/dist/text/embed.js.map +1 -1
- package/dist/text/title.d.ts.map +1 -1
- package/dist/text/title.js +4 -7
- package/dist/text/title.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +2 -1
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +21 -6
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +39 -6
- package/src/chores/probes.ts +3 -1
- package/src/contracts/agent.contract.ts +24 -3
- package/src/contracts/needs.contract.ts +2 -2
- package/src/contracts/offload.contract.ts +21 -1
- package/src/contracts/privacy.contract.ts +13 -0
- package/src/contracts/runner.contract.ts +10 -2
- package/src/contracts/settings.contract.ts +23 -0
- package/src/events/agent-events.ts +13 -0
- package/src/events/agent-words.ts +2 -3
- package/src/events/resume.test.ts +14 -2
- package/src/events/resume.ts +32 -3
- package/src/events/sandbox-notice.ts +31 -1
- package/src/events/transcript.ts +27 -2
- package/src/ids/tunnel-ids.test.ts +30 -0
- package/src/index.ts +12 -0
- package/src/models/agent-catalog.test.ts +4 -3
- package/src/models/agent-catalog.ts +10 -7
- package/src/models/agent-runtimes.ts +30 -1
- package/src/models/capability-ledger.test.ts +4 -0
- package/src/models/model-order.ts +13 -0
- package/src/models/model-pins.ts +5 -4
- package/src/models/model-roles.test.ts +9 -31
- package/src/models/model-roles.ts +45 -50
- package/src/models/model-setups.test.ts +100 -0
- package/src/models/model-setups.ts +151 -0
- package/src/models/plan-pools.test.ts +58 -0
- package/src/models/plan-pools.ts +38 -7
- package/src/models/provider-specs.ts +20 -2
- package/src/policy/command-classes.ts +34 -3
- package/src/policy/control-scopes.ts +27 -0
- package/src/policy/credential-material.ts +58 -16
- package/src/policy/device-programs.ts +137 -0
- package/src/policy/identity-assertion.ts +130 -0
- package/src/policy/sandbox-power.ts +247 -0
- package/src/policy/search-globs.ts +135 -0
- package/src/policy/secret-hosts.ts +7 -0
- package/src/policy/tests/command-classes.test.ts +35 -0
- package/src/policy/tests/credential-material.test.ts +20 -0
- package/src/policy/tests/device-programs.test.ts +107 -0
- package/src/policy/tests/identity-assertion.test.ts +79 -0
- package/src/policy/tests/sandbox-power.test.ts +181 -0
- package/src/policy/tests/search-globs.test.ts +46 -1
- package/src/policy/tests/secret-hosts.test.ts +17 -1
- package/src/policy/turned-away.ts +2 -0
- package/src/protocol/doors/runner-protocol.ts +21 -1
- package/src/protocol/pairing-redeem.test.ts +86 -0
- package/src/protocol/pairing-redeem.ts +79 -0
- package/src/protocol/peer-mcp-server.ts +1 -2
- package/src/protocol/raw/raw-json-routes.test.ts +87 -0
- package/src/protocol/raw/raw-json-routes.ts +127 -5
- package/src/protocol/raw/raw-routes.ts +11 -0
- package/src/protocol/sse.test.ts +50 -1
- package/src/protocol/sse.ts +172 -26
- package/src/schemas/agent.ts +23 -2
- package/src/schemas/automations.ts +16 -5
- package/src/schemas/daemon-health.test.ts +28 -0
- package/src/schemas/device-swaps.test.ts +39 -0
- package/src/schemas/device-swaps.ts +26 -0
- package/src/schemas/devices.ts +11 -2
- package/src/schemas/engines.ts +1 -1
- package/src/schemas/environment.ts +4 -0
- package/src/schemas/git/git-scope.test.ts +91 -0
- package/src/schemas/git/git-scope.ts +71 -0
- package/src/schemas/git/git.ts +15 -1
- package/src/schemas/hosts.ts +7 -0
- package/src/schemas/keep-warm.ts +2 -1
- package/src/schemas/logs.ts +25 -0
- package/src/schemas/members.ts +8 -0
- package/src/schemas/metrics.test.ts +80 -0
- package/src/schemas/metrics.ts +31 -9
- package/src/schemas/needs.ts +80 -4
- package/src/schemas/offload.ts +76 -0
- package/src/schemas/privacy.ts +27 -3
- package/src/schemas/project-bring-back.test.ts +15 -0
- package/src/schemas/project-bring-back.ts +78 -0
- package/src/schemas/providers/handoff.test.ts +42 -0
- package/src/schemas/providers/handoff.ts +128 -0
- package/src/schemas/providers/plan-limits.ts +14 -1
- package/src/schemas/providers/usage.ts +13 -7
- package/src/schemas/restart-intent.test.ts +45 -0
- package/src/schemas/restart-intent.ts +47 -0
- package/src/schemas/run-targets.ts +5 -2
- package/src/schemas/settings-history.ts +28 -0
- package/src/schemas/settings.test.ts +74 -21
- package/src/schemas/settings.ts +80 -44
- package/src/schemas/short-codes.ts +59 -0
- package/src/schemas/speech.ts +4 -0
- package/src/schemas/system.ts +106 -5
- package/src/schemas/voice.ts +161 -0
- package/src/schemas/workspace/workspace-upload.ts +11 -0
- package/src/state/arrival.ts +3 -0
- package/src/state/history-state.ts +9 -0
- package/src/state/runtime-state.test.ts +13 -0
- package/src/state/runtime-state.ts +11 -5
- package/src/state/update-probation.test.ts +45 -0
- package/src/state/update-probation.ts +169 -0
- package/src/state/versions.test.ts +30 -1
- package/src/state/versions.ts +68 -0
- package/src/state/workspace-state.ts +4 -1
- package/src/text/embed.test.ts +14 -0
- package/src/text/embed.ts +19 -10
- package/src/text/title.ts +5 -9
- package/src/text/transcript-fold.test.ts +61 -0
- package/src/text/transcript-fold.ts +31 -6
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Speaking a reply aloud: which voices a sandbox can speak with, which one speaks a language, and the live wire the
|
|
4
|
+
// editor plays speech over. The daemon's engine is src/speech/voice; the editor's half is
|
|
5
|
+
// _editor/web/src/features/chat/voice. Dictation, the other direction, is schemas/speech.ts.
|
|
6
|
+
|
|
7
|
+
// Where a voice is made. `kokoro` and `piper` run on this sandbox's own CPU (sherpa-onnx, the runtime dictation already
|
|
8
|
+
// loads), so nothing leaves it; the rest are providers the owner connected with an API key of their own.
|
|
9
|
+
export const VoiceEngineSchema = z.enum(["kokoro", "piper", "elevenlabs", "cartesia", "deepgram"]);
|
|
10
|
+
export type VoiceEngine = z.infer<typeof VoiceEngineSchema>;
|
|
11
|
+
|
|
12
|
+
export const CloudVoiceProviderSchema = z.enum(["elevenlabs", "cartesia", "deepgram"]);
|
|
13
|
+
export type CloudVoiceProvider = z.infer<typeof CloudVoiceProviderSchema>;
|
|
14
|
+
|
|
15
|
+
export const isCloudVoiceEngine = (engine: VoiceEngine): engine is CloudVoiceProvider => CloudVoiceProviderSchema.safeParse(engine).success;
|
|
16
|
+
|
|
17
|
+
// A voice, as `<engine>/<name>`: `kokoro/af_heart`, `piper/pl_PL-gosia-medium`, `elevenlabs/<voice id>`. One string so a
|
|
18
|
+
// setting, a preview and a log line all name a voice the same way.
|
|
19
|
+
export const VOICE_ID = /^(kokoro|piper|elevenlabs|cartesia|deepgram)\/[\w.-]+$/u;
|
|
20
|
+
export const VoiceIdSchema = z.string().regex(VOICE_ID);
|
|
21
|
+
export type VoiceId = z.infer<typeof VoiceIdSchema>;
|
|
22
|
+
|
|
23
|
+
export const voiceEngineOf = (id: VoiceId): VoiceEngine => VoiceEngineSchema.parse(id.slice(0, id.indexOf("/")));
|
|
24
|
+
export const voiceNameOf = (id: VoiceId): string => id.slice(id.indexOf("/") + 1);
|
|
25
|
+
|
|
26
|
+
// A language as an ISO 639-1 primary subtag, lowercased: `en`, `pl`. Regional voices (British English, Brazilian
|
|
27
|
+
// Portuguese) are voices of the one language, chosen like any other.
|
|
28
|
+
export const VoiceLanguageSchema = z.string().regex(/^[a-z]{2,3}$/u);
|
|
29
|
+
|
|
30
|
+
// A provider the owner connected for speaking: its key stays in the sandbox's secrets, named here.
|
|
31
|
+
export const CloudVoiceSchema = z.object({
|
|
32
|
+
provider: CloudVoiceProviderSchema.describe("Which provider speaks."),
|
|
33
|
+
secret: z.string().min(1).describe("The stored secret holding its API key, by name, as the Secrets page lists it."),
|
|
34
|
+
model: z.string().default("").describe("Which of its models speaks. Empty takes its fast default, the one made for conversation."),
|
|
35
|
+
voice: z.string().default("").describe("Which of its voices, by the provider's own voice id. Empty takes a default that speaks the language."),
|
|
36
|
+
});
|
|
37
|
+
export type CloudVoice = z.infer<typeof CloudVoiceSchema>;
|
|
38
|
+
|
|
39
|
+
// The sandbox's speaking choices (SandboxSettings.voice).
|
|
40
|
+
export const VoiceSettingsSchema = z.object({
|
|
41
|
+
replies: z
|
|
42
|
+
.boolean()
|
|
43
|
+
.default(true)
|
|
44
|
+
.describe(
|
|
45
|
+
"When you talk to the agent hands-free, have it answer out loud: a few spoken sentences saying what it did and what it needs from you, while the full reply stays on screen.",
|
|
46
|
+
),
|
|
47
|
+
interrupt: z
|
|
48
|
+
.boolean()
|
|
49
|
+
.default(false)
|
|
50
|
+
.describe(
|
|
51
|
+
"Stop speaking the moment you start talking over it. Off, the microphone rests while a reply is spoken, which is the safe choice on speakers: a voice the browser fails to cancel would otherwise be heard as yours.",
|
|
52
|
+
),
|
|
53
|
+
speed: z.number().min(0.5).max(2).default(1).describe("How fast it speaks, 1 being the voice's own pace."),
|
|
54
|
+
voices: z
|
|
55
|
+
.record(VoiceLanguageSchema, VoiceIdSchema)
|
|
56
|
+
.default({})
|
|
57
|
+
.describe(
|
|
58
|
+
"Which voice speaks each language, where you picked one. A language left out is spoken by the connected provider when there is one, and otherwise by its default on this sandbox: Kokoro for the eight languages it speaks, Piper for the rest.",
|
|
59
|
+
),
|
|
60
|
+
cloud: CloudVoiceSchema.optional().describe(
|
|
61
|
+
"A speech provider of your own, used for every language it speaks unless you picked a voice on this sandbox for it. Its key is a stored secret; text it speaks leaves the sandbox for that provider.",
|
|
62
|
+
),
|
|
63
|
+
});
|
|
64
|
+
export type VoiceSettings = z.infer<typeof VoiceSettingsSchema>;
|
|
65
|
+
|
|
66
|
+
// One voice the sandbox can speak with, as the settings page lists it.
|
|
67
|
+
export const VoiceInfoSchema = z.object({
|
|
68
|
+
id: VoiceIdSchema,
|
|
69
|
+
label: z.string().describe("Its name as a person would say it."),
|
|
70
|
+
languages: z.array(VoiceLanguageSchema).describe("What it speaks; a regional voice lists its language."),
|
|
71
|
+
accent: z.string().optional().describe("The region it sounds from, where the language has several (US, GB, BR)."),
|
|
72
|
+
gender: z.enum(["female", "male"]).optional(),
|
|
73
|
+
// The download a local voice's model costs, the first time anything asks it to speak; shared by every voice of
|
|
74
|
+
// one model (Kokoro's 54 voices are one download).
|
|
75
|
+
model: z.string().optional().describe("The model a local voice lives in."),
|
|
76
|
+
bytes: z.number().optional().describe("How much that model downloads, the first time it speaks."),
|
|
77
|
+
installed: z.boolean().optional().describe("Its model is on this sandbox already."),
|
|
78
|
+
});
|
|
79
|
+
export type VoiceInfo = z.infer<typeof VoiceInfoSchema>;
|
|
80
|
+
|
|
81
|
+
// GET /speech/voices: everything the settings page needs to choose.
|
|
82
|
+
export const VoicesViewSchema = z.object({
|
|
83
|
+
voices: z.array(VoiceInfoSchema),
|
|
84
|
+
// The local default for every language a local voice speaks; what speaks a language nobody chose for.
|
|
85
|
+
defaults: z.record(VoiceLanguageSchema, VoiceIdSchema),
|
|
86
|
+
cloud: z
|
|
87
|
+
.object({
|
|
88
|
+
provider: CloudVoiceProviderSchema,
|
|
89
|
+
ready: z.boolean().describe("Its secret resolves, so it can be asked."),
|
|
90
|
+
languages: z.array(VoiceLanguageSchema).describe("What it speaks; any other language falls back to this sandbox's voices."),
|
|
91
|
+
})
|
|
92
|
+
.optional(),
|
|
93
|
+
provisioned: z.boolean().describe("This sandbox can run speech synthesis at all."),
|
|
94
|
+
});
|
|
95
|
+
export type VoicesView = z.infer<typeof VoicesViewSchema>;
|
|
96
|
+
|
|
97
|
+
// Where the voice that speaks a phrase stands: its model on disk, still on its way, or a provider's.
|
|
98
|
+
export const VoiceStatusSchema = z.object({
|
|
99
|
+
voice: VoiceIdSchema,
|
|
100
|
+
model: z.enum(["ready", "downloading", "absent", "failed", "cloud"]),
|
|
101
|
+
received: z.number().optional().describe("Bytes of the model fetched so far, while it downloads."),
|
|
102
|
+
total: z.number().optional().describe("The model's whole download, while it downloads."),
|
|
103
|
+
error: z.string().optional(),
|
|
104
|
+
});
|
|
105
|
+
export type VoiceStatus = z.infer<typeof VoiceStatusSchema>;
|
|
106
|
+
|
|
107
|
+
// What a text is, so the daemon knows how to say it:
|
|
108
|
+
// - `plain`: words to say as they are (a preview, a question card read out, Discord).
|
|
109
|
+
// - `read`: a written reply read aloud whole: code is announced rather than spelled, tables and links shortened.
|
|
110
|
+
// - `reply`: a written reply turned into a few spoken sentences first (the `spoken-reply` job, or its first lines when
|
|
111
|
+
// no model is set for it); the words chosen come back as `spoken` before they are heard.
|
|
112
|
+
export const SpeakAsSchema = z.enum(["plain", "read", "reply"]);
|
|
113
|
+
export type SpeakAs = z.infer<typeof SpeakAsSchema>;
|
|
114
|
+
|
|
115
|
+
// The longest text one utterance takes: a long reply read whole, with headroom.
|
|
116
|
+
export const MAX_SPOKEN_TEXT = 40_000;
|
|
117
|
+
|
|
118
|
+
// GET /speech/say, a WebSocket (one per player, many utterances): `say` opens utterance `id`, `stop` ends it (and the
|
|
119
|
+
// audio still queued for it is dropped). The daemon answers, per utterance, `status` while its voice's model is on
|
|
120
|
+
// its way, `spoken` with the words chosen (always for `reply`, so they can be shown), then pairs of `audio` (a JSON
|
|
121
|
+
// header) and one binary frame of that many mono s16le samples, then `done`. Utterances are spoken one at a time, in
|
|
122
|
+
// the order they were asked.
|
|
123
|
+
export const VoiceClientMessageSchema = z.discriminatedUnion("type", [
|
|
124
|
+
z.object({
|
|
125
|
+
type: z.literal("say"),
|
|
126
|
+
id: z.number(),
|
|
127
|
+
text: z.string().min(1).max(MAX_SPOKEN_TEXT),
|
|
128
|
+
as: SpeakAsSchema.default("plain"),
|
|
129
|
+
lang: z.string().optional().describe("The reader's language, a hint: the text's own language decides when it can be told."),
|
|
130
|
+
voice: VoiceIdSchema.optional().describe("Speak with this voice whatever the settings say: a preview."),
|
|
131
|
+
asked: z.string().max(4000).optional().describe("For `reply`: what the person asked, so the spoken answer can be in their language."),
|
|
132
|
+
conversationId: z.string().optional().describe("For `reply`: the conversation it answers, for the providers allowed to read it."),
|
|
133
|
+
}),
|
|
134
|
+
z.object({ type: z.literal("stop"), id: z.number() }),
|
|
135
|
+
]);
|
|
136
|
+
export type VoiceClientMessage = z.infer<typeof VoiceClientMessageSchema>;
|
|
137
|
+
|
|
138
|
+
export const VoiceServerMessageSchema = z.discriminatedUnion("type", [
|
|
139
|
+
z.object({ type: z.literal("status"), id: z.number(), status: VoiceStatusSchema }),
|
|
140
|
+
z.object({
|
|
141
|
+
type: z.literal("spoken"),
|
|
142
|
+
id: z.number(),
|
|
143
|
+
text: z.string().describe("The words about to be said."),
|
|
144
|
+
lang: VoiceLanguageSchema,
|
|
145
|
+
voice: VoiceIdSchema,
|
|
146
|
+
}),
|
|
147
|
+
z.object({
|
|
148
|
+
type: z.literal("audio"),
|
|
149
|
+
id: z.number(),
|
|
150
|
+
sampleRate: z.number(),
|
|
151
|
+
samples: z.number().describe("How many mono s16le samples the binary frame right after this one holds."),
|
|
152
|
+
}),
|
|
153
|
+
z.object({ type: z.literal("done"), id: z.number() }),
|
|
154
|
+
z.object({
|
|
155
|
+
type: z.literal("notice"),
|
|
156
|
+
id: z.number(),
|
|
157
|
+
message: z.string().describe("Something worth saying that did not stop it: a provider that failed, so this sandbox's voice spoke instead."),
|
|
158
|
+
}),
|
|
159
|
+
z.object({ type: z.literal("error"), id: z.number().optional(), message: z.string() }),
|
|
160
|
+
]);
|
|
161
|
+
export type VoiceServerMessage = z.infer<typeof VoiceServerMessageSchema>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// What the editor sends ahead of a drop so a re-drop only moves what changed: POST /workspace/upload-diff takes each
|
|
4
|
+
// dropped file's destination path with its source size and mtime (ms, from File.lastModified), and answers the paths
|
|
5
|
+
// already identical on disk, whose bytes then never upload.
|
|
6
|
+
export const UploadManifestSchema = z.object({
|
|
7
|
+
files: z.array(z.object({ path: z.string(), size: z.number(), mtime: z.number() })),
|
|
8
|
+
});
|
|
9
|
+
export type UploadManifest = z.infer<typeof UploadManifestSchema>;
|
|
10
|
+
export const UploadSkipSchema = z.object({ skip: z.array(z.string()) });
|
|
11
|
+
export type UploadSkip = z.infer<typeof UploadSkipSchema>;
|
package/src/state/arrival.ts
CHANGED
|
@@ -108,6 +108,9 @@ export const ArrivalHostSchema = z.object({
|
|
|
108
108
|
export type ArrivalHost = z.infer<typeof ArrivalHostSchema>;
|
|
109
109
|
export const ArrivalHostsSchema = z.object({ hosts: z.array(ArrivalHostSchema) });
|
|
110
110
|
|
|
111
|
+
// DELETE /arrivals: whether there was a held plan to put away.
|
|
112
|
+
export const ArrivalAbandonedSchema = z.object({ ok: z.boolean() });
|
|
113
|
+
|
|
111
114
|
// Reads a setup off a connected device instead of an upload; answers with a plan exactly like the upload door does.
|
|
112
115
|
export const ArrivalScanSchema = z.object({ host: z.string().min(1) });
|
|
113
116
|
export type ArrivalScan = z.infer<typeof ArrivalScanSchema>;
|
|
@@ -31,6 +31,10 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
31
31
|
// (settings/member-audience.ts). Carried: it is theirs, and asking again after a move would be a question already
|
|
32
32
|
// answered.
|
|
33
33
|
{ path: "member-audience.json", portability: "carry" },
|
|
34
|
+
// Each person's own answers to the editor's getting-started checklist (put away, a step passed over), keyed by sign-in
|
|
35
|
+
// address (settings/member-getting-started.ts). Carried for the same reason: a checklist someone put away should not
|
|
36
|
+
// come back after a move.
|
|
37
|
+
{ path: "member-getting-started.json", portability: "carry" },
|
|
34
38
|
{ path: "activity.jsonl", portability: "carry" },
|
|
35
39
|
{ path: "usage.jsonl", portability: "carry" },
|
|
36
40
|
{ path: "account-usage.json", portability: "carry" },
|
|
@@ -223,6 +227,11 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
223
227
|
portability: "identity",
|
|
224
228
|
note: "Approve again from Approvals anything that was approved and had not run yet.",
|
|
225
229
|
},
|
|
230
|
+
// The wakes automations hold for a person, one file each (the daemon's automations/held-wakes-store.ts): daemon-minted, so
|
|
231
|
+
// kept where no turn writes. They travel with the workspace as they always did; a countdown hold arrives waiting for a
|
|
232
|
+
// person, since the key that signed it stays behind.
|
|
233
|
+
{ path: "automation-holds/", portability: "carry" },
|
|
234
|
+
{ path: "automation-holds.key", portability: "identity", note: "A held wake that was counting down arrives waiting for your yes instead." },
|
|
226
235
|
{ path: "local-cert/", portability: "identity" },
|
|
227
236
|
|
|
228
237
|
/* ---- the state engine's own bookkeeping (the daemon's store/evolution/state-journal.ts) ---- */
|
|
@@ -36,6 +36,19 @@ describe(`staleRuntimeQueryKeys`, () => {
|
|
|
36
36
|
// The feed is ["activity", <sandbox>] and the status ["activity-status", <sandbox>]: two keys, not one prefix.
|
|
37
37
|
expect(staleRuntimeQueryKeys([`activity`])).toEqual([[`activity`], [`activity-status`]]);
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
it(`re-reads the Allowed list when a card's yes is written or taken back, which no file push carries`, () => {
|
|
41
|
+
expect(staleRuntimeQueryKeys([`grants`])).toEqual([[`grants`]]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it(`re-reads each paired-machine roster on its own door's push, and the sync state on a device's`, () => {
|
|
45
|
+
// The dialogs and rows that name a machine read one roster each (the editor's PAIRED_* and RUNNERS families), so a
|
|
46
|
+
// socket opening or closing, or a sync key enrolled, moves exactly the keys that show it.
|
|
47
|
+
expect(staleRuntimeQueryKeys([`hosts`])).toEqual([[`capabilities`], [`devices`], [`hosts`], [`sync-health`]]);
|
|
48
|
+
expect(staleRuntimeQueryKeys([`webext`])).toEqual([[`capabilities`], [`webext`]]);
|
|
49
|
+
expect(staleRuntimeQueryKeys([`phones`])).toEqual([[`capabilities`], [`phones`]]);
|
|
50
|
+
expect(staleRuntimeQueryKeys([`runners`])).toEqual([[`runners`]]);
|
|
51
|
+
});
|
|
39
52
|
});
|
|
40
53
|
|
|
41
54
|
describe(`RUNTIME_DOMAIN_BINDINGS`, () => {
|
|
@@ -37,11 +37,13 @@ const RUNTIME_DOMAINS = [
|
|
|
37
37
|
// Daemon-held; changes continuously (tokens, tool uses), so it's rate-limited rather than pushed per mutation.
|
|
38
38
|
{ domain: "subagents", invalidates: [["subagents"]] },
|
|
39
39
|
|
|
40
|
-
// Machines on the other end of a socket (devices, browsers, runners); "online" is the socket itself, never
|
|
41
|
-
// hosts/webext land on capabilities since a pairing card's state is literally the hub's online answer
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
// Machines on the other end of a socket (devices, browsers, phones, runners); "online" is the socket itself, never
|
|
41
|
+
// sampled. hosts/webext/phones land on capabilities since a pairing card's state is literally the hub's online answer,
|
|
42
|
+
// and each on the roster its dialogs and rows read (`hosts`, `webext`, `phones`; the editor's PAIRED_* families).
|
|
43
|
+
// `hosts` also carries a device reading that landed too late for the reader waiting on it (hosts/device-reports.ts),
|
|
44
|
+
// and a desktop-sync enrollment made or revoked, which is what moves `sync-health` (GET /system/sync).
|
|
45
|
+
{ domain: "hosts", invalidates: [["capabilities"], ["devices"], ["hosts"], ["sync-health"]] },
|
|
46
|
+
{ domain: "webext", invalidates: [["capabilities"], ["webext"]] },
|
|
45
47
|
{ domain: "phones", invalidates: [["capabilities"], ["phones"]] },
|
|
46
48
|
{ domain: "runners", invalidates: [["runners"]] },
|
|
47
49
|
|
|
@@ -66,6 +68,10 @@ const RUNTIME_DOMAINS = [
|
|
|
66
68
|
// An automatic update started waiting, changed what it waits on, began counting down or handed itself to the
|
|
67
69
|
// machine: what every page needs to hear before the sandbox goes quiet under it, wherever the reader is.
|
|
68
70
|
{ domain: "update", invalidates: [["update"]] },
|
|
71
|
+
|
|
72
|
+
// A standing yes written or taken back outside a need's answer: a permission card's pass, a release, a purge. They
|
|
73
|
+
// live under the auth root, which no file push watches, and the Allowed list must move with the card that wrote one.
|
|
74
|
+
{ domain: "grants", invalidates: [["grants"]] },
|
|
69
75
|
] as const satisfies readonly RuntimeDomainBinding[];
|
|
70
76
|
|
|
71
77
|
export const RUNTIME_DOMAIN_BINDINGS: readonly RuntimeDomainBinding[] = RUNTIME_DOMAINS;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { packageRoot } from "@intentic/constants/node";
|
|
4
|
+
import { CRASH_LOOP_RESTARTS, judgeProbation, type ProbationLook, probationStep, type ProbationStep, type ProbationVerdict, UPDATE_PROBATION } from "./update-probation.js";
|
|
5
|
+
|
|
6
|
+
// The update probation policy and its rule, held to golden/update-probation.json: the cases hand-written from ic's Rust
|
|
7
|
+
// (probation.rs), which builds every one of them too. A number or a reason that moves in one language and not the other
|
|
8
|
+
// fails here or there, naming the case.
|
|
9
|
+
|
|
10
|
+
interface JudgeCase {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly look: Partial<ProbationLook>;
|
|
13
|
+
readonly swap: { readonly at: number; readonly reach: string | null };
|
|
14
|
+
readonly now: number;
|
|
15
|
+
readonly graceMs?: number;
|
|
16
|
+
readonly verdict: ProbationVerdict;
|
|
17
|
+
}
|
|
18
|
+
interface StepCase {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly verdict: ProbationVerdict;
|
|
21
|
+
readonly strikes: number;
|
|
22
|
+
readonly over: boolean;
|
|
23
|
+
readonly step: ProbationStep;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// SAFETY: the golden is this package's own file; the assertions below fail on any field it lacks.
|
|
27
|
+
const GOLDEN = JSON.parse(readFileSync(join(packageRoot(import.meta.url), "golden", "update-probation.json"), "utf8")) as {
|
|
28
|
+
readonly policy: Record<string, number>;
|
|
29
|
+
readonly base: ProbationLook;
|
|
30
|
+
readonly judge: JudgeCase[];
|
|
31
|
+
readonly steps: StepCase[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
test("the policy numbers are the ones the golden records for ic, to the unit", () => {
|
|
35
|
+
expect(Object.fromEntries(Object.entries(UPDATE_PROBATION))).toEqual(GOLDEN.policy);
|
|
36
|
+
expect(GOLDEN.policy).toHaveProperty("crashLoopRestarts", CRASH_LOOP_RESTARTS);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test.each(GOLDEN.judge)("judges: $name", ({ look, swap, now, graceMs, verdict }) => {
|
|
40
|
+
expect(judgeProbation({ ...GOLDEN.base, ...look }, swap, now, graceMs)).toEqual(verdict);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test.each(GOLDEN.steps)("steps: $name", ({ verdict, strikes, over, step }) => {
|
|
44
|
+
expect(probationStep(verdict, strikes, over)).toEqual(step);
|
|
45
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// THE POLICY AN UPDATE IS HELD TO AFTER IT TOOK, one policy on two substrates.
|
|
2
|
+
//
|
|
3
|
+
// A new version of a sandbox that came up is not yet a version that works. On a machine `ic` runs, the old container stays
|
|
4
|
+
// parked and ready while a watch looks at the new one (_sandbox/ic/src/sandbox/probation.rs), and puts the old one back
|
|
5
|
+
// by itself if it keeps failing. On Fly the platform's api does the same job over the image the machine's row keeps as
|
|
6
|
+
// its way back (_platform/api/src/sandbox/hosted/gate/hosted-probation.ts). The two cannot share code (one looks through
|
|
7
|
+
// Docker, the other through Fly's exec), so they share what decides: the numbers below, and `judgeProbation` /
|
|
8
|
+
// `probationStep`, the rule that turns one look at the new version into a verdict and a verdict into a step.
|
|
9
|
+
//
|
|
10
|
+
// `ic` owns the rule; this is its TS twin, and golden/update-probation.json holds each number and each case, hand-written
|
|
11
|
+
// from the Rust: ic's test (probation.rs) and this package's test (update-probation.test.ts) both build every one, so
|
|
12
|
+
// neither can drift without the other failing. A number changes here, in the golden and in ic's constant together.
|
|
13
|
+
|
|
14
|
+
// Restarts that make a crash loop, whoever counts them: netd restarting a crashed daemon inside a container that stays up,
|
|
15
|
+
// Docker restarting the container, Fly restarting the machine, or the editor reading a sandbox's vitals (diagnose.ts).
|
|
16
|
+
// ic's health.rs CRASH_LOOP_RESTARTS and the platform's gate (daemon-health.ts) are this number too.
|
|
17
|
+
export const CRASH_LOOP_RESTARTS = 3;
|
|
18
|
+
|
|
19
|
+
export const UPDATE_PROBATION = {
|
|
20
|
+
// How long the version before an update is kept ready to go back to (`IC_PROBATION_SECONDS` shortens ic's, for its
|
|
21
|
+
// nightly drill), and how long the platform watches a hosted machine's new image.
|
|
22
|
+
watchMs: 24 * 60 * 60_000,
|
|
23
|
+
// How long a new version may stay not-ready, unreachable or silent before that counts against it: longer than any
|
|
24
|
+
// healthy first boot (state conversions, a cold cache), which the switch's own health wait already sat through.
|
|
25
|
+
graceMs: 10 * 60_000,
|
|
26
|
+
// An open state journal is a boot still converting; past this it is a boot that will not finish.
|
|
27
|
+
journalMs: 15 * 60_000,
|
|
28
|
+
// Consecutive failed looks that end a probation in a rollback: never one bad answer.
|
|
29
|
+
strikes: 3,
|
|
30
|
+
// Rollback targets kept BEYOND `previous`, newest first: older builds `ic sandbox rollback --to` reaches without a
|
|
31
|
+
// download (record.rs MAX_KEPT). The hosted row keeps only `previous`; see the platform's gate-row.ts.
|
|
32
|
+
keptBuilds: 2,
|
|
33
|
+
// Restarts that are a crash loop on their own, of the container and of the daemon inside it.
|
|
34
|
+
crashLoopRestarts: CRASH_LOOP_RESTARTS,
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
// A tunnel that has been `checking` this much longer than the grace has not come up.
|
|
38
|
+
const TUNNEL_EXTRA_MS = 5 * 60_000;
|
|
39
|
+
|
|
40
|
+
// What one look at the new version found: the same facts ic gathers through Docker and the platform through Fly.
|
|
41
|
+
// `null` is "not known" (a daemon too old to say), which is never held against a version.
|
|
42
|
+
export interface ProbationLook {
|
|
43
|
+
// The container (the Fly machine) is running, and whether it is on its way down or up.
|
|
44
|
+
readonly running: boolean;
|
|
45
|
+
readonly restarting: boolean;
|
|
46
|
+
// How many times the container has restarted: past `crashLoopRestarts` it is a crash loop on its own.
|
|
47
|
+
readonly restarts: number;
|
|
48
|
+
// `/health` answered with a document.
|
|
49
|
+
readonly answered: boolean;
|
|
50
|
+
// `boot.ready` (or an older daemon's top-level `ready`).
|
|
51
|
+
readonly ready: boolean | null;
|
|
52
|
+
// `state.journal`: open, none, failed.
|
|
53
|
+
readonly journal: string | null;
|
|
54
|
+
// `reach.state`: off, checking, reachable, unreachable.
|
|
55
|
+
readonly reach: string | null;
|
|
56
|
+
// What the daemon wrote to /history/boot-failure.json after the swap began.
|
|
57
|
+
readonly bootFailure: string | null;
|
|
58
|
+
// How many times the watch has seen the daemon start again inside a running container.
|
|
59
|
+
readonly daemonRestarts: number;
|
|
60
|
+
// netd's own word that the daemon keeps crashing, already in words.
|
|
61
|
+
readonly crashLoop: string | null;
|
|
62
|
+
// The owner stopped it on purpose: there is nothing to judge until it runs.
|
|
63
|
+
readonly held: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The facts of the swap a look is judged against: when the new version began, and how the old one's tunnel read.
|
|
67
|
+
export interface ProbationSwap {
|
|
68
|
+
readonly at: number;
|
|
69
|
+
// Held only to a tunnel the old version had.
|
|
70
|
+
readonly reach: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type ProbationVerdict =
|
|
74
|
+
// The strikes start over.
|
|
75
|
+
| { readonly kind: "healthy" }
|
|
76
|
+
// Too early to hold anything against it.
|
|
77
|
+
| { readonly kind: "pending" }
|
|
78
|
+
// One failed look.
|
|
79
|
+
| { readonly kind: "strike"; readonly reason: string }
|
|
80
|
+
// Nothing to wait for: go back now.
|
|
81
|
+
| { readonly kind: "fail"; readonly reason: string };
|
|
82
|
+
|
|
83
|
+
const strike = (reason: string): ProbationVerdict => ({ kind: "strike", reason });
|
|
84
|
+
const fail = (reason: string): ProbationVerdict => ({ kind: "fail", reason });
|
|
85
|
+
const PENDING: ProbationVerdict = { kind: "pending" };
|
|
86
|
+
|
|
87
|
+
// The condition a stopped-on-purpose, failed-boot, failed-conversion or crash-looping version is judged by, before the
|
|
88
|
+
// slower ones. Split out so `judgeProbation` reads as the list of what it is judged on, in order.
|
|
89
|
+
const failure = (look: ProbationLook): ProbationVerdict | undefined => {
|
|
90
|
+
if (look.bootFailure !== null) {
|
|
91
|
+
return fail(`the new version could not start: ${look.bootFailure}`);
|
|
92
|
+
}
|
|
93
|
+
if (look.journal === "failed") {
|
|
94
|
+
return fail("the new version could not convert this sandbox's stored files; it put them back as they were");
|
|
95
|
+
}
|
|
96
|
+
if (look.daemonRestarts >= UPDATE_PROBATION.crashLoopRestarts) {
|
|
97
|
+
return fail(`the new version's daemon kept crashing (${look.daemonRestarts} restarts)`);
|
|
98
|
+
}
|
|
99
|
+
if (look.crashLoop !== null) {
|
|
100
|
+
return fail(`the new version's daemon kept crashing: ${look.crashLoop}`);
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Held only to a tunnel the old version had: a sandbox reached on loopback alone has none to lose.
|
|
106
|
+
const tunnel = (look: ProbationLook, swap: ProbationSwap, since: number, graceMs: number): ProbationVerdict | undefined => {
|
|
107
|
+
if (swap.reach !== "reachable") {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
if (look.reach === "unreachable" && since >= graceMs) {
|
|
111
|
+
return strike("the new version cannot be reached through its tunnel");
|
|
112
|
+
}
|
|
113
|
+
if (look.reach === "checking" && since >= graceMs + TUNNEL_EXTRA_MS) {
|
|
114
|
+
return strike("the new version's tunnel never came up");
|
|
115
|
+
}
|
|
116
|
+
return look.reach === "unreachable" || look.reach === "checking" ? PENDING : undefined;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// The judgement, pure over one look, the swap and the clock: ic's `judge` (probation.rs), reason for reason.
|
|
120
|
+
export const judgeProbation = (look: ProbationLook, swap: ProbationSwap, now: number, graceMs: number = UPDATE_PROBATION.graceMs): ProbationVerdict => {
|
|
121
|
+
const since = Math.max(0, now - swap.at);
|
|
122
|
+
const late = since >= graceMs;
|
|
123
|
+
// Stopped on purpose: there is nothing to judge, and going back would start a sandbox its owner stopped.
|
|
124
|
+
if (look.held && !look.running && !look.restarting) {
|
|
125
|
+
return PENDING;
|
|
126
|
+
}
|
|
127
|
+
const failed = failure(look);
|
|
128
|
+
if (failed !== undefined) {
|
|
129
|
+
return failed;
|
|
130
|
+
}
|
|
131
|
+
if (!look.running || look.restarting) {
|
|
132
|
+
return look.restarts >= UPDATE_PROBATION.crashLoopRestarts
|
|
133
|
+
? fail(`the new version kept crashing (${look.restarts} restarts)`)
|
|
134
|
+
: strike("the new version's container is not running");
|
|
135
|
+
}
|
|
136
|
+
if (!look.answered) {
|
|
137
|
+
return late ? strike("the new version's daemon is not answering") : PENDING;
|
|
138
|
+
}
|
|
139
|
+
if (look.journal === "open") {
|
|
140
|
+
return since >= UPDATE_PROBATION.journalMs ? strike("the new version is still converting stored files after 15 minutes") : PENDING;
|
|
141
|
+
}
|
|
142
|
+
if (look.ready === false) {
|
|
143
|
+
return late ? strike("the new version never finished starting up") : PENDING;
|
|
144
|
+
}
|
|
145
|
+
return tunnel(look, swap, since, graceMs) ?? { kind: "healthy" };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// What the watch does with a verdict: go back, keep watching with the strikes as they now stand, or let the old version
|
|
149
|
+
// go because the probation is over.
|
|
150
|
+
export type ProbationStep =
|
|
151
|
+
| { readonly action: "roll-back"; readonly reason: string }
|
|
152
|
+
| { readonly action: "watch"; readonly strikes: number; readonly reason?: string }
|
|
153
|
+
| { readonly action: "keep" };
|
|
154
|
+
|
|
155
|
+
// ic's judge_probation, as a decision over the verdict, the strikes so far and whether the probation is over. Pure.
|
|
156
|
+
export const probationStep = (verdict: ProbationVerdict, strikes: number, over: boolean): ProbationStep => {
|
|
157
|
+
switch (verdict.kind) {
|
|
158
|
+
case "fail":
|
|
159
|
+
return { action: "roll-back", reason: verdict.reason };
|
|
160
|
+
case "strike":
|
|
161
|
+
return strikes + 1 >= UPDATE_PROBATION.strikes
|
|
162
|
+
? { action: "roll-back", reason: verdict.reason }
|
|
163
|
+
: { action: "watch", strikes: strikes + 1, reason: verdict.reason };
|
|
164
|
+
case "healthy":
|
|
165
|
+
return over ? { action: "keep" } : { action: "watch", strikes: 0 };
|
|
166
|
+
case "pending":
|
|
167
|
+
return over ? { action: "keep" } : { action: "watch", strikes };
|
|
168
|
+
}
|
|
169
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEV_VERSION, isBehind, isNewer } from "./versions.js";
|
|
1
|
+
import { compareVersions, DEV_VERSION, isBehind, isNewer, parseVersion } from "./versions.js";
|
|
2
2
|
|
|
3
3
|
test("isNewer compares dotted numeric versions", () => {
|
|
4
4
|
expect(isNewer("1.3.0", "1.2.9")).toBe(true); // newer minor
|
|
@@ -34,3 +34,32 @@ test("a version that isn't dotted-numeric can only ever withhold the nudge", ()
|
|
|
34
34
|
test("isBehind flags the hand-written version agents used to carry", () => {
|
|
35
35
|
expect(isBehind("0.1.0", "1.183.0")).toBe(true);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
test("compareVersions puts a release ahead of its own prereleases", () => {
|
|
39
|
+
expect(compareVersions("1.2.0", "1.2.0-rc.1")).toBe(1);
|
|
40
|
+
expect(compareVersions("1.2.0-rc.1", "1.2.0")).toBe(-1);
|
|
41
|
+
expect(compareVersions("1.2.0-rc.2", "1.2.0-rc.1")).toBe(1);
|
|
42
|
+
expect(compareVersions("1.2.0-rc.10", "1.2.0-rc.9")).toBe(1); // numeric identifiers numerically
|
|
43
|
+
expect(compareVersions("1.2.0-rc.1.1", "1.2.0-rc.1")).toBe(1); // more identifiers, equal so far, is later
|
|
44
|
+
expect(compareVersions("1.2.0-1", "1.2.0-alpha")).toBe(-1); // a number sorts below a word
|
|
45
|
+
expect(compareVersions("1.1.9", "1.2.0-rc.1")).toBe(-1); // the core decides before the prerelease
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("compareVersions tolerates a leading v and ignores build metadata", () => {
|
|
49
|
+
expect(compareVersions("v1.10.0", "1.9.0")).toBe(1);
|
|
50
|
+
expect(compareVersions("V2.0.0", "v2.0.0")).toBe(0);
|
|
51
|
+
expect(compareVersions("1.2.0+build.5", "1.2.0")).toBe(0);
|
|
52
|
+
expect(compareVersions("1.2", "1.2.0")).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("compareVersions sorts anything unparseable below every version", () => {
|
|
56
|
+
expect(compareVersions("unknown", "0.0.1")).toBe(-1);
|
|
57
|
+
expect(compareVersions("1.0.0", "")).toBe(1);
|
|
58
|
+
expect(compareVersions("latest", "unknown")).toBe(0);
|
|
59
|
+
expect(["1.2.0-rc.1", "unknown", "1.10.0", "v1.2.0"].sort((a, b) => compareVersions(b, a))).toEqual(["1.10.0", "v1.2.0", "1.2.0-rc.1", "unknown"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("parseVersion reads the parts and the prerelease", () => {
|
|
63
|
+
expect(parseVersion("v1.2.3-rc.1+abc")).toEqual({ parts: [1, 2, 3], prerelease: ["rc", "1"] });
|
|
64
|
+
expect(parseVersion("1.x")).toBe(undefined);
|
|
65
|
+
});
|
package/src/state/versions.ts
CHANGED
|
@@ -24,3 +24,71 @@ export const DEV_VERSION = `0.0.0`;
|
|
|
24
24
|
// A malformed version compares as not-newer only, so it can withhold a nag but never invent one.
|
|
25
25
|
export const isBehind = (installed: string | undefined, latest: string | undefined): boolean =>
|
|
26
26
|
installed !== undefined && latest !== undefined && installed !== DEV_VERSION && isNewer(latest, installed);
|
|
27
|
+
|
|
28
|
+
// A version read as semver: a leading `v` tolerated, any number of dotted numeric parts (a missing one is 0), a
|
|
29
|
+
// prerelease after `-`, and build metadata after `+`, which never orders anything.
|
|
30
|
+
export interface ParsedVersion {
|
|
31
|
+
readonly parts: readonly number[];
|
|
32
|
+
// The dot-separated prerelease identifiers; empty for a release.
|
|
33
|
+
readonly prerelease: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const VERSION = /^v?(\d+(?:\.\d+)*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/i;
|
|
37
|
+
|
|
38
|
+
// Undefined for anything that is not a version (`unknown`, `latest`, an empty string).
|
|
39
|
+
export const parseVersion = (version: string): ParsedVersion | undefined => {
|
|
40
|
+
const match = VERSION.exec(version.trim());
|
|
41
|
+
if (match === null) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return { parts: match[1]!.split(`.`).map(Number), prerelease: match[2] === undefined ? [] : match[2].split(`.`) };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Semver's rule for one identifier: numbers numerically and below words, words by ASCII.
|
|
48
|
+
const compareIdentifier = (left: string, right: string): number => {
|
|
49
|
+
const [leftNumeric, rightNumeric] = [/^\d+$/.test(left), /^\d+$/.test(right)];
|
|
50
|
+
if (leftNumeric && rightNumeric) {
|
|
51
|
+
return Number(left) - Number(right);
|
|
52
|
+
}
|
|
53
|
+
if (leftNumeric !== rightNumeric) {
|
|
54
|
+
return leftNumeric ? -1 : 1;
|
|
55
|
+
}
|
|
56
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const comparePrerelease = (left: readonly string[], right: readonly string[]): number => {
|
|
60
|
+
// A release outranks every prerelease of itself: 1.2.0 > 1.2.0-rc.1.
|
|
61
|
+
if (left.length === 0 || right.length === 0) {
|
|
62
|
+
return right.length - left.length;
|
|
63
|
+
}
|
|
64
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
65
|
+
const [l, r] = [left[index], right[index]];
|
|
66
|
+
if (l === undefined || r === undefined) {
|
|
67
|
+
// Equal so far, so the longer set of identifiers is the later one: rc.1.1 > rc.1.
|
|
68
|
+
return l === undefined ? -1 : 1;
|
|
69
|
+
}
|
|
70
|
+
const difference = compareIdentifier(l, r);
|
|
71
|
+
if (difference !== 0) {
|
|
72
|
+
return difference;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Negative when `a` is older, positive when newer, zero when the same release, as a sort comparator wants (sort
|
|
79
|
+
// descending with `(a, b) => compareVersions(b, a)`). Anything that does not parse sorts below every version and
|
|
80
|
+
// equal to anything else that does not parse, so "unknown" never outranks a real release. `isNewer` above stays the
|
|
81
|
+
// numeric-only reading the update nag is built on, which treats a prerelease segment as uncertainty.
|
|
82
|
+
export const compareVersions = (a: string, b: string): number => {
|
|
83
|
+
const [left, right] = [parseVersion(a), parseVersion(b)];
|
|
84
|
+
if (left === undefined || right === undefined) {
|
|
85
|
+
return (left === undefined ? 0 : 1) - (right === undefined ? 0 : 1);
|
|
86
|
+
}
|
|
87
|
+
for (let index = 0; index < Math.max(left.parts.length, right.parts.length); index += 1) {
|
|
88
|
+
const difference = (left.parts[index] ?? 0) - (right.parts[index] ?? 0);
|
|
89
|
+
if (difference !== 0) {
|
|
90
|
+
return Math.sign(difference);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Math.sign(comparePrerelease(left.prerelease, right.prerelease));
|
|
94
|
+
};
|
|
@@ -209,10 +209,13 @@ const STATE_FILES = [
|
|
|
209
209
|
why: "A guard's own scratch file; the daemon never reads it and nothing in the browser renders it.",
|
|
210
210
|
portability: "carry",
|
|
211
211
|
},
|
|
212
|
+
// Where releases before 2026-10-12 kept the wakes automations held for a person. They now sit on the history volume
|
|
213
|
+
// (`automation-holds/`, history-state.ts), off the workspace every turn writes, and nothing reads this directory; a
|
|
214
|
+
// build that read it is only ever rolled back to. Declared still, since the extension that listed them binds it.
|
|
212
215
|
{
|
|
213
216
|
path: ".intentic/records/approvals/",
|
|
214
217
|
invalidates: [],
|
|
215
|
-
why: "
|
|
218
|
+
why: "Where releases before 2026-10-12 kept the wakes automations held for a person; they now sit on the history volume, and nothing reads this copy. The intentic.approvals extension's contributes.files still binds it, with `automation-approvals` as its query key, not core's.",
|
|
216
219
|
portability: "carry",
|
|
217
220
|
},
|
|
218
221
|
// Bug-report inbox, one file per fingerprint (daemon's issues-store), rendered by intentic.issues. `carry`: a fact
|
package/src/text/embed.test.ts
CHANGED
|
@@ -47,6 +47,20 @@ test("a JSON fetch answers the body or throws the refusal", async () => {
|
|
|
47
47
|
jest.fn(async () => new Response(JSON.stringify({ error: "rate limited" }), { status: 429 })),
|
|
48
48
|
);
|
|
49
49
|
await expect(fetchEmbedJson("https://x/y")).rejects.toMatchObject({ message: "rate limited", status: 429 });
|
|
50
|
+
stubGlobal(
|
|
51
|
+
"fetch",
|
|
52
|
+
jest.fn(async () => new Response("<html>bad gateway</html>", { status: 502 })),
|
|
53
|
+
);
|
|
54
|
+
await expect(fetchEmbedJson("https://x/y")).rejects.toMatchObject({ message: "request failed (502)", status: 502 });
|
|
55
|
+
unstubAllGlobals();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a door that cannot be reached says so in words a visitor can read, without the URL", async () => {
|
|
59
|
+
stubGlobal(
|
|
60
|
+
"fetch",
|
|
61
|
+
jest.fn(async () => { throw new TypeError("Failed to fetch"); }),
|
|
62
|
+
);
|
|
63
|
+
await expect(fetchEmbedJson("https://x/y")).rejects.toThrow("The server could not be reached: Failed to fetch");
|
|
50
64
|
unstubAllGlobals();
|
|
51
65
|
});
|
|
52
66
|
|