@hope_phenom/dsh-plugin-remote-use 0.0.0-stage → 0.2.24

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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -2
  3. package/README.zh.md +195 -0
  4. package/assets/icon.svg +47 -0
  5. package/cordis.patch.yml +41 -0
  6. package/lib/bridge/acp-bridge.js +905 -0
  7. package/lib/bridge/peer-connection.js +730 -0
  8. package/lib/client.js +2842 -0
  9. package/lib/core/acp-model-overlay.js +121 -0
  10. package/lib/core/approval-queue.js +216 -0
  11. package/lib/core/audit-log.js +322 -0
  12. package/lib/core/desktop-notify.js +173 -0
  13. package/lib/core/environment-prompt.js +76 -0
  14. package/lib/core/relay-supervisor.js +278 -0
  15. package/lib/core/review-engine.js +679 -0
  16. package/lib/core/sandbox-diagnosis.js +258 -0
  17. package/lib/core/sandbox-probe.js +310 -0
  18. package/lib/core/self-acp-spec.js +117 -0
  19. package/lib/core/selfcheck-runner.js +150 -0
  20. package/lib/core/settings-store.js +226 -0
  21. package/lib/core/shot-gc.js +334 -0
  22. package/lib/core/task-cwd.js +50 -0
  23. package/lib/core/task-memory.js +163 -0
  24. package/lib/core/task-registry.js +238 -0
  25. package/lib/core/toolcall-cache.js +206 -0
  26. package/lib/core/transfer-queue.js +58 -0
  27. package/lib/core/transfer.js +1144 -0
  28. package/lib/core/worker-arming.js +334 -0
  29. package/lib/index.js +2207 -0
  30. package/lib/provider/out-of-process-helpers.js +76 -0
  31. package/lib/provider/remote-provider.js +261 -0
  32. package/lib/tools/mediator-shim.mjs +180 -0
  33. package/lib/tools/prepare-sandbox-mediator.mjs +529 -0
  34. package/lib/tools/remote-tools.js +680 -0
  35. package/lib/tools/selfcheck-fix-prompt.md +206 -0
  36. package/locale/en.json +6 -0
  37. package/locale/zh.json +6 -0
  38. package/package.json +72 -4
  39. package/slave/acp-profile-approval.patch.yml +66 -0
  40. package/slave/file-transfer.mjs +552 -0
  41. package/slave/integrity-probe.ps1 +99 -0
  42. package/slave/make-launch-spec.mjs +184 -0
  43. package/slave/net-address.mjs +138 -0
  44. package/slave/node_modules/.package-lock.json +26 -0
  45. package/slave/node_modules/ws/LICENSE +20 -0
  46. package/slave/node_modules/ws/README.md +548 -0
  47. package/slave/node_modules/ws/browser.js +8 -0
  48. package/slave/node_modules/ws/index.js +22 -0
  49. package/slave/node_modules/ws/lib/buffer-util.js +131 -0
  50. package/slave/node_modules/ws/lib/constants.js +19 -0
  51. package/slave/node_modules/ws/lib/event-target.js +292 -0
  52. package/slave/node_modules/ws/lib/extension.js +203 -0
  53. package/slave/node_modules/ws/lib/limiter.js +55 -0
  54. package/slave/node_modules/ws/lib/permessage-deflate.js +530 -0
  55. package/slave/node_modules/ws/lib/receiver.js +743 -0
  56. package/slave/node_modules/ws/lib/sender.js +607 -0
  57. package/slave/node_modules/ws/lib/stream.js +161 -0
  58. package/slave/node_modules/ws/lib/subprotocol.js +62 -0
  59. package/slave/node_modules/ws/lib/validation.js +152 -0
  60. package/slave/node_modules/ws/lib/websocket-server.js +562 -0
  61. package/slave/node_modules/ws/lib/websocket.js +1421 -0
  62. package/slave/node_modules/ws/package.json +74 -0
  63. package/slave/node_modules/ws/wrapper.mjs +21 -0
  64. package/slave/pairing.mjs +175 -0
  65. package/slave/permission-pairing.mjs +240 -0
  66. package/slave/prune-shots.mjs +170 -0
  67. package/slave/relay.mjs +1351 -0
  68. package/slave/repair-workspace-integrity.ps1 +75 -0
  69. package/slave/screenshot.ps1 +114 -0
  70. package/slave/selfcheck.ps1 +350 -0
  71. package/slave/stage-app.ps1 +116 -0
  72. package/slave/workspace-integrity.mjs +157 -0
