@foro-sh/foro 0.10.0 → 0.12.1

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.
@@ -2,148 +2,316 @@
2
2
  // manifest-cases.json. Do not edit - edit the JSON and rebuild.
3
3
  export const rawManifestCases = [
4
4
  {
5
- "name": "minimal-valid",
6
- "yaml": "name: my-server\nentrypoint: server.py\n",
5
+ "name": "python-minimal",
6
+ "files": {
7
+ "pyproject.toml": "[project]\nname = \"my-server\"\n",
8
+ "server.py": ""
9
+ },
7
10
  "expect": {
8
11
  "ok": true
9
12
  }
10
13
  },
11
14
  {
12
- "name": "nested-entrypoint",
13
- "yaml": "name: my-server\nentrypoint: src/server.py\n",
15
+ "name": "python-poetry-only-name",
16
+ "files": {
17
+ "pyproject.toml": "[tool.poetry]\nname = \"my-server\"\n",
18
+ "main.py": ""
19
+ },
14
20
  "expect": {
15
21
  "ok": true
16
22
  }
17
23
  },
18
24
  {
19
- "name": "all-fields",
20
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: app\nruntime: python\nruntime_version: \"3.11\"\nport: 9000\ndependency_manager: poetry\n",
25
+ "name": "python-explicit-entrypoint",
26
+ "files": {
27
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"src/server.py\"\n"
28
+ },
21
29
  "expect": {
22
30
  "ok": true
23
31
  }
24
32
  },
25
33
  {
26
- "name": "unquoted-runtime-version",
27
- "yaml": "name: my-server\nentrypoint: server.py\nruntime_version: 3.12\n",
34
+ "name": "python-all-fields",
35
+ "files": {
36
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nbuild_path = \"app\"\nentrypoint = \"server.py\"\nruntime = \"python\"\nruntime_version = \"3.11\"\nport = 9000\ndependency_manager = \"poetry\"\n"
37
+ },
28
38
  "expect": {
29
39
  "ok": true
30
40
  }
31
41
  },
32
42
  {
33
- "name": "bad-name-uppercase",
34
- "yaml": "name: My-Server\nentrypoint: server.py\n",
43
+ "name": "python-requires-python-range",
44
+ "files": {
45
+ "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \">=3.11,<3.13\"\n",
46
+ "server.py": ""
47
+ },
48
+ "expect": {
49
+ "ok": true
50
+ }
51
+ },
52
+ {
53
+ "name": "python-requires-python-exclusion",
54
+ "files": {
55
+ "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \">=3.11,!=3.13\"\n",
56
+ "server.py": ""
57
+ },
58
+ "expect": {
59
+ "ok": true
60
+ }
61
+ },
62
+ {
63
+ "name": "python-requires-python-excludes-every-supported-version-one-by-one",
64
+ "files": {
65
+ "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \">=3.11,!=3.11,!=3.12,!=3.13\"\n",
66
+ "server.py": ""
67
+ },
35
68
  "expect": {
36
69
  "ok": false,
37
- "reason": "invalid_name"
70
+ "reason": "invalid_runtime_version"
71
+ }
72
+ },
73
+ {
74
+ "name": "python-hyphen-inside-a-path-segment-is-fine",
75
+ "files": {
76
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"my-tools/mcp-server.py\"\n"
77
+ },
78
+ "expect": {
79
+ "ok": true
38
80
  }
39
81
  },
40
82
  {
41
- "name": "bad-name-too-short",
42
- "yaml": "name: ab\nentrypoint: server.py\n",
83
+ "name": "python-scoped-unspellable-name-is-normalised",
84
+ "files": {
85
+ "pyproject.toml": "[project]\nname = \"My_Server\"\n",
86
+ "server.py": ""
87
+ },
88
+ "expect": {
89
+ "ok": true
90
+ }
91
+ },
92
+ {
93
+ "name": "python-no-entry-file",
94
+ "files": {
95
+ "pyproject.toml": "[project]\nname = \"my-server\"\n"
96
+ },
43
97
  "expect": {
44
98
  "ok": false,
45
- "reason": "invalid_name"
99
+ "reason": "invalid_entrypoint"
46
100
  }
47
101
  },
48
102
  {
49
- "name": "missing-name",
50
- "yaml": "entrypoint: server.py\n",
103
+ "name": "python-entrypoint-traversal",
104
+ "files": {
105
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"../server.py\"\n"
106
+ },
51
107
  "expect": {
52
108
  "ok": false,
53
- "reason": "invalid_name"
109
+ "reason": "invalid_entrypoint"
54
110
  }
55
111
  },
56
112
  {
57
- "name": "bad-entrypoint-traversal",
58
- "yaml": "name: my-server\nentrypoint: ../server.py\n",
113
+ "name": "python-entrypoint-absolute",
114
+ "files": {
115
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"/server.py\"\n"
116
+ },
59
117
  "expect": {
60
118
  "ok": false,
61
119
  "reason": "invalid_entrypoint"
62
120
  }
63
121
  },
64
122
  {
65
- "name": "bad-entrypoint-absolute",
66
- "yaml": "name: my-server\nentrypoint: /server.py\n",
123
+ "name": "python-entrypoint-shell-metachar",
124
+ "files": {
125
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py; rm -rf x\"\n"
126
+ },
67
127
  "expect": {
68
128
  "ok": false,
69
129
  "reason": "invalid_entrypoint"
70
130
  }
71
131
  },
72
132
  {
73
- "name": "bad-entrypoint-shell-metachar",
74
- "yaml": "name: my-server\nentrypoint: \"server.py; rm -rf x\"\n",
133
+ "name": "python-entrypoint-leading-dash",
134
+ "files": {
135
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"--isolated\"\n"
136
+ },
75
137
  "expect": {
76
138
  "ok": false,
77
139
  "reason": "invalid_entrypoint"
78
140
  }
79
141
  },
80
142
  {
81
- "name": "missing-entrypoint",
82
- "yaml": "name: my-server\n",
143
+ "name": "python-entrypoint-leading-dash-nested",
144
+ "files": {
145
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"src/-rf.py\"\n"
146
+ },
83
147
  "expect": {
84
148
  "ok": false,
85
149
  "reason": "invalid_entrypoint"
86
150
  }
87
151
  },
88
152
  {
89
- "name": "bad-build-path-traversal",
90
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: ../x\n",
153
+ "name": "python-build-path-traversal",
154
+ "files": {
155
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"../x\"\n"
156
+ },
157
+ "expect": {
158
+ "ok": false,
159
+ "reason": "invalid_build_path"
160
+ }
161
+ },
162
+ {
163
+ "name": "python-build-path-shell-metachar",
164
+ "files": {
165
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \". && whoami\"\n"
166
+ },
91
167
  "expect": {
92
168
  "ok": false,
93
169
  "reason": "invalid_build_path"
94
170
  }
95
171
  },
96
172
  {
97
- "name": "bad-build-path-shell-metachar",
98
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \". && whoami\"\n",
173
+ "name": "python-build-path-space",
174
+ "files": {
175
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"my services\"\n"
176
+ },
99
177
  "expect": {
100
178
  "ok": false,
101
179
  "reason": "invalid_build_path"
102
180
  }
103
181
  },
104
182
  {
105
- "name": "bad-build-path-space",
106
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \"my services\"\n",
183
+ "name": "python-build-path-newline",
184
+ "files": {
185
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"app\\nUSER root\"\n"
186
+ },
107
187
  "expect": {
108
188
  "ok": false,
109
189
  "reason": "invalid_build_path"
110
190
  }
111
191
  },
112
192
  {
113
- "name": "bad-build-path-newline",
114
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \"app\\nUSER root\"\n",
193
+ "name": "python-build-path-leading-dash",
194
+ "files": {
195
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"-rf\"\n"
196
+ },
115
197
  "expect": {
116
198
  "ok": false,
117
199
  "reason": "invalid_build_path"
118
200
  }
119
201
  },
120
202
  {
121
- "name": "bad-runtime-version",
122
- "yaml": "name: my-server\nentrypoint: server.py\nruntime_version: \"3.9\"\n",
203
+ "name": "python-runtime-version-outside-allowlist",
204
+ "files": {
205
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime_version = \"3.9\"\n"
206
+ },
123
207
  "expect": {
124
208
  "ok": false,
125
209
  "reason": "invalid_runtime_version"
126
210
  }
127
211
  },
128
212
  {
129
- "name": "bad-runtime",
130
- "yaml": "name: my-server\nentrypoint: server.py\nruntime: node\n",
213
+ "name": "python-requires-python-excludes-every-supported-version",
214
+ "files": {
215
+ "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \"==3.9\"\n",
216
+ "server.py": ""
217
+ },
131
218
  "expect": {
132
219
  "ok": false,
133
- "reason": "invalid_runtime"
220
+ "reason": "invalid_runtime_version"
134
221
  }
135
222
  },
136
223
  {
137
- "name": "runtime-version-from-another-runtime",
138
- "yaml": "name: my-server\nentrypoint: server.py\nruntime: python\nruntime_version: \"22\"\n",
224
+ "name": "python-runtime-version-from-another-runtime",
225
+ "files": {
226
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime_version = \"22\"\n"
227
+ },
139
228
  "expect": {
140
229
  "ok": false,
141
230
  "reason": "invalid_runtime_version"
142
231
  }
143
232
  },
233
+ {
234
+ "name": "runtime-outside-allowlist",
235
+ "files": {
236
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime = \"ruby\"\n"
237
+ },
238
+ "expect": {
239
+ "ok": false,
240
+ "reason": "invalid_runtime"
241
+ }
242
+ },
243
+ {
244
+ "name": "runtime-disagrees-with-the-file-it-was-read-from",
245
+ "files": {
246
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime = \"node\"\n"
247
+ },
248
+ "expect": {
249
+ "ok": false,
250
+ "reason": "invalid_runtime"
251
+ }
252
+ },
253
+ {
254
+ "name": "both-config-files-and-neither-claims-the-runtime",
255
+ "files": {
256
+ "pyproject.toml": "[project]\nname = \"my-server\"\n",
257
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\"}",
258
+ "server.py": ""
259
+ },
260
+ "expect": {
261
+ "ok": false,
262
+ "reason": "invalid_runtime"
263
+ }
264
+ },
265
+ {
266
+ "name": "both-config-files-and-both-claim-the-runtime",
267
+ "files": {
268
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nruntime = \"python\"\n",
269
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"runtime\": \"node\"}}",
270
+ "server.py": ""
271
+ },
272
+ "expect": {
273
+ "ok": false,
274
+ "reason": "invalid_runtime"
275
+ }
276
+ },
277
+ {
278
+ "name": "both-config-files-python-claims-the-runtime",
279
+ "files": {
280
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nruntime = \"python\"\n",
281
+ "package.json": "{\"name\": \"tooling-only\"}",
282
+ "server.py": ""
283
+ },
284
+ "expect": {
285
+ "ok": true
286
+ }
287
+ },
288
+ {
289
+ "name": "both-config-files-node-claims-the-runtime",
290
+ "files": {
291
+ "pyproject.toml": "[project]\nname = \"my-server\"\n",
292
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"runtime\": \"node\"}}",
293
+ "server.py": ""
294
+ },
295
+ "expect": {
296
+ "ok": true
297
+ }
298
+ },
299
+ {
300
+ "name": "no-config-file-only-a-legacy-manifest",
301
+ "files": {
302
+ "foro.yaml": "name: my-server\nentrypoint: server.py\n",
303
+ "server.py": ""
304
+ },
305
+ "expect": {
306
+ "ok": false,
307
+ "reason": "missing_manifest"
308
+ }
309
+ },
144
310
  {
145
311
  "name": "unknown-field-stale-python-version",
146
- "yaml": "name: my-server\nentrypoint: server.py\npython_version: \"3.13\"\n",
312
+ "files": {
313
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\npython_version = \"3.13\"\n"
314
+ },
147
315
  "expect": {
148
316
  "ok": false,
149
317
  "reason": "unknown_field"
@@ -151,103 +319,477 @@ export const rawManifestCases = [
151
319
  },
152
320
  {
153
321
  "name": "unknown-field-typo",
154
- "yaml": "name: my-server\nentrypoint: server.py\nentrypoints: server.py\n",
322
+ "files": {
323
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nentrypoints = \"server.py\"\n"
324
+ },
155
325
  "expect": {
156
326
  "ok": false,
157
327
  "reason": "unknown_field"
158
328
  }
159
329
  },
160
330
  {
161
- "name": "bad-port-too-low",
162
- "yaml": "name: my-server\nentrypoint: server.py\nport: 80\n",
331
+ "name": "unknown-field-name-belongs-to-the-package",
332
+ "files": {
333
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nname = \"other\"\n"
334
+ },
335
+ "expect": {
336
+ "ok": false,
337
+ "reason": "unknown_field"
338
+ }
339
+ },
340
+ {
341
+ "name": "port-too-low",
342
+ "files": {
343
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 80\n"
344
+ },
345
+ "expect": {
346
+ "ok": false,
347
+ "reason": "invalid_port"
348
+ }
349
+ },
350
+ {
351
+ "name": "port-too-high",
352
+ "files": {
353
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 70000\n"
354
+ },
163
355
  "expect": {
164
356
  "ok": false,
165
357
  "reason": "invalid_port"
166
358
  }
167
359
  },
168
360
  {
169
- "name": "bad-port-too-high",
170
- "yaml": "name: my-server\nentrypoint: server.py\nport: 70000\n",
361
+ "name": "port-sidecar-reserved",
362
+ "files": {
363
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 8001\n"
364
+ },
171
365
  "expect": {
172
366
  "ok": false,
173
367
  "reason": "invalid_port"
174
368
  }
175
369
  },
176
370
  {
177
- "name": "bad-port-sidecar-reserved",
178
- "yaml": "name: my-server\nentrypoint: server.py\nport: 8001\n",
371
+ "name": "port-gate-reserved",
372
+ "files": {
373
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 8002\n"
374
+ },
179
375
  "expect": {
180
376
  "ok": false,
181
377
  "reason": "invalid_port"
182
378
  }
183
379
  },
184
380
  {
185
- "name": "bad-port-non-integer",
186
- "yaml": "name: my-server\nentrypoint: server.py\nport: \"abc\"\n",
381
+ "name": "port-non-integer",
382
+ "files": {
383
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = \"abc\"\n"
384
+ },
187
385
  "expect": {
188
386
  "ok": false,
189
387
  "reason": "invalid_port"
190
388
  }
191
389
  },
192
390
  {
193
- "name": "bad-dependency-manager",
194
- "yaml": "name: my-server\nentrypoint: server.py\ndependency_manager: pipx\n",
391
+ "name": "dependency-manager-outside-allowlist",
392
+ "files": {
393
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\ndependency_manager = \"pipx\"\n"
394
+ },
195
395
  "expect": {
196
396
  "ok": false,
197
397
  "reason": "invalid_dependency_manager"
198
398
  }
199
399
  },
200
400
  {
201
- "name": "invalid-yaml",
202
- "yaml": "name: [unclosed\n",
401
+ "name": "node-dependency-manager-on-python",
402
+ "files": {
403
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\ndependency_manager = \"npm\"\n"
404
+ },
203
405
  "expect": {
204
406
  "ok": false,
205
- "reason": "invalid_yaml"
407
+ "reason": "invalid_dependency_manager"
206
408
  }
207
409
  },
208
410
  {
209
- "name": "invalid-shape-scalar",
210
- "yaml": "just a string\n",
411
+ "name": "unparseable-pyproject",
412
+ "files": {
413
+ "pyproject.toml": "[project\nname = \"my-server\"\n"
414
+ },
211
415
  "expect": {
212
416
  "ok": false,
213
417
  "reason": "invalid_shape"
214
418
  }
215
419
  },
216
420
  {
217
- "name": "invalid-shape-list",
218
- "yaml": "- one\n- two\n",
421
+ "name": "node-minimal",
422
+ "files": {
423
+ "package.json": "{\"name\": \"my-server\", \"main\": \"dist/index.js\"}"
424
+ },
425
+ "expect": {
426
+ "ok": true
427
+ }
428
+ },
429
+ {
430
+ "name": "node-scoped-name",
431
+ "files": {
432
+ "package.json": "{\"name\": \"@acme/my-server\", \"main\": \"dist/index.js\"}"
433
+ },
434
+ "expect": {
435
+ "ok": true
436
+ }
437
+ },
438
+ {
439
+ "name": "node-bin-map",
440
+ "files": {
441
+ "package.json": "{\"name\": \"my-server\", \"bin\": {\"my-server\": \"dist/cli.js\"}}"
442
+ },
443
+ "expect": {
444
+ "ok": true
445
+ }
446
+ },
447
+ {
448
+ "name": "node-implicit-index",
449
+ "files": {
450
+ "package.json": "{\"name\": \"my-server\"}",
451
+ "index.js": ""
452
+ },
453
+ "expect": {
454
+ "ok": true
455
+ }
456
+ },
457
+ {
458
+ "name": "node-engines-range",
459
+ "files": {
460
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \">=22\"}}"
461
+ },
462
+ "expect": {
463
+ "ok": true
464
+ }
465
+ },
466
+ {
467
+ "name": "node-engines-union-of-every-supported-major",
468
+ "files": {
469
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \"^22 || ^24\"}}"
470
+ },
471
+ "expect": {
472
+ "ok": true
473
+ }
474
+ },
475
+ {
476
+ "name": "node-engines-union-with-one-unsupported-branch",
477
+ "files": {
478
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \"^20 || ^22\"}}"
479
+ },
480
+ "expect": {
481
+ "ok": true
482
+ }
483
+ },
484
+ {
485
+ "name": "node-all-fields",
486
+ "files": {
487
+ "package.json": "{\"name\": \"my-server\", \"main\": \"dist/index.js\", \"foro\": {\"runtime\": \"node\", \"runtime_version\": \"22\", \"port\": 9000, \"dependency_manager\": \"pnpm\"}}"
488
+ },
489
+ "expect": {
490
+ "ok": true
491
+ }
492
+ },
493
+ {
494
+ "name": "node-no-entry-file",
495
+ "files": {
496
+ "package.json": "{\"name\": \"my-server\"}"
497
+ },
498
+ "expect": {
499
+ "ok": false,
500
+ "reason": "invalid_entrypoint"
501
+ }
502
+ },
503
+ {
504
+ "name": "node-engines-excludes-every-supported-version",
505
+ "files": {
506
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \"^20\"}}"
507
+ },
508
+ "expect": {
509
+ "ok": false,
510
+ "reason": "invalid_runtime_version"
511
+ }
512
+ },
513
+ {
514
+ "name": "node-runtime-version-from-another-runtime",
515
+ "files": {
516
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"runtime_version\": \"3.12\"}}"
517
+ },
518
+ "expect": {
519
+ "ok": false,
520
+ "reason": "invalid_runtime_version"
521
+ }
522
+ },
523
+ {
524
+ "name": "python-dependency-manager-on-node",
525
+ "files": {
526
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"dependency_manager\": \"uv\"}}"
527
+ },
528
+ "expect": {
529
+ "ok": false,
530
+ "reason": "invalid_dependency_manager"
531
+ }
532
+ },
533
+ {
534
+ "name": "unparseable-package-json",
535
+ "files": {
536
+ "package.json": "{\"name\": "
537
+ },
219
538
  "expect": {
220
539
  "ok": false,
221
540
  "reason": "invalid_shape"
222
541
  }
223
542
  },
224
543
  {
225
- "name": "bad-entrypoint-leading-dash",
226
- "yaml": "name: my-server\nentrypoint: --isolated\n",
544
+ "name": "python-egress-ip-entry",
545
+ "files": {
546
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"10.20.30.40:5432\"]\n"
547
+ },
548
+ "expect": {
549
+ "ok": true
550
+ }
551
+ },
552
+ {
553
+ "name": "python-egress-cidr-entry",
554
+ "files": {
555
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"10.20.0.0/16:443\"]\n"
556
+ },
557
+ "expect": {
558
+ "ok": true
559
+ }
560
+ },
561
+ {
562
+ "name": "python-egress-hostname-entry",
563
+ "files": {
564
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"sql.database.windows.net:1433\"]\n"
565
+ },
566
+ "expect": {
567
+ "ok": true
568
+ }
569
+ },
570
+ {
571
+ "name": "python-egress-multiple-entries",
572
+ "files": {
573
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"sql.database.windows.net:1433\", \"10.20.30.40:5432\", \"10.20.0.0/16:443\"]\n"
574
+ },
575
+ "expect": {
576
+ "ok": true
577
+ }
578
+ },
579
+ {
580
+ "name": "python-egress-empty-list-denies-everything",
581
+ "files": {
582
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = []\n"
583
+ },
584
+ "expect": {
585
+ "ok": true
586
+ }
587
+ },
588
+ {
589
+ "name": "python-egress-missing-port",
590
+ "files": {
591
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"example.com\"]\n"
592
+ },
227
593
  "expect": {
228
594
  "ok": false,
229
- "reason": "invalid_entrypoint"
595
+ "reason": "invalid_egress"
230
596
  }
231
597
  },
232
598
  {
233
- "name": "bad-entrypoint-leading-dash-nested",
234
- "yaml": "name: my-server\nentrypoint: src/-rf.py\n",
599
+ "name": "python-egress-port-zero",
600
+ "files": {
601
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"example.com:0\"]\n"
602
+ },
235
603
  "expect": {
236
604
  "ok": false,
237
- "reason": "invalid_entrypoint"
605
+ "reason": "invalid_egress"
238
606
  }
239
607
  },
240
608
  {
241
- "name": "bad-build-path-leading-dash",
242
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: -rf\n",
609
+ "name": "python-egress-port-too-high",
610
+ "files": {
611
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"example.com:65536\"]\n"
612
+ },
243
613
  "expect": {
244
614
  "ok": false,
245
- "reason": "invalid_build_path"
615
+ "reason": "invalid_egress"
616
+ }
617
+ },
618
+ {
619
+ "name": "python-egress-blocked-mail-port",
620
+ "files": {
621
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"smtp.example.com:25\"]\n"
622
+ },
623
+ "expect": {
624
+ "ok": false,
625
+ "reason": "invalid_egress"
626
+ }
627
+ },
628
+ {
629
+ "name": "python-egress-metadata-endpoint",
630
+ "files": {
631
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"169.254.169.254:80\"]\n"
632
+ },
633
+ "expect": {
634
+ "ok": false,
635
+ "reason": "invalid_egress"
636
+ }
637
+ },
638
+ {
639
+ "name": "python-egress-whole-internet",
640
+ "files": {
641
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"0.0.0.0/0:443\"]\n"
642
+ },
643
+ "expect": {
644
+ "ok": false,
645
+ "reason": "invalid_egress"
646
+ }
647
+ },
648
+ {
649
+ "name": "python-egress-wide-range-swallows-a-reserved-one",
650
+ "files": {
651
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"172.16.0.0/12:443\"]\n"
652
+ },
653
+ "expect": {
654
+ "ok": false,
655
+ "reason": "invalid_egress"
656
+ }
657
+ },
658
+ {
659
+ "name": "python-egress-hostname-over-253-chars",
660
+ "files": {
661
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa.aaaa:443\"]\n"
662
+ },
663
+ "expect": {
664
+ "ok": false,
665
+ "reason": "invalid_egress"
666
+ }
667
+ },
668
+ {
669
+ "name": "python-egress-string-instead-of-list",
670
+ "files": {
671
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = \"example.com:443\"\n"
672
+ },
673
+ "expect": {
674
+ "ok": false,
675
+ "reason": "invalid_egress"
676
+ }
677
+ },
678
+ {
679
+ "name": "python-egress-over-entry-cap",
680
+ "files": {
681
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"h0.example.com:443\", \"h1.example.com:443\", \"h2.example.com:443\", \"h3.example.com:443\", \"h4.example.com:443\", \"h5.example.com:443\", \"h6.example.com:443\", \"h7.example.com:443\", \"h8.example.com:443\", \"h9.example.com:443\", \"h10.example.com:443\", \"h11.example.com:443\", \"h12.example.com:443\", \"h13.example.com:443\", \"h14.example.com:443\", \"h15.example.com:443\", \"h16.example.com:443\", \"h17.example.com:443\", \"h18.example.com:443\", \"h19.example.com:443\", \"h20.example.com:443\"]\n"
682
+ },
683
+ "expect": {
684
+ "ok": false,
685
+ "reason": "invalid_egress"
686
+ }
687
+ },
688
+ {
689
+ "name": "node-egress-non-string-entry",
690
+ "files": {
691
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"egress\": [443]}}"
692
+ },
693
+ "expect": {
694
+ "ok": false,
695
+ "reason": "invalid_egress"
696
+ }
697
+ },
698
+ {
699
+ "name": "python-egress-shell-metacharacter",
700
+ "files": {
701
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"evil.com:443; rm -rf /\"]\n"
702
+ },
703
+ "expect": {
704
+ "ok": false,
705
+ "reason": "invalid_egress"
706
+ }
707
+ },
708
+ {
709
+ "name": "python-egress-embedded-whitespace",
710
+ "files": {
711
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"evil .com:443\"]\n"
712
+ },
713
+ "expect": {
714
+ "ok": false,
715
+ "reason": "invalid_egress"
716
+ }
717
+ },
718
+ {
719
+ "name": "python-egress-embedded-newline",
720
+ "files": {
721
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"evil.com:443\\nother:80\"]\n"
722
+ },
723
+ "expect": {
724
+ "ok": false,
725
+ "reason": "invalid_egress"
726
+ }
727
+ },
728
+ {
729
+ "name": "python-egress-ipv6-literal",
730
+ "files": {
731
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"::1:443\"]\n"
732
+ },
733
+ "expect": {
734
+ "ok": false,
735
+ "reason": "invalid_egress"
736
+ }
737
+ },
738
+ {
739
+ "name": "python-egress-cidr-prefix-out-of-range",
740
+ "files": {
741
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"10.0.0.0/33:443\"]\n"
742
+ },
743
+ "expect": {
744
+ "ok": false,
745
+ "reason": "invalid_egress"
746
+ }
747
+ },
748
+ {
749
+ "name": "python-egress-link-local-range",
750
+ "files": {
751
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"169.254.0.0/16:443\"]\n"
752
+ },
753
+ "expect": {
754
+ "ok": false,
755
+ "reason": "invalid_egress"
756
+ }
757
+ },
758
+ {
759
+ "name": "python-egress-loopback",
760
+ "files": {
761
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"127.0.0.1:5432\"]\n"
762
+ },
763
+ "expect": {
764
+ "ok": false,
765
+ "reason": "invalid_egress"
766
+ }
767
+ },
768
+ {
769
+ "name": "python-egress-platform-bridge-network",
770
+ "files": {
771
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"172.20.0.5:5432\"]\n"
772
+ },
773
+ "expect": {
774
+ "ok": false,
775
+ "reason": "invalid_egress"
776
+ }
777
+ },
778
+ {
779
+ "name": "python-egress-submission-port",
780
+ "files": {
781
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"smtp.example.com:587\"]\n"
782
+ },
783
+ "expect": {
784
+ "ok": false,
785
+ "reason": "invalid_egress"
246
786
  }
247
787
  },
248
788
  {
249
- "name": "hyphen-inside-a-path-segment-is-fine",
250
- "yaml": "name: my-server\nentrypoint: my-tools/mcp-server.py\n",
789
+ "name": "python-egress-private-ranges-stay-allowlistable",
790
+ "files": {
791
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\negress = [\"10.20.30.40:5432\", \"192.168.1.0/24:443\"]\n"
792
+ },
251
793
  "expect": {
252
794
  "ok": true
253
795
  }