Skip to content

Commit 172ddb2

Browse files
committed
fix: harden runtime boundaries and isolate workflows
1 parent d9e66e2 commit 172ddb2

22 files changed

Lines changed: 580 additions & 180 deletions

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ scripts/start.sh # macOS / Linux
2929
scripts\start.ps1 # Windows
3030
```
3131

32+
Optional runtime environment variables:
33+
34+
```
35+
PYTC_AUTH_SECRET=replace-me
36+
PYTC_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,null
37+
```
38+
3239
If restarting after a crash or interrupted session, kill any lingering processes first:
3340

3441
```

client/main.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ function createWindow() {
1919
height,
2020
icon: path.join(__dirname, "public", "favicon.ico"),
2121
webPreferences: {
22-
nodeIntegration: true,
23-
contextIsolation: false,
24-
webSecurity: false,
25-
allowRunningInsecureContent: true,
22+
preload: path.join(__dirname, "preload.js"),
23+
nodeIntegration: false,
24+
contextIsolation: true,
25+
sandbox: true,
26+
webSecurity: true,
27+
allowRunningInsecureContent: false,
2628
},
2729
});
2830

client/preload.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const { contextBridge, ipcRenderer } = require("electron");
2+
3+
function subscribe(channel) {
4+
return (listener) => {
5+
if (typeof listener !== "function") {
6+
return () => {};
7+
}
8+
9+
const wrappedListener = (_event, ...args) => listener(...args);
10+
ipcRenderer.on(channel, wrappedListener);
11+
12+
return () => {
13+
ipcRenderer.removeListener(channel, wrappedListener);
14+
};
15+
};
16+
}
17+
18+
contextBridge.exposeInMainWorld("electronAPI", {
19+
isElectron: true,
20+
openLocalFile: (options = {}) => ipcRenderer.invoke("open-local-file", options),
21+
revealInFinder: (targetPath) =>
22+
ipcRenderer.invoke("reveal-in-finder", targetPath),
23+
onToggleTab: subscribe("toggle-tab"),
24+
onChangeViews: subscribe("change-views"),
25+
});

client/src/components/Configurator.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { AppContext } from "../contexts/GlobalContext";
99
function Configurator(props) {
1010
const { fileList, type } = props;
1111
const context = useContext(AppContext);
12+
const workflow =
13+
type === "training" ? context.trainingState : context.inferenceState;
1214
const [current, setCurrent] = useState(0);
1315
const [hasAttemptedAdvance, setHasAttemptedAdvance] = useState(false);
1416
const storageKey = `configStep:${type}`;
@@ -33,11 +35,6 @@ function Configurator(props) {
3335
}
3436
const label = type === "training" ? "Training" : "Inference";
3537
message.success(`${label} configuration saved.`);
36-
if (type === "training") {
37-
localStorage.setItem("trainingConfig", context.trainingConfig);
38-
} else {
39-
localStorage.setItem("inferenceConfig", context.inferenceConfig);
40-
}
4138
};
4239

