@foggy-projects/deepseek-harness-plugin 0.4.0-beta.12 → 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 +13 -4
- package/docs/PUBLIC-BETA-READINESS.md +18 -8
- package/docs/WINDOWS-BETA-ACCEPTANCE.md +18 -5
- package/experience/linux/prepare.sh +1 -1
- package/lib/client.js +191 -24
- package/lib/diagnostics.js +86 -0
- package/lib/index.js +72 -1
- package/lib/remote-descriptor.js +15 -2
- package/lib/runtime-settings.js +89 -0
- package/package.json +2 -2
- package/skills/foggy-deepseek-onboarding/assets/env.example +1 -1
- package/skills/foggy-deepseek-onboarding/assets/versions.json +2 -2
- package/skills/foggy-deepseek-onboarding/scripts/onboarding.py +120 -7
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.
|
|
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,
|
|
@@ -60,9 +60,18 @@ and is restored by reinstalling or upgrading the plugin package.
|
|
|
60
60
|
|
|
61
61
|
The Foggy settings tab shows the persisted database/semantic onboarding stages,
|
|
62
62
|
offers pinned checks and repair for CLI, Launcher, and the managed analysis
|
|
63
|
-
Skill, private Python, and exports a private redacted diagnostics report.
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
Skill, private Python, and exports a private redacted diagnostics report. The report
|
|
64
|
+
includes bounded, sanitized tails of Runtime logs while refusing to read paths outside
|
|
65
|
+
the managed Runtime directory. Runtime start monitors the Launcher PID during
|
|
66
|
+
`wait-ready`, fails promptly if Java exits, and remains idempotent: an already-recorded
|
|
67
|
+
process is verified with `wait-ready` and `capabilities` instead of being treated as a
|
|
68
|
+
failed second start.
|
|
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.
|
|
66
75
|
|
|
67
76
|
This beta remains a local dev/test integration. The bundled Runtime reports
|
|
68
77
|
`securityMode=none-dev-test-only` and must not be exposed to a network or used as
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Public beta readiness: 0.4.0-beta.
|
|
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.
|
|
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
|
|
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,17 +27,27 @@ 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.
|
|
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
|
-
bounded progress value. A real
|
|
33
|
-
|
|
32
|
+
bounded progress value. A real Windows launch with Temurin 17 reached readiness
|
|
33
|
+
and passed capabilities in 18.9 seconds. A controlled post-launch Java exit was
|
|
34
|
+
detected after about 1 second instead of waiting for the 180-second deadline;
|
|
35
|
+
a restored healthy launch then passed again in 15.9 seconds.
|
|
34
36
|
- Diagnostic export success remains visible alongside a prior Runtime startup
|
|
35
|
-
error, including the generated local JSON path.
|
|
37
|
+
error, including the generated local JSON path. Exported failures include
|
|
38
|
+
bounded sanitized Runtime log tails and reject log paths outside the managed
|
|
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.
|
|
36
46
|
- Interrupted downloads resume with HTTP Range when the server supports it;
|
|
37
47
|
downloaded artifacts are not trusted until the complete SHA256 matches.
|
|
38
48
|
- Windows progress writes tolerate transient sharing violations without losing
|
|
39
49
|
the previous valid progress document or leaving temporary files behind.
|
|
40
|
-
- Automated regression passed on Windows and Linux/WSL2:
|
|
50
|
+
- Automated regression passed on Windows and Linux/WSL2: 20 Node tests and 17
|
|
41
51
|
Python tests on each platform, plus shell and PowerShell syntax checks.
|
|
42
52
|
|
|
43
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.
|
|
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,21 +43,34 @@ 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.
|
|
57
|
+
- onboarding Skill 0.4.0-beta.14;
|
|
53
58
|
- Java 17+ available;
|
|
54
59
|
- native DSH Skill registration available.
|
|
55
60
|
|
|
56
61
|
Runtime startup shows its current phase, elapsed time, and the 180-second
|
|
57
62
|
readiness deadline. Typical startup is 15–30 seconds on the validated WSL2 host
|
|
58
63
|
and may take up to 60 seconds on Windows with cold JVM or antivirus scanning.
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
The plugin monitors the Launcher PID and fails promptly when Java exits before
|
|
65
|
+
readiness. On failure, export diagnostics before retrying; the report includes
|
|
66
|
+
the startup phase, PID state, evidence directory, Runtime log locations, and
|
|
67
|
+
bounded sanitized log tails.
|
|
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.
|
|
61
74
|
|
|
62
75
|
## Database and semantic-layer acceptance
|
|
63
76
|
|
|
@@ -78,4 +91,4 @@ profile。请先确认连接和表结构,再围绕订单数据创建一个简
|
|
|
78
91
|
Pass only when Runtime readiness and capabilities succeed, datasource metadata
|
|
79
92
|
can be discovered, TM/QM models validate and publish, the bounded read-only
|
|
80
93
|
query succeeds, diagnostics contain no credentials, and stopping Runtime frees
|
|
81
|
-
port
|
|
94
|
+
the configured port (default `18166`).
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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) => ({
|
|
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',
|
|
418
|
-
|
|
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),
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises'
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_BYTES = 32 * 1024
|
|
5
|
+
const DEFAULT_MAX_LINES = 120
|
|
6
|
+
const ANSI_ESCAPE = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/g
|
|
7
|
+
const BEARER_SECRET = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi
|
|
8
|
+
const API_KEY_SECRET = /\bsk-[A-Za-z0-9._-]{12,}/g
|
|
9
|
+
const JDBC_URL = /\bjdbc:[^\s"'<>]+/gi
|
|
10
|
+
const URL_CREDENTIALS = /(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi
|
|
11
|
+
const SENSITIVE_QUERY_VALUE = /([?&](?:password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?code)=)[^&#\s]*/gi
|
|
12
|
+
const SENSITIVE_ASSIGNMENT = /\b([A-Za-z0-9_.-]*(?:password|passwd|pwd|secret|token|api[_-]?key|authorization|auth[_-]?code)[A-Za-z0-9_.-]*)(\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi
|
|
13
|
+
|
|
14
|
+
export function sanitizeDiagnosticText(value) {
|
|
15
|
+
return String(value ?? '')
|
|
16
|
+
.replace(ANSI_ESCAPE, '')
|
|
17
|
+
.replace(BEARER_SECRET, 'Bearer [REDACTED]')
|
|
18
|
+
.replace(API_KEY_SECRET, '[REDACTED_API_KEY]')
|
|
19
|
+
.replace(JDBC_URL, 'jdbc:[REDACTED_CONNECTION_URL]')
|
|
20
|
+
.replace(URL_CREDENTIALS, '$1[REDACTED]@')
|
|
21
|
+
.replace(SENSITIVE_QUERY_VALUE, '$1[REDACTED]')
|
|
22
|
+
.replace(SENSITIVE_ASSIGNMENT, '$1$2[REDACTED]')
|
|
23
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isInside(root, candidate) {
|
|
27
|
+
const value = relative(resolve(root), resolve(candidate))
|
|
28
|
+
return value !== '' && value !== '..' && !value.startsWith(`..${sep}`) && !isAbsolute(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function readDiagnosticLogTail(path, {
|
|
32
|
+
allowedRoot,
|
|
33
|
+
maxBytes = DEFAULT_MAX_BYTES,
|
|
34
|
+
maxLines = DEFAULT_MAX_LINES,
|
|
35
|
+
} = {}) {
|
|
36
|
+
if (!path || !allowedRoot || !isInside(allowedRoot, path)) {
|
|
37
|
+
return { sanitizedTail: '', tailTruncated: false, tailError: 'LOG_PATH_OUTSIDE_RUNTIME_ROOT' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let handle
|
|
41
|
+
try {
|
|
42
|
+
handle = await open(path, 'r')
|
|
43
|
+
const details = await handle.stat()
|
|
44
|
+
const bytesToRead = Math.min(details.size, maxBytes)
|
|
45
|
+
const buffer = Buffer.alloc(bytesToRead)
|
|
46
|
+
if (bytesToRead > 0) await handle.read(buffer, 0, bytesToRead, details.size - bytesToRead)
|
|
47
|
+
let text = buffer.toString('utf8')
|
|
48
|
+
let truncated = details.size > bytesToRead
|
|
49
|
+
if (truncated) {
|
|
50
|
+
const firstLineEnd = text.indexOf('\n')
|
|
51
|
+
if (firstLineEnd >= 0) text = text.slice(firstLineEnd + 1)
|
|
52
|
+
}
|
|
53
|
+
const lines = text.split(/\r?\n/)
|
|
54
|
+
if (lines.length > maxLines) {
|
|
55
|
+
text = lines.slice(-maxLines).join('\n')
|
|
56
|
+
truncated = true
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
sanitizedTail: sanitizeDiagnosticText(text).trim(),
|
|
60
|
+
tailTruncated: truncated,
|
|
61
|
+
tailBytesRead: bytesToRead,
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
sanitizedTail: '',
|
|
66
|
+
tailTruncated: false,
|
|
67
|
+
tailError: error?.code || 'LOG_READ_FAILED',
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
await handle?.close()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function enrichRuntimeStartFailure(failure, dataRoot) {
|
|
75
|
+
if (!failure || typeof failure !== 'object') return failure
|
|
76
|
+
const runtimeRoot = join(dataRoot, 'runtime')
|
|
77
|
+
const logs = Array.isArray(failure.logs)
|
|
78
|
+
? await Promise.all(failure.logs.map(async (entry) => ({
|
|
79
|
+
...entry,
|
|
80
|
+
...(entry?.exists && entry?.path
|
|
81
|
+
? await readDiagnosticLogTail(entry.path, { allowedRoot: runtimeRoot })
|
|
82
|
+
: {}),
|
|
83
|
+
})))
|
|
84
|
+
: []
|
|
85
|
+
return { ...failure, logs }
|
|
86
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -9,6 +9,12 @@ import { createFoggySkillProvider } from './skill-provider.js'
|
|
|
9
9
|
import { writeJsonAtomic } from './atomic-json.js'
|
|
10
10
|
import { ensurePythonRuntime, probePythonRuntime } from './python-runtime.js'
|
|
11
11
|
import { compatible, compatibleNode } from './version.js'
|
|
12
|
+
import { enrichRuntimeStartFailure } from './diagnostics.js'
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_RUNTIME_PORT,
|
|
15
|
+
readRuntimeSettings,
|
|
16
|
+
writeRuntimeSettings,
|
|
17
|
+
} from './runtime-settings.js'
|
|
12
18
|
|
|
13
19
|
const execFileAsync = promisify(execFile)
|
|
14
20
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
@@ -196,13 +202,19 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
196
202
|
const statePath = join(roots.installRoot, 'install-state.json')
|
|
197
203
|
const runtimeStatePath = join(roots.dataRoot, 'runtime-state.json')
|
|
198
204
|
const progressPath = join(roots.dataRoot, 'operation-progress.json')
|
|
205
|
+
const portConflictPath = join(roots.dataRoot, 'last-runtime-port-conflict.json')
|
|
199
206
|
const manifest = await readJson(versionsFile)
|
|
207
|
+
const runtimeSettings = await readRuntimeSettings(roots.dataRoot, {
|
|
208
|
+
defaultPort: manifest.defaults?.port ?? DEFAULT_RUNTIME_PORT,
|
|
209
|
+
})
|
|
200
210
|
const python = await probePythonRuntime({ installRoot: roots.installRoot, manifest })
|
|
201
211
|
const java = compatible(await commandVersion(process.env.JAVA_EXE || 'java', ['-version']), '17.0')
|
|
202
212
|
let state = null
|
|
203
213
|
let runtime = null
|
|
214
|
+
let lastRuntimePortConflict = null
|
|
204
215
|
try { state = await readJson(statePath) } catch {}
|
|
205
216
|
try { runtime = await readJson(runtimeStatePath) } catch {}
|
|
217
|
+
try { lastRuntimePortConflict = await readJson(portConflictPath) } catch {}
|
|
206
218
|
let onboarding = { success: true, profiles: [], profileCount: 0 }
|
|
207
219
|
let profileMigration = { success: true, entries: [], pendingCount: 0, conflictCount: 0, profileStore: roots.profileStore }
|
|
208
220
|
if (state) {
|
|
@@ -249,6 +261,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
249
261
|
&& components.analysisSkill.installed
|
|
250
262
|
&& components.onboardingSkill.installed
|
|
251
263
|
const running = Boolean(runtime && processRunning(Number(runtime.pid)))
|
|
264
|
+
const activePort = running && Number.isInteger(Number(runtime?.port)) ? Number(runtime.port) : null
|
|
252
265
|
const progress = await readOptionalJson(progressPath)
|
|
253
266
|
let operation = operationView(this.operation)
|
|
254
267
|
if (operation.state === 'running' && progress?.operationId === operation.id) {
|
|
@@ -272,6 +285,14 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
272
285
|
installed,
|
|
273
286
|
running,
|
|
274
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
|
+
},
|
|
275
296
|
roots,
|
|
276
297
|
components,
|
|
277
298
|
operation,
|
|
@@ -332,6 +353,10 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
332
353
|
async diagnostics() {
|
|
333
354
|
const roots = defaultRoots()
|
|
334
355
|
const status = await this.status()
|
|
356
|
+
const lastRuntimeStartFailure = await enrichRuntimeStartFailure(
|
|
357
|
+
await readOptionalJson(join(roots.dataRoot, 'last-runtime-start-failure.json')),
|
|
358
|
+
roots.dataRoot,
|
|
359
|
+
)
|
|
335
360
|
let doctor
|
|
336
361
|
try {
|
|
337
362
|
doctor = await runOnboarding(['doctor', '--no-fail'])
|
|
@@ -350,7 +375,8 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
350
375
|
components: status.components,
|
|
351
376
|
onboarding: status.onboarding,
|
|
352
377
|
profileMigration: status.profileMigration,
|
|
353
|
-
|
|
378
|
+
runtimeSettings: status.runtimeSettings,
|
|
379
|
+
lastRuntimeStartFailure,
|
|
354
380
|
doctor,
|
|
355
381
|
}
|
|
356
382
|
const directory = join(roots.dataRoot, 'diagnostics')
|
|
@@ -362,8 +388,23 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
362
388
|
|
|
363
389
|
async runtimeStart() {
|
|
364
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
|
+
}
|
|
365
405
|
return this.startOperation('runtime-start', false, [
|
|
366
406
|
'runtime-start', '--install-root', roots.installRoot, '--data-root', roots.dataRoot,
|
|
407
|
+
'--port', String(settings.port),
|
|
367
408
|
])
|
|
368
409
|
}
|
|
369
410
|
|
|
@@ -374,6 +415,35 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
374
415
|
])
|
|
375
416
|
}
|
|
376
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
|
+
|
|
377
447
|
startOperation(kind, replaceSkill, explicitArgs) {
|
|
378
448
|
if (this.operation?.state === 'running') {
|
|
379
449
|
return { success: false, accepted: false, operation: operationView(this.operation), error: 'another Foggy operation is running' }
|
|
@@ -462,6 +532,7 @@ const markerInitializers = []
|
|
|
462
532
|
for (const method of [
|
|
463
533
|
'status', 'plan', 'initialize', 'repair', 'repairPython', 'repairCli', 'repairLauncher', 'repairAnalysisSkill',
|
|
464
534
|
'migrateProfiles', 'diagnostics', 'runtimeStart', 'runtimeStop',
|
|
535
|
+
'saveRuntimeSettings',
|
|
465
536
|
]) {
|
|
466
537
|
Remote(method)(FoggyIntegrationGateway.prototype[method], {
|
|
467
538
|
kind: 'method',
|
package/lib/remote-descriptor.js
CHANGED
|
@@ -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.
|
|
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/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:
|
|
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.
|
|
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":
|
|
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
|
|
|
@@ -277,6 +342,46 @@ def command_result(command: list[str], timeout: int = 30, check: bool = False) -
|
|
|
277
342
|
return payload
|
|
278
343
|
|
|
279
344
|
|
|
345
|
+
def process_running(pid: int) -> bool:
|
|
346
|
+
"""Check liveness without spawning a shell or waiting on the target process."""
|
|
347
|
+
if pid <= 0:
|
|
348
|
+
return False
|
|
349
|
+
if os.name == "nt":
|
|
350
|
+
import ctypes
|
|
351
|
+
|
|
352
|
+
synchronize = 0x00100000
|
|
353
|
+
wait_timeout = 0x00000102
|
|
354
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
355
|
+
kernel32.OpenProcess.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_uint32]
|
|
356
|
+
kernel32.OpenProcess.restype = ctypes.c_void_p
|
|
357
|
+
kernel32.WaitForSingleObject.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
|
|
358
|
+
kernel32.WaitForSingleObject.restype = ctypes.c_uint32
|
|
359
|
+
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
|
360
|
+
handle = kernel32.OpenProcess(synchronize, False, pid)
|
|
361
|
+
if not handle:
|
|
362
|
+
return ctypes.get_last_error() == 5 # Access denied still proves the PID exists.
|
|
363
|
+
try:
|
|
364
|
+
return kernel32.WaitForSingleObject(handle, 0) == wait_timeout
|
|
365
|
+
finally:
|
|
366
|
+
kernel32.CloseHandle(handle)
|
|
367
|
+
proc_stat = Path("/proc") / str(pid) / "stat"
|
|
368
|
+
if proc_stat.is_file():
|
|
369
|
+
try:
|
|
370
|
+
fields = proc_stat.read_text(encoding="utf-8", errors="replace").rsplit(") ", 1)[1]
|
|
371
|
+
return bool(fields) and fields[0] != "Z"
|
|
372
|
+
except (OSError, IndexError):
|
|
373
|
+
pass
|
|
374
|
+
try:
|
|
375
|
+
os.kill(pid, 0)
|
|
376
|
+
except ProcessLookupError:
|
|
377
|
+
return False
|
|
378
|
+
except PermissionError:
|
|
379
|
+
return True
|
|
380
|
+
except OSError:
|
|
381
|
+
return False
|
|
382
|
+
return True
|
|
383
|
+
|
|
384
|
+
|
|
280
385
|
def command_result_with_progress(
|
|
281
386
|
command: list[str],
|
|
282
387
|
timeout: int,
|
|
@@ -289,6 +394,8 @@ def command_result_with_progress(
|
|
|
289
394
|
start_percent: int,
|
|
290
395
|
end_percent: int,
|
|
291
396
|
poll_interval: float = 1.0,
|
|
397
|
+
watched_pid: int | None = None,
|
|
398
|
+
watched_label: str = "Runtime",
|
|
292
399
|
) -> dict:
|
|
293
400
|
started = time.monotonic()
|
|
294
401
|
try:
|
|
@@ -318,6 +425,13 @@ def command_result_with_progress(
|
|
|
318
425
|
break
|
|
319
426
|
except subprocess.TimeoutExpired:
|
|
320
427
|
elapsed = time.monotonic() - started
|
|
428
|
+
if watched_pid and not process_running(watched_pid):
|
|
429
|
+
process.kill()
|
|
430
|
+
process.communicate()
|
|
431
|
+
raise OnboardingError(
|
|
432
|
+
f"{watched_label} process PID {watched_pid} exited before becoming ready "
|
|
433
|
+
f"after {round(elapsed * 1000)} ms"
|
|
434
|
+
)
|
|
321
435
|
readiness_fraction = min(elapsed / max(readiness_timeout, 1), 1.0)
|
|
322
436
|
percent = round(start_percent + (end_percent - start_percent) * readiness_fraction)
|
|
323
437
|
progress.update(
|
|
@@ -1463,6 +1577,7 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1463
1577
|
readiness_timeout=readiness_timeout,
|
|
1464
1578
|
start_percent=30,
|
|
1465
1579
|
end_percent=85,
|
|
1580
|
+
watched_pid=int(prior["pid"]),
|
|
1466
1581
|
)
|
|
1467
1582
|
wait_payload = parse_json_output(wait_result, "wait-ready")
|
|
1468
1583
|
if wait_payload.get("success") is not True:
|
|
@@ -1506,12 +1621,9 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1506
1621
|
}
|
|
1507
1622
|
existing.unlink()
|
|
1508
1623
|
progress.update("runtime-preflight", 1, "Checking port and Runtime workspace", percent=10)
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
probe.bind(("127.0.0.1", port))
|
|
1513
|
-
except OSError as exc:
|
|
1514
|
-
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)
|
|
1515
1627
|
work_dir = data_root / "runtime"
|
|
1516
1628
|
work_dir.mkdir(parents=True, exist_ok=True)
|
|
1517
1629
|
launcher_dir = normalized(state["launcher"]["path"])
|
|
@@ -1577,6 +1689,7 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
|
|
|
1577
1689
|
readiness_timeout=readiness_timeout,
|
|
1578
1690
|
start_percent=30,
|
|
1579
1691
|
end_percent=85,
|
|
1692
|
+
watched_pid=pid,
|
|
1580
1693
|
)
|
|
1581
1694
|
wait_payload = parse_json_output(wait_result, "wait-ready")
|
|
1582
1695
|
if wait_payload.get("success") is not True:
|
|
@@ -3211,7 +3324,7 @@ def main() -> None:
|
|
|
3211
3324
|
except OnboardingError as exc:
|
|
3212
3325
|
if ACTIVE_PROGRESS is not None:
|
|
3213
3326
|
ACTIVE_PROGRESS.fail(exc)
|
|
3214
|
-
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)
|
|
3215
3328
|
except Exception as exc:
|
|
3216
3329
|
if ACTIVE_PROGRESS is not None:
|
|
3217
3330
|
ACTIVE_PROGRESS.fail(exc)
|