@interf/compiler 0.9.5 → 0.13.0
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 +96 -92
- package/TRADEMARKS.md +2 -13
- package/agent-skills/interf-actions/SKILL.md +95 -36
- package/agent-skills/interf-actions/references/cli.md +118 -51
- package/builtin-methods/interf-default/README.md +3 -4
- package/builtin-methods/interf-default/compile/stages/shape/SKILL.md +2 -2
- package/builtin-methods/interf-default/compile/stages/summarize/SKILL.md +2 -1
- package/builtin-methods/interf-default/improve/SKILL.md +1 -1
- package/builtin-methods/interf-default/method.json +10 -4
- package/builtin-methods/interf-default/method.schema.json +0 -9
- package/builtin-methods/interf-default/use/query/SKILL.md +5 -5
- package/dist/cli/commands/compile.d.ts +8 -25
- package/dist/cli/commands/compile.js +75 -360
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/login.d.ts +7 -0
- package/dist/cli/commands/login.js +39 -0
- package/dist/cli/commands/logout.d.ts +2 -0
- package/dist/cli/commands/logout.js +16 -0
- package/dist/cli/commands/method.d.ts +2 -0
- package/dist/cli/commands/method.js +113 -0
- package/dist/cli/commands/prep.d.ts +2 -0
- package/dist/cli/commands/prep.js +134 -0
- package/dist/cli/commands/reset.d.ts +8 -1
- package/dist/cli/commands/reset.js +47 -26
- package/dist/cli/commands/runs.d.ts +2 -0
- package/dist/cli/commands/runs.js +120 -0
- package/dist/cli/commands/status.d.ts +6 -1
- package/dist/cli/commands/status.js +68 -111
- package/dist/cli/commands/test.d.ts +6 -14
- package/dist/cli/commands/test.js +65 -181
- package/dist/cli/commands/web.d.ts +0 -9
- package/dist/cli/commands/web.js +147 -120
- package/dist/cli/commands/wizard.d.ts +9 -0
- package/dist/cli/commands/wizard.js +442 -0
- package/dist/cli/index.d.ts +7 -6
- package/dist/cli/index.js +13 -10
- package/dist/compiler-ui/404.html +1 -1
- package/dist/compiler-ui/__next.__PAGE__.txt +2 -2
- package/dist/compiler-ui/__next._full.txt +3 -3
- package/dist/compiler-ui/__next._head.txt +1 -1
- package/dist/compiler-ui/__next._index.txt +2 -2
- package/dist/compiler-ui/__next._tree.txt +2 -2
- package/dist/compiler-ui/_next/static/chunks/{18a8f2jkv3z.c.css → 045gole2ojo3g.css} +1 -1
- package/dist/compiler-ui/_next/static/chunks/{177mvn4rse235.js → 17t-lulmyawg5.js} +9 -9
- package/dist/compiler-ui/_not-found/__next._full.txt +2 -2
- package/dist/compiler-ui/_not-found/__next._head.txt +1 -1
- package/dist/compiler-ui/_not-found/__next._index.txt +2 -2
- package/dist/compiler-ui/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/dist/compiler-ui/_not-found/__next._not-found.txt +1 -1
- package/dist/compiler-ui/_not-found/__next._tree.txt +2 -2
- package/dist/compiler-ui/_not-found.html +1 -1
- package/dist/compiler-ui/_not-found.txt +2 -2
- package/dist/compiler-ui/index.html +1 -1
- package/dist/compiler-ui/index.txt +3 -3
- package/dist/packages/agents/lib/shells.d.ts +1 -1
- package/dist/packages/agents/lib/shells.js +111 -52
- package/dist/packages/agents/lib/user-config.d.ts +4 -2
- package/dist/packages/agents/lib/user-config.js +15 -7
- package/dist/packages/compiler/compiled-paths.d.ts +9 -2
- package/dist/packages/compiler/compiled-paths.js +30 -15
- package/dist/packages/compiler/compiled-pipeline.js +23 -3
- package/dist/packages/compiler/compiled-stage-plan.js +4 -0
- package/dist/packages/compiler/compiled-target.d.ts +1 -1
- package/dist/packages/compiler/compiled-target.js +1 -1
- package/dist/packages/compiler/index.d.ts +1 -0
- package/dist/packages/compiler/index.js +1 -0
- package/dist/packages/compiler/lib/schema.d.ts +26 -31
- package/dist/packages/compiler/lib/schema.js +1 -12
- package/dist/packages/compiler/method-runs.d.ts +2 -3
- package/dist/packages/compiler/method-runs.js +2 -3
- package/dist/packages/compiler/reset.js +3 -1
- package/dist/packages/compiler/runtime-contracts.js +0 -3
- package/dist/packages/compiler/runtime-prompt.js +1 -1
- package/dist/packages/compiler/source-files.d.ts +46 -0
- package/dist/packages/compiler/source-files.js +149 -0
- package/dist/packages/compiler/state-artifacts.d.ts +3 -2
- package/dist/packages/compiler/state-artifacts.js +4 -3
- package/dist/packages/compiler/state-io.d.ts +3 -2
- package/dist/packages/compiler/state-io.js +11 -5
- package/dist/packages/compiler/state-paths.d.ts +2 -1
- package/dist/packages/compiler/state-paths.js +6 -3
- package/dist/packages/compiler/state-view.d.ts +3 -2
- package/dist/packages/compiler/state-view.js +18 -28
- package/dist/packages/compiler/state.d.ts +4 -4
- package/dist/packages/compiler/state.js +3 -3
- package/dist/packages/contracts/index.d.ts +1 -1
- package/dist/packages/contracts/lib/preparation-paths.d.ts +117 -0
- package/dist/packages/contracts/lib/preparation-paths.js +177 -0
- package/dist/packages/contracts/lib/schema.d.ts +85 -5
- package/dist/packages/contracts/lib/schema.js +46 -1
- package/dist/packages/execution/lib/schema.d.ts +50 -50
- package/dist/packages/execution/lib/schema.js +1 -1
- package/dist/packages/local-service/action-definitions.d.ts +14 -14
- package/dist/packages/local-service/action-definitions.js +27 -28
- package/dist/packages/local-service/action-planner.js +2 -1
- package/dist/packages/local-service/client.d.ts +51 -52
- package/dist/packages/local-service/client.js +132 -140
- package/dist/packages/local-service/connection-config.d.ts +38 -0
- package/dist/packages/local-service/connection-config.js +75 -0
- package/dist/packages/local-service/index.d.ts +11 -7
- package/dist/packages/local-service/index.js +6 -4
- package/dist/packages/local-service/instance-paths.d.ts +100 -0
- package/dist/packages/local-service/instance-paths.js +165 -0
- package/dist/packages/local-service/lib/schema.d.ts +405 -2297
- package/dist/packages/local-service/lib/schema.js +146 -62
- package/dist/packages/local-service/native-run-handlers.js +3 -3
- package/dist/packages/local-service/preparation-store.d.ts +92 -0
- package/dist/packages/local-service/preparation-store.js +171 -0
- package/dist/packages/local-service/routes.d.ts +33 -16
- package/dist/packages/local-service/routes.js +44 -20
- package/dist/packages/local-service/run-observability.js +11 -11
- package/dist/packages/local-service/runtime-caches.d.ts +76 -0
- package/dist/packages/local-service/runtime-caches.js +191 -0
- package/dist/packages/local-service/runtime-event-applier.d.ts +12 -0
- package/dist/packages/local-service/runtime-event-applier.js +177 -0
- package/dist/packages/local-service/runtime-persistence.d.ts +47 -0
- package/dist/packages/local-service/runtime-persistence.js +137 -0
- package/dist/packages/local-service/runtime-proposal-helpers.d.ts +35 -0
- package/dist/packages/local-service/runtime-proposal-helpers.js +251 -0
- package/dist/packages/local-service/runtime-resource-builders.d.ts +52 -0
- package/dist/packages/local-service/runtime-resource-builders.js +149 -0
- package/dist/packages/local-service/runtime.d.ts +197 -43
- package/dist/packages/local-service/runtime.js +800 -974
- package/dist/packages/local-service/server.d.ts +15 -0
- package/dist/packages/local-service/server.js +641 -273
- package/dist/packages/local-service/service-registry.d.ts +47 -0
- package/dist/packages/local-service/service-registry.js +137 -0
- package/dist/packages/method-authoring/method-authoring.d.ts +1 -1
- package/dist/packages/method-authoring/method-authoring.js +2 -2
- package/dist/packages/method-authoring/method-improvement.js +1 -1
- package/dist/packages/method-package/builtin-compiled-method.d.ts +4 -5
- package/dist/packages/method-package/builtin-compiled-method.js +8 -14
- package/dist/packages/method-package/context-interface.d.ts +4 -40
- package/dist/packages/method-package/context-interface.js +1 -23
- package/dist/packages/method-package/interf-method-package.d.ts +4 -4
- package/dist/packages/method-package/interf-method-package.js +21 -33
- package/dist/packages/method-package/local-methods.d.ts +10 -6
- package/dist/packages/method-package/local-methods.js +57 -39
- package/dist/packages/method-package/method-definitions.d.ts +8 -34
- package/dist/packages/method-package/method-definitions.js +49 -37
- package/dist/packages/method-package/method-helpers.d.ts +1 -13
- package/dist/packages/method-package/method-helpers.js +8 -42
- package/dist/packages/method-package/method-stage-runner.js +2 -2
- package/dist/packages/method-package/user-methods.d.ts +17 -0
- package/dist/packages/method-package/user-methods.js +77 -0
- package/dist/packages/project-model/index.d.ts +0 -1
- package/dist/packages/project-model/index.js +0 -1
- package/dist/packages/project-model/interf-detect.d.ts +8 -3
- package/dist/packages/project-model/interf-detect.js +34 -34
- package/dist/packages/project-model/interf-scaffold.d.ts +3 -3
- package/dist/packages/project-model/interf-scaffold.js +23 -32
- package/dist/packages/project-model/lib/schema.js +38 -1
- package/dist/packages/project-model/preparation-entries.d.ts +5 -5
- package/dist/packages/project-model/preparation-entries.js +14 -14
- package/dist/packages/project-model/source-config.d.ts +11 -11
- package/dist/packages/project-model/source-config.js +74 -46
- package/dist/packages/project-model/source-folders.d.ts +5 -5
- package/dist/packages/project-model/source-folders.js +14 -14
- package/dist/packages/shared/filesystem.d.ts +7 -0
- package/dist/packages/shared/filesystem.js +97 -10
- package/dist/packages/testing/lib/schema.d.ts +10 -10
- package/dist/packages/testing/lib/schema.js +2 -2
- package/dist/packages/testing/readiness-check-run.d.ts +4 -4
- package/dist/packages/testing/readiness-check-run.js +36 -36
- package/dist/packages/testing/test-execution.js +6 -6
- package/dist/packages/testing/test-paths.js +4 -3
- package/dist/packages/testing/test-sandbox.d.ts +0 -1
- package/dist/packages/testing/test-sandbox.js +14 -30
- package/dist/packages/testing/test-targets.d.ts +1 -1
- package/dist/packages/testing/test-targets.js +6 -6
- package/dist/packages/testing/test.d.ts +1 -1
- package/dist/packages/testing/test.js +1 -1
- package/package.json +3 -4
- package/CHANGELOG.md +0 -93
- package/LICENSE +0 -183
- package/dist/cli/commands/action-input-cli.d.ts +0 -25
- package/dist/cli/commands/action-input-cli.js +0 -73
- package/dist/cli/commands/control-path.d.ts +0 -11
- package/dist/cli/commands/control-path.js +0 -72
- package/dist/cli/commands/create-method-wizard.d.ts +0 -64
- package/dist/cli/commands/create-method-wizard.js +0 -434
- package/dist/cli/commands/create.d.ts +0 -6
- package/dist/cli/commands/create.js +0 -183
- package/dist/cli/commands/default.d.ts +0 -2
- package/dist/cli/commands/default.js +0 -39
- package/dist/cli/commands/executor-flow.d.ts +0 -29
- package/dist/cli/commands/executor-flow.js +0 -163
- package/dist/cli/commands/init.d.ts +0 -26
- package/dist/cli/commands/init.js +0 -771
- package/dist/cli/commands/list.d.ts +0 -2
- package/dist/cli/commands/list.js +0 -30
- package/dist/cli/commands/preparation-action.d.ts +0 -8
- package/dist/cli/commands/preparation-action.js +0 -29
- package/dist/cli/commands/preparation-picker.d.ts +0 -5
- package/dist/cli/commands/preparation-picker.js +0 -36
- package/dist/cli/commands/preparation-selection.d.ts +0 -6
- package/dist/cli/commands/preparation-selection.js +0 -11
- package/dist/cli/commands/service-action-flow.d.ts +0 -9
- package/dist/cli/commands/service-action-flow.js +0 -19
- package/dist/cli/commands/source-config-wizard.d.ts +0 -51
- package/dist/cli/commands/source-config-wizard.js +0 -670
- package/dist/cli/commands/verify.d.ts +0 -2
- package/dist/cli/commands/verify.js +0 -94
- package/dist/packages/compiler/raw-snapshot.d.ts +0 -49
- package/dist/packages/compiler/raw-snapshot.js +0 -101
- package/dist/packages/method-package/index.d.ts +0 -11
- package/dist/packages/method-package/index.js +0 -11
- package/dist/packages/method-package/method-stage-policy.d.ts +0 -5
- package/dist/packages/method-package/method-stage-policy.js +0 -31
- package/dist/packages/project-model/project-paths.d.ts +0 -12
- package/dist/packages/project-model/project-paths.js +0 -33
- /package/dist/compiler-ui/_next/static/{84FaeF3EzBF9kKTMjSEVN → C6vVfy3aeYuIO3d2AoNvC}/_buildManifest.js +0 -0
- /package/dist/compiler-ui/_next/static/{84FaeF3EzBF9kKTMjSEVN → C6vVfy3aeYuIO3d2AoNvC}/_clientMiddlewareManifest.js +0 -0
- /package/dist/compiler-ui/_next/static/{84FaeF3EzBF9kKTMjSEVN → C6vVfy3aeYuIO3d2AoNvC}/_ssgManifest.js +0 -0
|
@@ -1,12 +1,121 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { existsSync, statSync, readFileSync } from "node:fs";
|
|
4
5
|
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { LocalServiceConfigSchema, LocalServiceDiscoverySchema, LocalServiceErrorSchema,
|
|
7
|
+
import { LOCAL_SERVICE_LOOPBACK_HOSTS, LocalServiceConfigSchema, LocalServiceDiscoverySchema, LocalServiceErrorSchema, OpenPathRequestSchema, ServiceRegistryEntrySchema, } from "./lib/schema.js";
|
|
7
8
|
import { assertPathWithinRoot, } from "../shared/path-guards.js";
|
|
8
9
|
import { createLocalServiceRuntime, } from "./runtime.js";
|
|
9
|
-
import { LOCAL_SERVICE_DEFAULT_HOST, LOCAL_SERVICE_DEFAULT_PORT,
|
|
10
|
+
import { buildLocalServiceUrl, LOCAL_SERVICE_DEFAULT_HOST, LOCAL_SERVICE_DEFAULT_PORT, LOCAL_SERVICE_ROUTES, PREPARATION_SUBRESOURCES, } from "./routes.js";
|
|
11
|
+
import { registerServiceLocally, unregisterService, } from "./service-registry.js";
|
|
12
|
+
import { createStoredPreparation, deleteStoredPreparation, getStoredPreparation, listStoredPreparations, preparationWireShape, rehydratePreparations, } from "./preparation-store.js";
|
|
13
|
+
import { clearConnection, readActiveConnection, writeConnection, } from "./connection-config.js";
|
|
14
|
+
/** HTTP methods that require an authenticated bearer token + Origin guard. */
|
|
15
|
+
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
16
|
+
/** Generate a fresh per-instance bearer token. */
|
|
17
|
+
export function createLocalServiceAuthToken() {
|
|
18
|
+
return randomBytes(32).toString("hex");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the per-instance CORS / Origin allowlist. Only the loopback
|
|
22
|
+
* hostnames bound to the active port count as same-origin. `null` and
|
|
23
|
+
* absent Origin are also accepted (Electron / file:// / CLI tools).
|
|
24
|
+
*/
|
|
25
|
+
function buildAllowedOrigins(host, port) {
|
|
26
|
+
const hostnames = Array.from(new Set([host, ...LOCAL_SERVICE_LOOPBACK_HOSTS]));
|
|
27
|
+
return {
|
|
28
|
+
hostnames,
|
|
29
|
+
ports: [port],
|
|
30
|
+
hostUrls: hostnames.map((hostname) => {
|
|
31
|
+
const wrapped = hostname.includes(":") ? `[${hostname}]` : hostname;
|
|
32
|
+
return `http://${wrapped}:${port}`;
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function originHeaderValue(req) {
|
|
37
|
+
const raw = req.headers.origin;
|
|
38
|
+
if (raw === undefined)
|
|
39
|
+
return null;
|
|
40
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
41
|
+
if (typeof value !== "string")
|
|
42
|
+
return null;
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
45
|
+
}
|
|
46
|
+
function authorizationHeaderValue(req) {
|
|
47
|
+
const raw = req.headers.authorization;
|
|
48
|
+
if (!raw)
|
|
49
|
+
return null;
|
|
50
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
51
|
+
if (typeof value !== "string")
|
|
52
|
+
return null;
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
55
|
+
}
|
|
56
|
+
function isOriginAllowed(origin, allowed) {
|
|
57
|
+
if (origin === null)
|
|
58
|
+
return true; // CLI / Node fetch / native app — no Origin sent.
|
|
59
|
+
if (origin === "null")
|
|
60
|
+
return true; // Electron / sandboxed / file:// page.
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = new URL(origin);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
69
|
+
return false;
|
|
70
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
71
|
+
if (!allowed.hostnames.includes(hostname))
|
|
72
|
+
return false;
|
|
73
|
+
// Default ports (no explicit port in URL) cannot be the local service.
|
|
74
|
+
if (!parsed.port)
|
|
75
|
+
return false;
|
|
76
|
+
const portNumber = Number.parseInt(parsed.port, 10);
|
|
77
|
+
if (!Number.isInteger(portNumber))
|
|
78
|
+
return false;
|
|
79
|
+
return allowed.ports.includes(portNumber);
|
|
80
|
+
}
|
|
81
|
+
function corsHeadersFor(origin, allowed) {
|
|
82
|
+
if (!isOriginAllowed(origin, allowed))
|
|
83
|
+
return { vary: "origin" };
|
|
84
|
+
return {
|
|
85
|
+
"access-control-allow-origin": origin ?? allowed.hostUrls[0] ?? "",
|
|
86
|
+
"access-control-allow-credentials": "false",
|
|
87
|
+
"access-control-allow-methods": "GET,POST,PATCH,DELETE,OPTIONS",
|
|
88
|
+
"access-control-allow-headers": "content-type, authorization, x-interf-workspace, x-interf-idempotency-key",
|
|
89
|
+
vary: "origin",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function isMutatingMethod(method) {
|
|
93
|
+
return method !== undefined && MUTATING_METHODS.has(method.toUpperCase());
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate the bearer token on a mutating request. Returns true when:
|
|
97
|
+
* - the runtime has no token (test harness mode), or
|
|
98
|
+
* - the request includes `Authorization: Bearer <token>` matching the runtime token.
|
|
99
|
+
*/
|
|
100
|
+
function isAuthorizedMutation(req, runtime) {
|
|
101
|
+
if (!runtime.authToken)
|
|
102
|
+
return true;
|
|
103
|
+
const authorization = authorizationHeaderValue(req);
|
|
104
|
+
if (!authorization)
|
|
105
|
+
return false;
|
|
106
|
+
const match = /^Bearer\s+([A-Za-z0-9._\-]+)$/.exec(authorization);
|
|
107
|
+
if (!match)
|
|
108
|
+
return false;
|
|
109
|
+
const presented = match[1];
|
|
110
|
+
if (!presented || presented.length !== runtime.authToken.length)
|
|
111
|
+
return false;
|
|
112
|
+
// Constant-time compare to avoid timing oracles.
|
|
113
|
+
let mismatch = 0;
|
|
114
|
+
for (let index = 0; index < runtime.authToken.length; index += 1) {
|
|
115
|
+
mismatch |= presented.charCodeAt(index) ^ runtime.authToken.charCodeAt(index);
|
|
116
|
+
}
|
|
117
|
+
return mismatch === 0;
|
|
118
|
+
}
|
|
10
119
|
function packageRoot() {
|
|
11
120
|
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
12
121
|
}
|
|
@@ -25,29 +134,6 @@ function compilerUiStaticRoot() {
|
|
|
25
134
|
return resolve(explicit);
|
|
26
135
|
return resolveCompilerUiStaticRoot();
|
|
27
136
|
}
|
|
28
|
-
function localServicePointerPath(rootPath) {
|
|
29
|
-
return join(rootPath, ...LOCAL_SERVICE_POINTER_PATH);
|
|
30
|
-
}
|
|
31
|
-
function writeLocalServicePointer(rootPath, pointer) {
|
|
32
|
-
const pointerPath = localServicePointerPath(rootPath);
|
|
33
|
-
mkdirSync(dirname(pointerPath), { recursive: true });
|
|
34
|
-
const parsed = LocalServicePointerSchema.parse(pointer);
|
|
35
|
-
writeFileSync(pointerPath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
36
|
-
}
|
|
37
|
-
function removeLocalServicePointer(rootPath, serviceUrl) {
|
|
38
|
-
const pointerPath = localServicePointerPath(rootPath);
|
|
39
|
-
if (!existsSync(pointerPath))
|
|
40
|
-
return;
|
|
41
|
-
try {
|
|
42
|
-
const pointer = LocalServicePointerSchema.parse(JSON.parse(readFileSync(pointerPath, "utf8")));
|
|
43
|
-
if (pointer.service_url !== serviceUrl || pointer.pid !== process.pid)
|
|
44
|
-
return;
|
|
45
|
-
rmSync(pointerPath, { force: true });
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
137
|
function contentType(filePath) {
|
|
52
138
|
switch (extname(filePath)) {
|
|
53
139
|
case ".html":
|
|
@@ -76,28 +162,56 @@ function contentType(filePath) {
|
|
|
76
162
|
return "application/octet-stream";
|
|
77
163
|
}
|
|
78
164
|
}
|
|
79
|
-
|
|
165
|
+
const NO_ORIGIN_RESPONSE_CONTEXT = { cors: { vary: "origin" } };
|
|
166
|
+
/** Stash slot on the response object that holds the per-request CORS headers. */
|
|
167
|
+
const RESPONSE_CONTEXT_KEY = Symbol.for("interf.localService.responseContext");
|
|
168
|
+
function attachResponseContext(res, ctx) {
|
|
169
|
+
res[RESPONSE_CONTEXT_KEY] = ctx;
|
|
170
|
+
}
|
|
171
|
+
function activeResponseContext(res) {
|
|
172
|
+
const ctx = res[RESPONSE_CONTEXT_KEY];
|
|
173
|
+
return ctx ?? NO_ORIGIN_RESPONSE_CONTEXT;
|
|
174
|
+
}
|
|
175
|
+
function writeHeaders(res, statusCode, contextOrHeaders, headers = {}) {
|
|
176
|
+
// Compatibility shim while call sites migrate. Older callers pass a
|
|
177
|
+
// plain headers record as the third argument; new callers thread a
|
|
178
|
+
// `ResponseContext`. When neither is supplied (or only headers are),
|
|
179
|
+
// we read the per-request context the entry-point handler stashed on
|
|
180
|
+
// the response.
|
|
181
|
+
const isContext = contextOrHeaders !== undefined && "cors" in contextOrHeaders;
|
|
182
|
+
const context = isContext ? contextOrHeaders : activeResponseContext(res);
|
|
183
|
+
const extraHeaders = isContext ? headers : (contextOrHeaders ?? {});
|
|
80
184
|
res.writeHead(statusCode, {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"access-control-allow-headers": "content-type",
|
|
84
|
-
...headers,
|
|
185
|
+
...context.cors,
|
|
186
|
+
...extraHeaders,
|
|
85
187
|
});
|
|
86
188
|
}
|
|
87
|
-
function sendJson(res, statusCode,
|
|
88
|
-
|
|
189
|
+
function sendJson(res, statusCode, contextOrValue, maybeValue) {
|
|
190
|
+
const usingContext = contextOrValue !== null
|
|
191
|
+
&& typeof contextOrValue === "object"
|
|
192
|
+
&& "cors" in contextOrValue
|
|
193
|
+
&& maybeValue !== undefined;
|
|
194
|
+
const context = usingContext ? contextOrValue : activeResponseContext(res);
|
|
195
|
+
const value = usingContext ? maybeValue : contextOrValue;
|
|
196
|
+
writeHeaders(res, statusCode, context, {
|
|
89
197
|
"content-type": "application/json; charset=utf-8",
|
|
90
198
|
});
|
|
91
199
|
res.end(`${JSON.stringify(value, null, 2)}\n`);
|
|
92
200
|
}
|
|
93
|
-
function sendText(res, statusCode,
|
|
94
|
-
|
|
201
|
+
function sendText(res, statusCode, contextOrValue, maybeValue) {
|
|
202
|
+
const usingContext = typeof contextOrValue === "object" && contextOrValue !== null && "cors" in contextOrValue;
|
|
203
|
+
const context = usingContext ? contextOrValue : activeResponseContext(res);
|
|
204
|
+
const value = usingContext ? (maybeValue ?? "") : contextOrValue;
|
|
205
|
+
writeHeaders(res, statusCode, context, {
|
|
95
206
|
"content-type": "text/plain; charset=utf-8",
|
|
96
207
|
});
|
|
97
208
|
res.end(value);
|
|
98
209
|
}
|
|
99
|
-
function sendError(res, statusCode,
|
|
100
|
-
|
|
210
|
+
function sendError(res, statusCode, contextOrMessage, maybeMessage) {
|
|
211
|
+
const usingContext = typeof contextOrMessage === "object" && contextOrMessage !== null && "cors" in contextOrMessage;
|
|
212
|
+
const context = usingContext ? contextOrMessage : activeResponseContext(res);
|
|
213
|
+
const message = usingContext ? (maybeMessage ?? "") : contextOrMessage;
|
|
214
|
+
sendJson(res, statusCode, context, LocalServiceErrorSchema.parse({
|
|
101
215
|
error: {
|
|
102
216
|
message,
|
|
103
217
|
},
|
|
@@ -211,185 +325,368 @@ async function routeApi(req, res, runtime) {
|
|
|
211
325
|
const url = parseRequestUrl(req);
|
|
212
326
|
const path = url.pathname;
|
|
213
327
|
const method = req.method ?? "GET";
|
|
328
|
+
const origin = originHeaderValue(req);
|
|
329
|
+
const allowed = buildAllowedOrigins(runtime.host, runtime.port);
|
|
330
|
+
// CORS preflight — answered for allowed origins, refused otherwise.
|
|
214
331
|
if (method === "OPTIONS") {
|
|
332
|
+
if (!isOriginAllowed(origin, allowed)) {
|
|
333
|
+
attachResponseContext(res, NO_ORIGIN_RESPONSE_CONTEXT);
|
|
334
|
+
sendError(res, 403, "Origin not allowed.");
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
attachResponseContext(res, { cors: corsHeadersFor(origin, allowed) });
|
|
215
338
|
writeHeaders(res, 204);
|
|
216
339
|
res.end();
|
|
217
340
|
return true;
|
|
218
341
|
}
|
|
219
|
-
|
|
342
|
+
// For non-OPTIONS, attach the CORS context once.
|
|
343
|
+
attachResponseContext(res, { cors: corsHeadersFor(origin, allowed) });
|
|
344
|
+
// Mutating requests must:
|
|
345
|
+
// 1. Have an Origin that's local OR no Origin at all (CLI),
|
|
346
|
+
// 2. Carry a valid Authorization: Bearer <token> if the runtime issued one.
|
|
347
|
+
// Read-only GETs stay open so dev tooling can still inspect state.
|
|
348
|
+
if (isMutatingMethod(method)) {
|
|
349
|
+
if (!isOriginAllowed(origin, allowed)) {
|
|
350
|
+
sendError(res, 403, "Origin not allowed.");
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
if (!isAuthorizedMutation(req, runtime)) {
|
|
354
|
+
writeHeaders(res, 401, { "www-authenticate": "Bearer realm=\"interf-local-service\"" });
|
|
355
|
+
res.end(`${JSON.stringify(LocalServiceErrorSchema.parse({
|
|
356
|
+
error: { message: "Missing or invalid bearer token." },
|
|
357
|
+
}), null, 2)}\n`);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
362
|
+
// Top-level engine routes — instance metadata, health, discovery.
|
|
363
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
364
|
+
// GET /health — liveness probe used by the CLI bootstrap.
|
|
365
|
+
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.health) {
|
|
220
366
|
sendJson(res, 200, runtime.health());
|
|
221
367
|
return true;
|
|
222
368
|
}
|
|
369
|
+
// GET /v1 — discovery list of available resources.
|
|
223
370
|
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.api) {
|
|
224
371
|
sendJson(res, 200, LocalServiceDiscoverySchema.parse({
|
|
225
372
|
kind: "interf-local-service-discovery",
|
|
226
373
|
version: 1,
|
|
227
374
|
resources: {
|
|
375
|
+
instance: LOCAL_SERVICE_ROUTES.instance,
|
|
228
376
|
preparations: LOCAL_SERVICE_ROUTES.preparations,
|
|
229
377
|
methods: LOCAL_SERVICE_ROUTES.methods,
|
|
230
378
|
runs: LOCAL_SERVICE_ROUTES.runs,
|
|
231
|
-
readiness: LOCAL_SERVICE_ROUTES.readiness,
|
|
232
|
-
portable_contexts: LOCAL_SERVICE_ROUTES.portableContexts,
|
|
233
|
-
source_files: LOCAL_SERVICE_ROUTES.sourceFiles,
|
|
234
|
-
workspace_files: LOCAL_SERVICE_ROUTES.workspaceFiles,
|
|
235
379
|
action_proposals: LOCAL_SERVICE_ROUTES.actionProposals,
|
|
236
|
-
preparation_setups: LOCAL_SERVICE_ROUTES.preparationSetups,
|
|
237
|
-
preparation_changes: LOCAL_SERVICE_ROUTES.preparationChanges,
|
|
238
|
-
method_changes: LOCAL_SERVICE_ROUTES.methodChanges,
|
|
239
|
-
readiness_check_drafts: LOCAL_SERVICE_ROUTES.readinessCheckDrafts,
|
|
240
|
-
method_authoring_runs: LOCAL_SERVICE_ROUTES.methodAuthoringRuns,
|
|
241
|
-
method_improvement_runs: LOCAL_SERVICE_ROUTES.methodImprovementRuns,
|
|
242
|
-
compile_runs: LOCAL_SERVICE_ROUTES.compileRuns,
|
|
243
|
-
test_runs: LOCAL_SERVICE_ROUTES.testRuns,
|
|
244
|
-
reset: LOCAL_SERVICE_ROUTES.reset,
|
|
245
380
|
executor: LOCAL_SERVICE_ROUTES.executor,
|
|
381
|
+
open_path: LOCAL_SERVICE_ROUTES.openPath,
|
|
246
382
|
},
|
|
247
383
|
}));
|
|
248
384
|
return true;
|
|
249
385
|
}
|
|
386
|
+
// GET /v1/instance — engine metadata.
|
|
387
|
+
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.instance) {
|
|
388
|
+
const startedAtIso = runtime.startedAt ?? new Date().toISOString();
|
|
389
|
+
const startedMs = Date.parse(startedAtIso);
|
|
390
|
+
const uptimeSeconds = Number.isFinite(startedMs)
|
|
391
|
+
? Math.max(0, Math.floor((Date.now() - startedMs) / 1000))
|
|
392
|
+
: 0;
|
|
393
|
+
sendJson(res, 200, {
|
|
394
|
+
kind: "interf-instance",
|
|
395
|
+
version: 1,
|
|
396
|
+
url: buildLocalServiceUrl({ host: runtime.host, port: runtime.port }),
|
|
397
|
+
host: runtime.host,
|
|
398
|
+
port: runtime.port,
|
|
399
|
+
pid: process.pid,
|
|
400
|
+
started_at: startedAtIso,
|
|
401
|
+
uptime_seconds: uptimeSeconds,
|
|
402
|
+
package_version: runtime.packageVersion ?? null,
|
|
403
|
+
preparation_count: listStoredPreparations().length,
|
|
404
|
+
auth_required: Boolean(runtime.authToken),
|
|
405
|
+
});
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
409
|
+
// Preparation collection routes
|
|
410
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
411
|
+
// GET /v1/preparations — list every preparation on the instance.
|
|
250
412
|
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.preparations) {
|
|
251
|
-
|
|
413
|
+
const items = listStoredPreparations().map(preparationWireShape);
|
|
414
|
+
sendJson(res, 200, { preparations: items });
|
|
252
415
|
return true;
|
|
253
416
|
}
|
|
254
|
-
|
|
417
|
+
// POST /v1/preparations — create a new preparation.
|
|
418
|
+
if (method === "POST" && path === LOCAL_SERVICE_ROUTES.preparations) {
|
|
255
419
|
try {
|
|
256
|
-
const body = await readJsonBody(req);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const resource = await runtime.createCompileRun({
|
|
261
|
-
preparation: result.preparation,
|
|
262
|
-
method: result.method,
|
|
263
|
-
});
|
|
264
|
-
sendJson(res, 202, {
|
|
265
|
-
...result,
|
|
266
|
-
submitted_run_id: resource.run.run_id,
|
|
267
|
-
submitted_run_type: "compile-run",
|
|
268
|
-
message: `${result.message} Prepare run started.`,
|
|
269
|
-
});
|
|
420
|
+
const body = (await readJsonBody(req));
|
|
421
|
+
if (!body || typeof body !== "object") {
|
|
422
|
+
sendError(res, 400, "Request body must be a JSON object.");
|
|
423
|
+
return true;
|
|
270
424
|
}
|
|
271
|
-
|
|
272
|
-
|
|
425
|
+
if (!body.id || typeof body.id !== "string") {
|
|
426
|
+
sendError(res, 400, "Missing required field: id");
|
|
427
|
+
return true;
|
|
273
428
|
}
|
|
429
|
+
if (!body.method_id || typeof body.method_id !== "string") {
|
|
430
|
+
sendError(res, 400, "Missing required field: method_id");
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (!body.source || typeof body.source !== "object" || !body.source.locator) {
|
|
434
|
+
sendError(res, 400, "Missing required field: source.locator");
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
const stored = createStoredPreparation(runtime, {
|
|
438
|
+
id: body.id,
|
|
439
|
+
source: { kind: "local-folder", locator: body.source.locator },
|
|
440
|
+
method_id: body.method_id,
|
|
441
|
+
about: body.about,
|
|
442
|
+
checks: body.checks,
|
|
443
|
+
max_attempts: body.max_attempts,
|
|
444
|
+
max_loops: body.max_loops,
|
|
445
|
+
});
|
|
446
|
+
sendJson(res, 201, preparationWireShape(stored));
|
|
274
447
|
}
|
|
275
448
|
catch (error) {
|
|
276
|
-
sendError(res,
|
|
449
|
+
sendError(res, 400, error instanceof Error ? error.message : String(error));
|
|
277
450
|
}
|
|
278
451
|
return true;
|
|
279
452
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
453
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
454
|
+
// Per-preparation routes (under /v1/preparations/{id}/…)
|
|
455
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
456
|
+
if (path.startsWith(`${LOCAL_SERVICE_ROUTES.preparations}/`)) {
|
|
457
|
+
const tail = path.slice(LOCAL_SERVICE_ROUTES.preparations.length + 1);
|
|
458
|
+
const slashIndex = tail.indexOf("/");
|
|
459
|
+
const prepId = slashIndex === -1 ? tail : tail.slice(0, slashIndex);
|
|
460
|
+
const subPath = slashIndex === -1 ? "" : tail.slice(slashIndex + 1);
|
|
461
|
+
const decodedPrepId = decodeURIComponent(prepId);
|
|
462
|
+
const storedPrep = getStoredPreparation(decodedPrepId);
|
|
463
|
+
if (!storedPrep) {
|
|
464
|
+
sendError(res, 404, `Preparation not found: ${decodedPrepId}`);
|
|
465
|
+
return true;
|
|
284
466
|
}
|
|
285
|
-
|
|
286
|
-
|
|
467
|
+
runtime.touchPreparation(storedPrep.prepDataDir);
|
|
468
|
+
if (subPath === "") {
|
|
469
|
+
// Bare /v1/preparations/{id}
|
|
470
|
+
if (method === "GET") {
|
|
471
|
+
sendJson(res, 200, preparationWireShape(storedPrep));
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
if (method === "DELETE") {
|
|
475
|
+
deleteStoredPreparation(runtime, decodedPrepId);
|
|
476
|
+
sendJson(res, 200, { id: decodedPrepId, deleted: true });
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
287
479
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
480
|
+
else if (subPath === PREPARATION_SUBRESOURCES.compileRuns) {
|
|
481
|
+
if (method === "POST") {
|
|
482
|
+
try {
|
|
483
|
+
const body = (await readJsonBody(req));
|
|
484
|
+
const request = { preparation: storedPrep.id, ...(body ?? {}) };
|
|
485
|
+
const idempotencyKeyRaw = req.headers["x-interf-idempotency-key"];
|
|
486
|
+
const idempotencyKey = Array.isArray(idempotencyKeyRaw)
|
|
487
|
+
? idempotencyKeyRaw[0]
|
|
488
|
+
: idempotencyKeyRaw;
|
|
489
|
+
const trimmedKey = typeof idempotencyKey === "string" ? idempotencyKey.trim() : "";
|
|
490
|
+
const dedupedRunId = trimmedKey
|
|
491
|
+
? runtime.findIdempotentCompileRun(storedPrep.prepDataDir, trimmedKey)
|
|
492
|
+
: null;
|
|
493
|
+
if (dedupedRunId) {
|
|
494
|
+
const existing = runtime.getCompileRun(storedPrep.prepDataDir, dedupedRunId);
|
|
495
|
+
if (existing) {
|
|
496
|
+
sendJson(res, 200, existing);
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const resource = await runtime.createCompileRun(storedPrep.prepDataDir, request);
|
|
501
|
+
if (trimmedKey) {
|
|
502
|
+
runtime.recordIdempotentCompileRun(storedPrep.prepDataDir, trimmedKey, resource.run.run_id);
|
|
503
|
+
}
|
|
504
|
+
sendJson(res, 201, resource);
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
sendError(res, 400, error instanceof Error ? error.message : String(error));
|
|
508
|
+
}
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
294
511
|
}
|
|
295
|
-
|
|
296
|
-
|
|
512
|
+
else if (subPath === PREPARATION_SUBRESOURCES.testRuns) {
|
|
513
|
+
if (method === "POST") {
|
|
514
|
+
try {
|
|
515
|
+
const body = (await readJsonBody(req));
|
|
516
|
+
const request = { preparation: storedPrep.id, ...(body ?? {}) };
|
|
517
|
+
const resource = await runtime.createTestRun(storedPrep.prepDataDir, request);
|
|
518
|
+
sendJson(res, 201, resource);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
sendError(res, 400, error instanceof Error ? error.message : String(error));
|
|
522
|
+
}
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
297
525
|
}
|
|
526
|
+
else if (subPath === PREPARATION_SUBRESOURCES.methodAuthoringRuns) {
|
|
527
|
+
if (method === "POST") {
|
|
528
|
+
try {
|
|
529
|
+
const body = (await readJsonBody(req));
|
|
530
|
+
const job = await runtime.createMethodAuthoringRun(storedPrep.prepDataDir, body);
|
|
531
|
+
sendJson(res, 202, job);
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
sendError(res, 400, error instanceof Error ? error.message : String(error));
|
|
535
|
+
}
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else if (subPath === PREPARATION_SUBRESOURCES.methodImprovementRuns) {
|
|
540
|
+
if (method === "POST") {
|
|
541
|
+
try {
|
|
542
|
+
const body = (await readJsonBody(req));
|
|
543
|
+
const job = await runtime.createMethodAuthoringRun(storedPrep.prepDataDir, body, "method-improvement");
|
|
544
|
+
sendJson(res, 202, job);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
sendError(res, 400, error instanceof Error ? error.message : String(error));
|
|
548
|
+
}
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else if (subPath === PREPARATION_SUBRESOURCES.readinessCheckDrafts) {
|
|
553
|
+
if (method === "POST") {
|
|
554
|
+
try {
|
|
555
|
+
const body = (await readJsonBody(req));
|
|
556
|
+
const job = await runtime.createReadinessCheckDraftRun(storedPrep.prepDataDir, body);
|
|
557
|
+
sendJson(res, 202, job);
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
sendError(res, 400, error instanceof Error ? error.message : String(error));
|
|
561
|
+
}
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else if (subPath === PREPARATION_SUBRESOURCES.changes) {
|
|
566
|
+
if (method === "POST") {
|
|
567
|
+
try {
|
|
568
|
+
const body = (await readJsonBody(req));
|
|
569
|
+
sendJson(res, 202, runtime.applyPreparationChange(storedPrep.prepDataDir, body));
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
sendError(res, 409, error instanceof Error ? error.message : String(error));
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else if (subPath === PREPARATION_SUBRESOURCES.reset) {
|
|
578
|
+
if (method === "POST") {
|
|
579
|
+
try {
|
|
580
|
+
const body = (await readJsonBody(req));
|
|
581
|
+
const request = { preparation: storedPrep.id, scope: "compile", ...(body ?? {}) };
|
|
582
|
+
const result = runtime.applyReset(storedPrep.prepDataDir, request);
|
|
583
|
+
sendJson(res, 200, result);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
sendError(res, 400, error instanceof Error ? error.message : String(error));
|
|
587
|
+
}
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
else if (subPath === PREPARATION_SUBRESOURCES.readiness) {
|
|
592
|
+
if (method === "GET") {
|
|
593
|
+
const readiness = runtime.getReadiness(storedPrep.prepDataDir, storedPrep.id);
|
|
594
|
+
sendJson(res, 200, readiness);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else if (subPath === PREPARATION_SUBRESOURCES.runs) {
|
|
599
|
+
if (method === "GET") {
|
|
600
|
+
const runs = runtime.listCompileRunsForPreparation(storedPrep.prepDataDir, storedPrep.id);
|
|
601
|
+
sendJson(res, 200, { runs });
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else if (subPath === PREPARATION_SUBRESOURCES.sourceFiles) {
|
|
606
|
+
if (method === "GET") {
|
|
607
|
+
sendJson(res, 200, {
|
|
608
|
+
source_files: runtime.listSourceFiles(storedPrep.prepDataDir, storedPrep.id),
|
|
609
|
+
});
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
else if (subPath === PREPARATION_SUBRESOURCES.portableContext) {
|
|
614
|
+
if (method === "GET") {
|
|
615
|
+
const context = runtime.getPortableContext(storedPrep.prepDataDir, storedPrep.id);
|
|
616
|
+
if (!context)
|
|
617
|
+
sendError(res, 404, "Portable context not found.");
|
|
618
|
+
else
|
|
619
|
+
sendJson(res, 200, context);
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
sendError(res, 404, `Unknown preparation sub-route: ${subPath}`);
|
|
298
624
|
return true;
|
|
299
625
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.readiness) {
|
|
305
|
-
sendJson(res, 200, { readiness: runtime.listPreparationReadiness() });
|
|
306
|
-
return true;
|
|
307
|
-
}
|
|
308
|
-
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.sourceFiles) {
|
|
309
|
-
const preparation = url.searchParams.get("preparation");
|
|
310
|
-
sendJson(res, 200, { source_files: runtime.listSourceFiles(preparation) });
|
|
311
|
-
return true;
|
|
312
|
-
}
|
|
313
|
-
if (method === "POST" && path === LOCAL_SERVICE_ROUTES.openPath) {
|
|
314
|
-
const body = OpenPathRequestSchema.parse(await readJsonBody(req));
|
|
315
|
-
const health = runtime.health();
|
|
316
|
-
const allowedRoots = [
|
|
317
|
-
runtime.rootPath,
|
|
318
|
-
...(health.source_folder_path ? [health.source_folder_path] : []),
|
|
319
|
-
];
|
|
320
|
-
const openedPath = await openLocalPath(allowedRoots, body.path);
|
|
321
|
-
sendJson(res, 202, { opened: true, path: openedPath });
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
const preparationMatch = path.match(/^\/v1\/preparations\/([^/]+)$/);
|
|
325
|
-
if (method === "GET" && preparationMatch?.[1]) {
|
|
326
|
-
const preparation = runtime.getPreparation(decodeURIComponent(preparationMatch[1]));
|
|
327
|
-
if (!preparation)
|
|
328
|
-
sendError(res, 404, "Preparation not found.");
|
|
329
|
-
else
|
|
330
|
-
sendJson(res, 200, preparation);
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
const preparationReadinessMatch = path.match(/^\/v1\/preparations\/([^/]+)\/readiness$/);
|
|
334
|
-
if (method === "GET" && preparationReadinessMatch?.[1]) {
|
|
335
|
-
const readiness = runtime.getPreparationReadiness(decodeURIComponent(preparationReadinessMatch[1]));
|
|
336
|
-
if (!readiness)
|
|
337
|
-
sendError(res, 404, "Preparation not found.");
|
|
338
|
-
else
|
|
339
|
-
sendJson(res, 200, readiness);
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
626
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
627
|
+
// Method resources — preparation-independent (workspace draft + user lib + bundled).
|
|
628
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
342
629
|
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.methods) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const body = await readJsonBody(req);
|
|
349
|
-
sendJson(res, 202, runtime.applyMethodChange(body));
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
sendError(res, 409, error instanceof Error ? error.message : String(error));
|
|
353
|
-
}
|
|
630
|
+
// The runtime needs SOME prep data dir to discover preparation-draft methods.
|
|
631
|
+
// Fall back to the first registered preparation (if any) so user-library
|
|
632
|
+
// and bundled methods still surface even when no preparation has drafts.
|
|
633
|
+
const firstPrep = listStoredPreparations()[0];
|
|
634
|
+
sendJson(res, 200, { methods: runtime.listMethods(firstPrep?.prepDataDir ?? runtime.rootPath) });
|
|
354
635
|
return true;
|
|
355
636
|
}
|
|
356
637
|
const methodMatch = path.match(/^\/v1\/methods\/([^/]+)$/);
|
|
357
638
|
if (method === "GET" && methodMatch?.[1]) {
|
|
358
|
-
const
|
|
639
|
+
const firstPrep = listStoredPreparations()[0];
|
|
640
|
+
const methodResource = runtime.getMethod(firstPrep?.prepDataDir ?? runtime.rootPath, decodeURIComponent(methodMatch[1]));
|
|
359
641
|
if (!methodResource)
|
|
360
642
|
sendError(res, 404, "Method not found.");
|
|
361
643
|
else
|
|
362
644
|
sendJson(res, 200, methodResource);
|
|
363
645
|
return true;
|
|
364
646
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.
|
|
370
|
-
|
|
647
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
648
|
+
// Run observability — instance-wide. Each run record carries a workspace,
|
|
649
|
+
// so the runtime takes a "first prep" hint to scan registered preparations.
|
|
650
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
651
|
+
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.runs) {
|
|
652
|
+
const firstPrep = listStoredPreparations()[0];
|
|
653
|
+
sendJson(res, 200, { runs: runtime.listRunObservability(firstPrep?.prepDataDir ?? runtime.rootPath) });
|
|
371
654
|
return true;
|
|
372
655
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
656
|
+
const observableRunMatch = path.match(/^\/v1\/runs\/([^/]+)$/);
|
|
657
|
+
if (method === "GET" && observableRunMatch?.[1]) {
|
|
658
|
+
const firstPrep = listStoredPreparations()[0];
|
|
659
|
+
const run = runtime.getRunObservability(firstPrep?.prepDataDir ?? runtime.rootPath, decodeURIComponent(observableRunMatch[1]));
|
|
660
|
+
if (!run)
|
|
661
|
+
sendError(res, 404, "Run not found.");
|
|
662
|
+
else
|
|
663
|
+
sendJson(res, 200, run);
|
|
376
664
|
return true;
|
|
377
665
|
}
|
|
666
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
667
|
+
// Action proposals (chat / wizard) — instance-wide.
|
|
668
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
378
669
|
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.actionProposals) {
|
|
379
|
-
|
|
670
|
+
const firstPrep = listStoredPreparations()[0];
|
|
671
|
+
sendJson(res, 200, {
|
|
672
|
+
action_proposals: runtime.listActionProposals(firstPrep?.prepDataDir ?? runtime.rootPath),
|
|
673
|
+
});
|
|
380
674
|
return true;
|
|
381
675
|
}
|
|
382
676
|
if (method === "POST" && path === LOCAL_SERVICE_ROUTES.actionProposals) {
|
|
383
677
|
const body = await readJsonBody(req);
|
|
384
|
-
|
|
678
|
+
const firstPrep = listStoredPreparations()[0];
|
|
679
|
+
sendJson(res, 202, await runtime.createActionProposal(firstPrep?.prepDataDir ?? runtime.rootPath, body));
|
|
385
680
|
return true;
|
|
386
681
|
}
|
|
387
682
|
const actionProposalMatch = path.match(/^\/v1\/action-proposals\/([^/]+)(?:\/([^/]+))?$/);
|
|
388
683
|
if (actionProposalMatch?.[1]) {
|
|
389
684
|
const proposalId = decodeURIComponent(actionProposalMatch[1]);
|
|
390
685
|
const child = actionProposalMatch[2];
|
|
686
|
+
const firstPrep = listStoredPreparations()[0];
|
|
687
|
+
const prepDataDir = firstPrep?.prepDataDir ?? runtime.rootPath;
|
|
391
688
|
if (method === "GET" && !child) {
|
|
392
|
-
const proposal = runtime.getActionProposal(proposalId);
|
|
689
|
+
const proposal = runtime.getActionProposal(prepDataDir, proposalId);
|
|
393
690
|
if (!proposal)
|
|
394
691
|
sendError(res, 404, "Action proposal not found.");
|
|
395
692
|
else
|
|
@@ -398,7 +695,7 @@ async function routeApi(req, res, runtime) {
|
|
|
398
695
|
}
|
|
399
696
|
if (method === "POST" && child === "decision") {
|
|
400
697
|
const body = await readJsonBody(req);
|
|
401
|
-
const proposal = await runtime.decideActionProposal(proposalId, body);
|
|
698
|
+
const proposal = await runtime.decideActionProposal(prepDataDir, proposalId, body);
|
|
402
699
|
if (!proposal)
|
|
403
700
|
sendError(res, 404, "Action proposal not found.");
|
|
404
701
|
else
|
|
@@ -406,81 +703,17 @@ async function routeApi(req, res, runtime) {
|
|
|
406
703
|
return true;
|
|
407
704
|
}
|
|
408
705
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
const observableRunMatch = path.match(/^\/v1\/runs\/([^/]+)$/);
|
|
414
|
-
if (method === "GET" && observableRunMatch?.[1]) {
|
|
415
|
-
const run = runtime.getRunObservability(decodeURIComponent(observableRunMatch[1]));
|
|
416
|
-
if (!run)
|
|
417
|
-
sendError(res, 404, "Run not found.");
|
|
418
|
-
else
|
|
419
|
-
sendJson(res, 200, run);
|
|
420
|
-
return true;
|
|
421
|
-
}
|
|
422
|
-
const jobMatch = path.match(/^\/v1\/jobs\/([^/]+)(?:\/([^/]+))?$/);
|
|
423
|
-
if (jobMatch?.[1]) {
|
|
424
|
-
const runId = decodeURIComponent(jobMatch[1]);
|
|
425
|
-
const child = jobMatch[2];
|
|
426
|
-
if (method === "GET" && !child) {
|
|
427
|
-
const job = runtime.getJob(runId);
|
|
428
|
-
if (!job)
|
|
429
|
-
sendError(res, 404, "Job run not found.");
|
|
430
|
-
else
|
|
431
|
-
sendJson(res, 200, job);
|
|
432
|
-
return true;
|
|
433
|
-
}
|
|
434
|
-
if (method === "GET" && child === "events") {
|
|
435
|
-
const events = runtime.getJobEvents(runId);
|
|
436
|
-
if (!events)
|
|
437
|
-
sendError(res, 404, "Job run not found.");
|
|
438
|
-
else
|
|
439
|
-
sendJson(res, 200, { events });
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
if (method === "POST" && path === "/v1/readiness-check-drafts") {
|
|
444
|
-
const body = await readJsonBody(req);
|
|
445
|
-
const job = await runtime.createReadinessCheckDraftRun(body);
|
|
446
|
-
sendJson(res, 202, job);
|
|
447
|
-
return true;
|
|
448
|
-
}
|
|
449
|
-
if (method === "POST" &&
|
|
450
|
-
path === LOCAL_SERVICE_ROUTES.methodAuthoringRuns) {
|
|
451
|
-
const body = await readJsonBody(req);
|
|
452
|
-
const job = await runtime.createMethodAuthoringRun(body);
|
|
453
|
-
sendJson(res, 202, job);
|
|
454
|
-
return true;
|
|
455
|
-
}
|
|
456
|
-
if (method === "POST" &&
|
|
457
|
-
path === LOCAL_SERVICE_ROUTES.methodImprovementRuns) {
|
|
458
|
-
const body = await readJsonBody(req);
|
|
459
|
-
const job = await runtime.createMethodAuthoringRun(body, "method-improvement");
|
|
460
|
-
sendJson(res, 202, job);
|
|
461
|
-
return true;
|
|
462
|
-
}
|
|
463
|
-
if (method === "GET" && path === "/v1/compile-runs") {
|
|
464
|
-
sendJson(res, 200, { compile_runs: runtime.listCompileRuns() });
|
|
465
|
-
return true;
|
|
466
|
-
}
|
|
467
|
-
if (method === "POST" && path === "/v1/compile-runs") {
|
|
468
|
-
try {
|
|
469
|
-
const body = await readJsonBody(req);
|
|
470
|
-
const resource = await runtime.createCompileRun(body);
|
|
471
|
-
sendJson(res, 202, resource);
|
|
472
|
-
}
|
|
473
|
-
catch (error) {
|
|
474
|
-
sendError(res, 409, error instanceof Error ? error.message : String(error));
|
|
475
|
-
}
|
|
476
|
-
return true;
|
|
477
|
-
}
|
|
706
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
707
|
+
// Compile-run sub-resources (instance-wide; runs are addressable by run_id).
|
|
708
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
478
709
|
const compileRunMatch = path.match(/^\/v1\/compile-runs\/([^/]+)(?:\/([^/]+))?$/);
|
|
479
710
|
if (compileRunMatch?.[1]) {
|
|
480
711
|
const runId = decodeURIComponent(compileRunMatch[1]);
|
|
481
712
|
const child = compileRunMatch[2];
|
|
713
|
+
const firstPrep = listStoredPreparations()[0];
|
|
714
|
+
const prepDataDir = firstPrep?.prepDataDir ?? runtime.rootPath;
|
|
482
715
|
if (method === "GET" && !child) {
|
|
483
|
-
const run = runtime.getCompileRun(runId);
|
|
716
|
+
const run = runtime.getCompileRun(prepDataDir, runId);
|
|
484
717
|
if (!run)
|
|
485
718
|
sendError(res, 404, "Compile run not found.");
|
|
486
719
|
else
|
|
@@ -488,7 +721,7 @@ async function routeApi(req, res, runtime) {
|
|
|
488
721
|
return true;
|
|
489
722
|
}
|
|
490
723
|
if (method === "GET" && child === "events") {
|
|
491
|
-
const events = runtime.getCompileRunEvents(runId);
|
|
724
|
+
const events = runtime.getCompileRunEvents(prepDataDir, runId);
|
|
492
725
|
if (!events)
|
|
493
726
|
sendError(res, 404, "Compile run not found.");
|
|
494
727
|
else
|
|
@@ -496,7 +729,7 @@ async function routeApi(req, res, runtime) {
|
|
|
496
729
|
return true;
|
|
497
730
|
}
|
|
498
731
|
if (method === "GET" && child === "proof") {
|
|
499
|
-
const proof = runtime.getCompileRunProof(runId);
|
|
732
|
+
const proof = runtime.getCompileRunProof(prepDataDir, runId);
|
|
500
733
|
if (!proof)
|
|
501
734
|
sendError(res, 404, "Compile run not found.");
|
|
502
735
|
else
|
|
@@ -504,7 +737,7 @@ async function routeApi(req, res, runtime) {
|
|
|
504
737
|
return true;
|
|
505
738
|
}
|
|
506
739
|
if (method === "GET" && child === "artifacts") {
|
|
507
|
-
const artifacts = runtime.getCompileRunArtifacts(runId);
|
|
740
|
+
const artifacts = runtime.getCompileRunArtifacts(prepDataDir, runId);
|
|
508
741
|
if (!artifacts)
|
|
509
742
|
sendError(res, 404, "Compile run not found.");
|
|
510
743
|
else
|
|
@@ -512,40 +745,76 @@ async function routeApi(req, res, runtime) {
|
|
|
512
745
|
return true;
|
|
513
746
|
}
|
|
514
747
|
if (method === "POST" && child === "cancel") {
|
|
515
|
-
|
|
748
|
+
const existing = runtime.getCompileRun(prepDataDir, runId);
|
|
749
|
+
if (!existing) {
|
|
750
|
+
sendError(res, 404, "Compile run not found.");
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
const result = runtime.cancelCompileRun(runId);
|
|
754
|
+
sendJson(res, 200, result);
|
|
516
755
|
return true;
|
|
517
756
|
}
|
|
518
757
|
}
|
|
519
|
-
if (method === "GET" && path === "/v1/test-runs") {
|
|
520
|
-
sendJson(res, 200, { test_runs: runtime.listTestRuns() });
|
|
521
|
-
return true;
|
|
522
|
-
}
|
|
523
|
-
if (method === "POST" && path === "/v1/test-runs") {
|
|
524
|
-
const body = await readJsonBody(req);
|
|
525
|
-
const resource = await runtime.createTestRun(body);
|
|
526
|
-
sendJson(res, 202, resource);
|
|
527
|
-
return true;
|
|
528
|
-
}
|
|
529
758
|
const testRunMatch = path.match(/^\/v1\/test-runs\/([^/]+)$/);
|
|
530
759
|
if (method === "GET" && testRunMatch?.[1]) {
|
|
531
|
-
const
|
|
760
|
+
const firstPrep = listStoredPreparations()[0];
|
|
761
|
+
const run = runtime.getTestRun(firstPrep?.prepDataDir ?? runtime.rootPath, decodeURIComponent(testRunMatch[1]));
|
|
532
762
|
if (!run)
|
|
533
763
|
sendError(res, 404, "Readiness check run not found.");
|
|
534
764
|
else
|
|
535
765
|
sendJson(res, 200, run);
|
|
536
766
|
return true;
|
|
537
767
|
}
|
|
538
|
-
|
|
539
|
-
|
|
768
|
+
const jobMatch = path.match(/^\/v1\/jobs\/([^/]+)(?:\/([^/]+))?$/);
|
|
769
|
+
if (jobMatch?.[1]) {
|
|
770
|
+
const runId = decodeURIComponent(jobMatch[1]);
|
|
771
|
+
const child = jobMatch[2];
|
|
772
|
+
const firstPrep = listStoredPreparations()[0];
|
|
773
|
+
const prepDataDir = firstPrep?.prepDataDir ?? runtime.rootPath;
|
|
774
|
+
if (method === "GET" && !child) {
|
|
775
|
+
const job = runtime.getJob(prepDataDir, runId);
|
|
776
|
+
if (!job)
|
|
777
|
+
sendError(res, 404, "Job run not found.");
|
|
778
|
+
else
|
|
779
|
+
sendJson(res, 200, job);
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
if (method === "GET" && child === "events") {
|
|
783
|
+
const events = runtime.getJobEvents(prepDataDir, runId);
|
|
784
|
+
if (!events)
|
|
785
|
+
sendError(res, 404, "Job run not found.");
|
|
786
|
+
else
|
|
787
|
+
sendJson(res, 200, { events });
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
792
|
+
// Executor + open-path — instance-wide.
|
|
793
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
794
|
+
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.executor) {
|
|
795
|
+
sendJson(res, 200, runtime.getExecutorStatus());
|
|
540
796
|
return true;
|
|
541
797
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
798
|
+
if (method === "POST" && path === LOCAL_SERVICE_ROUTES.executor) {
|
|
799
|
+
const body = await readJsonBody(req);
|
|
800
|
+
sendJson(res, 202, runtime.selectExecutor(body));
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
if (method === "POST" && path === LOCAL_SERVICE_ROUTES.openPath) {
|
|
804
|
+
const body = OpenPathRequestSchema.parse(await readJsonBody(req));
|
|
805
|
+
// Permit opening any registered preparation root or its bound source folder.
|
|
806
|
+
const allowedRoots = [];
|
|
807
|
+
for (const stored of listStoredPreparations()) {
|
|
808
|
+
allowedRoots.push(stored.prepDataDir);
|
|
809
|
+
if (stored.source.locator)
|
|
810
|
+
allowedRoots.push(stored.source.locator);
|
|
811
|
+
}
|
|
812
|
+
if (allowedRoots.length === 0) {
|
|
813
|
+
sendError(res, 400, "No preparation registered; nothing to open.");
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
const openedPath = await openLocalPath(allowedRoots, body.path);
|
|
817
|
+
sendJson(res, 202, { opened: true, path: openedPath });
|
|
549
818
|
return true;
|
|
550
819
|
}
|
|
551
820
|
return false;
|
|
@@ -553,6 +822,13 @@ async function routeApi(req, res, runtime) {
|
|
|
553
822
|
export function createLocalServiceServer(runtime) {
|
|
554
823
|
return createServer((req, res) => {
|
|
555
824
|
void (async () => {
|
|
825
|
+
// Pre-attach a CORS context so static-asset GETs and the 404
|
|
826
|
+
// fallback emit the right headers even before routeApi has a
|
|
827
|
+
// chance to set its own. The OPTIONS preflight handling and the
|
|
828
|
+
// mutating-method guards still happen inside routeApi.
|
|
829
|
+
const origin = originHeaderValue(req);
|
|
830
|
+
const allowed = buildAllowedOrigins(runtime.host, runtime.port);
|
|
831
|
+
attachResponseContext(res, { cors: corsHeadersFor(origin, allowed) });
|
|
556
832
|
try {
|
|
557
833
|
const routed = await routeApi(req, res, runtime);
|
|
558
834
|
if (routed)
|
|
@@ -569,57 +845,149 @@ export function createLocalServiceServer(runtime) {
|
|
|
569
845
|
}
|
|
570
846
|
function resolvePort(optionPort) {
|
|
571
847
|
if (typeof optionPort === "number")
|
|
572
|
-
return optionPort;
|
|
848
|
+
return { port: optionPort, pinned: true };
|
|
573
849
|
const envPort = Number.parseInt(process.env.INTERF_SERVICE_PORT ?? "", 10);
|
|
574
|
-
|
|
850
|
+
if (Number.isInteger(envPort))
|
|
851
|
+
return { port: envPort, pinned: true };
|
|
852
|
+
return { port: LOCAL_SERVICE_DEFAULT_PORT, pinned: false };
|
|
853
|
+
}
|
|
854
|
+
const LOCAL_SERVICE_PORT_FALLBACK_RANGE = 50;
|
|
855
|
+
async function listenWithFallback(server, host, startPort, pinned) {
|
|
856
|
+
const maxPort = pinned ? startPort : startPort + LOCAL_SERVICE_PORT_FALLBACK_RANGE;
|
|
857
|
+
for (let port = startPort; port <= maxPort; port += 1) {
|
|
858
|
+
try {
|
|
859
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
860
|
+
const onError = (error) => {
|
|
861
|
+
server.off("listening", onListening);
|
|
862
|
+
rejectListen(error);
|
|
863
|
+
};
|
|
864
|
+
const onListening = () => {
|
|
865
|
+
server.off("error", onError);
|
|
866
|
+
resolveListen();
|
|
867
|
+
};
|
|
868
|
+
server.once("error", onError);
|
|
869
|
+
server.once("listening", onListening);
|
|
870
|
+
server.listen(port, host);
|
|
871
|
+
});
|
|
872
|
+
return port;
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
const code = error?.code;
|
|
876
|
+
if (code !== "EADDRINUSE" || port === maxPort)
|
|
877
|
+
throw error;
|
|
878
|
+
// Retry on the next port; the server will emit a fresh "listening" event on the next attempt.
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
throw new Error(`Could not bind a free port between ${startPort} and ${maxPort}.`);
|
|
575
882
|
}
|
|
576
883
|
export async function startLocalService(options = {}) {
|
|
884
|
+
const requestedPort = typeof options.searchFromPort === "number"
|
|
885
|
+
? { port: options.searchFromPort, pinned: false }
|
|
886
|
+
: resolvePort(options.port);
|
|
887
|
+
const requestedHost = options.host ?? process.env.INTERF_SERVICE_HOST ?? LOCAL_SERVICE_DEFAULT_HOST;
|
|
888
|
+
// Reject non-loopback host bindings before we even configure anything.
|
|
889
|
+
// This guards against `INTERF_SERVICE_HOST=0.0.0.0` env injection or a
|
|
890
|
+
// call site that passes a LAN IP. The schema-level refinement also
|
|
891
|
+
// rejects, but we want a clearer error message.
|
|
892
|
+
if (!LOCAL_SERVICE_LOOPBACK_HOSTS.includes(requestedHost)) {
|
|
893
|
+
throw new Error(`Refusing to bind local service to non-loopback host '${requestedHost}'. ` +
|
|
894
|
+
`Use one of ${LOCAL_SERVICE_LOOPBACK_HOSTS.join(", ")} ` +
|
|
895
|
+
`(adjust the --host flag or INTERF_SERVICE_HOST env var).`);
|
|
896
|
+
}
|
|
577
897
|
const config = LocalServiceConfigSchema.parse({
|
|
578
|
-
host:
|
|
579
|
-
port:
|
|
898
|
+
host: requestedHost,
|
|
899
|
+
port: requestedPort.port,
|
|
580
900
|
});
|
|
581
901
|
const rootPath = options.rootPath ?? process.cwd();
|
|
902
|
+
const resolvedRoot = resolve(rootPath);
|
|
903
|
+
// 0.13 auth-token policy: default to `null` (no token) on loopback. CORS
|
|
904
|
+
// + loopback bind enforces the security boundary. Callers can opt in by
|
|
905
|
+
// passing `"auto"` (generate fresh) or a pinned string (tests).
|
|
906
|
+
const authToken = (() => {
|
|
907
|
+
if (options.authToken === null)
|
|
908
|
+
return null;
|
|
909
|
+
if (options.authToken === "auto")
|
|
910
|
+
return createLocalServiceAuthToken();
|
|
911
|
+
if (typeof options.authToken === "string")
|
|
912
|
+
return options.authToken;
|
|
913
|
+
return null;
|
|
914
|
+
})();
|
|
582
915
|
const runtime = createLocalServiceRuntime({
|
|
583
|
-
rootPath,
|
|
916
|
+
rootPath: resolvedRoot,
|
|
584
917
|
host: config.host,
|
|
585
918
|
port: config.port,
|
|
586
919
|
packageVersion: options.packageVersion,
|
|
587
920
|
handlers: options.handlers,
|
|
921
|
+
...(authToken ? { authToken } : {}),
|
|
588
922
|
});
|
|
923
|
+
// Rehydrate 0.13 preparations as synthetic workspaces so subsequent
|
|
924
|
+
// compile / test / readiness calls find them after a service restart.
|
|
925
|
+
try {
|
|
926
|
+
rehydratePreparations(runtime);
|
|
927
|
+
}
|
|
928
|
+
catch {
|
|
929
|
+
// best effort
|
|
930
|
+
}
|
|
589
931
|
const server = createLocalServiceServer(runtime);
|
|
590
|
-
await
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
932
|
+
const boundPort = await listenWithFallback(server, config.host, config.port, requestedPort.pinned);
|
|
933
|
+
runtime.setBoundPort(boundPort);
|
|
934
|
+
const health = runtime.health();
|
|
935
|
+
const url = health.service_url;
|
|
936
|
+
const startedAt = health.instance_started_at ?? new Date().toISOString();
|
|
937
|
+
// Central stop-lookup registry — one entry per running instance.
|
|
938
|
+
// (Server-side write only; CLI no longer reads this.)
|
|
939
|
+
const writeRegistryEntry = () => {
|
|
940
|
+
try {
|
|
941
|
+
registerServiceLocally(ServiceRegistryEntrySchema.parse({
|
|
942
|
+
pid: process.pid,
|
|
943
|
+
host: config.host,
|
|
944
|
+
port: boundPort,
|
|
945
|
+
url,
|
|
946
|
+
started_at: startedAt,
|
|
947
|
+
workspaces: runtime.registeredPreparationSnapshots(),
|
|
948
|
+
...(authToken ? { auth_token: authToken } : {}),
|
|
949
|
+
}));
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
// best effort: registry corruption shouldn't take the service down
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
writeRegistryEntry();
|
|
956
|
+
runtime.setOnRegistryChanged(writeRegistryEntry);
|
|
957
|
+
// Write the 0.13 single-record CLI connection so `interf prep`,
|
|
958
|
+
// `interf compile`, etc. can find this engine without a pointer file.
|
|
959
|
+
try {
|
|
960
|
+
writeConnection({ url, auth_token: authToken });
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
// best effort: connection file is convenience, not correctness
|
|
964
|
+
}
|
|
612
965
|
return {
|
|
613
966
|
runtime,
|
|
614
967
|
server,
|
|
615
968
|
url,
|
|
616
969
|
close: () => new Promise((resolveClose, rejectClose) => {
|
|
970
|
+
runtime.setOnRegistryChanged(null);
|
|
617
971
|
server.close((error) => {
|
|
618
972
|
if (error) {
|
|
619
973
|
rejectClose(error);
|
|
620
974
|
return;
|
|
621
975
|
}
|
|
622
|
-
|
|
976
|
+
try {
|
|
977
|
+
unregisterService(process.pid);
|
|
978
|
+
}
|
|
979
|
+
catch {
|
|
980
|
+
// best effort
|
|
981
|
+
}
|
|
982
|
+
try {
|
|
983
|
+
// Clear connection record only if it still points at us.
|
|
984
|
+
const current = readActiveConnection();
|
|
985
|
+
if (current?.url === url)
|
|
986
|
+
clearConnection();
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
// best effort
|
|
990
|
+
}
|
|
623
991
|
resolveClose();
|
|
624
992
|
});
|
|
625
993
|
}),
|