@foggy-projects/deepseek-harness-plugin 0.4.0-beta.13 → 0.4.0-beta.14

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 CHANGED
@@ -14,7 +14,7 @@ modify `PATH`, or register Python globally. Advanced users may explicitly set
14
14
  ## Local beta installation
15
15
 
16
16
  ```powershell
17
- dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.13.tgz
17
+ dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.14.tgz
18
18
  ```
19
19
 
20
20
  Restart `dsh web`, use the browser it opens (or the complete printed URL,
@@ -67,6 +67,12 @@ the managed Runtime directory. Runtime start monitors the Launcher PID during
67
67
  process is verified with `wait-ready` and `capabilities` instead of being treated as a
68
68
  failed second start.
69
69
 
70
+ The same settings tab owns a persistent local Runtime port. New installations default
71
+ to `18166`; users can choose another port while Runtime is stopped. Startup checks the
72
+ same wildcard binding used by the Java server, so a conflicting application or Windows
73
+ port proxy produces an immediate, high-visibility error instead of a readiness timeout.
74
+ The CLI and Skills resolve the resulting stable Runtime URL from managed state.
75
+
70
76
  This beta remains a local dev/test integration. The bundled Runtime reports
71
77
  `securityMode=none-dev-test-only` and must not be exposed to a network or used as
72
78
  a production service.
@@ -1,10 +1,10 @@
1
- # Public beta readiness: 0.4.0-beta.13
1
+ # Public beta readiness: 0.4.0-beta.14
2
2
 
3
3
  Assessment date: 2026-09-04
4
4
 
5
5
  ## Verdict
6
6
 
7
- `0.4.0-beta.13` is suitable for a scoped public beta on Windows 10/11 x64 and
7
+ `0.4.0-beta.14` is suitable for a scoped public beta on Windows 10/11 x64 and
8
8
  Linux/WSL2 x64. It is not a general-availability release.
9
9
 
10
10
  The plugin package stays small and downloads large components only during
@@ -14,7 +14,7 @@ system PATH, registry, or a user's existing Python installation.
14
14
 
15
15
  ## Verified behavior
16
16
 
17
- - The npm tarball is about 67 KB compressed and contains no Python or Java
17
+ - The npm tarball is about 78 KB compressed and contains no Python or Java
18
18
  runtime binary.
19
19
  - Managed CPython assets are pinned by URL, byte size, and SHA256 for Windows,
20
20
  Linux, and macOS on x64 and arm64.
@@ -27,7 +27,7 @@ system PATH, registry, or a user's existing Python installation.
27
27
  as ready; an isolated Windows profile correctly reported its fresh component
28
28
  state and rejected the host's Java 12 as below the Java 17 prerequisite.
29
29
  - The UI reported Python 3.12.13 as `Foggy private`, and CLI 0.1.23, Launcher
30
- 0.1.18, analysis Skill 0.1.17, and onboarding Skill 0.4.0-beta.13 as installed.
30
+ 0.1.18, analysis Skill 0.1.17, and onboarding Skill 0.4.0-beta.14 as installed.
31
31
  - Runtime startup now reports the current phase, elapsed/timeout seconds, and a
32
32
  bounded progress value. A real Windows launch with Temurin 17 reached readiness
33
33
  and passed capabilities in 18.9 seconds. A controlled post-launch Java exit was
@@ -37,11 +37,17 @@ system PATH, registry, or a user's existing Python installation.
37
37
  error, including the generated local JSON path. Exported failures include
38
38
  bounded sanitized Runtime log tails and reject log paths outside the managed
39
39
  Runtime directory.
40
+ - Runtime port configuration is persisted below the private Foggy data root and
41
+ defaults to `18166`. The settings UI validates `1024–65535`, prevents changes
42
+ while Runtime is active, and shows port conflicts as an assertive, red bordered
43
+ alert. Preflight now checks `0.0.0.0` with exclusive Windows binding semantics,
44
+ matching the Java server closely enough to detect address-specific `portproxy`
45
+ conflicts that a loopback-only probe missed.
40
46
  - Interrupted downloads resume with HTTP Range when the server supports it;
41
47
  downloaded artifacts are not trusted until the complete SHA256 matches.
42
48
  - Windows progress writes tolerate transient sharing violations without losing
43
49
  the previous valid progress document or leaving temporary files behind.
44
- - Automated regression passed on Windows and Linux/WSL2: 17 Node tests and 10
50
+ - Automated regression passed on Windows and Linux/WSL2: 20 Node tests and 17
45
51
  Python tests on each platform, plus shell and PowerShell syntax checks.
46
52
 
47
53
  ## Beta boundaries
@@ -1,7 +1,7 @@
1
1
  # Native Windows Beta acceptance
2
2
 
3
3
  This runbook validates DeepSeek Harness `0.1.2-rc.1` with Foggy plugin
4
- `0.4.0-beta.13` on a clean 64-bit Windows 10 or Windows 11 machine. Use a local
4
+ `0.4.0-beta.14` on a clean 64-bit Windows 10 or Windows 11 machine. Use a local
5
5
  directory that is not synchronized by OneDrive.
6
6
 
7
7
  ## Prerequisites
@@ -43,13 +43,18 @@ temporary local access material and do not paste it into chat or diagnostics.
43
43
  Configure an LLM provider, select the `C:\FoggyAcceptance` workspace, then open
44
44
  **Settings → Plugins → Foggy Data Analysis** and choose **Initialize Foggy**.
45
45
 
46
+ Before starting Runtime, confirm the configured port in **Runtime connection
47
+ settings**. A fresh profile defaults to `18166`. To use another port, enter an
48
+ integer from `1024` through `65535` and choose **Save port**. The value remains
49
+ stable across DSH restarts and cannot be changed while Runtime is running.
50
+
46
51
  Expected component state:
47
52
 
48
53
  - Python 3.12.13, source `Foggy private`;
49
54
  - CLI 0.1.23;
50
55
  - Launcher 0.1.18;
51
56
  - analysis Skill 0.1.17;
52
- - onboarding Skill 0.4.0-beta.13;
57
+ - onboarding Skill 0.4.0-beta.14;
53
58
  - Java 17+ available;
54
59
  - native DSH Skill registration available.
55
60
 
@@ -61,6 +66,12 @@ readiness. On failure, export diagnostics before retrying; the report includes
61
66
  the startup phase, PID state, evidence directory, Runtime log locations, and
62
67
  bounded sanitized log tails.
63
68
 
69
+ If the configured port is occupied, startup must fail during preflight instead
70
+ of waiting for the readiness deadline. The settings page displays a red bordered
71
+ alert naming the port conflict and tells the user to save another port or release
72
+ the conflicting application or Windows port proxy. Saving a free port and
73
+ starting again must clear the applicable conflict state.
74
+
64
75
  ## Database and semantic-layer acceptance
65
76
 
66
77
  Keep database credentials out of this public repository. Create a private CLI
@@ -80,4 +91,4 @@ profile。请先确认连接和表结构,再围绕订单数据创建一个简
80
91
  Pass only when Runtime readiness and capabilities succeed, datasource metadata
81
92
  can be discovered, TM/QM models validate and publish, the bounded read-only
82
93
  query succeeds, diagnostics contain no credentials, and stopping Runtime frees
83
- port 18066.
94
+ the configured port (default `18166`).
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  DSH_VERSION="0.1.2-rc.1"
5
5
  PNPM_VERSION="11.7.0"
6
- PLUGIN_VERSION="0.4.0-beta.13"
6
+ PLUGIN_VERSION="0.4.0-beta.14"
7
7
  PLUGIN_REF="v${PLUGIN_VERSION}"
8
8
  PLUGIN_REPOSITORY="https://github.com/foggy-projects/foggy-deepseek-harness-plugin.git"
9
9
 
package/lib/client.js CHANGED
@@ -8,25 +8,47 @@ window.__ModuleLoader__.load({
8
8
 
9
9
  const NS = 'settings.foggy'
10
10
  const passthroughSchema = { parse: (value) => value }
11
+ const runtimeSettingsInputSchema = {
12
+ parse: (value) => {
13
+ const port = value?.port
14
+ if (!Number.isInteger(port) || port < 1024 || port > 65535 || Object.keys(value || {}).some((key) => key !== 'port')) {
15
+ throw new Error('Runtime port must be an integer between 1024 and 65535')
16
+ }
17
+ return { port }
18
+ },
19
+ }
20
+ const descriptor = (method, parameters = []) => ({
21
+ id: `@foggy-projects/deepseek-harness-plugin#foggyIntegration/${method}`,
22
+ service: 'foggyIntegration',
23
+ namespace: 'foggyIntegration',
24
+ method,
25
+ invocation: { kind: 'direct' },
26
+ parameters,
27
+ result: {
28
+ mode: 'strict',
29
+ typeSymbol: '@foggy-projects/deepseek-harness-plugin#FoggyIntegrationResult',
30
+ schema: passthroughSchema,
31
+ },
32
+ sourceLocation: { file: 'lib/index.js', line: 1, column: 1 },
33
+ })
11
34
  const TYPERT_REMOTE = {
12
35
  package: '@foggy-projects/deepseek-harness-plugin',
13
36
  descriptors: [
37
+ ...[
14
38
  'status', 'plan', 'initialize', 'repair', 'repairPython', 'repairCli', 'repairLauncher', 'repairAnalysisSkill',
15
39
  'migrateProfiles', 'diagnostics', 'runtimeStart', 'runtimeStop',
16
- ].map((method) => ({
17
- id: `@foggy-projects/deepseek-harness-plugin#foggyIntegration/${method}`,
18
- service: 'foggyIntegration',
19
- namespace: 'foggyIntegration',
20
- method,
21
- invocation: { kind: 'direct' },
22
- parameters: [],
23
- result: {
24
- mode: 'strict',
25
- typeSymbol: '@foggy-projects/deepseek-harness-plugin#FoggyIntegrationResult',
26
- schema: passthroughSchema,
27
- },
28
- sourceLocation: { file: 'lib/index.js', line: 1, column: 1 },
29
- })),
40
+ ].map((method) => descriptor(method)),
41
+ descriptor('saveRuntimeSettings', [{
42
+ name: 'input',
43
+ wire: 'input',
44
+ source: 'json',
45
+ codec: {
46
+ mode: 'strict',
47
+ typeSymbol: '@foggy-projects/deepseek-harness-plugin#FoggyRuntimeSettingsInput',
48
+ schema: runtimeSettingsInputSchema,
49
+ },
50
+ }]),
51
+ ],
30
52
  }
31
53
 
32
54
  const zh = {
@@ -108,6 +130,21 @@ window.__ModuleLoader__.load({
108
130
  stepSemanticPublished: '发布',
109
131
  stepSemanticVerified: '首次查询',
110
132
  runtimeUrl: 'Runtime 地址',
133
+ runtimeSettingsTitle: 'Runtime 连接设置',
134
+ runtimePort: 'Runtime 端口',
135
+ runtimePortHint: '端口会持久保存,CLI 与 Skill 将使用同一个 Runtime 地址。',
136
+ configuredRuntimeUrl: '下次启动地址',
137
+ saveRuntimePort: '保存端口',
138
+ runtimePortSaved: 'Runtime 端口已保存',
139
+ stopToChangePort: 'Runtime 运行时不能修改端口,请先停止 Runtime。',
140
+ errorTitle: 'Foggy 操作失败',
141
+ portUnavailableTitle: 'Runtime 端口被占用',
142
+ portUnavailableMessage: '已被其他程序或 Windows 端口代理占用,Runtime 无法启动。',
143
+ portUnavailableHelp: '请更换端口后保存并重新启动;也可以先释放占用该端口的程序或 Windows 端口代理。',
144
+ portInvalidTitle: '端口配置无效',
145
+ portInvalidMessage: '当前值无法保存为 Runtime 端口。',
146
+ portInvalidHelp: '请输入 1024 到 65535 之间的整数端口。',
147
+ settingsInvalidTitle: 'Runtime 设置文件异常',
111
148
  nextTitle: '后续配置',
112
149
  nextCopy: 'Skills 已通过 DeepSeek Harness 原生注册表提供给所有工作区;每个会话直接使用自己的工作目录。Runtime 启动成功后,将继续进入数据库连接与语义层向导。',
113
150
  beta: 'Beta',
@@ -192,6 +229,21 @@ window.__ModuleLoader__.load({
192
229
  stepSemanticPublished: 'Publish',
193
230
  stepSemanticVerified: 'First query',
194
231
  runtimeUrl: 'Runtime URL',
232
+ runtimeSettingsTitle: 'Runtime connection settings',
233
+ runtimePort: 'Runtime port',
234
+ runtimePortHint: 'The port is persisted so the CLI and Skills use one stable Runtime URL.',
235
+ configuredRuntimeUrl: 'Next startup URL',
236
+ saveRuntimePort: 'Save port',
237
+ runtimePortSaved: 'Runtime port saved',
238
+ stopToChangePort: 'Stop Runtime before changing its port.',
239
+ errorTitle: 'Foggy operation failed',
240
+ portUnavailableTitle: 'Runtime port is already in use',
241
+ portUnavailableMessage: 'is already used by another application or Windows port proxy, so Runtime cannot start.',
242
+ portUnavailableHelp: 'Save a different port and start again, or release the application or Windows port proxy using this port.',
243
+ portInvalidTitle: 'Invalid port setting',
244
+ portInvalidMessage: 'The current value cannot be saved as the Runtime port.',
245
+ portInvalidHelp: 'Enter an integer port between 1024 and 65535.',
246
+ settingsInvalidTitle: 'Runtime settings file is invalid',
195
247
  nextTitle: 'Next configuration',
196
248
  nextCopy: 'Skills are provided to every workspace through the native DeepSeek Harness registry, and each session uses its own working directory. After Runtime starts, the database connection and semantic-layer wizard comes next.',
197
249
  beta: 'Beta',
@@ -206,8 +258,10 @@ window.__ModuleLoader__.load({
206
258
  .foggy-actions{display:flex;gap:8px;flex-wrap:wrap}.foggy-button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:7px 12px;cursor:pointer}.foggy-button:hover{background:var(--dsw-alias-interactive-bg-hover)}.foggy-button:disabled{opacity:.55;cursor:not-allowed}.foggy-button-primary{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-primary);color:white}
207
259
  .foggy-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.foggy-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:13px 14px}.foggy-card-head{display:flex;justify-content:space-between;gap:8px}.foggy-card strong{font-size:13px}.foggy-badge{font-size:11px;color:var(--dsw-alias-label-secondary)}.foggy-card code{display:block;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;overflow-wrap:anywhere}
208
260
  .foggy-progress{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:13px 14px}.foggy-progress-head{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px}.foggy-progress-head strong{font-weight:600}.foggy-progress-percent{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}.foggy-progress-track{height:8px;margin-top:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.foggy-progress-fill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-business-primary);transition:width .25s ease}.foggy-progress-meta{display:flex;justify-content:space-between;gap:12px;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.foggy-progress-file{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
261
+ .foggy-runtime-settings{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:14px}.foggy-runtime-settings h4{margin:0;font-size:13px}.foggy-runtime-settings-row{display:grid;grid-template-columns:minmax(150px,220px) minmax(0,1fr) auto;align-items:end;gap:10px;margin-top:10px}.foggy-field{display:flex;flex-direction:column;gap:6px}.foggy-field label{font-size:12px;font-weight:600}.foggy-input{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-variant-numeric:tabular-nums;padding:8px 10px;outline:none}.foggy-input:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 18%,transparent)}.foggy-input[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-error-primary) 14%,transparent)}.foggy-setting-meta{display:flex;flex-direction:column;gap:4px;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:11px}.foggy-setting-meta code{overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary)}
262
+ .foggy-error-panel{border:2px solid var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,var(--dsw-alias-bg-layer-3));border-radius:10px;padding:13px 14px;box-shadow:0 6px 18px color-mix(in srgb,var(--dsw-alias-state-error-primary) 13%,transparent)}.foggy-error-panel-head{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-state-error-primary);font-size:14px;font-weight:700}.foggy-error-icon{display:grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--dsw-alias-state-error-primary);color:white;font-size:14px}.foggy-error-panel p{margin:7px 0 0;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;overflow-wrap:anywhere}.foggy-error-help{color:var(--dsw-alias-state-error-primary)!important;font-weight:600}
209
263
  .foggy-message{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}.foggy-message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.foggy-paths,.foggy-next,.foggy-section{border-top:1px solid var(--dsw-alias-border-l2);padding-top:14px}.foggy-paths h4,.foggy-next h4,.foggy-section h4{margin:0 0 8px;font-size:13px}.foggy-paths dl{display:grid;grid-template-columns:100px minmax(0,1fr);gap:6px 10px;margin:0}.foggy-paths dt{color:var(--dsw-alias-label-tertiary);font-size:12px}.foggy-paths dd{margin:0;min-width:0;overflow-wrap:anywhere;font-family:var(--ds-font-family-code);font-size:11px}.foggy-next p,.foggy-section p{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}.foggy-section-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.foggy-onboarding-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.foggy-onboarding-head code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.foggy-step-list{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;margin-top:10px}.foggy-step{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.foggy-step[data-state=completed]{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,var(--dsw-alias-border-l2));color:var(--dsw-alias-state-success-primary)}.foggy-step[data-state=failed],.foggy-step[data-state=invalid]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.foggy-warning{border:1px solid color-mix(in srgb,var(--dsw-alias-state-warning-primary) 55%,var(--dsw-alias-border-l2));background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent);border-radius:10px;padding:12px}
