@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.
Files changed (214) hide show
  1. package/README.md +96 -92
  2. package/TRADEMARKS.md +2 -13
  3. package/agent-skills/interf-actions/SKILL.md +95 -36
  4. package/agent-skills/interf-actions/references/cli.md +118 -51
  5. package/builtin-methods/interf-default/README.md +3 -4
  6. package/builtin-methods/interf-default/compile/stages/shape/SKILL.md +2 -2
  7. package/builtin-methods/interf-default/compile/stages/summarize/SKILL.md +2 -1
  8. package/builtin-methods/interf-default/improve/SKILL.md +1 -1
  9. package/builtin-methods/interf-default/method.json +10 -4
  10. package/builtin-methods/interf-default/method.schema.json +0 -9
  11. package/builtin-methods/interf-default/use/query/SKILL.md +5 -5
  12. package/dist/cli/commands/compile.d.ts +8 -25
  13. package/dist/cli/commands/compile.js +75 -360
  14. package/dist/cli/commands/doctor.js +1 -1
  15. package/dist/cli/commands/login.d.ts +7 -0
  16. package/dist/cli/commands/login.js +39 -0
  17. package/dist/cli/commands/logout.d.ts +2 -0
  18. package/dist/cli/commands/logout.js +16 -0
  19. package/dist/cli/commands/method.d.ts +2 -0
  20. package/dist/cli/commands/method.js +113 -0
  21. package/dist/cli/commands/prep.d.ts +2 -0
  22. package/dist/cli/commands/prep.js +134 -0
  23. package/dist/cli/commands/reset.d.ts +8 -1
  24. package/dist/cli/commands/reset.js +47 -26
  25. package/dist/cli/commands/runs.d.ts +2 -0
  26. package/dist/cli/commands/runs.js +120 -0
  27. package/dist/cli/commands/status.d.ts +6 -1
  28. package/dist/cli/commands/status.js +68 -111
  29. package/dist/cli/commands/test.d.ts +6 -14
  30. package/dist/cli/commands/test.js +65 -181
  31. package/dist/cli/commands/web.d.ts +0 -9
  32. package/dist/cli/commands/web.js +147 -120
  33. package/dist/cli/commands/wizard.d.ts +9 -0
  34. package/dist/cli/commands/wizard.js +442 -0
  35. package/dist/cli/index.d.ts +7 -6
  36. package/dist/cli/index.js +13 -10
  37. package/dist/compiler-ui/404.html +1 -1
  38. package/dist/compiler-ui/__next.__PAGE__.txt +2 -2
  39. package/dist/compiler-ui/__next._full.txt +3 -3
  40. package/dist/compiler-ui/__next._head.txt +1 -1
  41. package/dist/compiler-ui/__next._index.txt +2 -2
  42. package/dist/compiler-ui/__next._tree.txt +2 -2
  43. package/dist/compiler-ui/_next/static/chunks/{18a8f2jkv3z.c.css → 045gole2ojo3g.css} +1 -1
  44. package/dist/compiler-ui/_next/static/chunks/{177mvn4rse235.js → 17t-lulmyawg5.js} +9 -9
  45. package/dist/compiler-ui/_not-found/__next._full.txt +2 -2
  46. package/dist/compiler-ui/_not-found/__next._head.txt +1 -1
  47. package/dist/compiler-ui/_not-found/__next._index.txt +2 -2
  48. package/dist/compiler-ui/_not-found/__next._not-found.__PAGE__.txt +1 -1
  49. package/dist/compiler-ui/_not-found/__next._not-found.txt +1 -1
  50. package/dist/compiler-ui/_not-found/__next._tree.txt +2 -2
  51. package/dist/compiler-ui/_not-found.html +1 -1
  52. package/dist/compiler-ui/_not-found.txt +2 -2
  53. package/dist/compiler-ui/index.html +1 -1
  54. package/dist/compiler-ui/index.txt +3 -3
  55. package/dist/packages/agents/lib/shells.d.ts +1 -1
  56. package/dist/packages/agents/lib/shells.js +111 -52
  57. package/dist/packages/agents/lib/user-config.d.ts +4 -2
  58. package/dist/packages/agents/lib/user-config.js +15 -7
  59. package/dist/packages/compiler/compiled-paths.d.ts +9 -2
  60. package/dist/packages/compiler/compiled-paths.js +30 -15
  61. package/dist/packages/compiler/compiled-pipeline.js +23 -3
  62. package/dist/packages/compiler/compiled-stage-plan.js +4 -0
  63. package/dist/packages/compiler/compiled-target.d.ts +1 -1
  64. package/dist/packages/compiler/compiled-target.js +1 -1
  65. package/dist/packages/compiler/index.d.ts +1 -0
  66. package/dist/packages/compiler/index.js +1 -0
  67. package/dist/packages/compiler/lib/schema.d.ts +26 -31
  68. package/dist/packages/compiler/lib/schema.js +1 -12
  69. package/dist/packages/compiler/method-runs.d.ts +2 -3
  70. package/dist/packages/compiler/method-runs.js +2 -3
  71. package/dist/packages/compiler/reset.js +3 -1
  72. package/dist/packages/compiler/runtime-contracts.js +0 -3
  73. package/dist/packages/compiler/runtime-prompt.js +1 -1
  74. package/dist/packages/compiler/source-files.d.ts +46 -0
  75. package/dist/packages/compiler/source-files.js +149 -0
  76. package/dist/packages/compiler/state-artifacts.d.ts +3 -2
  77. package/dist/packages/compiler/state-artifacts.js +4 -3
  78. package/dist/packages/compiler/state-io.d.ts +3 -2
  79. package/dist/packages/compiler/state-io.js +11 -5
  80. package/dist/packages/compiler/state-paths.d.ts +2 -1
  81. package/dist/packages/compiler/state-paths.js +6 -3
  82. package/dist/packages/compiler/state-view.d.ts +3 -2
  83. package/dist/packages/compiler/state-view.js +18 -28
  84. package/dist/packages/compiler/state.d.ts +4 -4
  85. package/dist/packages/compiler/state.js +3 -3
  86. package/dist/packages/contracts/index.d.ts +1 -1
  87. package/dist/packages/contracts/lib/preparation-paths.d.ts +117 -0
  88. package/dist/packages/contracts/lib/preparation-paths.js +177 -0
  89. package/dist/packages/contracts/lib/schema.d.ts +85 -5
  90. package/dist/packages/contracts/lib/schema.js +46 -1
  91. package/dist/packages/execution/lib/schema.d.ts +50 -50
  92. package/dist/packages/execution/lib/schema.js +1 -1
  93. package/dist/packages/local-service/action-definitions.d.ts +14 -14
  94. package/dist/packages/local-service/action-definitions.js +27 -28
  95. package/dist/packages/local-service/action-planner.js +2 -1
  96. package/dist/packages/local-service/client.d.ts +51 -52
  97. package/dist/packages/local-service/client.js +132 -140
  98. package/dist/packages/local-service/connection-config.d.ts +38 -0
  99. package/dist/packages/local-service/connection-config.js +75 -0
  100. package/dist/packages/local-service/index.d.ts +11 -7
  101. package/dist/packages/local-service/index.js +6 -4
  102. package/dist/packages/local-service/instance-paths.d.ts +100 -0
  103. package/dist/packages/local-service/instance-paths.js +165 -0
  104. package/dist/packages/local-service/lib/schema.d.ts +405 -2297
  105. package/dist/packages/local-service/lib/schema.js +146 -62
  106. package/dist/packages/local-service/native-run-handlers.js +3 -3
  107. package/dist/packages/local-service/preparation-store.d.ts +92 -0
  108. package/dist/packages/local-service/preparation-store.js +171 -0
  109. package/dist/packages/local-service/routes.d.ts +33 -16
  110. package/dist/packages/local-service/routes.js +44 -20
  111. package/dist/packages/local-service/run-observability.js +11 -11
  112. package/dist/packages/local-service/runtime-caches.d.ts +76 -0
  113. package/dist/packages/local-service/runtime-caches.js +191 -0
  114. package/dist/packages/local-service/runtime-event-applier.d.ts +12 -0
  115. package/dist/packages/local-service/runtime-event-applier.js +177 -0
  116. package/dist/packages/local-service/runtime-persistence.d.ts +47 -0
  117. package/dist/packages/local-service/runtime-persistence.js +137 -0
  118. package/dist/packages/local-service/runtime-proposal-helpers.d.ts +35 -0
  119. package/dist/packages/local-service/runtime-proposal-helpers.js +251 -0
  120. package/dist/packages/local-service/runtime-resource-builders.d.ts +52 -0
  121. package/dist/packages/local-service/runtime-resource-builders.js +149 -0
  122. package/dist/packages/local-service/runtime.d.ts +197 -43
  123. package/dist/packages/local-service/runtime.js +800 -974
  124. package/dist/packages/local-service/server.d.ts +15 -0
  125. package/dist/packages/local-service/server.js +641 -273
  126. package/dist/packages/local-service/service-registry.d.ts +47 -0
  127. package/dist/packages/local-service/service-registry.js +137 -0
  128. package/dist/packages/method-authoring/method-authoring.d.ts +1 -1
  129. package/dist/packages/method-authoring/method-authoring.js +2 -2
  130. package/dist/packages/method-authoring/method-improvement.js +1 -1
  131. package/dist/packages/method-package/builtin-compiled-method.d.ts +4 -5
  132. package/dist/packages/method-package/builtin-compiled-method.js +8 -14
  133. package/dist/packages/method-package/context-interface.d.ts +4 -40
  134. package/dist/packages/method-package/context-interface.js +1 -23
  135. package/dist/packages/method-package/interf-method-package.d.ts +4 -4
  136. package/dist/packages/method-package/interf-method-package.js +21 -33
  137. package/dist/packages/method-package/local-methods.d.ts +10 -6
  138. package/dist/packages/method-package/local-methods.js +57 -39
  139. package/dist/packages/method-package/method-definitions.d.ts +8 -34
  140. package/dist/packages/method-package/method-definitions.js +49 -37
  141. package/dist/packages/method-package/method-helpers.d.ts +1 -13
  142. package/dist/packages/method-package/method-helpers.js +8 -42
  143. package/dist/packages/method-package/method-stage-runner.js +2 -2
  144. package/dist/packages/method-package/user-methods.d.ts +17 -0
  145. package/dist/packages/method-package/user-methods.js +77 -0
  146. package/dist/packages/project-model/index.d.ts +0 -1
  147. package/dist/packages/project-model/index.js +0 -1
  148. package/dist/packages/project-model/interf-detect.d.ts +8 -3
  149. package/dist/packages/project-model/interf-detect.js +34 -34
  150. package/dist/packages/project-model/interf-scaffold.d.ts +3 -3
  151. package/dist/packages/project-model/interf-scaffold.js +23 -32
  152. package/dist/packages/project-model/lib/schema.js +38 -1
  153. package/dist/packages/project-model/preparation-entries.d.ts +5 -5
  154. package/dist/packages/project-model/preparation-entries.js +14 -14
  155. package/dist/packages/project-model/source-config.d.ts +11 -11
  156. package/dist/packages/project-model/source-config.js +74 -46
  157. package/dist/packages/project-model/source-folders.d.ts +5 -5
  158. package/dist/packages/project-model/source-folders.js +14 -14
  159. package/dist/packages/shared/filesystem.d.ts +7 -0
  160. package/dist/packages/shared/filesystem.js +97 -10
  161. package/dist/packages/testing/lib/schema.d.ts +10 -10
  162. package/dist/packages/testing/lib/schema.js +2 -2
  163. package/dist/packages/testing/readiness-check-run.d.ts +4 -4
  164. package/dist/packages/testing/readiness-check-run.js +36 -36
  165. package/dist/packages/testing/test-execution.js +6 -6
  166. package/dist/packages/testing/test-paths.js +4 -3
  167. package/dist/packages/testing/test-sandbox.d.ts +0 -1
  168. package/dist/packages/testing/test-sandbox.js +14 -30
  169. package/dist/packages/testing/test-targets.d.ts +1 -1
  170. package/dist/packages/testing/test-targets.js +6 -6
  171. package/dist/packages/testing/test.d.ts +1 -1
  172. package/dist/packages/testing/test.js +1 -1
  173. package/package.json +3 -4
  174. package/CHANGELOG.md +0 -93
  175. package/LICENSE +0 -183
  176. package/dist/cli/commands/action-input-cli.d.ts +0 -25
  177. package/dist/cli/commands/action-input-cli.js +0 -73
  178. package/dist/cli/commands/control-path.d.ts +0 -11
  179. package/dist/cli/commands/control-path.js +0 -72
  180. package/dist/cli/commands/create-method-wizard.d.ts +0 -64
  181. package/dist/cli/commands/create-method-wizard.js +0 -434
  182. package/dist/cli/commands/create.d.ts +0 -6
  183. package/dist/cli/commands/create.js +0 -183
  184. package/dist/cli/commands/default.d.ts +0 -2
  185. package/dist/cli/commands/default.js +0 -39
  186. package/dist/cli/commands/executor-flow.d.ts +0 -29
  187. package/dist/cli/commands/executor-flow.js +0 -163
  188. package/dist/cli/commands/init.d.ts +0 -26
  189. package/dist/cli/commands/init.js +0 -771
  190. package/dist/cli/commands/list.d.ts +0 -2
  191. package/dist/cli/commands/list.js +0 -30
  192. package/dist/cli/commands/preparation-action.d.ts +0 -8
  193. package/dist/cli/commands/preparation-action.js +0 -29
  194. package/dist/cli/commands/preparation-picker.d.ts +0 -5
  195. package/dist/cli/commands/preparation-picker.js +0 -36
  196. package/dist/cli/commands/preparation-selection.d.ts +0 -6
  197. package/dist/cli/commands/preparation-selection.js +0 -11
  198. package/dist/cli/commands/service-action-flow.d.ts +0 -9
  199. package/dist/cli/commands/service-action-flow.js +0 -19
  200. package/dist/cli/commands/source-config-wizard.d.ts +0 -51
  201. package/dist/cli/commands/source-config-wizard.js +0 -670
  202. package/dist/cli/commands/verify.d.ts +0 -2
  203. package/dist/cli/commands/verify.js +0 -94
  204. package/dist/packages/compiler/raw-snapshot.d.ts +0 -49
  205. package/dist/packages/compiler/raw-snapshot.js +0 -101
  206. package/dist/packages/method-package/index.d.ts +0 -11
  207. package/dist/packages/method-package/index.js +0 -11
  208. package/dist/packages/method-package/method-stage-policy.d.ts +0 -5
  209. package/dist/packages/method-package/method-stage-policy.js +0 -31
  210. package/dist/packages/project-model/project-paths.d.ts +0 -12
  211. package/dist/packages/project-model/project-paths.js +0 -33
  212. /package/dist/compiler-ui/_next/static/{84FaeF3EzBF9kKTMjSEVN → C6vVfy3aeYuIO3d2AoNvC}/_buildManifest.js +0 -0
  213. /package/dist/compiler-ui/_next/static/{84FaeF3EzBF9kKTMjSEVN → C6vVfy3aeYuIO3d2AoNvC}/_clientMiddlewareManifest.js +0 -0
  214. /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 { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
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, LocalServicePointerSchema, OpenPathRequestSchema, PreparationSetupCreateRequestSchema, } from "./lib/schema.js";
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, LOCAL_SERVICE_POINTER_PATH, LOCAL_SERVICE_ROUTES, } from "./routes.js";
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
- function writeHeaders(res, statusCode, headers = {}) {
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
- "access-control-allow-origin": "*",
82
- "access-control-allow-methods": "GET,POST,PATCH,OPTIONS",
83
- "access-control-allow-headers": "content-type",
84
- ...headers,
185
+ ...context.cors,
186
+ ...extraHeaders,
85
187
  });
