@interf/compiler 0.9.4 → 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 -91
- package/TRADEMARKS.md +2 -13
- package/agent-skills/interf-actions/SKILL.md +97 -32
- package/agent-skills/interf-actions/references/cli.md +124 -71
- 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 +9 -31
- package/dist/cli/commands/compile.js +75 -388
- 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 -15
- 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 +61 -220
- package/dist/cli/commands/test.d.ts +6 -15
- package/dist/cli/commands/test.js +63 -342
- package/dist/cli/commands/web.d.ts +0 -9
- package/dist/cli/commands/web.js +140 -367
- 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/045gole2ojo3g.css +3 -0
- package/dist/compiler-ui/_next/static/chunks/17t-lulmyawg5.js +89 -0
- 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/index.d.ts +0 -23
- package/dist/index.js +0 -16
- package/dist/packages/agents/lib/shells.d.ts +1 -1
- package/dist/packages/agents/lib/shells.js +113 -54
- 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 +27 -32
- package/dist/packages/compiler/lib/schema.js +2 -13
- 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 -6
- package/dist/packages/contracts/lib/schema.js +46 -2
- package/dist/packages/execution/lib/schema.d.ts +50 -57
- package/dist/packages/execution/lib/schema.js +1 -2
- package/dist/packages/local-service/action-definitions.d.ts +246 -0
- package/dist/packages/local-service/action-definitions.js +1147 -0
- package/dist/packages/local-service/action-planner.d.ts +9 -0
- package/dist/packages/local-service/action-planner.js +135 -0
- package/dist/packages/local-service/action-values.d.ts +1 -23
- package/dist/packages/local-service/action-values.js +1 -31
- package/dist/packages/local-service/client.d.ts +76 -46
- package/dist/packages/local-service/client.js +184 -149
- 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 +14 -7
- package/dist/packages/local-service/index.js +8 -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 +689 -2575
- package/dist/packages/local-service/lib/schema.js +260 -101
- package/dist/packages/local-service/native-run-handlers.d.ts +23 -0
- package/dist/{cli/commands/compile-controller.js → packages/local-service/native-run-handlers.js} +204 -20
- package/dist/packages/local-service/preparation-store.d.ts +92 -0
- package/dist/packages/local-service/preparation-store.js +171 -0
- package/dist/{cli/commands/check-draft.d.ts → packages/local-service/readiness-check-draft.d.ts} +2 -2
- package/dist/packages/local-service/routes.d.ts +33 -11
- package/dist/packages/local-service/routes.js +44 -15
- package/dist/packages/local-service/run-observability.js +25 -27
- 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 +201 -44
- package/dist/packages/local-service/runtime.js +1062 -1106
- package/dist/packages/local-service/server.d.ts +15 -0
- package/dist/packages/local-service/server.js +651 -233
- 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-review-paths.d.ts +1 -1
- package/dist/packages/method-package/method-review-paths.js +5 -5
- 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 +1 -1
- package/dist/packages/project-model/index.js +1 -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 +11 -0
- package/dist/packages/project-model/preparation-entries.js +49 -0
- package/dist/packages/project-model/source-config.d.ts +11 -10
- package/dist/packages/project-model/source-config.js +83 -44
- 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 +12 -13
- package/dist/packages/testing/lib/schema.js +4 -5
- package/dist/packages/testing/readiness-check-run.d.ts +7 -7
- package/dist/packages/testing/readiness-check-run.js +46 -51
- 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 +6 -26
- package/LICENSE +0 -183
- package/dist/cli/commands/compile-controller.d.ts +0 -17
- package/dist/cli/commands/compiled-flow.d.ts +0 -25
- package/dist/cli/commands/compiled-flow.js +0 -112
- 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 -76
- package/dist/cli/commands/create-method-wizard.js +0 -465
- package/dist/cli/commands/create.d.ts +0 -8
- package/dist/cli/commands/create.js +0 -189
- 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 -11
- package/dist/cli/commands/init.js +0 -784
- package/dist/cli/commands/list.d.ts +0 -2
- package/dist/cli/commands/list.js +0 -30
- package/dist/cli/commands/preparation-selection.d.ts +0 -6
- package/dist/cli/commands/preparation-selection.js +0 -11
- package/dist/cli/commands/source-config-wizard.d.ts +0 -52
- package/dist/cli/commands/source-config-wizard.js +0 -680
- package/dist/cli/commands/test-flow.d.ts +0 -58
- package/dist/cli/commands/test-flow.js +0 -231
- package/dist/cli/commands/verify.d.ts +0 -2
- package/dist/cli/commands/verify.js +0 -94
- package/dist/compiler-ui/_next/static/chunks/0d~8t0zm6545p.js +0 -118
- package/dist/compiler-ui/_next/static/chunks/0xnel.ax9a.2c.css +0 -3
- 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/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_buildManifest.js +0 -0
- /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_clientMiddlewareManifest.js +0 -0
- /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_ssgManifest.js +0 -0
- /package/dist/{cli/commands/check-draft.js → packages/local-service/readiness-check-draft.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,
|
|
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 localInstancePointerPath(rootPath) {
|
|
29
|
-
return join(rootPath, ...LOCAL_SERVICE_INSTANCE_POINTER_PATH);
|
|
30
|
-
}
|
|
31
|
-
function writeLocalInstancePointer(rootPath, pointer) {
|
|
32
|
-
const pointerPath = localInstancePointerPath(rootPath);
|
|
33
|
-
mkdirSync(dirname(pointerPath), { recursive: true });
|
|
34
|
-
const parsed = LocalServiceInstancePointerSchema.parse(pointer);
|
|
35
|
-
writeFileSync(pointerPath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
36
|
-
}
|
|
37
|
-
function removeLocalInstancePointer(rootPath, serviceUrl) {
|
|
38
|
-
const pointerPath = localInstancePointerPath(rootPath);
|
|
39
|
-
if (!existsSync(pointerPath))
|
|
40
|
-
return;
|
|
41
|
-
try {
|
|
42
|
-
const pointer = LocalServiceInstancePointerSchema.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,32 +162,60 @@ 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
|
},
|
|
104
|
-
});
|
|
218
|
+
}));
|
|
105
219
|
}
|
|
106
220
|
function resolveOpenPath(allowedRoots, pathValue) {
|
|
107
221
|
const roots = allowedRoots.map((root) => resolve(root));
|
|
@@ -211,120 +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
380
|
executor: LOCAL_SERVICE_ROUTES.executor,
|
|
381
|
+
open_path: LOCAL_SERVICE_ROUTES.openPath,
|
|
237
382
|
},
|
|
238
383
|
}));
|
|
239
384
|
return true;
|
|
240
385
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
});
|
|
256
406
|
return true;
|
|
257
407
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const openedPath = await openLocalPath(allowedRoots, body.path);
|
|
266
|
-
sendJson(res, 202, { opened: true, path: openedPath });
|
|
408
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
409
|
+
// Preparation collection routes
|
|
410
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
411
|
+
// GET /v1/preparations — list every preparation on the instance.
|
|
412
|
+
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.preparations) {
|
|
413
|
+
const items = listStoredPreparations().map(preparationWireShape);
|
|
414
|
+
sendJson(res, 200, { preparations: items });
|
|
267
415
|
return true;
|
|
268
416
|
}
|
|
269
|
-
|
|
270
|
-
if (method === "
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
417
|
+
// POST /v1/preparations — create a new preparation.
|
|
418
|
+
if (method === "POST" && path === LOCAL_SERVICE_ROUTES.preparations) {
|
|
419
|
+
try {
|
|
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;
|
|
424
|
+
}
|
|
425
|
+
if (!body.id || typeof body.id !== "string") {
|
|
426
|
+
sendError(res, 400, "Missing required field: id");
|
|
427
|
+
return true;
|
|
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));
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
sendError(res, 400, error instanceof Error ? error.message : String(error));
|
|
450
|
+
}
|
|
276
451
|
return true;
|
|
277
452
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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;
|
|
466
|
+
}
|
|
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
|
+
}
|
|
479
|
+
}
|
|
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
|
+
}
|
|
511
|
+
}
|
|
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
|
+
}
|
|
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}`);
|
|
285
624
|
return true;
|
|
286
625
|
}
|
|
626
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
627
|
+
// Method resources — preparation-independent (workspace draft + user lib + bundled).
|
|
628
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
287
629
|
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.methods) {
|
|
288
|
-
|
|
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) });
|
|
289
635
|
return true;
|
|
290
636
|
}
|
|
291
637
|
const methodMatch = path.match(/^\/v1\/methods\/([^/]+)$/);
|
|
292
638
|
if (method === "GET" && methodMatch?.[1]) {
|
|
293
|
-
const
|
|
639
|
+
const firstPrep = listStoredPreparations()[0];
|
|
640
|
+
const methodResource = runtime.getMethod(firstPrep?.prepDataDir ?? runtime.rootPath, decodeURIComponent(methodMatch[1]));
|
|
294
641
|
if (!methodResource)
|
|
295
642
|
sendError(res, 404, "Method not found.");
|
|
296
643
|
else
|
|
297
644
|
sendJson(res, 200, methodResource);
|
|
298
645
|
return true;
|
|
299
646
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.
|
|
305
|
-
|
|
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) });
|
|
306
654
|
return true;
|
|
307
655
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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);
|
|
311
664
|
return true;
|
|
312
665
|
}
|
|
666
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
667
|
+
// Action proposals (chat / wizard) — instance-wide.
|
|
668
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
313
669
|
if (method === "GET" && path === LOCAL_SERVICE_ROUTES.actionProposals) {
|
|
314
|
-
|
|
670
|
+
const firstPrep = listStoredPreparations()[0];
|
|
671
|
+
sendJson(res, 200, {
|
|
672
|
+
action_proposals: runtime.listActionProposals(firstPrep?.prepDataDir ?? runtime.rootPath),
|
|
673
|
+
});
|
|
315
674
|
return true;
|
|
316
675
|
}
|
|
317
676
|
if (method === "POST" && path === LOCAL_SERVICE_ROUTES.actionProposals) {
|
|
318
677
|
const body = await readJsonBody(req);
|
|
319
|
-
|
|
678
|
+
const firstPrep = listStoredPreparations()[0];
|
|
679
|
+
sendJson(res, 202, await runtime.createActionProposal(firstPrep?.prepDataDir ?? runtime.rootPath, body));
|
|
320
680
|
return true;
|
|
321
681
|
}
|
|
322
682
|
const actionProposalMatch = path.match(/^\/v1\/action-proposals\/([^/]+)(?:\/([^/]+))?$/);
|
|
323
683
|
if (actionProposalMatch?.[1]) {
|
|
324
684
|
const proposalId = decodeURIComponent(actionProposalMatch[1]);
|
|
325
685
|
const child = actionProposalMatch[2];
|
|
686
|
+
const firstPrep = listStoredPreparations()[0];
|
|
687
|
+
const prepDataDir = firstPrep?.prepDataDir ?? runtime.rootPath;
|
|
326
688
|
if (method === "GET" && !child) {
|
|
327
|
-
const proposal = runtime.getActionProposal(proposalId);
|
|
689
|
+
const proposal = runtime.getActionProposal(prepDataDir, proposalId);
|
|
328
690
|
if (!proposal)
|
|
329
691
|
sendError(res, 404, "Action proposal not found.");
|
|
330
692
|
else
|
|
@@ -333,7 +695,7 @@ async function routeApi(req, res, runtime) {
|
|
|
333
695
|
}
|
|
334
696
|
if (method === "POST" && child === "decision") {
|
|
335
697
|
const body = await readJsonBody(req);
|
|
336
|
-
const proposal = await runtime.decideActionProposal(proposalId, body);
|
|
698
|
+
const proposal = await runtime.decideActionProposal(prepDataDir, proposalId, body);
|
|
337
699
|
if (!proposal)
|
|
338
700
|
sendError(res, 404, "Action proposal not found.");
|
|
339
701
|
else
|
|
@@ -341,96 +703,17 @@ async function routeApi(req, res, runtime) {
|
|
|
341
703
|
return true;
|
|
342
704
|
}
|
|
343
705
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
if (method === "POST" && path === LOCAL_SERVICE_ROUTES.runs) {
|
|
349
|
-
const request = RunCreateRequestSchema.parse(await readJsonBody(req));
|
|
350
|
-
if (request.run_type === "compile" || request.run_type === "prepare") {
|
|
351
|
-
const { run_type: _ignoredRunType, ...compileRequest } = request;
|
|
352
|
-
const resource = await runtime.createCompileRun(compileRequest);
|
|
353
|
-
sendJson(res, 202, runtime.getRunObservability(resource.run.run_id) ?? resource);
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
const { run_type: _ignoredRunType, ...testRequest } = request;
|
|
357
|
-
const resource = await runtime.createTestRun(testRequest);
|
|
358
|
-
sendJson(res, 202, runtime.getRunObservability(resource.run_id) ?? resource);
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
const observableRunMatch = path.match(/^\/v1\/runs\/([^/]+)$/);
|
|
362
|
-
if (method === "GET" && observableRunMatch?.[1]) {
|
|
363
|
-
const run = runtime.getRunObservability(decodeURIComponent(observableRunMatch[1]));
|
|
364
|
-
if (!run)
|
|
365
|
-
sendError(res, 404, "Run not found.");
|
|
366
|
-
else
|
|
367
|
-
sendJson(res, 200, run);
|
|
368
|
-
return true;
|
|
369
|
-
}
|
|
370
|
-
if (method === "POST" && path === "/v1/jobs") {
|
|
371
|
-
const body = await readJsonBody(req);
|
|
372
|
-
sendJson(res, 202, runtime.createJobRun(body));
|
|
373
|
-
return true;
|
|
374
|
-
}
|
|
375
|
-
const jobMatch = path.match(/^\/v1\/jobs\/([^/]+)(?:\/([^/]+))?$/);
|
|
376
|
-
if (jobMatch?.[1]) {
|
|
377
|
-
const runId = decodeURIComponent(jobMatch[1]);
|
|
378
|
-
const child = jobMatch[2];
|
|
379
|
-
if (method === "GET" && !child) {
|
|
380
|
-
const job = runtime.getJob(runId);
|
|
381
|
-
if (!job)
|
|
382
|
-
sendError(res, 404, "Job run not found.");
|
|
383
|
-
else
|
|
384
|
-
sendJson(res, 200, job);
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
if (method === "GET" && child === "events") {
|
|
388
|
-
const events = runtime.getJobEvents(runId);
|
|
389
|
-
if (!events)
|
|
390
|
-
sendError(res, 404, "Job run not found.");
|
|
391
|
-
else
|
|
392
|
-
sendJson(res, 200, { events });
|
|
393
|
-
return true;
|
|
394
|
-
}
|
|
395
|
-
if (method === "POST" && child === "events") {
|
|
396
|
-
const body = await readJsonBody(req);
|
|
397
|
-
const job = runtime.appendJobRunEvent(runId, body);
|
|
398
|
-
if (!job)
|
|
399
|
-
sendError(res, 404, "Job run not found.");
|
|
400
|
-
else
|
|
401
|
-
sendJson(res, 202, job);
|
|
402
|
-
return true;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
if (method === "POST" && path === "/v1/readiness-check-drafts") {
|
|
406
|
-
const body = await readJsonBody(req);
|
|
407
|
-
const job = await runtime.createReadinessCheckDraftRun(body);
|
|
408
|
-
sendJson(res, 202, job);
|
|
409
|
-
return true;
|
|
410
|
-
}
|
|
411
|
-
if (method === "POST" &&
|
|
412
|
-
path === LOCAL_SERVICE_ROUTES.methodAuthoringRuns) {
|
|
413
|
-
const body = await readJsonBody(req);
|
|
414
|
-
const job = await runtime.createMethodAuthoringRun(body);
|
|
415
|
-
sendJson(res, 202, job);
|
|
416
|
-
return true;
|
|
417
|
-
}
|
|
418
|
-
if (method === "GET" && path === "/v1/compile-runs") {
|
|
419
|
-
sendJson(res, 200, { compile_runs: runtime.listCompileRuns() });
|
|
420
|
-
return true;
|
|
421
|
-
}
|
|
422
|
-
if (method === "POST" && path === "/v1/compile-runs") {
|
|
423
|
-
const body = await readJsonBody(req);
|
|
424
|
-
const resource = await runtime.createCompileRun(body);
|
|
425
|
-
sendJson(res, 202, resource);
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
706
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
707
|
+
// Compile-run sub-resources (instance-wide; runs are addressable by run_id).
|
|
708
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
428
709
|
const compileRunMatch = path.match(/^\/v1\/compile-runs\/([^/]+)(?:\/([^/]+))?$/);
|
|
429
710
|
if (compileRunMatch?.[1]) {
|
|
430
711
|
const runId = decodeURIComponent(compileRunMatch[1]);
|
|
431
712
|
const child = compileRunMatch[2];
|
|
713
|
+
const firstPrep = listStoredPreparations()[0];
|
|
714
|
+
const prepDataDir = firstPrep?.prepDataDir ?? runtime.rootPath;
|
|
432
715
|
if (method === "GET" && !child) {
|
|
433
|
-
const run = runtime.getCompileRun(runId);
|
|
716
|
+
const run = runtime.getCompileRun(prepDataDir, runId);
|
|
434
717
|
if (!run)
|
|
435
718
|
sendError(res, 404, "Compile run not found.");
|
|
436
719
|
else
|
|
@@ -438,7 +721,7 @@ async function routeApi(req, res, runtime) {
|
|
|
438
721
|
return true;
|
|
439
722
|
}
|
|
440
723
|
if (method === "GET" && child === "events") {
|
|
441
|
-
const events = runtime.getCompileRunEvents(runId);
|
|
724
|
+
const events = runtime.getCompileRunEvents(prepDataDir, runId);
|
|
442
725
|
if (!events)
|
|
443
726
|
sendError(res, 404, "Compile run not found.");
|
|
444
727
|
else
|
|
@@ -446,7 +729,7 @@ async function routeApi(req, res, runtime) {
|
|
|
446
729
|
return true;
|
|
447
730
|
}
|
|
448
731
|
if (method === "GET" && child === "proof") {
|
|
449
|
-
const proof = runtime.getCompileRunProof(runId);
|
|
732
|
+
const proof = runtime.getCompileRunProof(prepDataDir, runId);
|
|
450
733
|
if (!proof)
|
|
451
734
|
sendError(res, 404, "Compile run not found.");
|
|
452
735
|
else
|
|
@@ -454,7 +737,7 @@ async function routeApi(req, res, runtime) {
|
|
|
454
737
|
return true;
|
|
455
738
|
}
|
|
456
739
|
if (method === "GET" && child === "artifacts") {
|
|
457
|
-
const artifacts = runtime.getCompileRunArtifacts(runId);
|
|
740
|
+
const artifacts = runtime.getCompileRunArtifacts(prepDataDir, runId);
|
|
458
741
|
if (!artifacts)
|
|
459
742
|
sendError(res, 404, "Compile run not found.");
|
|
460
743
|
else
|
|
@@ -462,40 +745,76 @@ async function routeApi(req, res, runtime) {
|
|
|
462
745
|
return true;
|
|
463
746
|
}
|
|
464
747
|
if (method === "POST" && child === "cancel") {
|
|
465
|
-
|
|
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);
|
|
466
755
|
return true;
|
|
467
756
|
}
|
|
468
757
|
}
|
|
469
|
-
if (method === "GET" && path === "/v1/test-runs") {
|
|
470
|
-
sendJson(res, 200, { test_runs: runtime.listTestRuns() });
|
|
471
|
-
return true;
|
|
472
|
-
}
|
|
473
|
-
if (method === "POST" && path === "/v1/test-runs") {
|
|
474
|
-
const body = await readJsonBody(req);
|
|
475
|
-
const resource = await runtime.createTestRun(body);
|
|
476
|
-
sendJson(res, 202, resource);
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
758
|
const testRunMatch = path.match(/^\/v1\/test-runs\/([^/]+)$/);
|
|
480
759
|
if (method === "GET" && testRunMatch?.[1]) {
|
|
481
|
-
const
|
|
760
|
+
const firstPrep = listStoredPreparations()[0];
|
|
761
|
+
const run = runtime.getTestRun(firstPrep?.prepDataDir ?? runtime.rootPath, decodeURIComponent(testRunMatch[1]));
|
|
482
762
|
if (!run)
|
|
483
763
|
sendError(res, 404, "Readiness check run not found.");
|
|
484
764
|
else
|
|
485
765
|
sendJson(res, 200, run);
|
|
486
766
|
return true;
|
|
487
767
|
}
|
|
488
|
-
|
|
489
|
-
|
|
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());
|
|
490
796
|
return true;
|
|
491
797
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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 });
|
|
499
818
|
return true;
|
|
500
819
|
}
|
|
501
820
|
return false;
|
|
@@ -503,6 +822,13 @@ async function routeApi(req, res, runtime) {
|
|
|
503
822
|
export function createLocalServiceServer(runtime) {
|
|
504
823
|
return createServer((req, res) => {
|
|
505
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) });
|
|
506
832
|
try {
|
|
507
833
|
const routed = await routeApi(req, res, runtime);
|
|
508
834
|
if (routed)
|
|
@@ -519,57 +845,149 @@ export function createLocalServiceServer(runtime) {
|
|
|
519
845
|
}
|
|
520
846
|
function resolvePort(optionPort) {
|
|
521
847
|
if (typeof optionPort === "number")
|
|
522
|
-
return optionPort;
|
|
848
|
+
return { port: optionPort, pinned: true };
|
|
523
849
|
const envPort = Number.parseInt(process.env.INTERF_SERVICE_PORT ?? "", 10);
|
|
524
|
-
|
|
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}.`);
|
|
525
882
|
}
|
|
526
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
|
+
}
|
|
527
897
|
const config = LocalServiceConfigSchema.parse({
|
|
528
|
-
host:
|
|
529
|
-
port:
|
|
898
|
+
host: requestedHost,
|
|
899
|
+
port: requestedPort.port,
|
|
530
900
|
});
|
|
531
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
|
+
})();
|
|
532
915
|
const runtime = createLocalServiceRuntime({
|
|
533
|
-
rootPath,
|
|
916
|
+
rootPath: resolvedRoot,
|
|
534
917
|
host: config.host,
|
|
535
918
|
port: config.port,
|
|
536
919
|
packageVersion: options.packageVersion,
|
|
537
920
|
handlers: options.handlers,
|
|
921
|
+
...(authToken ? { authToken } : {}),
|
|
538
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
|
+
}
|
|
539
931
|
const server = createLocalServiceServer(runtime);
|
|
540
|
-
await
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
+
}
|
|
562
965
|
return {
|
|
563
966
|
runtime,
|
|
564
967
|
server,
|
|
565
968
|
url,
|
|
566
969
|
close: () => new Promise((resolveClose, rejectClose) => {
|
|
970
|
+
runtime.setOnRegistryChanged(null);
|
|
567
971
|
server.close((error) => {
|
|
568
972
|
if (error) {
|
|
569
973
|
rejectClose(error);
|
|
570
974
|
return;
|
|
571
975
|
}
|
|
572
|
-
|
|
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
|
+
}
|
|
573
991
|
resolveClose();
|
|
574
992
|
});
|
|
575
993
|
}),
|