@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.
Files changed (222) hide show
  1. package/README.md +96 -91
  2. package/TRADEMARKS.md +2 -13
  3. package/agent-skills/interf-actions/SKILL.md +97 -32
  4. package/agent-skills/interf-actions/references/cli.md +124 -71
  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 +9 -31
  13. package/dist/cli/commands/compile.js +75 -388
  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 -15
  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 +61 -220
  29. package/dist/cli/commands/test.d.ts +6 -15
  30. package/dist/cli/commands/test.js +63 -342
  31. package/dist/cli/commands/web.d.ts +0 -9
  32. package/dist/cli/commands/web.js +140 -367
  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/045gole2ojo3g.css +3 -0
  44. package/dist/compiler-ui/_next/static/chunks/17t-lulmyawg5.js +89 -0
  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/index.d.ts +0 -23
  56. package/dist/index.js +0 -16
  57. package/dist/packages/agents/lib/shells.d.ts +1 -1
  58. package/dist/packages/agents/lib/shells.js +113 -54
  59. package/dist/packages/agents/lib/user-config.d.ts +4 -2
  60. package/dist/packages/agents/lib/user-config.js +15 -7
  61. package/dist/packages/compiler/compiled-paths.d.ts +9 -2
  62. package/dist/packages/compiler/compiled-paths.js +30 -15
  63. package/dist/packages/compiler/compiled-pipeline.js +23 -3
  64. package/dist/packages/compiler/compiled-stage-plan.js +4 -0
  65. package/dist/packages/compiler/compiled-target.d.ts +1 -1
  66. package/dist/packages/compiler/compiled-target.js +1 -1
  67. package/dist/packages/compiler/index.d.ts +1 -0
  68. package/dist/packages/compiler/index.js +1 -0
  69. package/dist/packages/compiler/lib/schema.d.ts +27 -32
  70. package/dist/packages/compiler/lib/schema.js +2 -13
  71. package/dist/packages/compiler/method-runs.d.ts +2 -3
  72. package/dist/packages/compiler/method-runs.js +2 -3
  73. package/dist/packages/compiler/reset.js +3 -1
  74. package/dist/packages/compiler/runtime-contracts.js +0 -3
  75. package/dist/packages/compiler/runtime-prompt.js +1 -1
  76. package/dist/packages/compiler/source-files.d.ts +46 -0
  77. package/dist/packages/compiler/source-files.js +149 -0
  78. package/dist/packages/compiler/state-artifacts.d.ts +3 -2
  79. package/dist/packages/compiler/state-artifacts.js +4 -3
  80. package/dist/packages/compiler/state-io.d.ts +3 -2
  81. package/dist/packages/compiler/state-io.js +11 -5
  82. package/dist/packages/compiler/state-paths.d.ts +2 -1
  83. package/dist/packages/compiler/state-paths.js +6 -3
  84. package/dist/packages/compiler/state-view.d.ts +3 -2
  85. package/dist/packages/compiler/state-view.js +18 -28
  86. package/dist/packages/compiler/state.d.ts +4 -4
  87. package/dist/packages/compiler/state.js +3 -3
  88. package/dist/packages/contracts/index.d.ts +1 -1
  89. package/dist/packages/contracts/lib/preparation-paths.d.ts +117 -0
  90. package/dist/packages/contracts/lib/preparation-paths.js +177 -0
  91. package/dist/packages/contracts/lib/schema.d.ts +85 -6
  92. package/dist/packages/contracts/lib/schema.js +46 -2
  93. package/dist/packages/execution/lib/schema.d.ts +50 -57
  94. package/dist/packages/execution/lib/schema.js +1 -2
  95. package/dist/packages/local-service/action-definitions.d.ts +246 -0
  96. package/dist/packages/local-service/action-definitions.js +1147 -0
  97. package/dist/packages/local-service/action-planner.d.ts +9 -0
  98. package/dist/packages/local-service/action-planner.js +135 -0
  99. package/dist/packages/local-service/action-values.d.ts +1 -23
  100. package/dist/packages/local-service/action-values.js +1 -31
  101. package/dist/packages/local-service/client.d.ts +76 -46
  102. package/dist/packages/local-service/client.js +184 -149
  103. package/dist/packages/local-service/connection-config.d.ts +38 -0
  104. package/dist/packages/local-service/connection-config.js +75 -0
  105. package/dist/packages/local-service/index.d.ts +14 -7
  106. package/dist/packages/local-service/index.js +8 -4
  107. package/dist/packages/local-service/instance-paths.d.ts +100 -0
  108. package/dist/packages/local-service/instance-paths.js +165 -0
  109. package/dist/packages/local-service/lib/schema.d.ts +689 -2575
  110. package/dist/packages/local-service/lib/schema.js +260 -101
  111. package/dist/packages/local-service/native-run-handlers.d.ts +23 -0
  112. package/dist/{cli/commands/compile-controller.js → packages/local-service/native-run-handlers.js} +204 -20
  113. package/dist/packages/local-service/preparation-store.d.ts +92 -0
  114. package/dist/packages/local-service/preparation-store.js +171 -0
  115. package/dist/{cli/commands/check-draft.d.ts → packages/local-service/readiness-check-draft.d.ts} +2 -2
  116. package/dist/packages/local-service/routes.d.ts +33 -11
  117. package/dist/packages/local-service/routes.js +44 -15
  118. package/dist/packages/local-service/run-observability.js +25 -27
  119. package/dist/packages/local-service/runtime-caches.d.ts +76 -0
  120. package/dist/packages/local-service/runtime-caches.js +191 -0
  121. package/dist/packages/local-service/runtime-event-applier.d.ts +12 -0
  122. package/dist/packages/local-service/runtime-event-applier.js +177 -0
  123. package/dist/packages/local-service/runtime-persistence.d.ts +47 -0
  124. package/dist/packages/local-service/runtime-persistence.js +137 -0
  125. package/dist/packages/local-service/runtime-proposal-helpers.d.ts +35 -0
  126. package/dist/packages/local-service/runtime-proposal-helpers.js +251 -0
  127. package/dist/packages/local-service/runtime-resource-builders.d.ts +52 -0
  128. package/dist/packages/local-service/runtime-resource-builders.js +149 -0
  129. package/dist/packages/local-service/runtime.d.ts +201 -44
  130. package/dist/packages/local-service/runtime.js +1062 -1106
  131. package/dist/packages/local-service/server.d.ts +15 -0
  132. package/dist/packages/local-service/server.js +651 -233
  133. package/dist/packages/local-service/service-registry.d.ts +47 -0
  134. package/dist/packages/local-service/service-registry.js +137 -0
  135. package/dist/packages/method-authoring/method-authoring.d.ts +1 -1
  136. package/dist/packages/method-authoring/method-authoring.js +2 -2
  137. package/dist/packages/method-authoring/method-improvement.js +1 -1
  138. package/dist/packages/method-package/builtin-compiled-method.d.ts +4 -5
  139. package/dist/packages/method-package/builtin-compiled-method.js +8 -14
  140. package/dist/packages/method-package/context-interface.d.ts +4 -40
  141. package/dist/packages/method-package/context-interface.js +1 -23
  142. package/dist/packages/method-package/interf-method-package.d.ts +4 -4
  143. package/dist/packages/method-package/interf-method-package.js +21 -33
  144. package/dist/packages/method-package/local-methods.d.ts +10 -6
  145. package/dist/packages/method-package/local-methods.js +57 -39
  146. package/dist/packages/method-package/method-definitions.d.ts +8 -34
  147. package/dist/packages/method-package/method-definitions.js +49 -37
  148. package/dist/packages/method-package/method-helpers.d.ts +1 -13
  149. package/dist/packages/method-package/method-helpers.js +8 -42
  150. package/dist/packages/method-package/method-review-paths.d.ts +1 -1
  151. package/dist/packages/method-package/method-review-paths.js +5 -5
  152. package/dist/packages/method-package/method-stage-runner.js +2 -2
  153. package/dist/packages/method-package/user-methods.d.ts +17 -0
  154. package/dist/packages/method-package/user-methods.js +77 -0
  155. package/dist/packages/project-model/index.d.ts +1 -1
  156. package/dist/packages/project-model/index.js +1 -1
  157. package/dist/packages/project-model/interf-detect.d.ts +8 -3
  158. package/dist/packages/project-model/interf-detect.js +34 -34
  159. package/dist/packages/project-model/interf-scaffold.d.ts +3 -3
  160. package/dist/packages/project-model/interf-scaffold.js +23 -32
  161. package/dist/packages/project-model/lib/schema.js +38 -1
  162. package/dist/packages/project-model/preparation-entries.d.ts +11 -0
  163. package/dist/packages/project-model/preparation-entries.js +49 -0
  164. package/dist/packages/project-model/source-config.d.ts +11 -10
  165. package/dist/packages/project-model/source-config.js +83 -44
  166. package/dist/packages/project-model/source-folders.d.ts +5 -5
  167. package/dist/packages/project-model/source-folders.js +14 -14
  168. package/dist/packages/shared/filesystem.d.ts +7 -0
  169. package/dist/packages/shared/filesystem.js +97 -10
  170. package/dist/packages/testing/lib/schema.d.ts +12 -13
  171. package/dist/packages/testing/lib/schema.js +4 -5
  172. package/dist/packages/testing/readiness-check-run.d.ts +7 -7
  173. package/dist/packages/testing/readiness-check-run.js +46 -51
  174. package/dist/packages/testing/test-execution.js +6 -6
  175. package/dist/packages/testing/test-paths.js +4 -3
  176. package/dist/packages/testing/test-sandbox.d.ts +0 -1
  177. package/dist/packages/testing/test-sandbox.js +14 -30
  178. package/dist/packages/testing/test-targets.d.ts +1 -1
  179. package/dist/packages/testing/test-targets.js +6 -6
  180. package/dist/packages/testing/test.d.ts +1 -1
  181. package/dist/packages/testing/test.js +1 -1
  182. package/package.json +6 -26
  183. package/LICENSE +0 -183
  184. package/dist/cli/commands/compile-controller.d.ts +0 -17
  185. package/dist/cli/commands/compiled-flow.d.ts +0 -25
  186. package/dist/cli/commands/compiled-flow.js +0 -112
  187. package/dist/cli/commands/control-path.d.ts +0 -11
  188. package/dist/cli/commands/control-path.js +0 -72
  189. package/dist/cli/commands/create-method-wizard.d.ts +0 -76
  190. package/dist/cli/commands/create-method-wizard.js +0 -465
  191. package/dist/cli/commands/create.d.ts +0 -8
  192. package/dist/cli/commands/create.js +0 -189
  193. package/dist/cli/commands/default.d.ts +0 -2
  194. package/dist/cli/commands/default.js +0 -39
  195. package/dist/cli/commands/executor-flow.d.ts +0 -29
  196. package/dist/cli/commands/executor-flow.js +0 -163
  197. package/dist/cli/commands/init.d.ts +0 -11
  198. package/dist/cli/commands/init.js +0 -784
  199. package/dist/cli/commands/list.d.ts +0 -2
  200. package/dist/cli/commands/list.js +0 -30
  201. package/dist/cli/commands/preparation-selection.d.ts +0 -6
  202. package/dist/cli/commands/preparation-selection.js +0 -11
  203. package/dist/cli/commands/source-config-wizard.d.ts +0 -52
  204. package/dist/cli/commands/source-config-wizard.js +0 -680
  205. package/dist/cli/commands/test-flow.d.ts +0 -58
  206. package/dist/cli/commands/test-flow.js +0 -231
  207. package/dist/cli/commands/verify.d.ts +0 -2
  208. package/dist/cli/commands/verify.js +0 -94
  209. package/dist/compiler-ui/_next/static/chunks/0d~8t0zm6545p.js +0 -118
  210. package/dist/compiler-ui/_next/static/chunks/0xnel.ax9a.2c.css +0 -3
  211. package/dist/packages/compiler/raw-snapshot.d.ts +0 -49
  212. package/dist/packages/compiler/raw-snapshot.js +0 -101
  213. package/dist/packages/method-package/index.d.ts +0 -11
  214. package/dist/packages/method-package/index.js +0 -11
  215. package/dist/packages/method-package/method-stage-policy.d.ts +0 -5
  216. package/dist/packages/method-package/method-stage-policy.js +0 -31
  217. package/dist/packages/project-model/project-paths.d.ts +0 -12
  218. package/dist/packages/project-model/project-paths.js +0 -33
  219. /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_buildManifest.js +0 -0
  220. /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_clientMiddlewareManifest.js +0 -0
  221. /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_ssgManifest.js +0 -0
  222. /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 { 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, LocalServiceInstancePointerSchema, OpenPathRequestSchema, RunCreateRequestSchema, } 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_INSTANCE_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 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