86
188
  }
87
- function sendJson(res, statusCode, value) {
88
- writeHeaders(res, statusCode, {
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, value) {
94
- writeHeaders(res, statusCode, {
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, message) {
100
- sendJson(res, statusCode, LocalServiceErrorSchema.parse({
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
- if (method === "GET" && path === "/health") {
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
- sendJson(res, 200, { preparations: runtime.listPreparations() });
413
+ const items = listStoredPreparations().map(preparationWireShape);
414
+ sendJson(res, 200, { preparations: items });
252
415
  return true;
253
416
  }
254
- if (method === "POST" && path === LOCAL_SERVICE_ROUTES.preparationSetups) {
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
- const request = PreparationSetupCreateRequestSchema.parse(body);
258
- const result = runtime.applyPreparationSetup(request);
259
- if (request.prepare_after_setup) {
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
- else {
272
- sendJson(res, 202, result);
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, 409, error instanceof Error ? error.message : String(error));
449
+ sendError(res, 400, error instanceof Error ? error.message : String(error));
277
450
  }
278
451
  return true;
279
452
  }
280
- if (method === "POST" && path === LOCAL_SERVICE_ROUTES.reset) {
281
- try {
282
- const body = await readJsonBody(req);
283
- sendJson(res, 202, runtime.applyReset(body));
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
- catch (error) {
286
- sendError(res, 409, error instanceof Error ? error.message : String(error));
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
- return true;
289
- }
290
- if (method === "POST" && path === LOCAL_SERVICE_ROUTES.preparationChanges) {
291
- try {
292
- const body = await readJsonBody(req);
293
- sendJson(res, 202, runtime.applyPreparationChange(body));
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
- catch (error) {
296
- sendError(res, 409, error instanceof Error ? error.message : String(error));
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
- if (method === "GET" && path === LOCAL_SERVICE_ROUTES.workspaceFiles) {
301
- sendJson(res, 200, { workspace_files: runtime.listWorkspaceFiles() });
302
- return true;
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
- sendJson(res, 200, { methods: runtime.listMethods() });
344
- return true;
345
- }
346
- if (method === "POST" && path === LOCAL_SERVICE_ROUTES.methodChanges) {
347
- try {
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 methodResource = runtime.getMethod(decodeURIComponent(methodMatch[1]));
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
- if (method === "GET" && path === "/v1/jobs") {
366
- sendJson(res, 200, { jobs: runtime.listJobs() });
367
- return true;
368
- }
369
- if (method === "GET" && path === LOCAL_SERVICE_ROUTES.executor) {
370
- sendJson(res, 200, runtime.getExecutorStatus());
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
- if (method === "POST" && path === LOCAL_SERVICE_ROUTES.executor) {
374
- const body = await readJsonBody(req);
375
- sendJson(res, 202, runtime.selectExecutor(body));
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
- sendJson(res, 200, { action_proposals: runtime.listActionProposals() });
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
- sendJson(res, 202, await runtime.createActionProposal(body));
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
- if (method === "GET" && path === LOCAL_SERVICE_ROUTES.runs) {
410
- sendJson(res, 200, { runs: runtime.listRunObservability() });
411
- return true;
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
- sendError(res, 501, "Compile-run cancellation is not implemented yet.");
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 run = runtime.getTestRun(decodeURIComponent(testRunMatch[1]));
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
- if (method === "GET" && path === "/v1/portable-contexts") {
539
- sendJson(res, 200, { portable_contexts: runtime.listPortableContexts() });
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
- const portableContextMatch = path.match(/^\/v1\/portable-contexts\/([^/]+)$/);
543
- if (method === "GET" && portableContextMatch?.[1]) {
544
- const context = runtime.getPortableContext(decodeURIComponent(portableContextMatch[1]));
545
- if (!context)
546
- sendError(res, 404, "Portable context not found.");
547
- else
548
- sendJson(res, 200, context);
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
- return Number.isInteger(envPort) ? envPort : LOCAL_SERVICE_DEFAULT_PORT;
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: options.host ?? process.env.INTERF_SERVICE_HOST ?? LOCAL_SERVICE_DEFAULT_HOST,
579
- port: resolvePort(options.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 new Promise((resolveListen, rejectListen) => {
591
- const onError = (error) => {
592
- server.off("listening", onListening);
593
- rejectListen(error);
594
- };
595
- const onListening = () => {
596
- server.off("error", onError);
597
- resolveListen();
598
- };
599
- server.once("error", onError);
600
- server.once("listening", onListening);
601
- server.listen(config.port, config.host);
602
- });
603
- const url = runtime.health().service_url;
604
- writeLocalServicePointer(rootPath, {
605
- service_url: url,
606
- host: config.host,
607
- port: config.port,
608
- pid: process.pid,
609
- control_path: resolve(rootPath),
610
- started_at: new Date().toISOString(),
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
- removeLocalServicePointer(rootPath, url);
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
  }),