4340
const getPathValue = (val) => {
@@ -48,18 +45,20 @@ function Configurator(props) {
4845

4946
const missingInputs = useMemo(() => {
5047
const missing = [];
51-
if (!getPathValue(context.inputImage)) missing.push("input image");
52-
if (!getPathValue(context.inputLabel)) missing.push("input label");
53-
if (!getPathValue(context.outputPath)) missing.push("output path");
54-
if (type === "inference" && !getPathValue(context.checkpointPath)) {
48+
if (!getPathValue(workflow.inputImage)) missing.push("input image");
49+
if (type === "training" && !getPathValue(workflow.inputLabel)) {
50+
missing.push("input label");
51+
}
52+
if (!getPathValue(workflow.outputPath)) missing.push("output path");
53+
if (type === "inference" && !getPathValue(workflow.checkpointPath)) {
5554
missing.push("checkpoint path");
5655
}
5756
return missing;
5857
}, [
59-
context.inputImage,
60-
context.inputLabel,
61-
context.outputPath,
62-
context.checkpointPath,
58+
workflow.inputImage,
59+
workflow.inputLabel,
60+
workflow.outputPath,
61+
workflow.checkpointPath,
6362
type,
6463
]);
6564

client/src/components/InputSelector.js

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import InlineHelpChat from "./InlineHelpChat";
77
function InputSelector(props) {
88
const context = useContext(AppContext);
99
const { type } = props;
10+
const workflow =
11+
type === "training" ? context.trainingState : context.inferenceState;
1012

1113
const projectContext =
1214
"Biomedical image segmentation using PyTorch Connectomics.";
@@ -16,25 +18,25 @@ function InputSelector(props) {
1618
: "Model inference configuration — Step 1: Set Inputs.";
1719

1820
const handleLogPathChange = (value) => {
19-
context.setLogPath(value);
21+
workflow.setLogPath(value);
2022
};
2123

2224
const handleOutputPathChange = (value) => {
23-
context.setOutputPath(value);
25+
workflow.setOutputPath(value);
2426
};
2527

2628
const handleCheckpointPathChange = (value) => {
27-
context.setCheckpointPath(value);
29+
workflow.setCheckpointPath(value);
2830
};
2931

3032
const handleImageChange = (value) => {
3133
console.log(`selected image:`, value);
32-
context.setInputImage(value);
34+
workflow.setInputImage(value);
3335
};
3436

3537
const handleLabelChange = (value) => {
3638
console.log(`selected label:`, value);
37-
context.setInputLabel(value);
39+
workflow.setInputLabel(value);
3840
};
3941

4042
// Helper to get value for UnifiedFileInput (can be object or string)
@@ -61,7 +63,7 @@ function InputSelector(props) {
6163
taskKey={type}
6264
label="Input Image"
6365
yamlKey="DATASET.INPUT_PATH"
64-
value={context.inputImage}
66+
value={workflow.inputImage}
6567
projectContext={projectContext}
6668
taskContext={taskContext}
6769
/>
@@ -71,7 +73,7 @@ function InputSelector(props) {
7173
<UnifiedFileInput
7274
placeholder="Please select or input image path"
7375
onChange={handleImageChange}
74-
value={getValue(context.inputImage)}
76+
value={getValue(workflow.inputImage)}
7577
selectionType={
7678
type === "training" || type === "inference"
7779
? "fileOrDirectory"
@@ -82,12 +84,14 @@ function InputSelector(props) {
8284
<Form.Item
8385
label={
8486
<Space align="center">
85-
<span>Input Label</span>
87+
<span>
88+
{type === "training" ? "Input Label" : "Input Label (Optional)"}
89+
</span>
8690
<InlineHelpChat
8791
taskKey={type}
8892
label="Input Label"
8993
yamlKey="DATASET.LABEL_NAME"
90-
value={context.inputLabel}
94+
value={workflow.inputLabel}
9195
projectContext={projectContext}
9296
taskContext={taskContext}
9397
/>
@@ -97,7 +101,7 @@ function InputSelector(props) {
97101
<UnifiedFileInput
98102
placeholder="Please select or input label path"
99103
onChange={handleLabelChange}
100-
value={getValue(context.inputLabel)}
104+
value={getValue(workflow.inputLabel)}
101105
selectionType={
102106
type === "training" || type === "inference"
103107
? "fileOrDirectory"
@@ -114,7 +118,7 @@ function InputSelector(props) {
114118
taskKey={type}
115119
label="Output Path"
116120
yamlKey="DATASET.OUTPUT_PATH"
117-
value={context.outputPath}
121+
value={workflow.outputPath}
118122
projectContext={projectContext}
119123
taskContext={taskContext}
120124
/>
@@ -123,7 +127,7 @@ function InputSelector(props) {
123127
>
124128
<UnifiedFileInput
125129
placeholder="Directory for outputs (e.g., /path/to/outputs/)"
126-
value={context.outputPath || ""}
130+
value={workflow.outputPath || ""}
127131
onChange={handleOutputPathChange}
128132
selectionType="directory"
129133
/>
@@ -137,7 +141,7 @@ function InputSelector(props) {
137141
taskKey={type}
138142
label="Output Path"
139143
yamlKey="INFERENCE.OUTPUT_PATH"
140-
value={context.outputPath}
144+
value={workflow.outputPath}
141145
projectContext={projectContext}
142146
taskContext={taskContext}
143147
/>
@@ -147,7 +151,7 @@ function InputSelector(props) {
147151
>
148152
<UnifiedFileInput
149153
placeholder="Directory for results (e.g., /path/to/inference_output/)"
150-
value={context.outputPath || ""}
154+
value={workflow.outputPath || ""}
151155
onChange={handleOutputPathChange}
152156
selectionType="directory"
153157
/>
@@ -162,7 +166,7 @@ function InputSelector(props) {
162166
taskKey={type}
163167
label="Log Path"
164168
yamlKey="SOLVER.LOG_DIR"
165-
value={context.logPath}
169+
value={workflow.logPath}
166170
projectContext={projectContext}
167171
taskContext={taskContext}
168172
/>
@@ -171,7 +175,7 @@ function InputSelector(props) {
171175
>
172176
<UnifiedFileInput
173177
placeholder="Please type training log path"
174-
value={context.logPath || ""}
178+
value={workflow.logPath || ""}
175179
onChange={handleLogPathChange}
176180
selectionType="directory"
177181
/>
@@ -185,7 +189,7 @@ function InputSelector(props) {
185189
taskKey={type}
186190
label="Checkpoint Path"
187191
yamlKey="MODEL.PRE_MODEL"
188-
value={context.checkpointPath}
192+
value={workflow.checkpointPath}
189193
projectContext={projectContext}
190194
taskContext={taskContext}
191195
/>
@@ -195,7 +199,7 @@ function InputSelector(props) {
195199
>
196200
<UnifiedFileInput
197201
placeholder="Model checkpoint file (e.g., /path/to/checkpoint_00010.pth.tar)"
198-
value={context.checkpointPath || ""}
202+
value={workflow.checkpointPath || ""}
199203
onChange={handleCheckpointPathChange}
200204
selectionType="file"
201205
/>

client/src/components/YamlFileEditor.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ const CONTROL_SECTIONS = {
275275

276276
const YamlFileEditor = (props) => {
277277
const context = useContext(AppContext);
278+
const workflow =
279+
props.type === "training" ? context.trainingState : context.inferenceState;
278280
const [yamlContent, setYamlContent] = useState("");
279281
const [showRaw, setShowRaw] = useState(false);
280282

@@ -333,14 +335,15 @@ const YamlFileEditor = (props) => {
333335
setYamlContent(context.inferenceConfig || "");
334336
}
335337
}, [
336-
context.uploadedYamlFile,
338+
workflow.uploadedYamlFile,
339+
workflow.selectedYamlPreset,
337340
context.trainingConfig,
338341
context.inferenceConfig,
339342
type,
340343
]);
341344

342345
const displayName =
343-
context.uploadedYamlFile?.name || context.selectedYamlPreset;
346+
workflow.uploadedYamlFile?.name || workflow.selectedYamlPreset;
344347

345348
const renderControl = (control) => {
346349
const value = getYamlValue(yamlData, control.path);

0 commit comments

Comments
 (0)