@intentic/sandbox-contract 1.344.0 → 1.346.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 +27 -9
- package/dist/contracts/accounts.contract.d.ts +24 -80
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +247 -8
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +13 -2
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +1570 -33
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +47 -4
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/device.contract.d.ts +244 -1
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.js +5 -1
- package/dist/contracts/device.contract.js.map +1 -1
- package/dist/contracts/endpoints.contract.d.ts +2 -1
- package/dist/contracts/endpoints.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.js +1 -0
- package/dist/contracts/endpoints.contract.js.map +1 -1
- package/dist/contracts/providers.contract.d.ts +0 -1
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/push.contract.d.ts +11 -0
- package/dist/contracts/push.contract.d.ts.map +1 -1
- package/dist/contracts/push.contract.js +10 -1
- package/dist/contracts/push.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +149 -123
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.d.ts +169 -0
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.js +35 -0
- package/dist/contracts/secrets.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +2 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +37 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +235 -31
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +24 -80
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +11 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.js +11 -1
- package/dist/contracts/workspace.contract.js.map +1 -1
- package/dist/events/agent-events.d.ts +60 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +44 -38
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-transcript.d.ts +727 -0
- package/dist/events/agent-transcript.d.ts.map +1 -0
- package/dist/events/agent-transcript.js +34 -0
- package/dist/events/agent-transcript.js.map +1 -0
- package/dist/events/agent-words.d.ts +5 -0
- package/dist/events/agent-words.d.ts.map +1 -1
- package/dist/events/agent-words.js +9 -0
- package/dist/events/agent-words.js.map +1 -1
- package/dist/events/failure-rows.d.ts +14 -0
- package/dist/events/failure-rows.d.ts.map +1 -0
- package/dist/events/failure-rows.js +109 -0
- package/dist/events/failure-rows.js.map +1 -0
- package/dist/events/requests.d.ts +143 -0
- package/dist/events/requests.d.ts.map +1 -1
- package/dist/events/requests.js +35 -1
- package/dist/events/requests.js.map +1 -1
- package/dist/events/sandbox-notice.d.ts +23 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +2 -0
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +156 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/system-events.js +7 -1
- package/dist/events/system-events.js.map +1 -1
- package/dist/events/transcript.d.ts +20 -720
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +17 -19
- package/dist/events/transcript.js.map +1 -1
- package/dist/ids/conversation-ids.d.ts +2 -0
- package/dist/ids/conversation-ids.d.ts.map +1 -1
- package/dist/ids/conversation-ids.js +2 -0
- package/dist/ids/conversation-ids.js.map +1 -1
- package/dist/ids/row-ids.d.ts +3 -0
- package/dist/ids/row-ids.d.ts.map +1 -0
- package/dist/ids/row-ids.js +11 -0
- package/dist/ids/row-ids.js.map +1 -0
- package/dist/index.d.ts +2296 -199
- package/dist/index.d.ts.map +1 -1
- package/dist/models/agent-lanes.d.ts.map +1 -1
- package/dist/models/agent-lanes.js +6 -7
- package/dist/models/agent-lanes.js.map +1 -1
- package/dist/models/agent-standing.d.ts +6 -19
- package/dist/models/agent-standing.d.ts.map +1 -1
- package/dist/models/agent-standing.js +14 -38
- package/dist/models/agent-standing.js.map +1 -1
- package/dist/models/model-pins.d.ts +3 -0
- package/dist/models/model-pins.d.ts.map +1 -1
- package/dist/models/model-pins.js +2 -0
- package/dist/models/model-pins.js.map +1 -1
- package/dist/models/plan-pools.d.ts +26 -4
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +159 -40
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/netd/generated/wire.d.ts +5 -0
- package/dist/netd/generated/wire.d.ts.map +1 -1
- package/dist/policy/command-classes.d.ts +1 -1
- package/dist/policy/command-classes.d.ts.map +1 -1
- package/dist/policy/command-classes.js +2 -2
- package/dist/policy/command-classes.js.map +1 -1
- package/dist/policy/keyring-origins.d.ts +27 -0
- package/dist/policy/keyring-origins.d.ts.map +1 -0
- package/dist/policy/keyring-origins.js +213 -0
- package/dist/policy/keyring-origins.js.map +1 -0
- package/dist/policy/keyring-seal.d.ts +9 -0
- package/dist/policy/keyring-seal.d.ts.map +1 -1
- package/dist/policy/keyring-seal.js +18 -6
- package/dist/policy/keyring-seal.js.map +1 -1
- package/dist/policy/safety-policy.d.ts +1 -1
- package/dist/policy/safety-policy.d.ts.map +1 -1
- package/dist/policy/safety-policy.js +1 -1
- package/dist/policy/safety-policy.js.map +1 -1
- package/dist/policy/turned-away.d.ts +1 -0
- package/dist/policy/turned-away.d.ts.map +1 -1
- package/dist/policy/turned-away.js +10 -0
- package/dist/policy/turned-away.js.map +1 -1
- package/dist/protocol/doors/host-protocol.d.ts +1 -0
- package/dist/protocol/doors/host-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/host-protocol.js +1 -0
- package/dist/protocol/doors/host-protocol.js.map +1 -1
- package/dist/protocol/doors/runner-protocol.d.ts +1 -0
- package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/runner-protocol.js +1 -0
- package/dist/protocol/doors/runner-protocol.js.map +1 -1
- package/dist/protocol/doors/webext-links.d.ts +1 -0
- package/dist/protocol/doors/webext-links.d.ts.map +1 -1
- package/dist/protocol/doors/webext-links.js +1 -0
- package/dist/protocol/doors/webext-links.js.map +1 -1
- package/dist/protocol/peer-dial.d.ts +2 -0
- package/dist/protocol/peer-dial.d.ts.map +1 -1
- package/dist/protocol/peer-dial.js +62 -29
- package/dist/protocol/peer-dial.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +179 -1
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-json-routes.js +5 -0
- package/dist/protocol/raw/raw-json-routes.js.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +35 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +9 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/schemas/agent-next.d.ts +21 -0
- package/dist/schemas/agent-next.d.ts.map +1 -0
- package/dist/schemas/agent-next.js +21 -0
- package/dist/schemas/agent-next.js.map +1 -0
- package/dist/schemas/agent-place.d.ts +10 -0
- package/dist/schemas/agent-place.d.ts.map +1 -0
- package/dist/schemas/agent-place.js +15 -0
- package/dist/schemas/agent-place.js.map +1 -0
- package/dist/schemas/agent-search.d.ts +45 -0
- package/dist/schemas/agent-search.d.ts.map +1 -0
- package/dist/schemas/agent-search.js +28 -0
- package/dist/schemas/agent-search.js.map +1 -0
- package/dist/schemas/agent.d.ts +120 -78
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +40 -49
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents-list.d.ts +54 -0
- package/dist/schemas/agents-list.d.ts.map +1 -1
- package/dist/schemas/agents.d.ts +165 -59
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +25 -50
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/attach.d.ts +8 -0
- package/dist/schemas/attach.d.ts.map +1 -0
- package/dist/schemas/attach.js +14 -0
- package/dist/schemas/attach.js.map +1 -0
- package/dist/schemas/command-admission.d.ts +71 -0
- package/dist/schemas/command-admission.d.ts.map +1 -0
- package/dist/schemas/command-admission.js +27 -0
- package/dist/schemas/command-admission.js.map +1 -0
- package/dist/schemas/compose-overlay.d.ts +9 -0
- package/dist/schemas/compose-overlay.d.ts.map +1 -0
- package/dist/schemas/compose-overlay.js +11 -0
- package/dist/schemas/compose-overlay.js.map +1 -0
- package/dist/schemas/composer-drafts.d.ts +169 -0
- package/dist/schemas/composer-drafts.d.ts.map +1 -0
- package/dist/schemas/composer-drafts.js +60 -0
- package/dist/schemas/composer-drafts.js.map +1 -0
- package/dist/schemas/conversation-links.d.ts +44 -0
- package/dist/schemas/conversation-links.d.ts.map +1 -0
- package/dist/schemas/conversation-links.js +35 -0
- package/dist/schemas/conversation-links.js.map +1 -0
- package/dist/schemas/data-protection.d.ts +1 -1
- package/dist/schemas/data-protection.d.ts.map +1 -1
- package/dist/schemas/device-commands.d.ts +63 -0
- package/dist/schemas/device-commands.d.ts.map +1 -0
- package/dist/schemas/device-commands.js +42 -0
- package/dist/schemas/device-commands.js.map +1 -0
- package/dist/schemas/device-folder.d.ts +11 -0
- package/dist/schemas/device-folder.d.ts.map +1 -1
- package/dist/schemas/device-folder.js +1 -0
- package/dist/schemas/device-folder.js.map +1 -1
- package/dist/schemas/devices.d.ts +18 -68
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +8 -45
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/hosts.d.ts +29 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +23 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/keyring-native.d.ts +137 -0
- package/dist/schemas/keyring-native.d.ts.map +1 -0
- package/dist/schemas/keyring-native.js +69 -0
- package/dist/schemas/keyring-native.js.map +1 -0
- package/dist/schemas/keyring.d.ts +413 -0
- package/dist/schemas/keyring.d.ts.map +1 -1
- package/dist/schemas/keyring.js +139 -4
- package/dist/schemas/keyring.js.map +1 -1
- package/dist/schemas/phone-passwords.d.ts +186 -0
- package/dist/schemas/phone-passwords.d.ts.map +1 -0
- package/dist/schemas/phone-passwords.js +25 -0
- package/dist/schemas/phone-passwords.js.map +1 -0
- package/dist/schemas/privacy-egress.d.ts +41 -0
- package/dist/schemas/privacy-egress.d.ts.map +1 -0
- package/dist/schemas/privacy-egress.js +30 -0
- package/dist/schemas/privacy-egress.js.map +1 -0
- package/dist/schemas/privacy.d.ts +1 -0
- package/dist/schemas/privacy.d.ts.map +1 -1
- package/dist/schemas/privacy.js +1 -0
- package/dist/schemas/privacy.js.map +1 -1
- package/dist/schemas/providers/plan-limits.d.ts +76 -130
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +36 -23
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +24 -82
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/provider-oauth.js +1 -6
- package/dist/schemas/providers/provider-oauth.js.map +1 -1
- package/dist/schemas/providers/refusals.d.ts +64 -0
- package/dist/schemas/providers/refusals.d.ts.map +1 -0
- package/dist/schemas/providers/refusals.js +33 -0
- package/dist/schemas/providers/refusals.js.map +1 -0
- package/dist/schemas/providers/usage.d.ts +1 -0
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +1 -0
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/push.d.ts +13 -0
- package/dist/schemas/push.d.ts.map +1 -1
- package/dist/schemas/push.js +11 -1
- package/dist/schemas/push.js.map +1 -1
- package/dist/schemas/sandbox-passwords.d.ts +288 -0
- package/dist/schemas/sandbox-passwords.d.ts.map +1 -0
- package/dist/schemas/sandbox-passwords.js +66 -0
- package/dist/schemas/sandbox-passwords.js.map +1 -0
- package/dist/schemas/secrets.d.ts +2 -0
- package/dist/schemas/secrets.d.ts.map +1 -1
- package/dist/schemas/secrets.js +4 -0
- package/dist/schemas/secrets.js.map +1 -1
- package/dist/schemas/sessions.js +1 -1
- package/dist/schemas/sessions.js.map +1 -1
- package/dist/schemas/settings-savings.d.ts +709 -0
- package/dist/schemas/settings-savings.d.ts.map +1 -0
- package/dist/schemas/settings-savings.js +53 -0
- package/dist/schemas/settings-savings.js.map +1 -0
- package/dist/schemas/settings.d.ts +7 -671
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +5 -50
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +16 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +11 -0
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/terminal.d.ts +5 -0
- package/dist/schemas/terminal.d.ts.map +1 -1
- package/dist/schemas/terminal.js +4 -0
- package/dist/schemas/terminal.js.map +1 -1
- package/dist/schemas/work-assurance.d.ts +24 -0
- package/dist/schemas/work-assurance.d.ts.map +1 -0
- package/dist/schemas/work-assurance.js +23 -0
- package/dist/schemas/work-assurance.js.map +1 -0
- package/dist/schemas/workspace/workspace-tree.d.ts +23 -0
- package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace/workspace-tree.js +20 -0
- package/dist/schemas/workspace/workspace-tree.js.map +1 -1
- package/dist/state/auth-state.d.ts +4 -0
- package/dist/state/auth-state.d.ts.map +1 -0
- package/dist/state/auth-state.js +37 -0
- package/dist/state/auth-state.js.map +1 -0
- package/dist/state/definition.d.ts +12 -0
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +1 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/sandbox-build.d.ts +20 -0
- package/dist/state/sandbox-build.d.ts.map +1 -0
- package/dist/state/sandbox-build.js +43 -0
- package/dist/state/sandbox-build.js.map +1 -0
- package/dist/state/ways-back.d.ts +4 -0
- package/dist/state/ways-back.d.ts.map +1 -0
- package/dist/state/ways-back.js +8 -0
- package/dist/state/ways-back.js.map +1 -0
- package/dist/text/land-rows.d.ts +14 -0
- package/dist/text/land-rows.d.ts.map +1 -0
- package/dist/text/land-rows.js +86 -0
- package/dist/text/land-rows.js.map +1 -0
- package/dist/text/transcript-fold.d.ts +4 -2
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +21 -187
- package/dist/text/transcript-fold.js.map +1 -1
- package/dist/webext/index.d.ts +1 -0
- package/dist/webext/index.d.ts.map +1 -1
- package/dist/webext/index.js +1 -0
- package/dist/webext/index.js.map +1 -1
- package/package.json +7 -6
- package/src/contracts/agent.contract.ts +13 -1
- package/src/contracts/agents.contract.ts +53 -28
- package/src/contracts/device.contract.ts +16 -0
- package/src/contracts/endpoints.contract.ts +3 -0
- package/src/contracts/push.contract.ts +23 -1
- package/src/contracts/secrets.contract.ts +39 -0
- package/src/contracts/workspace.contract.ts +16 -0
- package/src/events/agent-events.ts +99 -79
- package/src/events/agent-transcript.ts +48 -0
- package/src/events/agent-words.ts +13 -0
- package/src/events/failure-rows.ts +148 -0
- package/src/events/requests.ts +41 -3
- package/src/events/sandbox-notice.ts +14 -2
- package/src/events/system-events.ts +14 -4
- package/src/events/transcript.ts +30 -28
- package/src/ids/conversation-ids.ts +7 -0
- package/src/ids/project-dir.test.ts +2 -2
- package/src/ids/project-dir.ts +4 -4
- package/src/ids/row-ids.ts +20 -0
- package/src/models/agent-lanes.ts +16 -24
- package/src/models/agent-runtimes.ts +2 -2
- package/src/models/agent-standing.ts +38 -95
- package/src/models/model-pins.test.ts +31 -1
- package/src/models/model-pins.ts +17 -0
- package/src/models/plan-pools.test.ts +274 -31
- package/src/models/plan-pools.ts +294 -79
- package/src/netd/generated/netd-wire.json +25 -0
- package/src/netd/generated/wire.ts +1 -1
- package/src/policy/command-classes.ts +2 -2
- package/src/policy/keyring-origins.test.ts +132 -0
- package/src/policy/keyring-origins.ts +287 -0
- package/src/policy/keyring-seal.test.ts +37 -2
- package/src/policy/keyring-seal.ts +33 -7
- package/src/policy/safety-policy.ts +1 -1
- package/src/policy/tests/safety-policy.test.ts +1 -1
- package/src/policy/turned-away.ts +15 -0
- package/src/protocol/doors/host-protocol.ts +3 -0
- package/src/protocol/doors/runner-protocol.ts +4 -1
- package/src/protocol/doors/webext-links.ts +3 -0
- package/src/protocol/peer-dial.test.ts +90 -4
- package/src/protocol/peer-dial.ts +83 -39
- package/src/protocol/raw/raw-json-routes.ts +13 -0
- package/src/protocol/raw/raw-routes.ts +13 -1
- package/src/schemas/agent-next.ts +34 -0
- package/src/schemas/agent-place.ts +22 -0
- package/src/schemas/agent-search.ts +51 -0
- package/src/schemas/agent.ts +63 -90
- package/src/schemas/agents.ts +55 -99
- package/src/schemas/attach.ts +23 -0
- package/src/schemas/command-admission.ts +57 -0
- package/src/schemas/compose-overlay.test.ts +35 -0
- package/src/schemas/compose-overlay.ts +19 -0
- package/src/schemas/composer-drafts.ts +86 -0
- package/src/schemas/conversation-links.ts +62 -0
- package/src/schemas/data-protection.ts +1 -1
- package/src/schemas/device-commands.ts +89 -0
- package/src/schemas/device-folder.ts +9 -0
- package/src/schemas/devices.ts +28 -110
- package/src/schemas/environment.ts +8 -9
- package/src/schemas/hosts.ts +60 -0
- package/src/schemas/keyring-native.test.ts +13 -0
- package/src/schemas/keyring-native.ts +101 -0
- package/src/schemas/keyring.ts +202 -4
- package/src/schemas/machine-presence.test.ts +33 -0
- package/src/schemas/phone-passwords.ts +51 -0
- package/src/schemas/privacy-egress.test.ts +83 -0
- package/src/schemas/privacy-egress.ts +86 -0
- package/src/schemas/privacy.ts +4 -0
- package/src/schemas/project-delivery.ts +1 -1
- package/src/schemas/providers/plan-limits.test.ts +16 -0
- package/src/schemas/providers/plan-limits.ts +55 -49
- package/src/schemas/providers/provider-oauth.ts +1 -13
- package/src/schemas/providers/refusals.ts +62 -0
- package/src/schemas/providers/usage.ts +2 -0
- package/src/schemas/push.ts +19 -3
- package/src/schemas/sandbox-passwords.ts +88 -0
- package/src/schemas/secrets.ts +4 -0
- package/src/schemas/sessions.ts +1 -1
- package/src/schemas/settings-savings.ts +125 -0
- package/src/schemas/settings.ts +10 -116
- package/src/schemas/system.ts +15 -0
- package/src/schemas/terminal.ts +8 -0
- package/src/schemas/work-assurance.ts +36 -0
- package/src/schemas/workspace/workspace-tree.ts +32 -0
- package/src/state/auth-state.ts +89 -0
- package/src/state/runtime-state.ts +10 -4
- package/src/state/sandbox-build.test.ts +95 -0
- package/src/state/sandbox-build.ts +96 -0
- package/src/state/ways-back.test.ts +36 -0
- package/src/state/ways-back.ts +19 -0
- package/src/state/workspace-state.ts +1 -1
- package/src/text/land-rows.ts +111 -0
- package/src/text/transcript-fold.test.ts +105 -9
- package/src/text/transcript-fold.ts +30 -232
- package/src/webext/index.ts +2 -0
- package/dist/events/undelivered.d.ts +0 -6
- package/dist/events/undelivered.d.ts.map +0 -1
- package/dist/events/undelivered.js +0 -9
- package/dist/events/undelivered.js.map +0 -1
- package/dist/state/history-state.d.ts +0 -3
- package/dist/state/history-state.d.ts.map +0 -1
- package/dist/state/history-state.js +0 -137
- package/dist/state/history-state.js.map +0 -1
- package/src/events/undelivered.ts +0 -15
- package/src/state/history-state.ts +0 -270
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { KEYRING_NAME_RE, type KeyringOrigin } from "../schemas/keyring.js";
|
|
2
|
+
import {
|
|
3
|
+
type CaptureCandidate,
|
|
4
|
+
entriesFor,
|
|
5
|
+
entryNameFor,
|
|
6
|
+
findCaptureTarget,
|
|
7
|
+
gitOrigin,
|
|
8
|
+
mergeRules,
|
|
9
|
+
originKey,
|
|
10
|
+
originsOfRules,
|
|
11
|
+
rememberSwitchFor,
|
|
12
|
+
rulesForOrigin,
|
|
13
|
+
rulesServe,
|
|
14
|
+
titleForOrigin,
|
|
15
|
+
} from "./keyring-origins.js";
|
|
16
|
+
|
|
17
|
+
const NONE = { programs: [], packages: [], hosts: [], askpass: [] };
|
|
18
|
+
|
|
19
|
+
const entry = (name: string, origin: KeyringOrigin, username?: string): CaptureCandidate => ({ name, rules: rulesForOrigin(origin), username, title: titleForOrigin(origin) });
|
|
20
|
+
|
|
21
|
+
describe("origins and rules", () => {
|
|
22
|
+
test("a site keeps its host, without www and case", () => {
|
|
23
|
+
expect(rulesForOrigin({ kind: "site", host: "WWW.GitHub.com" })).toEqual({ ...NONE, hosts: ["github.com"] });
|
|
24
|
+
expect(originKey({ kind: "site", host: "www.github.com." })).toBe(originKey({ kind: "site", host: "github.com" }));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("terminal places become askpass matchers, and read back as the same places", () => {
|
|
28
|
+
const places: KeyringOrigin[] = [
|
|
29
|
+
{ kind: "ssh", target: "radar@build-01" },
|
|
30
|
+
{ kind: "ssh-key", path: "/home/radar/.ssh/id_ed25519" },
|
|
31
|
+
{ kind: "sudo" },
|
|
32
|
+
{ kind: "sudo", user: "root" },
|
|
33
|
+
];
|
|
34
|
+
for (const place of places) {
|
|
35
|
+
expect(originsOfRules({ ...NONE, ...rulesForOrigin(place) })).toEqual([place]);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("git answers for every repository on its host", () => {
|
|
40
|
+
expect(gitOrigin("https://github.com/intentic/intentic.git")).toBe("https://github.com");
|
|
41
|
+
const rules = rulesForOrigin({ kind: "git", url: "https://github.com/intentic/intentic" });
|
|
42
|
+
expect(rules).toEqual({ ...NONE, askpass: ["git:https://github.com"], hosts: ["github.com"] });
|
|
43
|
+
expect(rulesServe(rules, { kind: "git", url: "https://github.com/other/repo" })).toBe(true);
|
|
44
|
+
// Its site is said once, by the git chip.
|
|
45
|
+
expect(originsOfRules(rules)).toEqual([{ kind: "git", url: "https://github.com" }]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a sandbox terminal's prompt places nothing on this computer", () => {
|
|
49
|
+
expect(rulesForOrigin({ kind: "prompt", text: "Password:", command: "npm login", sandbox: "https://x.intentic.dev" })).toEqual(NONE);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("merging adds a place without repeating one", () => {
|
|
53
|
+
const a = rulesForOrigin({ kind: "site", host: "github.com" });
|
|
54
|
+
const b = rulesForOrigin({ kind: "git", url: "https://github.com" });
|
|
55
|
+
expect(mergeRules(a, b)).toEqual({ ...NONE, hosts: ["github.com"], askpass: ["git:https://github.com"] });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("serving a place", () => {
|
|
60
|
+
test("a saved site fills its subdomains, and a wildcard its domain", () => {
|
|
61
|
+
expect(rulesServe({ ...NONE, hosts: ["github.com"] }, { kind: "site", host: "gist.github.com" })).toBe(true);
|
|
62
|
+
expect(rulesServe({ ...NONE, hosts: ["*.corp.example"] }, { kind: "site", host: "corp.example" })).toBe(true);
|
|
63
|
+
expect(rulesServe({ ...NONE, hosts: ["github.com"] }, { kind: "site", host: "notgithub.com" })).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a program matches by its file name, without case or folder", () => {
|
|
67
|
+
expect(rulesServe({ ...NONE, programs: ["C:\\Program Files\\Fortinet\\FortiClient.exe"] }, { kind: "program", program: "forticlient.EXE" })).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("the entry a capture updates", () => {
|
|
72
|
+
const github = entry("GITHUB_COM", { kind: "site", host: "github.com" }, "radarsu");
|
|
73
|
+
const work = entry("GITHUB_COM_WORK", { kind: "site", host: "github.com" }, "radar-work");
|
|
74
|
+
|
|
75
|
+
test("the same place and account is an update", () => {
|
|
76
|
+
expect(findCaptureTarget([github, work], { kind: "site", host: "www.github.com" }, "Radarsu")?.name).toBe("GITHUB_COM");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("another account at the same place is a new entry", () => {
|
|
80
|
+
expect(findCaptureTarget([github], { kind: "site", host: "github.com" }, "someone-else")).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a sandbox prompt is found again by its title", () => {
|
|
84
|
+
const origin: KeyringOrigin = { kind: "prompt", text: "Password:", command: "npm login", sandbox: "https://x.intentic.dev" };
|
|
85
|
+
expect(findCaptureTarget([entry("NPM_LOGIN", origin)], origin, undefined)?.name).toBe("NPM_LOGIN");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a need offers the asked-for account first", () => {
|
|
89
|
+
expect(entriesFor([github, work], { kind: "site", host: "github.com" }, "radar-work").map((found) => found.name)).toEqual(["GITHUB_COM_WORK", "GITHUB_COM"]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("titles and names", () => {
|
|
94
|
+
test("each place reads as a person would say it", () => {
|
|
95
|
+
expect(titleForOrigin({ kind: "site", host: "www.github.com" })).toBe("github.com");
|
|
96
|
+
expect(titleForOrigin({ kind: "program", program: "C:\\x\\FortiClient.exe" })).toBe("FortiClient");
|
|
97
|
+
expect(titleForOrigin({ kind: "program", program: "FortiClient.exe", label: "FortiClient VPN" })).toBe("FortiClient VPN");
|
|
98
|
+
expect(titleForOrigin({ kind: "ssh", target: "radar@build-01" })).toBe("ssh radar@build-01");
|
|
99
|
+
expect(titleForOrigin({ kind: "git", url: "https://github.com/a/b" })).toBe("git github.com");
|
|
100
|
+
expect(titleForOrigin({ kind: "prompt", text: "Password for npm: ", command: "npm login" })).toBe("npm login");
|
|
101
|
+
expect(titleForOrigin({ kind: "site", host: "github.com" }, " GitHub ")).toBe("GitHub");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a name takes the title, then the account, then a number", () => {
|
|
105
|
+
const taken = new Set<string>();
|
|
106
|
+
const name = (title: string, user?: string): string => {
|
|
107
|
+
const made = entryNameFor(title, user, (candidate) => taken.has(candidate));
|
|
108
|
+
taken.add(made);
|
|
109
|
+
return made;
|
|
110
|
+
};
|
|
111
|
+
expect(name("github.com", "radarsu@example.com")).toBe("GITHUB_COM");
|
|
112
|
+
expect(name("github.com", "radarsu@example.com")).toBe("GITHUB_COM_RADARSU");
|
|
113
|
+
expect(name("github.com", "radarsu@example.com")).toBe("GITHUB_COM_RADARSU_2");
|
|
114
|
+
expect(name("ssh radar@build-01")).toBe("SSH_RADAR_BUILD_01");
|
|
115
|
+
expect(name("1Password")).toBe("_1PASSWORD");
|
|
116
|
+
expect(name("Zürich Bank")).toBe("ZURICH_BANK");
|
|
117
|
+
expect(name("···")).toBe("PASSWORD");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("every name fits a reference", () => {
|
|
121
|
+
for (const title of ["a".repeat(400), "ąę", "--", "x y z", "9"]) {
|
|
122
|
+
expect(entryNameFor(title, "u", () => false)).toMatch(KEYRING_NAME_RE);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("deliberate acts need no remember switch", () => {
|
|
128
|
+
expect(rememberSwitchFor("browser")).toBe("browser");
|
|
129
|
+
expect(rememberSwitchFor("git")).toBe("terminal");
|
|
130
|
+
expect(rememberSwitchFor("terminal")).toBe("sandbox");
|
|
131
|
+
expect(rememberSwitchFor("import")).toBeUndefined();
|
|
132
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { KEYRING_NAME_RE, type KeyringCaptureSource, type KeyringOrigin, type KeyringRemember } from "../schemas/keyring.js";
|
|
2
|
+
|
|
3
|
+
// WHERE A CREDENTIAL BELONGS, in the two shapes the product keeps it in: an ORIGIN (schemas/keyring.ts), which is how a
|
|
4
|
+
// capture, an agent's need and the Passwords app speak of it ("github.com", "ssh radar@build-01"), and an entry's RULES
|
|
5
|
+
// (programs, packages, hosts, askpass matchers), which is how the keyring decides where a value may go. One file maps
|
|
6
|
+
// each onto the other, names and titles a new entry, and finds the entry a capture updates, so the keyring, the
|
|
7
|
+
// sandbox and the app agree on all four. Pure: no crypto, no I/O, safe in a browser bundle.
|
|
8
|
+
|
|
9
|
+
/** The parts of an entry's rules an origin speaks to. */
|
|
10
|
+
export interface OriginRules {
|
|
11
|
+
readonly programs: readonly string[];
|
|
12
|
+
readonly packages: readonly string[];
|
|
13
|
+
readonly hosts: readonly string[];
|
|
14
|
+
readonly askpass: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const EMPTY: OriginRules = { programs: [], packages: [], hosts: [], askpass: [] };
|
|
18
|
+
|
|
19
|
+
/** A host as two captures of the same site agree on it: lower case, no trailing dot, no leading `www.`. */
|
|
20
|
+
export const normalizeHost = (host: string): string =>
|
|
21
|
+
host
|
|
22
|
+
.trim()
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/\.$/u, "")
|
|
25
|
+
.replace(/^www\./u, "");
|
|
26
|
+
|
|
27
|
+
// A program by its executable's file name, as Windows compares them: without case, without its folder.
|
|
28
|
+
const programName = (program: string): string => (program.split(/[\\/]/u).pop() ?? program).toLowerCase();
|
|
29
|
+
|
|
30
|
+
const hostOfUrl = (url: string): string | undefined => {
|
|
31
|
+
try {
|
|
32
|
+
return normalizeHost(new URL(url).host);
|
|
33
|
+
} catch {
|
|
34
|
+
// allow(silent-catch): a git remote named without a scheme is not an address to take a host from
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** `https://github.com/org/repo` → `https://github.com`: git asks per host, and an entry answers for all its repos. */
|
|
40
|
+
export const gitOrigin = (url: string): string => {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = new URL(url);
|
|
43
|
+
return `${parsed.protocol}//${normalizeHost(parsed.host)}`;
|
|
44
|
+
} catch {
|
|
45
|
+
// allow(silent-catch): kept as written; it still matches itself
|
|
46
|
+
return url;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** One string per place, so two captures of the same place compare equal. */
|
|
51
|
+
export const originKey = (origin: KeyringOrigin): string => {
|
|
52
|
+
switch (origin.kind) {
|
|
53
|
+
case "site":
|
|
54
|
+
return `site:${normalizeHost(origin.host)}`;
|
|
55
|
+
case "program":
|
|
56
|
+
return `program:${programName(origin.program)}`;
|
|
57
|
+
case "package":
|
|
58
|
+
return `package:${origin.package}`;
|
|
59
|
+
case "ssh":
|
|
60
|
+
return `ssh:${origin.target.toLowerCase()}`;
|
|
61
|
+
case "ssh-key":
|
|
62
|
+
return `ssh-key:${origin.path}`;
|
|
63
|
+
case "sudo":
|
|
64
|
+
return `sudo:${origin.user ?? ""}`;
|
|
65
|
+
case "git":
|
|
66
|
+
return `git:${gitOrigin(origin.url)}`;
|
|
67
|
+
case "prompt":
|
|
68
|
+
return `prompt:${origin.sandbox ?? ""}:${origin.command ?? ""}:${origin.text.trim()}`;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The rules an entry needs to be used where `origin` is: what a captured credential's entry gets without anyone typing it. */
|
|
73
|
+
export const rulesForOrigin = (origin: KeyringOrigin): OriginRules => {
|
|
74
|
+
switch (origin.kind) {
|
|
75
|
+
case "site":
|
|
76
|
+
return { ...EMPTY, hosts: [normalizeHost(origin.host)] };
|
|
77
|
+
case "program":
|
|
78
|
+
return { ...EMPTY, programs: [origin.program] };
|
|
79
|
+
case "package":
|
|
80
|
+
return { ...EMPTY, packages: [origin.package] };
|
|
81
|
+
case "ssh":
|
|
82
|
+
return { ...EMPTY, askpass: [`ssh:${origin.target}`] };
|
|
83
|
+
case "ssh-key":
|
|
84
|
+
return { ...EMPTY, askpass: [`ssh-key:${origin.path}`] };
|
|
85
|
+
case "sudo":
|
|
86
|
+
return { ...EMPTY, askpass: [origin.user === undefined ? "sudo" : `sudo:${origin.user}`] };
|
|
87
|
+
case "git": {
|
|
88
|
+
const host = hostOfUrl(origin.url);
|
|
89
|
+
return { ...EMPTY, askpass: [`git:${gitOrigin(origin.url)}`], hosts: host === undefined ? [] : [host] };
|
|
90
|
+
}
|
|
91
|
+
case "prompt":
|
|
92
|
+
// A sandbox terminal's prompt is no rule on this computer: the entry is found by its title, and shared on a need.
|
|
93
|
+
return origin.sandbox === undefined ? { ...EMPTY, askpass: [`prompt:${origin.text.trim()}`] } : EMPTY;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Two sets of rules as one, each list without repeats: how a second place is added to an entry. */
|
|
98
|
+
export const mergeRules = (a: OriginRules, b: OriginRules): OriginRules => {
|
|
99
|
+
const union = (x: readonly string[], y: readonly string[]): string[] => [...new Set([...x, ...y])];
|
|
100
|
+
return { programs: union(a.programs, b.programs), packages: union(a.packages, b.packages), hosts: union(a.hosts, b.hosts), askpass: union(a.askpass, b.askpass) };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// An askpass matcher back into the origin it was made from; undefined for one this file did not make.
|
|
104
|
+
const originOfMatcher = (matcher: string): KeyringOrigin | undefined => {
|
|
105
|
+
const colon = matcher.indexOf(":");
|
|
106
|
+
const head = colon === -1 ? matcher : matcher.slice(0, colon);
|
|
107
|
+
const rest = colon === -1 ? "" : matcher.slice(colon + 1);
|
|
108
|
+
switch (head) {
|
|
109
|
+
case "ssh":
|
|
110
|
+
return rest === "" ? undefined : { kind: "ssh", target: rest };
|
|
111
|
+
case "ssh-key":
|
|
112
|
+
return rest === "" ? undefined : { kind: "ssh-key", path: rest };
|
|
113
|
+
case "sudo":
|
|
114
|
+
return rest === "" ? { kind: "sudo" } : { kind: "sudo", user: rest };
|
|
115
|
+
case "git":
|
|
116
|
+
return rest === "" ? undefined : { kind: "git", url: rest };
|
|
117
|
+
case "prompt":
|
|
118
|
+
return rest === "" ? undefined : { kind: "prompt", text: rest };
|
|
119
|
+
default:
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Every place an entry's rules say it is used, for the app's "Works on" chips. A `*.domain` host reads as the domain. */
|
|
125
|
+
export const originsOfRules = (rules: OriginRules): KeyringOrigin[] => {
|
|
126
|
+
const fromAskpass = rules.askpass.map(originOfMatcher).filter((origin): origin is KeyringOrigin => origin !== undefined);
|
|
127
|
+
// A git entry's host is said by its `git:` matcher; listing the site again would say it twice.
|
|
128
|
+
const gitHosts = new Set(fromAskpass.flatMap((origin) => (origin.kind === "git" ? [hostOfUrl(origin.url)] : [])));
|
|
129
|
+
return [
|
|
130
|
+
...rules.hosts.map((host) => host.replace(/^\*\./u, "")).filter((host) => !gitHosts.has(normalizeHost(host))).map((host): KeyringOrigin => ({ kind: "site", host })),
|
|
131
|
+
...rules.programs.map((program): KeyringOrigin => ({ kind: "program", program })),
|
|
132
|
+
...rules.packages.map((pkg): KeyringOrigin => ({ kind: "package", package: pkg })),
|
|
133
|
+
...fromAskpass,
|
|
134
|
+
];
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const hostServes = (host: string, pattern: string): boolean => {
|
|
138
|
+
const want = normalizeHost(host);
|
|
139
|
+
const wildcard = pattern.startsWith("*.");
|
|
140
|
+
const base = normalizeHost(wildcard ? pattern.slice(2) : pattern);
|
|
141
|
+
// A saved `github.com` also fills `gist.github.com`, as every password manager's site matching does.
|
|
142
|
+
return want === base || want.endsWith(`.${base}`);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Whether an entry with these rules is the one for `origin`: the test a need, a fill and a capture all use. */
|
|
146
|
+
export const rulesServe = (rules: OriginRules, origin: KeyringOrigin): boolean => {
|
|
147
|
+
switch (origin.kind) {
|
|
148
|
+
case "site":
|
|
149
|
+
return rules.hosts.some((pattern) => hostServes(origin.host, pattern));
|
|
150
|
+
case "program": {
|
|
151
|
+
const want = programName(origin.program);
|
|
152
|
+
return rules.programs.some((program) => programName(program) === want);
|
|
153
|
+
}
|
|
154
|
+
case "package":
|
|
155
|
+
return rules.packages.includes(origin.package);
|
|
156
|
+
case "git": {
|
|
157
|
+
const want = `git:${gitOrigin(origin.url)}`;
|
|
158
|
+
const host = hostOfUrl(origin.url);
|
|
159
|
+
return rules.askpass.some((matcher) => matcher.startsWith("git:") && `git:${gitOrigin(matcher.slice(4))}` === want) || (host !== undefined && rules.hosts.some((pattern) => hostServes(host, pattern)));
|
|
160
|
+
}
|
|
161
|
+
case "ssh":
|
|
162
|
+
case "ssh-key":
|
|
163
|
+
case "sudo":
|
|
164
|
+
case "prompt": {
|
|
165
|
+
const wanted = rulesForOrigin(origin).askpass;
|
|
166
|
+
return wanted.length > 0 && rules.askpass.some((matcher) => wanted.includes(matcher));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const sameUser = (a: string | undefined, b: string | undefined): boolean => (a ?? "").trim().toLowerCase() === (b ?? "").trim().toLowerCase();
|
|
172
|
+
|
|
173
|
+
/** What a capture is compared against: each entry's name, the rules that place it, and the account it signs in as. */
|
|
174
|
+
export interface CaptureCandidate {
|
|
175
|
+
readonly name: string;
|
|
176
|
+
readonly rules: OriginRules;
|
|
177
|
+
readonly username?: string | undefined;
|
|
178
|
+
readonly title?: string | undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The entry a capture updates: one used at the same place for the same account. Undefined: the capture is a new entry,
|
|
183
|
+
* even where the place has entries for other accounts (a second GitHub login is a second entry, not an overwrite).
|
|
184
|
+
*/
|
|
185
|
+
export const findCaptureTarget = <T extends CaptureCandidate>(entries: readonly T[], origin: KeyringOrigin, username: string | undefined): T | undefined =>
|
|
186
|
+
entries.find((entry) => rulesServe(entry.rules, origin) && sameUser(entry.username, username)) ??
|
|
187
|
+
// A sandbox terminal's prompt places nothing on this computer, so its entry is found by the title it was given.
|
|
188
|
+
(origin.kind === "prompt" && origin.sandbox !== undefined ? entries.find((entry) => entry.title === titleForOrigin(origin) && sameUser(entry.username, username)) : undefined);
|
|
189
|
+
|
|
190
|
+
/** Entries that serve `origin`, the given account first: what a need, a browser's or a phone's fill offers. */
|
|
191
|
+
export const entriesFor = <T extends CaptureCandidate>(entries: readonly T[], origin: KeyringOrigin, username?: string): T[] => {
|
|
192
|
+
const serving = entries.filter((entry) => rulesServe(entry.rules, origin));
|
|
193
|
+
return username === undefined ? serving : [...serving.filter((entry) => sameUser(entry.username, username)), ...serving.filter((entry) => !sameUser(entry.username, username))];
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const baseName = (path: string): string => path.split(/[\\/]/u).pop() ?? path;
|
|
197
|
+
|
|
198
|
+
/** What a person calls the place: `github.com`, `FortiClient VPN`, `ssh radar@build-01`, `sudo`, `git github.com`. */
|
|
199
|
+
export const titleForOrigin = (origin: KeyringOrigin, given?: string): string => {
|
|
200
|
+
const named = given?.trim();
|
|
201
|
+
if (named !== undefined && named !== "") {
|
|
202
|
+
return named.slice(0, 200);
|
|
203
|
+
}
|
|
204
|
+
switch (origin.kind) {
|
|
205
|
+
case "site":
|
|
206
|
+
return normalizeHost(origin.host);
|
|
207
|
+
case "program":
|
|
208
|
+
return origin.label ?? baseName(origin.program).replace(/\.exe$/iu, "");
|
|
209
|
+
case "package":
|
|
210
|
+
return origin.label ?? origin.package.split(".").pop() ?? origin.package;
|
|
211
|
+
case "ssh":
|
|
212
|
+
return `ssh ${origin.target}`;
|
|
213
|
+
case "ssh-key":
|
|
214
|
+
return `ssh key ${baseName(origin.path)}`;
|
|
215
|
+
case "sudo":
|
|
216
|
+
return origin.user === undefined ? "sudo" : `sudo as ${origin.user}`;
|
|
217
|
+
case "git":
|
|
218
|
+
return `git ${hostOfUrl(origin.url) ?? origin.url}`;
|
|
219
|
+
case "prompt":
|
|
220
|
+
return origin.command ?? origin.text.replace(/[:\s]+$/u, "");
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const NAME_MAX = 128;
|
|
225
|
+
|
|
226
|
+
// Upper case, every run of anything else one underscore, never starting with a digit: the shape a reference takes.
|
|
227
|
+
const referenceName = (text: string): string => {
|
|
228
|
+
const upper = text
|
|
229
|
+
.normalize("NFKD")
|
|
230
|
+
.replace(/[\u0300-\u036f]/gu, "")
|
|
231
|
+
.toUpperCase()
|
|
232
|
+
.replace(/[^A-Z0-9]+/gu, "_")
|
|
233
|
+
.replace(/^_+|_+$/gu, "");
|
|
234
|
+
const named = upper === "" ? "PASSWORD" : /^[0-9]/u.test(upper) ? `_${upper}` : upper;
|
|
235
|
+
return named.slice(0, NAME_MAX).replace(/_+$/u, "");
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A new entry's name, the `{{secret:NAME}}` an agent writes: from its title (`GITHUB_COM`), then with the account
|
|
240
|
+
* (`GITHUB_COM_RADARSU`) when that is taken, then numbered. Always matches KEYRING_NAME_RE.
|
|
241
|
+
*/
|
|
242
|
+
export const entryNameFor = (title: string, username: string | undefined, taken: (name: string) => boolean): string => {
|
|
243
|
+
const base = referenceName(title);
|
|
244
|
+
const tries = [base];
|
|
245
|
+
if (username !== undefined && username.trim() !== "") {
|
|
246
|
+
tries.push(referenceName(`${base}_${username.split("@")[0] ?? username}`));
|
|
247
|
+
}
|
|
248
|
+
for (const name of tries) {
|
|
249
|
+
if (!taken(name) && KEYRING_NAME_RE.test(name)) {
|
|
250
|
+
return name;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const stem = (tries.at(-1) ?? base).slice(0, NAME_MAX - 6);
|
|
254
|
+
for (let n = 2; ; n++) {
|
|
255
|
+
const name = `${stem}_${String(n)}`;
|
|
256
|
+
if (!taken(name)) {
|
|
257
|
+
return name;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The remember switch a capture source needs on, or undefined for a deliberate act (an import, an agent's `secrets ask`,
|
|
264
|
+
* adding by hand) that needs none.
|
|
265
|
+
*/
|
|
266
|
+
export const rememberSwitchFor = (source: KeyringCaptureSource): keyof KeyringRemember | undefined => {
|
|
267
|
+
switch (source) {
|
|
268
|
+
case "browser":
|
|
269
|
+
return "browser";
|
|
270
|
+
case "askpass":
|
|
271
|
+
case "git":
|
|
272
|
+
return "terminal";
|
|
273
|
+
case "app":
|
|
274
|
+
return "apps";
|
|
275
|
+
case "phone":
|
|
276
|
+
return "phone";
|
|
277
|
+
case "terminal":
|
|
278
|
+
return "sandbox";
|
|
279
|
+
case "import":
|
|
280
|
+
case "agent":
|
|
281
|
+
case "manual":
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** Remembering off everywhere: a vault's settings before the person's one yes. */
|
|
287
|
+
export const REMEMBER_NOTHING: KeyringRemember = { browser: false, terminal: false, apps: false, phone: false, sandbox: false };
|
|
@@ -1,5 +1,24 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { packageRoot } from "@intentic/constants/node";
|
|
4
|
+
import type { KeyringGrant, SealedBox } from "../schemas/keyring.js";
|
|
5
|
+
import { SealedBoxSchema } from "../schemas/keyring.js";
|
|
6
|
+
import { grantNonce, keyFingerprint, newSealKeyPair, newSignKeyPair, seal, sealWithEphemeral, signGrant, unseal, verifyGrant, type SealPurpose } from "./keyring-seal.js";
|
|
7
|
+
|
|
8
|
+
const goldenPath = join(packageRoot(import.meta.url), "golden", "keyring-seal.json");
|
|
9
|
+
const golden = JSON.parse(readFileSync(goldenPath, "utf8")) as {
|
|
10
|
+
recipient: { publicKey: string; privateKey: string };
|
|
11
|
+
phone: { publicKey: string; privateKey: string };
|
|
12
|
+
vectors: Array<{
|
|
13
|
+
name: string;
|
|
14
|
+
plaintext: string;
|
|
15
|
+
purpose: SealPurpose;
|
|
16
|
+
ephemeral: { publicKey: string; privateKey: string };
|
|
17
|
+
iv: string;
|
|
18
|
+
box: SealedBox;
|
|
19
|
+
}>;
|
|
20
|
+
unseal: { phonePlaintext: string; phonePurpose: SealPurpose };
|
|
21
|
+
};
|
|
3
22
|
|
|
4
23
|
const SANDBOX = "https://a1b2c3d4e5f6.intentic.dev";
|
|
5
24
|
|
|
@@ -91,3 +110,19 @@ describe("grants", () => {
|
|
|
91
110
|
test("fingerprints read in groups of four", () => {
|
|
92
111
|
expect(keyFingerprint(newSealKeyPair().publicKey)).toMatch(/^[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}$/u);
|
|
93
112
|
});
|
|
113
|
+
|
|
114
|
+
describe("golden vectors (shared with the Android app)", () => {
|
|
115
|
+
test.each(golden.vectors)("$name seals to the golden box", ({ name, plaintext, purpose, ephemeral, iv, box }) => {
|
|
116
|
+
const recipient = name === "phone" ? golden.phone.publicKey : golden.recipient.publicKey;
|
|
117
|
+
const rebuilt = sealWithEphemeral(recipient, plaintext, purpose, ephemeral, Buffer.from(iv, "base64url"));
|
|
118
|
+
expect(rebuilt).toEqual(SealedBoxSchema.parse(box));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("the phone opens the golden phone box", () => {
|
|
122
|
+
const phoneVector = golden.vectors.find((vector) => vector.name === "phone");
|
|
123
|
+
if (phoneVector === undefined) {
|
|
124
|
+
throw new Error("the golden file has no phone vector");
|
|
125
|
+
}
|
|
126
|
+
expect(unseal(golden.phone, phoneVector.box, phoneVector.purpose)).toBe(golden.unseal.phonePlaintext);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -51,7 +51,12 @@ export type SealPurpose =
|
|
|
51
51
|
| { readonly kind: "type"; readonly sandbox: string; readonly secret: string; readonly nonce: string }
|
|
52
52
|
| { readonly kind: "inbox"; readonly sandbox: string; readonly secret: string }
|
|
53
53
|
| { readonly kind: "copy"; readonly sandbox: string; readonly secret: string; readonly updatedAt: number }
|
|
54
|
-
| { readonly kind: "sync"; readonly account: string; readonly device: string; readonly revision: number }
|
|
54
|
+
| { readonly kind: "sync"; readonly account: string; readonly device: string; readonly revision: number }
|
|
55
|
+
// A credential seen in use and sent to be kept (schemas/keyring.ts KeyringCaptureItem): bound to who saw it and when,
|
|
56
|
+
// so a box lifted from one capture cannot be replayed as another's.
|
|
57
|
+
| { readonly kind: "capture"; readonly via: string; readonly at: number }
|
|
58
|
+
// A value released to a paired phone for one autofill (KeyringPhoneFill), bound to that request's nonce.
|
|
59
|
+
| { readonly kind: "phone"; readonly device: string; readonly nonce: string };
|
|
55
60
|
|
|
56
61
|
export const sealAad = (purpose: SealPurpose): Buffer => {
|
|
57
62
|
switch (purpose.kind) {
|
|
@@ -64,21 +69,42 @@ export const sealAad = (purpose: SealPurpose): Buffer => {
|
|
|
64
69
|
return Buffer.from(`copy\0${purpose.sandbox}\0${purpose.secret}\0${String(purpose.updatedAt)}`, "utf8");
|
|
65
70
|
case "sync":
|
|
66
71
|
return Buffer.from(`sync\0${purpose.account}\0${purpose.device}\0${String(purpose.revision)}`, "utf8");
|
|
72
|
+
case "capture":
|
|
73
|
+
return Buffer.from(`capture\0${purpose.via}\0${String(purpose.at)}`, "utf8");
|
|
74
|
+
case "phone":
|
|
75
|
+
return Buffer.from(`phone\0${purpose.device}\0${purpose.nonce}`, "utf8");
|
|
67
76
|
}
|
|
68
77
|
};
|
|
69
78
|
|
|
70
79
|
const boxKey = (shared: Buffer, epk: string, recipient: string): Buffer =>
|
|
71
80
|
Buffer.from(hkdfSync("sha256", shared, Buffer.concat([Buffer.from(epk, "base64url"), Buffer.from(recipient, "base64url")]), SEAL_INFO, 32));
|
|
72
81
|
|
|
82
|
+
// Seals `plaintext` to the holder of `recipient`'s private half, for `purpose` alone, with a caller-chosen ephemeral
|
|
83
|
+
// key and IV. The phone app's unit tests and golden/keyring-seal.json call this with fixed inputs so Kotlin can match.
|
|
84
|
+
export const sealWithEphemeral = (
|
|
85
|
+
recipient: string,
|
|
86
|
+
plaintext: string,
|
|
87
|
+
purpose: SealPurpose,
|
|
88
|
+
ephemeralPair: KeyringKeyPair,
|
|
89
|
+
iv: Buffer,
|
|
90
|
+
): SealedBox => {
|
|
91
|
+
const shared = diffieHellman({ privateKey: privateKeyOf("X25519", ephemeralPair), publicKey: publicKeyOf("X25519", recipient) });
|
|
92
|
+
const cipher = createCipheriv("aes-256-gcm", boxKey(shared, ephemeralPair.publicKey, recipient), iv);
|
|
93
|
+
cipher.setAAD(sealAad(purpose));
|
|
94
|
+
const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
95
|
+
return {
|
|
96
|
+
v: 1,
|
|
97
|
+
epk: ephemeralPair.publicKey,
|
|
98
|
+
iv: iv.toString("base64url"),
|
|
99
|
+
tag: cipher.getAuthTag().toString("base64url"),
|
|
100
|
+
data: data.toString("base64url"),
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
73
104
|
// Seals `plaintext` to the holder of `recipient`'s private half, for `purpose` alone.
|
|
74
105
|
export const seal = (recipient: string, plaintext: string, purpose: SealPurpose): SealedBox => {
|
|
75
106
|
const ephemeral = newSealKeyPair();
|
|
76
|
-
|
|
77
|
-
const iv = randomBytes(12);
|
|
78
|
-
const cipher = createCipheriv("aes-256-gcm", boxKey(shared, ephemeral.publicKey, recipient), iv);
|
|
79
|
-
cipher.setAAD(sealAad(purpose));
|
|
80
|
-
const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
81
|
-
return { v: 1, epk: ephemeral.publicKey, iv: iv.toString("base64url"), tag: cipher.getAuthTag().toString("base64url"), data: data.toString("base64url") };
|
|
107
|
+
return sealWithEphemeral(recipient, plaintext, purpose, ephemeral, randomBytes(12));
|
|
82
108
|
};
|
|
83
109
|
|
|
84
110
|
// Opens a box sealed to `pair` for `purpose`; throws on any way of not being one (malformed, another key, another purpose).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { COMMAND_CLASS_LABELS, COMMAND_CLASS_PATTERNS, type CommandPattern } from "./command-classes.js";
|
|
3
|
-
import { type CommandClass, CommandClassSchema, type CommandLocus, CommandLocusSchema } from "../schemas/
|
|
3
|
+
import { type CommandClass, CommandClassSchema, type CommandLocus, CommandLocusSchema } from "../schemas/command-admission.js";
|
|
4
4
|
|
|
5
5
|
// The owner's safety policy as prose, judged by a model rather than a table of verdicts, since deciding whether a
|
|
6
6
|
// command is malicious takes understanding a regex can't reach. Patterns (command-classes.ts) are now just triage,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { COMMAND_CLASS_LABELS, COMMAND_CLASS_PATTERNS } from "../command-classes.js";
|
|
2
2
|
import { COMMAND_RULE_CATALOG, DEFAULT_SAFETY_POLICY, hardRuleClasses } from "../safety-policy.js";
|
|
3
|
-
import { CommandClassSchema, CommandLocusSchema } from "../../schemas/
|
|
3
|
+
import { CommandClassSchema, CommandLocusSchema } from "../../schemas/command-admission.js";
|
|
4
4
|
|
|
5
5
|
// The hard rule is the one thing on this page nobody can argue with, so it's pinned here rather than trusted to review:
|
|
6
6
|
// a class added silently is one an owner can never decide about for themselves.
|
|
@@ -30,5 +30,20 @@ const TURNED_AWAY: ReadonlySet<string> = new Set([
|
|
|
30
30
|
"engine-version-floor",
|
|
31
31
|
]);
|
|
32
32
|
|
|
33
|
+
// The refusals about the model itself (its allowance, its account, its catalogue, its window), which another model
|
|
34
|
+
// gets past: a turn one of these stops partway offers to carry on elsewhere rather than to wait.
|
|
35
|
+
const ANOTHER_MODEL_CLEARS: ReadonlySet<string> = new Set([
|
|
36
|
+
"claude-not-entitled",
|
|
37
|
+
"context-window-too-small",
|
|
38
|
+
"model-helper-only",
|
|
39
|
+
"trial-unavailable",
|
|
40
|
+
"trial-model-unavailable",
|
|
41
|
+
"trial-exhausted",
|
|
42
|
+
"model-unavailable",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** Whether another model would get past a refusal: the trial's, the model's, or the account's on it. */
|
|
46
|
+
export const anotherModelClears = (code: string | undefined): boolean => code !== undefined && ANOTHER_MODEL_CLEARS.has(code);
|
|
47
|
+
|
|
33
48
|
/** Whether a failure code refuses a turn before the model sees any of it. */
|
|
34
49
|
export const turnedAwayCode = (code: string | undefined): boolean => code !== undefined && TURNED_AWAY.has(code);
|
|
@@ -19,3 +19,6 @@ export type HostHello = z.infer<typeof HostHelloSchema>;
|
|
|
19
19
|
// The URL the machine's agent dials, given the sandbox's public URL. Carries no credential (the token rides the hello
|
|
20
20
|
// frame); one place builds it, so the agent and the daemon route can't disagree.
|
|
21
21
|
export const hostConnectUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/^http/, "ws").replace(/\/$/, "")}/system/hosts/connect`;
|
|
22
|
+
|
|
23
|
+
// Where a connected machine tells its sandbox that Passwords shares changed (2026-10-20), bearer-authenticated.
|
|
24
|
+
export const hostKeyringChangedUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/\/$/, "")}/system/hosts/keyring-changed`;
|
|
@@ -24,6 +24,9 @@ export const RunnerHelloSchema = z.object({
|
|
|
24
24
|
// tag names its own container (`intentic-sandbox-env-<slug>:<hash>`), so two sides on one base and one overlay
|
|
25
25
|
// never share it. Absent on an old image, which is then compared by `image` alone.
|
|
26
26
|
baseImage: z.string().optional(),
|
|
27
|
+
// That base's registry digest (SANDBOX_BASE_DIGEST), when the runner's machine could read one; parity then compares
|
|
28
|
+
// the exact image rather than a tag two machines may have pulled at different moments (state/sandbox-build.ts).
|
|
29
|
+
baseDigest: z.string().optional(),
|
|
27
30
|
channel: z.string().optional(),
|
|
28
31
|
overlayHash: z.string().optional(),
|
|
29
32
|
// Runner's declared settings-only sandbox.toml (definition.ts), diffed against the parent's to itemize drift;
|
|
@@ -147,7 +150,7 @@ export const RunnerSummarySchema = z.object({
|
|
|
147
150
|
overlayHash: z.string().optional(),
|
|
148
151
|
facts: RunnerFactsSchema.optional(),
|
|
149
152
|
lastSeen: z.number().optional(),
|
|
150
|
-
// Computed once by the daemon (
|
|
153
|
+
// Computed once by the daemon (buildParity, state/sandbox-build.ts), so every surface shows the same badge.
|
|
151
154
|
parity: RunnerParitySchema,
|
|
152
155
|
// Where the runner's environment differs from this sandbox's, one line per difference; absent if never reported,
|
|
153
156
|
// empty if they agree. A Setting line is fixable live; an overlay line needs a rebuild.
|
|
@@ -22,6 +22,9 @@ export const webextLendUrl = (sandboxUrl: string): string => `${sandboxUrl.repla
|
|
|
22
22
|
// are: a tool answer over the socket lands in the model's context, and this value must never reach one.
|
|
23
23
|
export const webextSecretUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/\/$/, "")}/system/webext/secret`;
|
|
24
24
|
|
|
25
|
+
// Where the extension tells its sandbox that site access changed outside the popup (2026-10-20), bearer-authenticated.
|
|
26
|
+
export const webextGrantsChangedUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/\/$/, "")}/system/webext/grants-changed`;
|
|
27
|
+
|
|
25
28
|
// The tool the daemon calls on a browser to type a secret into its focused field, taking only a ticket the daemon
|
|
26
29
|
// minted for that one call. The agent's own mount of the browser never lists it (webext-peer.ts): a model holding the
|
|
27
30
|
// tool would hold nothing it could use, so it is not offered.
|