@@ -0,0 +1,184 @@
1
+ // make-launch-spec.mjs -- generate the ACP agent launch spec for THIS machine.
2
+ //
3
+ // We deliberately do NOT launch `dsh.cmd`. That shim is:
4
+ // set "ELECTRON_RUN_AS_NODE=1"
5
+ // "<install>\DeepSeek Harness.exe" --expose-internals "<install>\resources\app.asar\
6
+ // dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js" %*
7
+ // and we replicate it exactly with an argv ARRAY, which avoids every Windows quoting
8
+ // hazard: `cmd.exe /c` loses quotes through the PowerShell -> node hand-off, and Node
9
+ // refuses to spawn a `.cmd` without a shell (EINVAL).
10
+ //
11
+ // NOTE: the cli.js path lives INSIDE app.asar, so ordinary fs existence checks report
12
+ // false -- Electron can read it, the filesystem API cannot. We therefore only test the
13
+ // .exe and let the argv path be constructed.
14
+ //
15
+ // Usage: node make-launch-spec.mjs [--install <dir>] [--profile acp] [--out <file>]
16
+ // [--permission-mode <mode>] [--dsh-home <dir>]
17
+ // [--settings-file <file>]
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
19
+ import { dirname, join, resolve } from 'node:path'
20
+
21
+ import { checkPairing } from './permission-pairing.mjs'
22
+
23
+ const argv = process.argv.slice(2)
24
+ function arg(name, fallback) {
25
+ const i = argv.indexOf(`--${name}`)
26
+ return i >= 0 && argv[i + 1] !== undefined ? argv[i + 1] : fallback
27
+ }
28
+ const PROFILE = arg('profile', 'acp')
29
+ const OUT = arg('out', 'agent-spec.json')
30
+ // Permission mode is read by dsh-base's sandbox-policy/approval rows from the
31
+ // environment, so it must be set in the CHILD's env to make experiments deterministic.
32
+ // 'workspace-write' => sandbox denies out-of-workspace writes and approval policy is 'ask',
33
+ // which is exactly the path that produces session/request_permission.
34
+ //
35
+ // NO --permission-mode MEANS "USE THE MACHINE'S CONFIGURED MODE", not "leave it unset": a
36
+ // hand-managed relay and the plugin's settings page both write this one value, and letting the
37
+ // manual path default to nothing is how a spec can silently keep an old mode after the setting was
38
+ // changed. The flag still wins when given, so an explicit deployment stays reproducible.
39
+ const PERMISSION_FLAG = arg('permission-mode', '')
40
+ let PERMISSION_MODE = PERMISSION_FLAG
41
+ let PERMISSION_MODE_SOURCE = PERMISSION_FLAG === '' ? 'none' : '--permission-mode'
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // GATE: refuse a mode this worker's profile cannot compose (PITFALLS F34).
45
+ //
46
+ // This is the point where a spec is WRITTEN, so it is the point where a bad mode must be refused --
47
+ // whether it came from --permission-mode or from the settings file. The plugin also knows the mode,
48
+ // but arming fails at spec-build time; letting a typo through here would start a child whose
49
+ // dsh-base rejects the value at load.
50
+ //
51
+ // Blocks on a KNOWN-BAD combination, and only warns when the profile cannot be read: refusing on
52
+ // "unknown" would make a legitimate worker undeployable, while staying silent about "unknown" would
53
+ // let the operator read silence as approval. The two cases are therefore reported differently.
54
+ // ---------------------------------------------------------------------------
55
+ const DSH_HOME = arg('dsh-home', process.env.DSH_HOME ?? join(process.env.USERPROFILE ?? '', '.dsh'))
56
+ const SETTINGS_FILE = arg('settings-file', join(DSH_HOME, 'storages', 'dsh-plugin-remote-use', 'settings.json'))
57
+ // Read once: the mode and the model both live in this file, and reading it twice invites the two
58
+ // values coming from different generations of it.
59
+ let SETTINGS = {}
60
+ try {
61
+ SETTINGS = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8').replace(/^\uFEFF/, ''))
62
+ } catch {
63
+ // Missing settings file is the normal case for a hand-managed worker that never opened the
64
+ // plugin's page. A corrupt one is caught by the mode gate if it produced no usable value.
65
+ SETTINGS = {}
66
+ }
67
+ if (PERMISSION_MODE === '') {
68
+ const fromSettings = SETTINGS !== null && typeof SETTINGS === 'object' ? SETTINGS.workerSessionMode : undefined
69
+ if (typeof fromSettings === 'string' && fromSettings !== '') {
70
+ PERMISSION_MODE = fromSettings
71
+ PERMISSION_MODE_SOURCE = `settings: ${SETTINGS_FILE}`
72
+ }
73
+ }
74
+ /**
75
+ * The worker model, as `provider/model`. Empty means "leave the profile's acp row alone".
76
+ *
77
+ * It is applied as an OVERLAY passed with `--patch`, not by editing the profile: the plugin does
78
+ * not own `cordis.patch.yml`. The overlay is applied after the profile layer, so it wins.
79
+ */
80
+ const WORKER_MODEL = SETTINGS !== null && typeof SETTINGS === 'object' && typeof SETTINGS.workerModel === 'string'
81
+ ? SETTINGS.workerModel.trim()
82
+ : ''
83
+ let modelPatch = ''
84
+ if (WORKER_MODEL !== '') {
85
+ const slash = WORKER_MODEL.indexOf('/')
86
+ const provider = slash > 0 ? WORKER_MODEL.slice(0, slash).trim() : ''
87
+ const model = slash > 0 ? WORKER_MODEL.slice(slash + 1).trim() : ''
88
+ if (provider === '' || model === '' || /[\r\n]/.test(WORKER_MODEL) || /\s/.test(model)) {
89
+ console.error(`[make-launch-spec] REFUSING: workerModel "${WORKER_MODEL}" is not "provider/model"`)
90
+ console.error('Nothing was written. Fix the setting, then run this again.')
91
+ process.exit(3)
92
+ }
93
+ modelPatch = join(DSH_HOME, 'storages', 'dsh-plugin-remote-use', 'acp-model-overlay.yml')
94
+ const overlay = [
95
+ '# --- dsh-plugin-remote-use managed: worker model (generated from this machine\'s "workerModel" setting) ---',
96
+ '- id: acp',
97
+ " name: '@deepseek-ai/dsh-acp'",
98
+ ' config:',
99
+ ` provider: ${JSON.stringify(provider)}`,
100
+ ` model: ${JSON.stringify(model)}`,
101
+ '# --- end dsh-plugin-remote-use managed: worker model ---',
102
+ '',
103
+ ].join('\n')
104
+ try {
105
+ mkdirSync(dirname(modelPatch), { recursive: true })
106
+ writeFileSync(modelPatch, overlay, 'utf8')
107
+ } catch (error) {
108
+ console.error(`[make-launch-spec] REFUSING: cannot write the model overlay ${modelPatch}: ${error.message}`)
109
+ console.error('Nothing was written. Fix DSH_HOME or permissions, then run this again.')
110
+ process.exit(3)
111
+ }
112
+ }
113
+ const PROFILE_PATH = join(DSH_HOME, 'profiles', PROFILE, 'cordis.patch.yml')
114
+ let profileText
115
+ try {
116
+ profileText = readFileSync(PROFILE_PATH, 'utf8')
117
+ } catch {
118
+ profileText = undefined
119
+ }
120
+
121
+ const pairing = checkPairing({ mode: PERMISSION_MODE, profileText, profilePath: PROFILE_PATH })
122
+ if (pairing.severity === 'block') {
123
+ console.error(`[make-launch-spec] REFUSING to generate this spec: --permission-mode ${PERMISSION_MODE}`)
124
+ console.error('')
125
+ console.error(pairing.reason)
126
+ console.error('')
127
+ console.error('Nothing was written. Fix the profile, then run this again.')
128
+ process.exit(3)
129
+ }
130
+ if (pairing.severity === 'warn') {
131
+ console.error(`[make-launch-spec] WARNING: ${pairing.reason}`)
132
+ if (profileText === undefined) {
133
+ // "Could not verify" is not "verified fine", and the operator still needs to know what to check
134
+ // by hand when this script cannot read the profile (an unusual DSH_HOME, for instance).
135
+ console.error(` expected the profile patch at ${PROFILE_PATH}`)
136
+ console.error(' if it pins the approval policy, apply slave/acp-profile-approval.patch.yml')
137
+ }
138
+ }
139
+
140
+ // Hard-coded profile paths first: under a SYSTEM scheduled task LOCALAPPDATA points at
141
+ // C:\Windows\system32\config\systemprofile, so environment-derived candidates miss.
142
+ const CANDIDATES = [
143
+ arg('install', ''),
144
+ join(process.env.LOCALAPPDATA ?? '', 'Programs', 'DeepSeek Harness'),
145
+ 'C:\\Program Files\\DeepSeek Harness',
146
+ ].filter((p) => typeof p === 'string' && p !== '')
147
+
148
+ let installDir = null
149
+ for (const c of CANDIDATES) {
150
+ if (existsSync(join(c, 'DeepSeek Harness.exe'))) { installDir = c; break }
151
+ }
152
+ if (installDir === null) {
153
+ console.error('[make-launch-spec] could not find "DeepSeek Harness.exe". Tried:')
154
+ for (const c of CANDIDATES) console.error(' ' + c)
155
+ process.exit(2)
156
+ }
157
+
158
+ const exe = join(installDir, 'DeepSeek Harness.exe')
159
+ const cli = [
160
+ installDir, 'resources', 'app.asar', 'dsh', 'node_modules',
161
+ '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js',
162
+ ].join('\\')
163
+
164
+ const spec = {
165
+ command: exe,
166
+ // `--patch` is applied AFTER the profile layer: that is how the model setting wins without
167
+ // editing the operator's `cordis.patch.yml`.
168
+ args: ['--expose-internals', cli, '--profile', PROFILE, ...(modelPatch === '' ? [] : ['--patch', modelPatch])],
169
+ env: {
170
+ ELECTRON_RUN_AS_NODE: '1',
171
+ ...(PERMISSION_MODE === '' ? {} : { DSH_PERMISSION_MODE: PERMISSION_MODE }),
172
+ },
173
+ _note: 'replicates dsh.cmd with an argv array; never launch the .cmd shim from Node on Windows',
174
+ _installDir: installDir,
175
+ _permissionMode: PERMISSION_MODE === '' ? '(inherited)' : PERMISSION_MODE,
176
+ _permissionModeSource: PERMISSION_MODE_SOURCE,
177
+ _workerModel: WORKER_MODEL === '' ? '(inherit profile)' : WORKER_MODEL,
178
+ _modelOverlay: modelPatch,
179
+ }
180
+
181
+ writeFileSync(OUT, JSON.stringify(spec, null, 2), 'utf8')
182
+ console.log(`wrote ${resolve(OUT)}`)
183
+ console.log(`mode: ${spec._permissionMode}${PERMISSION_MODE_SOURCE === 'none' ? '' : ` (from ${PERMISSION_MODE_SOURCE})`}`)
184
+ console.log(JSON.stringify(spec, null, 2))
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Choosing which local address to advertise, and which ones may be bound.
3
+ *
4
+ * WHY THIS IS ITS OWN MODULE: two callers need the same answer -- the relay, to put an address
5
+ * in a pairing string, and the plugin's arming path, to offer a list of interfaces and to
6
+ * refuse a bind that is not one of them. Two copies of "which address is the real LAN one"
7
+ * would drift, and the failure mode is a pairing string or a listener that is well-formed and
8
+ * unreachable.
9
+ *
10
+ * THE MEASUREMENT THAT SHAPED IT: a Windows machine with Hyper-V, WSL, Docker or VirtualBox
11
+ * installed has several non-internal IPv4 addresses, enumerated in no useful order. Taking the
12
+ * FIRST one produced `172.20.0.1` on the development host -- a Hyper-V switch no peer can
13
+ * reach. So candidates are scored rather than filtered, and interfaces whose names give them
14
+ * away as virtual are rejected outright.
15
+ *
16
+ * It is a heuristic, which is why an explicit override exists at every call site.
17
+ */
18
+
19
+ /**
20
+ * Interface names that are virtual adapters, with the switch that owns them.
21
+ * Rejected outright rather than down-ranked: an operator who wants one can still name it
22
+ * explicitly, but nothing should ever pick it by accident.
23
+ */
24
+ const VIRTUAL_ADAPTER = /(hyper-v|vethernet|wsl|docker|vmware|virtualbox|vbox|loopback|tailscale|zerotier|tap|tun|npcap|bluetooth)/iu
25
+
26
+ /** `a.b.c.d` -> a number, or -1 when it is not a dotted quad. */
27
+ export function ipv4Value(address) {
28
+ const parts = String(address).split('.')
29
+ if (parts.length !== 4) return -1
30
+ let value = 0
31
+ for (const part of parts) {
32
+ if (!/^\d{1,3}$/u.test(part)) return -1
33
+ const n = Number(part)
34
+ if (n > 255) return -1
35
+ value = (value * 256) + n
36
+ }
37
+ return value
38
+ }
39
+
40
+ /** True for the private ranges a LAN peer can normally reach. */
41
+ export function isPrivateIpv4(address) {
42
+ return address.startsWith('192.168.')
43
+ || address.startsWith('10.')
44
+ || /^172\.(1[6-9]|2\d|3[01])\./u.test(address)
45
+ }
46
+
47
+ /**
48
+ * How good an address is to advertise or bind. Higher wins; `-1` means never choose it.
49
+ *
50
+ * Private ranges beat anything else because a peer on the LAN can reach them, and the classic
51
+ * `192.168.0.0/16` beats `10/8` beats the `172.16/12` block -- where container and switch
52
+ * addresses usually land. Link-local (`169.254.`) scores lowest but is not rejected: it is a
53
+ * real answer when nothing else is configured, and saying so beats offering nothing.
54
+ */
55
+ export function addressScore(address, name) {
56
+ if (VIRTUAL_ADAPTER.test(String(name))) return -1
57
+ const value = ipv4Value(address)
58
+ if (value < 0) return -1
59
+ if (address === '127.0.0.1' || address.startsWith('127.')) return -1
60
+ if (address.startsWith('192.168.')) return 40
61
+ if (address.startsWith('10.')) return 30
62
+ if (/^172\.(1[6-9]|2\d|3[01])\./u.test(address)) return 20
63
+ if (address.startsWith('169.254.')) return 1
64
+ return 10
65
+ }
66
+
67
+ /**
68
+ * Every IPv4 address this machine could plausibly be reached on.
69
+ *
70
+ * @param {Record<string, Array<{address: string, family: string, internal: boolean}>>} interfaces
71
+ * as `os.networkInterfaces()` returns it -- injected so this is testable off-machine
72
+ * @returns {Array<{ address: string, name: string, score: number, private: boolean, virtual: boolean }>}
73
+ * best first; virtual adapters are included but scored -1, so a UI can explain why
74
+ * they are not offered
75
+ */
76
+ export function candidateAddresses(interfaces) {
77
+ const out = []
78
+ for (const [name, list] of Object.entries(interfaces ?? {})) {
79
+ for (const info of list ?? []) {
80
+ if (info?.family !== 'IPv4' || info.internal === true) continue
81
+ const address = String(info.address ?? '')
82
+ // A malformed entry is not an address at all, so it is dropped. A VIRTUAL adapter is a
83
+ // real address that merely should not be chosen by default, so it stays in the list with
84
+ // a negative score -- that is what lets a UI say why it is not offered.
85
+ if (ipv4Value(address) < 0) continue
86
+ const virtual = VIRTUAL_ADAPTER.test(name)
87
+ out.push({
88
+ address,
89
+ name,
90
+ score: addressScore(address, name),
91
+ private: isPrivateIpv4(address),
92
+ virtual,
93
+ })
94
+ }
95
+ }
96
+ // Best score first, then the lower address, so the order is stable across calls.
97
+ return out.sort((a, b) => (b.score - a.score) || (ipv4Value(a.address) - ipv4Value(b.address)))
98
+ }
99
+
100
+ /**
101
+ * The best address to advertise, or '' when there is none.
102
+ * @param {Record<string, any>} interfaces
103
+ * @returns {string}
104
+ */
105
+ export function bestAddress(interfaces) {
106
+ const [first] = candidateAddresses(interfaces)
107
+ return first === undefined || first.score <= 0 ? '' : first.address
108
+ }
109
+
110
+ /**
111
+ * May `address` be bound as the worker listener?
112
+ *
113
+ * `0.0.0.0` is REFUSED, not merely defaulted against. The decision was "bind a specific LAN
114
+ * interface, and do not offer a one-click all-interfaces option", and a rule that only lives in
115
+ * a dropdown is a rule the next caller ignores.
116
+ *
117
+ * @param {string} address
118
+ * @param {Record<string, any>} interfaces
119
+ * @returns {{ ok: boolean, error: string }}
120
+ */
121
+ export function assertBindable(address, interfaces) {
122
+ const value = String(address ?? '').trim()
123
+ if (value === '') return { ok: false, error: 'no bind address given' }
124
+ if (value === '0.0.0.0' || value === '::' || value === '*') {
125
+ return { ok: false, error: 'binding every interface is not offered; name the LAN address to listen on' }
126
+ }
127
+ if (value === '127.0.0.1' || value.startsWith('127.')) {
128
+ // Allowed, but only as an explicit choice: a loopback listener cannot be reached by a peer,
129
+ // so it is a test mode rather than a pairing.
130
+ return { ok: true, error: '' }
131
+ }
132
+ if (ipv4Value(value) < 0) return { ok: false, error: `not an IPv4 address: ${value}` }
133
+ const known = candidateAddresses(interfaces).find((c) => c.address === value)
134
+ if (known === undefined) {
135
+ return { ok: false, error: `${value} is not an address of this machine (choose one of the listed interfaces)` }
136
+ }
137
+ return { ok: true, error: '' }
138
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "dsh-plugin-remote-use-slave",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "node_modules/ws": {
7
+ "version": "8.22.0",
8
+ "license": "MIT",
9
+ "engines": {
10
+ "node": ">=10.0.0"
11
+ },
12
+ "peerDependencies": {
13
+ "bufferutil": "^4.0.1",
14
+ "utf-8-validate": ">=5.0.2"
15
+ },
16
+ "peerDependenciesMeta": {
17
+ "bufferutil": {
18
+ "optional": true
19
+ },
20
+ "utf-8-validate": {
21
+ "optional": true
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
2
+ Copyright (c) 2013 Arnout Kazemier and contributors
3
+ Copyright (c) 2016 Luigi Pinca and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.