- 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, {
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
- 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
380
  executor: LOCAL_SERVICE_ROUTES.executor,
381
+ open_path: LOCAL_SERVICE_ROUTES.openPath,
237
382
  },
238
383
  }));
239
384
  return true;
240
385
  }
241
- if (method === "GET" && path === LOCAL_SERVICE_ROUTES.preparations) {
242
- sendJson(res, 200, { preparations: runtime.listPreparations() });
243
- return true;
244
- }
245
- if (method === "GET" && path === LOCAL_SERVICE_ROUTES.workspaceFiles) {
246
- sendJson(res, 200, { workspace_files: runtime.listWorkspaceFiles() });
247
- return true;
248
- }
249
- if (method === "GET" && path === LOCAL_SERVICE_ROUTES.readiness) {
250
- sendJson(res, 200, { readiness: runtime.listPreparationReadiness() });
251
- return true;
252
- }
253
- if (method === "GET" && path === LOCAL_SERVICE_ROUTES.sourceFiles) {
254
- const preparation = url.searchParams.get("preparation");
255
- sendJson(res, 200, { source_files: runtime.listSourceFiles(preparation) });
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
- if (method === "POST" && path === LOCAL_SERVICE_ROUTES.openPath) {
259
- const body = OpenPathRequestSchema.parse(await readJsonBody(req));
260
- const health = runtime.health();
261
- const allowedRoots = [
262
- runtime.rootPath,
263
- ...(health.source_folder_path ? [health.source_folder_path] : []),
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
- const preparationMatch = path.match(/^\/v1\/preparations\/([^/]+)$/);
270
- if (method === "GET" && preparationMatch?.[1]) {
271
- const preparation = runtime.getPreparation(decodeURIComponent(preparationMatch[1]));
272
- if (!preparation)
273
- sendError(res, 404, "Preparation not found.");
274
- else
275
- sendJson(res, 200, preparation);
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
- const preparationReadinessMatch = path.match(/^\/v1\/preparations\/([^/]+)\/readiness$/);
279
- if (method === "GET" && preparationReadinessMatch?.[1]) {
280
- const readiness = runtime.getPreparationReadiness(decodeURIComponent(preparationReadinessMatch[1]));
281
- if (!readiness)
282
- sendError(res, 404, "Preparation not found.");
283
- else
284
- sendJson(res, 200, readiness);
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
- sendJson(res, 200, { methods: runtime.listMethods() });
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 methodResource = runtime.getMethod(decodeURIComponent(methodMatch[1]));
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
- if (method === "GET" && path === "/v1/jobs") {
301
- sendJson(res, 200, { jobs: runtime.listJobs() });
302
- return true;
303
- }
304
- if (method === "GET" && path === LOCAL_SERVICE_ROUTES.executor) {
305
- 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) });
306
654
  return true;
307
655
  }
308
- if (method === "POST" && path === LOCAL_SERVICE_ROUTES.executor) {
309
- const body = await readJsonBody(req);
310
- 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);
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
- 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
+ });
315
674
  return true;