210
- @media (width<=680px){.foggy-grid{grid-template-columns:minmax(0,1fr)}}
264
+ @media (width<=680px){.foggy-grid{grid-template-columns:minmax(0,1fr)}.foggy-runtime-settings-row{grid-template-columns:minmax(0,1fr)}.foggy-runtime-settings-row .foggy-button{width:100%}}
211
265
  `
212
266
 
213
267
  if (typeof document !== 'undefined' && !document.querySelector('style[data-foggy-settings]')) {
@@ -347,18 +401,44 @@ window.__ModuleLoader__.load({
347
401
  ] })
348
402
  }
349
403
 
404
+ function errorPanel(title, message, help) {
405
+ if (!message) return null
406
+ return jsxs('div', {
407
+ className: 'foggy-error-panel',
408
+ role: 'alert',
409
+ 'aria-live': 'assertive',
410
+ children: [
411
+ jsxs('div', { className: 'foggy-error-panel-head', children: [
412
+ jsx('span', { className: 'foggy-error-icon', 'aria-hidden': true, children: '!' }),
413
+ jsx('strong', { children: title }),
414
+ ] }),
415
+ jsx('p', { children: message }),
416
+ help ? jsx('p', { className: 'foggy-error-help', children: help }) : null,
417
+ ],
418
+ })
419
+ }
420
+
350
421
  function FoggySettingsTab({ api, t }) {
351
- const [view, setView] = useState({ phase: 'loading', status: null, message: '', error: false })
422
+ const [view, setView] = useState({ phase: 'loading', status: null, message: '', error: false, errorCode: null })
423
+ const [portDraft, setPortDraft] = useState('')
424
+ const [portDirty, setPortDirty] = useState(false)
352
425
 
353
426
  const refresh = useCallback(async () => {
354
427
  setView((current) => ({ ...current, phase: current.status ? 'ready' : 'loading', error: false }))
355
428
  try {
356
429
  const status = unwrap(await api.status(), 'status')
357
- setView((current) => ({ ...current, phase: 'ready', status, error: false }))
430
+ setView((current) => ({
431
+ ...current,
432
+ phase: 'ready',
433
+ status,
434
+ error: false,
435
+ message: current.message === t('accepted') && status.operation?.state !== 'running' ? '' : current.message,
436
+ }))
437
+ if (!portDirty) setPortDraft(String(status.runtimeSettings?.port ?? 18166))
358
438
  } catch (error) {
359
439
  setView((current) => ({ ...current, phase: current.status ? 'ready' : 'error', message: String(error.message || error), error: true }))
360
440
  }
361
- }, [api])
441
+ }, [api, portDirty, t])
362
442
 
363
443
  useEffect(() => { refresh() }, [refresh])
364
444
  useEffect(() => {
@@ -368,18 +448,40 @@ window.__ModuleLoader__.load({
368
448
  }, [refresh, view.status?.operation?.state])
369
449
 
370
450
  const run = async (method) => {
371
- setView((current) => ({ ...current, message: t('working'), error: false }))
451
+ setView((current) => ({ ...current, message: t('working'), error: false, errorCode: null }))
372
452
  try {
373
453
  const result = unwrap(await api[method](), method)
374
454
  const message = result.accepted
375
455
  ? t('accepted')
376
456
  : result.path
377
457
  ? `${t('diagnosticsSaved')}: ${result.path}`
378
- : ''
379
- setView((current) => ({ ...current, message, error: result.success === false }))
458
+ : result.error?.message || ''
459
+ setView((current) => ({ ...current, message, error: result.success === false, errorCode: result.error?.code || null }))
380
460
  await refresh()
381
461
  } catch (error) {
382
- setView((current) => ({ ...current, message: `${t('actionFailed')}: ${String(error.message || error)}`, error: true }))
462
+ setView((current) => ({ ...current, message: `${t('actionFailed')}: ${String(error.message || error)}`, error: true, errorCode: 'REMOTE_ERROR' }))
463
+ }
464
+ }
465
+
466
+ const saveRuntimePort = async () => {
467
+ const port = Number(portDraft)
468
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
469
+ setView((current) => ({ ...current, message: t('portInvalidMessage'), error: true, errorCode: 'RUNTIME_PORT_INVALID' }))
470
+ return
471
+ }
472
+ setView((current) => ({ ...current, message: t('working'), error: false, errorCode: null }))
473
+ try {
474
+ const result = unwrap(await api.saveRuntimeSettings({ port }), 'saveRuntimeSettings')
475
+ if (result.success === false) {
476
+ setView((current) => ({ ...current, message: result.error?.message || t('actionFailed'), error: true, errorCode: result.error?.code || null }))
477
+ return
478
+ }
479
+ setPortDirty(false)
480
+ setPortDraft(String(result.settings.port))
481
+ setView((current) => ({ ...current, message: `${t('runtimePortSaved')}: ${result.settings.runtimeUrl}`, error: false, errorCode: null }))
482
+ await refresh()
483
+ } catch (error) {
484
+ setView((current) => ({ ...current, message: `${t('actionFailed')}: ${String(error.message || error)}`, error: true, errorCode: 'REMOTE_ERROR' }))
383
485
  }
384
486
  }
385
487
 
@@ -391,9 +493,38 @@ window.__ModuleLoader__.load({
391
493
 
392
494
  const status = view.status
393
495
  const busy = status.operation?.state === 'running'
496
+ const portNumber = Number(portDraft)
497
+ const portValid = Number.isInteger(portNumber) && portNumber >= 1024 && portNumber <= 65535
394
498
  const operationError = status.operation?.state === 'failed'
395
499
  ? status.operation.error || status.operation.result?.error?.message || t('actionFailed')
396
500
  : ''
501
+ const operationErrorCode = status.operation?.state === 'failed' ? status.operation.result?.error?.code || null : null
502
+ const settingsError = status.runtimeSettings?.valid === false
503
+ ? status.runtimeSettings.error || t('settingsInvalidTitle')
504
+ : ''
505
+ const portConflict = status.runtimeSettings?.conflictApplies ? status.runtimeSettings.lastConflict : null
506
+ const validationError = portDirty && !portValid ? t('portInvalidMessage') : ''
507
+ const rawAlertMessage = validationError || (view.error ? view.message : '') || operationError || settingsError || portConflict?.message || ''
508
+ const alertCode = validationError
509
+ ? 'RUNTIME_PORT_INVALID'
510
+ : view.error
511
+ ? view.errorCode
512
+ : operationErrorCode || (settingsError ? 'RUNTIME_SETTINGS_INVALID' : portConflict ? 'RUNTIME_PORT_UNAVAILABLE' : null)
513
+ const alertTitle = alertCode === 'RUNTIME_PORT_UNAVAILABLE'
514
+ ? t('portUnavailableTitle')
515
+ : alertCode === 'RUNTIME_PORT_INVALID'
516
+ ? t('portInvalidTitle')
517
+ : alertCode === 'RUNTIME_SETTINGS_INVALID'
518
+ ? t('settingsInvalidTitle')
519
+ : t('errorTitle')
520
+ const alertMessage = alertCode === 'RUNTIME_PORT_UNAVAILABLE'
521
+ ? `${t('runtimePort')} ${portConflict?.port ?? status.runtimeSettings?.port ?? portNumber} ${t('portUnavailableMessage')}`
522
+ : rawAlertMessage
523
+ const alertHelp = alertCode === 'RUNTIME_PORT_UNAVAILABLE'
524
+ ? t('portUnavailableHelp')
525
+ : alertCode === 'RUNTIME_PORT_INVALID'
526
+ ? t('portInvalidHelp')
527
+ : ''
397
528
  const progress = busy ? progressPanel(status.operation?.progress, t) : null
398
529
  const stateLabels = { 'not-installed': 'notInstalled', degraded: 'degraded', ready: 'ready', running: 'running' }
399
530
  return jsxs('section', { className: 'foggy-settings', children: [
@@ -412,10 +543,46 @@ window.__ModuleLoader__.load({
412
543
  status.running ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('runtimeStop'), children: t('stop') }) : null,
413
544
  ] }),
414
545
  ] }),
546
+ errorPanel(alertTitle, alertMessage, alertHelp),
415
547
  progress,
416
548
  busy ? jsx('p', { className: 'foggy-message', children: t('working') }) : null,
417
- operationError ? jsx('p', { className: 'foggy-message', 'data-error': 'true', children: operationError }) : null,
418
- !busy && view.message ? jsx('p', { className: 'foggy-message', 'data-error': view.error ? 'true' : undefined, children: view.message }) : null,
549
+ !busy && view.message && !view.error && !operationError ? jsx('p', { className: 'foggy-message', children: view.message }) : null,
550
+ jsxs('div', { className: 'foggy-runtime-settings', children: [
551
+ jsx('h4', { children: t('runtimeSettingsTitle') }),
552
+ jsxs('div', { className: 'foggy-runtime-settings-row', children: [
553
+ jsxs('div', { className: 'foggy-field', children: [
554
+ jsx('label', { htmlFor: 'foggy-runtime-port', children: t('runtimePort') }),
555
+ jsx('input', {
556
+ id: 'foggy-runtime-port',
557
+ className: 'foggy-input',
558
+ type: 'number',
559
+ min: 1024,
560
+ max: 65535,
561
+ step: 1,
562
+ value: portDraft,
563
+ disabled: busy || status.running,
564
+ 'aria-invalid': portDirty && !portValid ? 'true' : undefined,
565
+ onChange: (event) => {
566
+ setPortDraft(event.target.value)
567
+ setPortDirty(true)
568
+ setView((current) => ({ ...current, message: '', error: false, errorCode: null }))
569
+ },
570
+ }),
571
+ ] }),
572
+ jsxs('div', { className: 'foggy-setting-meta', children: [
573
+ jsx('span', { children: t('runtimePortHint') }),
574
+ jsx('code', { children: `${t('configuredRuntimeUrl')}: http://127.0.0.1:${portValid ? portNumber : '—'}` }),
575
+ status.running ? jsx('span', { children: t('stopToChangePort') }) : null,
576
+ ] }),
577
+ jsx('button', {
578
+ className: 'foggy-button foggy-button-primary',
579
+ type: 'button',
580
+ disabled: busy || status.running || !portDirty || !portValid,
581
+ onClick: saveRuntimePort,
582
+ children: t('saveRuntimePort'),
583
+ }),
584
+ ] }),
585
+ ] }),
419
586
  jsxs('div', { className: 'foggy-grid', children: [
420
587
  componentCard(t('python'), status.components.python, 'tool', t),
421
588
  componentCard(t('java'), status.components.java, 'tool', t),
package/lib/index.js CHANGED
@@ -10,6 +10,11 @@ import { writeJsonAtomic } from './atomic-json.js'
10
10
  import { ensurePythonRuntime, probePythonRuntime } from './python-runtime.js'
11
11
  import { compatible, compatibleNode } from './version.js'
12
12
  import { enrichRuntimeStartFailure } from './diagnostics.js'
13
+ import {
14
+ DEFAULT_RUNTIME_PORT,
15
+ readRuntimeSettings,
16
+ writeRuntimeSettings,
17
+ } from './runtime-settings.js'
13
18
 
14
19
  const execFileAsync = promisify(execFile)
15
20
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
@@ -197,13 +202,19 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
197
202
  const statePath = join(roots.installRoot, 'install-state.json')
198
203
  const runtimeStatePath = join(roots.dataRoot, 'runtime-state.json')
199
204
  const progressPath = join(roots.dataRoot, 'operation-progress.json')
205
+ const portConflictPath = join(roots.dataRoot, 'last-runtime-port-conflict.json')
200
206
  const manifest = await readJson(versionsFile)
207
+ const runtimeSettings = await readRuntimeSettings(roots.dataRoot, {
208
+ defaultPort: manifest.defaults?.port ?? DEFAULT_RUNTIME_PORT,
209
+ })
201
210
  const python = await probePythonRuntime({ installRoot: roots.installRoot, manifest })
202
211
  const java = compatible(await commandVersion(process.env.JAVA_EXE || 'java', ['-version']), '17.0')
203
212
  let state = null
204
213
  let runtime = null
214
+ let lastRuntimePortConflict = null
205
215
  try { state = await readJson(statePath) } catch {}
206
216
  try { runtime = await readJson(runtimeStatePath) } catch {}
217
+ try { lastRuntimePortConflict = await readJson(portConflictPath) } catch {}
207
218
  let onboarding = { success: true, profiles: [], profileCount: 0 }
208
219
  let profileMigration = { success: true, entries: [], pendingCount: 0, conflictCount: 0, profileStore: roots.profileStore }
209
220
  if (state) {
@@ -250,6 +261,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
250
261
  && components.analysisSkill.installed
251
262
  && components.onboardingSkill.installed
252
263
  const running = Boolean(runtime && processRunning(Number(runtime.pid)))
264
+ const activePort = running && Number.isInteger(Number(runtime?.port)) ? Number(runtime.port) : null
253
265
  const progress = await readOptionalJson(progressPath)
254
266
  let operation = operationView(this.operation)
255
267
  if (operation.state === 'running' && progress?.operationId === operation.id) {
@@ -273,6 +285,14 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
273
285
  installed,
274
286
  running,
275
287
  runtimeUrl: running ? runtime.runtimeUrl ?? null : null,
288
+ runtimeSettings: {
289
+ ...runtimeSettings,
290
+ activePort,
291
+ activeRuntimeUrl: running ? runtime.runtimeUrl ?? null : null,
292
+ pendingRestart: Boolean(running && activePort && activePort !== runtimeSettings.port),
293
+ lastConflict: lastRuntimePortConflict,
294
+ conflictApplies: Number(lastRuntimePortConflict?.port) === runtimeSettings.port,
295
+ },
276
296
  roots,
277
297
  components,
278
298
  operation,
@@ -355,6 +375,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
355
375
  components: status.components,
356
376
  onboarding: status.onboarding,
357
377
  profileMigration: status.profileMigration,
378
+ runtimeSettings: status.runtimeSettings,
358
379
  lastRuntimeStartFailure,
359
380
  doctor,
360
381
  }
@@ -367,8 +388,23 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
367
388
 
368
389
  async runtimeStart() {
369
390
  const roots = defaultRoots()
391
+ const manifest = await readJson(versionsFile)
392
+ const settings = await readRuntimeSettings(roots.dataRoot, {
393
+ defaultPort: manifest.defaults?.port ?? DEFAULT_RUNTIME_PORT,
394
+ })
395
+ if (!settings.valid) {
396
+ return {
397
+ success: false,
398
+ accepted: false,
399
+ error: {
400
+ code: 'RUNTIME_SETTINGS_INVALID',
401
+ message: `Runtime settings are invalid: ${settings.error}. Save a valid port in Foggy plugin settings.`,
402
+ },
403
+ }
404
+ }
370
405
  return this.startOperation('runtime-start', false, [
371
406
  'runtime-start', '--install-root', roots.installRoot, '--data-root', roots.dataRoot,
407
+ '--port', String(settings.port),
372
408
  ])
373
409
  }
374
410
 
@@ -379,6 +415,35 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
379
415
  ])
380
416
  }
381
417
 
418
+ async saveRuntimeSettings(input) {
419
+ const roots = defaultRoots()
420
+ if (this.operation?.state === 'running') {
421
+ return {
422
+ success: false,
423
+ error: { code: 'OPERATION_RUNNING', message: 'Wait for the current Foggy operation to finish before changing the Runtime port.' },
424
+ }
425
+ }
426
+ const runtime = await readOptionalJson(join(roots.dataRoot, 'runtime-state.json'))
427
+ if (runtime && processRunning(Number(runtime.pid))) {
428
+ return {
429
+ success: false,
430
+ error: { code: 'RUNTIME_RUNNING', message: 'Stop Foggy Runtime before changing its port.' },
431
+ }
432
+ }
433
+ try {
434
+ const settings = await writeRuntimeSettings(roots.dataRoot, input)
435
+ if (this.operation?.state === 'failed' && this.operation.result?.error?.code === 'RUNTIME_PORT_UNAVAILABLE') {
436
+ this.operation = null
437
+ }
438
+ return { success: true, settings }
439
+ } catch (error) {
440
+ return {
441
+ success: false,
442
+ error: { code: 'RUNTIME_PORT_INVALID', message: String(error?.message ?? error) },
443
+ }
444
+ }
445
+ }
446
+
382
447
  startOperation(kind, replaceSkill, explicitArgs) {
383
448
  if (this.operation?.state === 'running') {
384
449
  return { success: false, accepted: false, operation: operationView(this.operation), error: 'another Foggy operation is running' }
@@ -467,6 +532,7 @@ const markerInitializers = []
467
532
  for (const method of [
468
533
  'status', 'plan', 'initialize', 'repair', 'repairPython', 'repairCli', 'repairLauncher', 'repairAnalysisSkill',
469
534
  'migrateProfiles', 'diagnostics', 'runtimeStart', 'runtimeStop',
535
+ 'saveRuntimeSettings',
470
536
  ]) {
471
537
  Remote(method)(FoggyIntegrationGateway.prototype[method], {
472
538
  kind: 'method',
@@ -1,15 +1,18 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  const resultSchema = z.unknown()
4
+ const runtimeSettingsInputSchema = z.object({
5
+ port: z.number().int().min(1024).max(65535),
6
+ }).strict()
4
7
 
5
- function descriptor(method) {
8
+ function descriptor(method, parameters = []) {
6
9
  return {
7
10
  id: `@foggy-projects/deepseek-harness-plugin#foggyIntegration/${method}`,
8
11
  service: 'foggyIntegration',
9
12
  namespace: 'foggyIntegration',
10
13
  method,
11
14
  invocation: { kind: 'direct' },
12
- parameters: [],
15
+ parameters,
13
16
  result: {
14
17
  mode: 'strict',
15
18
  typeSymbol: '@foggy-projects/deepseek-harness-plugin#FoggyIntegrationResult',
@@ -27,4 +30,14 @@ export const descriptors = [
27
30
  descriptor('repairPython'),
28
31
  descriptor('runtimeStart'),
29
32
  descriptor('runtimeStop'),
33
+ descriptor('saveRuntimeSettings', [{
34
+ name: 'input',
35
+ wire: 'input',
36
+ source: 'json',
37
+ codec: {
38
+ mode: 'strict',
39
+ typeSymbol: '@foggy-projects/deepseek-harness-plugin#FoggyRuntimeSettingsInput',
40
+ schema: runtimeSettingsInputSchema,
41
+ },
42
+ }]),
30
43
  ]
@@ -0,0 +1,89 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { writeJsonAtomic } from './atomic-json.js'
4
+
5
+ export const RUNTIME_SETTINGS_SCHEMA = 'foggy-deepseek-runtime-settings/v1'
6
+ export const DEFAULT_RUNTIME_PORT = 18166
7
+ export const MIN_RUNTIME_PORT = 1024
8
+ export const MAX_RUNTIME_PORT = 65535
9
+
10
+ export function normalizeRuntimePort(value) {
11
+ const port = typeof value === 'string' && value.trim() !== '' ? Number(value) : value
12
+ if (!Number.isInteger(port) || port < MIN_RUNTIME_PORT || port > MAX_RUNTIME_PORT) {
13
+ throw new Error(`Runtime port must be an integer between ${MIN_RUNTIME_PORT} and ${MAX_RUNTIME_PORT}`)
14
+ }
15
+ return port
16
+ }
17
+
18
+ export function runtimeUrlForPort(port) {
19
+ return `http://127.0.0.1:${normalizeRuntimePort(port)}`
20
+ }
21
+
22
+ export function runtimeSettingsPath(dataRoot) {
23
+ return join(dataRoot, 'runtime-settings.json')
24
+ }
25
+
26
+ export async function readRuntimeSettings(dataRoot, { defaultPort = DEFAULT_RUNTIME_PORT } = {}) {
27
+ const path = runtimeSettingsPath(dataRoot)
28
+ const fallbackPort = normalizeRuntimePort(defaultPort)
29
+ try {
30
+ const payload = JSON.parse(await readFile(path, 'utf8'))
31
+ if (payload?.schemaVersion !== RUNTIME_SETTINGS_SCHEMA) {
32
+ throw new Error(`expected schemaVersion ${RUNTIME_SETTINGS_SCHEMA}`)
33
+ }
34
+ const port = normalizeRuntimePort(payload.runtimePort)
35
+ return {
36
+ valid: true,
37
+ source: 'configured',
38
+ schemaVersion: RUNTIME_SETTINGS_SCHEMA,
39
+ port,
40
+ runtimeUrl: runtimeUrlForPort(port),
41
+ updatedAt: payload.updatedAt ?? null,
42
+ path,
43
+ error: null,
44
+ }
45
+ } catch (error) {
46
+ if (error?.code === 'ENOENT') {
47
+ return {
48
+ valid: true,
49
+ source: 'default',
50
+ schemaVersion: RUNTIME_SETTINGS_SCHEMA,
51
+ port: fallbackPort,
52
+ runtimeUrl: runtimeUrlForPort(fallbackPort),
53
+ updatedAt: null,
54
+ path,
55
+ error: null,
56
+ }
57
+ }
58
+ return {
59
+ valid: false,
60
+ source: 'invalid',
61
+ schemaVersion: RUNTIME_SETTINGS_SCHEMA,
62
+ port: fallbackPort,
63
+ runtimeUrl: runtimeUrlForPort(fallbackPort),
64
+ updatedAt: null,
65
+ path,
66
+ error: String(error?.message ?? error),
67
+ }
68
+ }
69
+ }
70
+
71
+ export async function writeRuntimeSettings(dataRoot, input) {
72
+ const runtimePort = normalizeRuntimePort(input?.port)
73
+ const payload = {
74
+ schemaVersion: RUNTIME_SETTINGS_SCHEMA,
75
+ runtimePort,
76
+ updatedAt: new Date().toISOString(),
77
+ }
78
+ const path = runtimeSettingsPath(dataRoot)
79
+ await writeJsonAtomic(path, payload)
80
+ return {
81
+ valid: true,
82
+ source: 'configured',
83
+ ...payload,
84
+ port: runtimePort,
85
+ runtimeUrl: runtimeUrlForPort(runtimePort),
86
+ path,
87
+ error: null,
88
+ }
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foggy-projects/deepseek-harness-plugin",
3
- "version": "0.4.0-beta.13",
3
+ "version": "0.4.0-beta.14",
4
4
  "description": "Foggy Java data analysis engine integration for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -57,7 +57,7 @@
57
57
  "@deepseek-ai/dsh-typert-protocol": "^0.1.2-rc.1"
58
58
  },
59
59
  "scripts": {
60
- "check": "node --check lib/index.js && node --check lib/atomic-json.js && node --check lib/diagnostics.js && node --check lib/python-runtime.js && node --check lib/skill-provider.js && node --check lib/client.js && node --check lib/typert.js && node --check lib/remote.js",
60
+ "check": "node --check lib/index.js && node --check lib/atomic-json.js && node --check lib/diagnostics.js && node --check lib/python-runtime.js && node --check lib/runtime-settings.js && node --check lib/skill-provider.js && node --check lib/client.js && node --check lib/typert.js && node --check lib/remote.js",
61
61
  "test": "node --test test/package.test.js && node test/run-onboarding-tests.js"
62
62
  }
63
63
  }
@@ -1,7 +1,7 @@
1
1
  # Copy only the variables you need into a private, gitignored env file.
2
2
  # Never put provider keys or database passwords in DSH settings, Skill files, or evidence.
3
3
 
4
- FOGGY_RUNTIME_API_URL=http://127.0.0.1:18066
4
+ FOGGY_RUNTIME_API_URL=http://127.0.0.1:18166
5
5
  FOGGY_NAMESPACE=default
6
6
 
7
7
  # Optional when Runtime reports securityMode=auth-code.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "foggy-deepseek-onboarding-versions/v1",
3
- "packageVersion": "0.4.0-beta.13",
3
+ "packageVersion": "0.4.0-beta.14",
4
4
  "validatedAt": "2026-09-04",
5
5
  "components": {
6
6
  "deepseekHarness": {
@@ -129,7 +129,7 @@
129
129
  }
130
130
  },
131
131
  "defaults": {
132
- "port": 18066,
132
+ "port": 18166,
133
133
  "namespace": "default",
134
134
  "readinessTimeoutSeconds": 180,
135
135
  "securityMode": "none-dev-test-only"
@@ -25,6 +25,8 @@ import zipfile
25
25
 
26
26
  STATE_SCHEMA = "foggy-deepseek-onboarding-install/v1"
27
27
  RUNTIME_STATE_SCHEMA = "foggy-deepseek-onboarding-runtime/v1"
28
+ RUNTIME_SETTINGS_SCHEMA = "foggy-deepseek-runtime-settings/v1"
29
+ RUNTIME_PORT_CONFLICT_SCHEMA = "foggy-deepseek-runtime-port-conflict/v1"
28
30
  ONBOARDING_STATE_SCHEMA = "foggy-deepseek-onboarding-state/v1"
29
31
  MANAGED_SKILL_SCHEMA = "foggy-managed-skill/v1"
30
32
  MANAGED_SKILL_MARKER = ".foggy-managed-skill.json"
@@ -43,6 +45,14 @@ class OnboardingError(RuntimeError):
43
45
  pass
44
46
 
45
47
 
48
+ class RuntimeSettingsError(OnboardingError):
49
+ code = "RUNTIME_SETTINGS_INVALID"
50
+
51
+
52
+ class RuntimePortUnavailableError(OnboardingError):
53
+ code = "RUNTIME_PORT_UNAVAILABLE"
54
+
55
+
46
56
  PROGRESS_SCHEMA = "foggy-deepseek-onboarding-progress/v1"
47
57
  ACTIVE_PROGRESS: "ProgressReporter | None" = None
48
58
 
@@ -206,6 +216,61 @@ def default_data_root() -> Path:
206
216
  return Path(base) / "foggy" / "deepseek-harness" if base else Path.home() / ".local" / "state" / "foggy" / "deepseek-harness"
207
217
 
208
218
 
219
+ def validate_runtime_port(value: object) -> int:
220
+ if isinstance(value, bool):
221
+ raise RuntimeSettingsError("Runtime port must be an integer between 1024 and 65535")
222
+ try:
223
+ port = int(value)
224
+ except (TypeError, ValueError) as exc:
225
+ raise RuntimeSettingsError("Runtime port must be an integer between 1024 and 65535") from exc
226
+ if str(value).strip() != str(port) or port < 1024 or port > 65535:
227
+ raise RuntimeSettingsError("Runtime port must be an integer between 1024 and 65535")
228
+ return port
229
+
230
+
231
+ def read_runtime_settings(data_root: Path, default_port: object) -> dict:
232
+ path = data_root / "runtime-settings.json"
233
+ if not path.is_file():
234
+ port = validate_runtime_port(default_port)
235
+ return {"schemaVersion": RUNTIME_SETTINGS_SCHEMA, "port": port, "source": "default", "path": str(path)}
236
+ try:
237
+ payload = json.loads(path.read_text(encoding="utf-8"))
238
+ except (OSError, json.JSONDecodeError) as exc:
239
+ raise RuntimeSettingsError(f"Runtime settings file is invalid: {path}") from exc
240
+ if payload.get("schemaVersion") != RUNTIME_SETTINGS_SCHEMA:
241
+ raise RuntimeSettingsError(f"Unexpected Runtime settings schema in {path}")
242
+ port = validate_runtime_port(payload.get("runtimePort"))
243
+ return {"schemaVersion": RUNTIME_SETTINGS_SCHEMA, "port": port, "source": "configured", "path": str(path)}
244
+
245
+
246
+ def assert_runtime_port_available(data_root: Path, port: int) -> None:
247
+ conflict_path = data_root / "last-runtime-port-conflict.json"
248
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
249
+ if os.name == "nt" and hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
250
+ probe.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
251
+ try:
252
+ # The packaged Java server uses a wildcard listener. Checking only
253
+ # 127.0.0.1 misses Windows portproxy entries bound to another local address.
254
+ probe.bind(("0.0.0.0", port))
255
+ except OSError as exc:
256
+ message = (
257
+ f"Runtime port {port} is unavailable. Another application or Windows port proxy "
258
+ "is already listening. Choose a different port in Foggy plugin settings or release "
259
+ "the port before retrying."
260
+ )
261
+ conflict = {
262
+ "schemaVersion": RUNTIME_PORT_CONFLICT_SCHEMA,
263
+ "detectedAt": now_utc(),
264
+ "port": port,
265
+ "bindAddress": "0.0.0.0",
266
+ "runtimeUrl": f"http://127.0.0.1:{port}",
267
+ "message": message,
268
+ }
269
+ atomic_json(conflict_path, conflict)
270
+ raise RuntimePortUnavailableError(message) from exc
271
+ conflict_path.unlink(missing_ok=True)
272
+
273
+
209
274
  def normalized(path: str | Path) -> Path:
210
275
  return Path(path).expanduser().resolve(strict=False)
211
276
 
@@ -1556,12 +1621,9 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1556
1621
  }
1557
1622
  existing.unlink()
1558
1623
  progress.update("runtime-preflight", 1, "Checking port and Runtime workspace", percent=10)
1559
- port = args.port or int(versions["defaults"]["port"])
1560
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
1561
- try:
1562
- probe.bind(("127.0.0.1", port))
1563
- except OSError as exc:
1564
- raise OnboardingError(f"Port {port} is not available") from exc
1624
+ configured_runtime = read_runtime_settings(data_root, versions["defaults"]["port"])
1625
+ port = validate_runtime_port(args.port) if args.port is not None else configured_runtime["port"]
1626
+ assert_runtime_port_available(data_root, port)
1565
1627
  work_dir = data_root / "runtime"
1566
1628
  work_dir.mkdir(parents=True, exist_ok=True)
1567
1629
  launcher_dir = normalized(state["launcher"]["path"])
@@ -3262,7 +3324,7 @@ def main() -> None:
3262
3324
  except OnboardingError as exc:
3263
3325
  if ACTIVE_PROGRESS is not None:
3264
3326
  ACTIVE_PROGRESS.fail(exc)
3265
- emit({"success": False, "error": {"code": "ONBOARDING_ERROR", "message": str(exc)}, "productionReady": False}, 1)
3327
+ emit({"success": False, "error": {"code": getattr(exc, "code", "ONBOARDING_ERROR"), "message": str(exc)}, "productionReady": False}, 1)
3266
3328
  except Exception as exc:
3267
3329
  if ACTIVE_PROGRESS is not None:
3268
3330
  ACTIVE_PROGRESS.fail(exc)