316
675
  }
317
676
  if (method === "POST" && path === LOCAL_SERVICE_ROUTES.actionProposals) {
318
677
  const body = await readJsonBody(req);
319
- sendJson(res, 202, await runtime.createActionProposal(body));
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
- if (method === "GET" && path === LOCAL_SERVICE_ROUTES.runs) {
345
- sendJson(res, 200, { runs: runtime.listRunObservability() });
346
- return true;
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
- 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);
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 run = runtime.getTestRun(decodeURIComponent(testRunMatch[1]));
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
- if (method === "GET" && path === "/v1/portable-contexts") {
489
- 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());
490
796
  return true;
491
797
  }
492
- const portableContextMatch = path.match(/^\/v1\/portable-contexts\/([^/]+)$/);
493
- if (method === "GET" && portableContextMatch?.[1]) {
494
- const context = runtime.getPortableContext(decodeURIComponent(portableContextMatch[1]));
495
- if (!context)
496
- sendError(res, 404, "Portable context not found.");
497
- else
498
- 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 });
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
- 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}.`);
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: options.host ?? process.env.INTERF_SERVICE_HOST ?? LOCAL_SERVICE_DEFAULT_HOST,
529
- port: resolvePort(options.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 new Promise((resolveListen, rejectListen) => {
541
- const onError = (error) => {
542
- server.off("listening", onListening);
543
- rejectListen(error);
544
- };
545
- const onListening = () => {
546
- server.off("error", onError);
547
- resolveListen();
548
- };
549
- server.once("error", onError);
550
- server.once("listening", onListening);
551
- server.listen(config.port, config.host);
552
- });
553
- const url = runtime.health().service_url;
554
- writeLocalInstancePointer(rootPath, {
555
- service_url: url,
556
- host: config.host,
557
- port: config.port,
558
- pid: process.pid,
559
- control_path: resolve(rootPath),
560
- started_at: new Date().toISOString(),
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
- removeLocalInstancePointer(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
+ }
573
991
  resolveClose();
574
992
  });
575
993
  }),