ima2-gen 3.13.1 → 3.14.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 (157) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +15 -2
  4. package/bin/commands/capabilities.js +5 -3
  5. package/bin/commands/config.js +15 -4
  6. package/bin/commands/defaults.js +7 -5
  7. package/bin/commands/doctor.js +88 -39
  8. package/bin/commands/gen.js +21 -4
  9. package/bin/commands/models.js +2 -2
  10. package/bin/commands/ping.js +1 -1
  11. package/bin/commands/prompt.js +5 -3
  12. package/bin/commands/service.js +32 -4
  13. package/bin/commands/tools.js +19 -3
  14. package/bin/commands/upscale.js +18 -4
  15. package/bin/commands/video.js +37 -50
  16. package/bin/ima2.js +28 -9
  17. package/bin/lib/characterResolve.js +2 -1
  18. package/bin/lib/client.js +155 -20
  19. package/bin/lib/config-store.js +40 -7
  20. package/bin/lib/doctor-bundle.js +6 -2
  21. package/bin/lib/doctor-checks.js +13 -29
  22. package/bin/lib/doctor-media.js +3 -3
  23. package/bin/lib/doctor-providers.js +48 -35
  24. package/bin/lib/doctor-report.js +78 -0
  25. package/bin/lib/doctor-runtime.js +173 -0
  26. package/bin/lib/mcpJob.js +19 -7
  27. package/bin/lib/output.js +3 -0
  28. package/bin/lib/platform.js +12 -9
  29. package/bin/lib/recover-output.js +9 -2
  30. package/bin/lib/sse.js +9 -1
  31. package/bin/lib/videoMcp.js +16 -8
  32. package/config.js +41 -5
  33. package/docs/API.md +154 -16
  34. package/docs/CLI.md +65 -1
  35. package/docs/DOCKER.md +19 -4
  36. package/docs/DOCKER.zh-CN.md +11 -4
  37. package/docs/DOCKER.zh-TW.md +11 -4
  38. package/docs/FAQ.md +13 -0
  39. package/docs/NPX_QUICKSTART.md +6 -0
  40. package/docs/README.ja.md +15 -2
  41. package/docs/README.ko.md +15 -2
  42. package/docs/README.zh-CN.md +15 -2
  43. package/docs/README.zh-TW.md +15 -2
  44. package/docs/migration/runtime-test-inventory.md +79 -8
  45. package/lib/agyArtifact.js +82 -0
  46. package/lib/agyArtifactRead.js +249 -0
  47. package/lib/agyImageAdapter.js +2 -329
  48. package/lib/agyProcess.js +122 -0
  49. package/lib/apiRequestBudget.js +48 -0
  50. package/lib/assetLifecycle.js +42 -9
  51. package/lib/canvasVersionStore.js +46 -14
  52. package/lib/capabilities.js +7 -1
  53. package/lib/configKeys.js +2 -0
  54. package/lib/errors/providerMap.js +26 -0
  55. package/lib/eventBus.js +10 -6
  56. package/lib/eventsPolicy.js +9 -0
  57. package/lib/geminiApiImageAdapter.js +1 -214
  58. package/lib/generatePipeline.js +39 -165
  59. package/lib/generatedMediaAccess.js +74 -0
  60. package/lib/generationErrors.js +3 -22
  61. package/lib/grokImageAdapter.js +2 -353
  62. package/lib/grokImageCore.js +1 -57
  63. package/lib/grokImageDownload.js +159 -0
  64. package/lib/grokImageDownloadPolicy.js +124 -0
  65. package/lib/grokImagePlanner.js +301 -0
  66. package/lib/grokMultimodeAdapter.js +1 -75
  67. package/lib/grokVideoDownload.js +137 -34
  68. package/lib/inflight.js +112 -112
  69. package/lib/jobs/terminalStore.js +45 -0
  70. package/lib/lanSessionStore.js +131 -0
  71. package/lib/localAccessPolicy.js +180 -0
  72. package/lib/localLanAccess.js +281 -0
  73. package/lib/logger.js +22 -13
  74. package/lib/mcp/downloadMediaResult.js +72 -78
  75. package/lib/multimodePipeline.js +39 -126
  76. package/lib/nodeGeneration.js +44 -101
  77. package/lib/nodeStore.js +25 -11
  78. package/lib/nodeValidation.js +5 -1
  79. package/lib/pinnedHttpGet.js +174 -0
  80. package/lib/promptImport/githubFolder.js +32 -18
  81. package/lib/promptImport/githubSource.js +14 -10
  82. package/lib/promptImport/parsePromptCandidates.js +32 -2
  83. package/lib/providers/adapters/agyOperations.js +179 -0
  84. package/lib/providers/adapters/comfy.js +1 -0
  85. package/lib/providers/adapters/geminiOperations.js +214 -0
  86. package/lib/providers/adapters/googleExecution.js +97 -0
  87. package/lib/providers/adapters/grokExecution.js +137 -0
  88. package/lib/providers/adapters/grokMultimodeOperations.js +83 -0
  89. package/lib/providers/adapters/grokOperations.js +63 -0
  90. package/lib/providers/adapters/openaiExecution.js +118 -0
  91. package/lib/providers/adapters/openaiOperations.js +201 -0
  92. package/lib/providers/adapters/openaiTypes.js +1 -0
  93. package/lib/providers/derive.js +4 -0
  94. package/lib/providers/deriveCore.js +2 -7
  95. package/lib/providers/execution/admission.js +25 -0
  96. package/lib/providers/execution/index.js +34 -0
  97. package/lib/providers/execution/legacy.js +17 -0
  98. package/lib/providers/execution/legacyClassic.js +66 -0
  99. package/lib/providers/execution/legacyEdit.js +30 -0
  100. package/lib/providers/execution/legacyMultimode.js +34 -0
  101. package/lib/providers/execution/legacyNode.js +41 -0
  102. package/lib/providers/execution/types.js +1 -0
  103. package/lib/providers/registry.js +19 -14
  104. package/lib/providers/surfaceSupport.js +22 -0
  105. package/lib/responsesImageAdapter.js +1 -389
  106. package/lib/responsesTransport.js +190 -0
  107. package/lib/spriteJobEvents.js +20 -2
  108. package/lib/ssePublish.js +5 -4
  109. package/lib/videoExtendI2vOperation.js +76 -0
  110. package/package.json +5 -2
  111. package/routes/edit.js +31 -123
  112. package/routes/events.js +140 -63
  113. package/routes/mcpMedia.js +10 -2
  114. package/routes/models.js +28 -25
  115. package/routes/videoExtended.js +9 -68
  116. package/server.js +24 -46
  117. package/ui/dist/.vite/manifest.json +90 -63
  118. package/ui/dist/assets/{AgentWorkspace-PbBytTeV.js → AgentWorkspace-CL3MLv3l.js} +2 -2
  119. package/ui/dist/assets/App-B9cEM6dJ.js +9 -0
  120. package/ui/dist/assets/AssetGenWorkspace-swWPvXrx.js +2 -0
  121. package/ui/dist/assets/AssetsWorkspace-CRVeYU2F.js +1 -0
  122. package/ui/dist/assets/{CardNewsWorkspace-BS3jKOja.js → CardNewsWorkspace-BAOxWLKf.js} +2 -2
  123. package/ui/dist/assets/GenerationRequestLogPanel-DZtYZm-D.js +1 -0
  124. package/ui/dist/assets/HomeWorkspace-BeZ1s0Jm.js +1 -0
  125. package/ui/dist/assets/KeyingPanel-ZyBSXioh.js +1 -0
  126. package/ui/dist/assets/NodeCanvas-BvvcCErf.js +7 -0
  127. package/ui/dist/assets/{PromptBuilderPanel-D97A4pwB.js → PromptBuilderPanel-DBycmShA.js} +2 -2
  128. package/ui/dist/assets/PromptImportDialog-ChvMv7ek.js +2 -0
  129. package/ui/dist/assets/PromptImportDiscoverySection-60py7whH.js +1 -0
  130. package/ui/dist/assets/PromptImportFolderSection-CCRh_-_7.js +1 -0
  131. package/ui/dist/assets/PromptLibraryPanel-BFWQO8OT.js +2 -0
  132. package/ui/dist/assets/SettingsWorkspace-B5Ghsgdj.js +1 -0
  133. package/ui/dist/assets/SpriteRecipeWorkspace-DRWdkmwU.js +1 -0
  134. package/ui/dist/assets/index-7ngEOVxt.js +5 -0
  135. package/ui/dist/assets/index-B5jy7ePg.css +1 -0
  136. package/ui/dist/assets/index-gxh_02DQ.js +22 -0
  137. package/ui/dist/assets/{pptxgen.es-D4pedbwM.js → pptxgen.es-OUNhQLEV.js} +1 -1
  138. package/ui/dist/assets/promptBuilderStore-BTUUzbBT.js +1 -0
  139. package/ui/dist/assets/useAgentDialogFocus-DSc2qY8p.js +1 -0
  140. package/ui/dist/index.html +3 -2
  141. package/ui/dist/assets/AssetGenWorkspace-B-TGlvVb.js +0 -2
  142. package/ui/dist/assets/AssetsWorkspace-LnGaW151.js +0 -1
  143. package/ui/dist/assets/GenerationRequestLogPanel-DuDWD_ic.js +0 -1
  144. package/ui/dist/assets/HomeWorkspace-DVP7UG6O.js +0 -1
  145. package/ui/dist/assets/KeyingPanel-5bRUFQaq.js +0 -1
  146. package/ui/dist/assets/NodeCanvas-BBEpERTb.js +0 -7
  147. package/ui/dist/assets/PromptImportDialog-C89MN5JY.js +0 -2
  148. package/ui/dist/assets/PromptImportDiscoverySection-BgRfDOs2.js +0 -1
  149. package/ui/dist/assets/PromptImportFolderSection-zTMyriT1.js +0 -1
  150. package/ui/dist/assets/PromptLibraryPanel-BD96qZu_.js +0 -2
  151. package/ui/dist/assets/SettingsWorkspace-BAOk3X_8.js +0 -1
  152. package/ui/dist/assets/SpriteRecipeWorkspace-ClIM5QRo.js +0 -1
  153. package/ui/dist/assets/index-0fpfY8vu.css +0 -1
  154. package/ui/dist/assets/index-BFciPGTn.js +0 -5
  155. package/ui/dist/assets/index-BbykeXR9.js +0 -30
  156. package/ui/dist/assets/promptBuilderStore-Do7M8g7J.js +0 -1
  157. package/ui/dist/assets/useAgentDialogFocus-B6-QgEVI.js +0 -1
@@ -2,6 +2,7 @@ import { copyFile, mkdir } from "node:fs/promises";
2
2
  import { join, dirname, basename, isAbsolute } from "node:path";
3
3
  import { config } from "../../config.js";
4
4
  import { request } from "./client.js";
5
+ import { exitCodeForError } from "./output.js";
5
6
  export function createCliRequestId(prefix = "req_cli") {
6
7
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
7
8
  }
@@ -91,13 +92,19 @@ export async function recoverGeneratedOutputs(base, requestId, target) {
91
92
  if (result)
92
93
  return result;
93
94
  }
94
- catch { }
95
+ catch (error) {
96
+ if (exitCodeForError(error) === 4)
97
+ throw error;
98
+ }
95
99
  try {
96
100
  const result = await tryHistoryRecovery(base, requestId, target);
97
101
  if (result)
98
102
  return result;
99
103
  }
100
- catch { }
104
+ catch (error) {
105
+ if (exitCodeForError(error) === 4)
106
+ throw error;
107
+ }
101
108
  return { recovered: false, paths: [], requestId, source: "none" };
102
109
  }
103
110
  export function formatRecoveryHint(result) {
package/bin/lib/sse.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // SSE consumer for CLI streaming endpoints. Plain fetch + line-based parser, no external libs.
2
+ import { parseEventCursor } from "../../lib/eventsPolicy.js";
3
+ import { fetchServerUrl } from "./client.js";
2
4
  let CLI_VERSION = "0.0.0";
3
5
  export function setCliVersion(v) { CLI_VERSION = v; }
4
6
  function requestHeaders(init) {
@@ -82,7 +84,7 @@ async function openWithMethod(url, init, defaultMethod) {
82
84
  if (init.signal?.aborted)
83
85
  close();
84
86
  try {
85
- const res = await fetch(url, {
87
+ const res = await fetchServerUrl(url, {
86
88
  method: init.method || defaultMethod,
87
89
  headers: requestHeaders(init),
88
90
  ...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
@@ -92,9 +94,15 @@ async function openWithMethod(url, init, defaultMethod) {
92
94
  close();
93
95
  throw await errorFromResponse(res);
94
96
  }
97
+ const cursorHeader = res.headers.get("x-ima2-event-cursor");
98
+ if (cursorHeader !== null && parseEventCursor(cursorHeader) === null) {
99
+ close();
100
+ throw new Error("SSE_INVALID_EVENT_CURSOR");
101
+ }
95
102
  const events = parseBody(res.body, () => closed);
96
103
  return {
97
104
  events,
105
+ ...(cursorHeader !== null ? { initialEventId: cursorHeader } : {}),
98
106
  close: () => {
99
107
  init.signal?.removeEventListener("abort", forwardAbort);
100
108
  close();
@@ -4,7 +4,7 @@ import { config } from "../../config.js";
4
4
  import { GROK_VIDEO_MODEL_15, GROK_VIDEO_MODEL_15_PREVIEW_ALIAS, validateVideoResolutionForRequest, } from "../../lib/imageModels.js";
5
5
  import { VIDEO_CLIENT_TIMEOUT_MS } from "../../lib/videoClientTimeouts.js";
6
6
  import { wasFlagPassed } from "./argsExplicit.js";
7
- import { resolveHistoryReference, resolveServer, request } from "./client.js";
7
+ import { resolveHistoryReference, resolveServer, request, fetchServer } from "./client.js";
8
8
  import { loadCliDefaults } from "./config-store.js";
9
9
  import { runMcpJob } from "./mcpJob.js";
10
10
  import { characterElementIdForMcp } from "./characterResolve.js";
@@ -41,7 +41,10 @@ async function writeBuffer(path, buffer) {
41
41
  await writeFile(path, buffer);
42
42
  }
43
43
  function failServer(jsonMode, error) {
44
+ const code = error?.code ?? "SERVER_REQUEST_FAILED";
44
45
  const message = error?.message || "server unreachable";
46
+ if (code !== "SERVER_UNREACHABLE")
47
+ fail({ json: jsonMode, code, message, exitCode: exitCodeForError(error) });
45
48
  if (jsonMode)
46
49
  err("Hint: start the server with `ima2 serve`.");
47
50
  fail({ json: jsonMode, code: "SERVER_UNREACHABLE", message: `${message}\nHint: run ima2 serve`, exitCode: 3 });
@@ -110,7 +113,7 @@ function validateCoreOptions(args, refs, model, lane) {
110
113
  die(2, check.error);
111
114
  return { duration, resolution, aspectRatio };
112
115
  }
113
- async function coreReferences(serverBase, refs) {
116
+ async function coreReferences(serverBase, refs, jsonMode) {
114
117
  let latestPromise;
115
118
  return Promise.all(refs.map(async (path) => {
116
119
  if (path === "@last")
@@ -121,7 +124,8 @@ async function coreReferences(serverBase, refs) {
121
124
  return (await readFile(resolved)).toString("base64");
122
125
  })).catch((error) => {
123
126
  const typed = error;
124
- return die(typed.code === "HISTORY_EMPTY" ? 5 : 1, typed.message || String(error));
127
+ return fail({ json: jsonMode, code: typed.code ?? "REFERENCE_FAILED", message: typed.message || String(error),
128
+ exitCode: typed.code === "HISTORY_EMPTY" ? 5 : exitCodeForError(error) });
125
129
  });
126
130
  }
127
131
  function coreBody(args, context, options, references, requestId) {
@@ -195,7 +199,10 @@ async function consumeCoreSse(url, body, args, requestId) {
195
199
  return null;
196
200
  if (!args.json && lastProgress >= 0)
197
201
  process.stdout.write("\n");
198
- die(exitCodeForError(error), error.message);
202
+ if (!(error instanceof Error))
203
+ throw error;
204
+ fail({ json: Boolean(args.json), code: error.code ?? "VIDEO_FAILED",
205
+ message: error.message, exitCode: exitCodeForError(error) });
199
206
  }
200
207
  finally {
201
208
  clearTimeout(timer);
@@ -207,14 +214,14 @@ async function consumeCoreSse(url, body, args, requestId) {
207
214
  async function runCoreVideo(args, context) {
208
215
  const refs = (Array.isArray(args.ref) ? args.ref : []);
209
216
  const options = validateCoreOptions(args, refs, context.target.model, context.target.lane);
210
- const references = await coreReferences(context.server.base, refs);
217
+ const references = await coreReferences(context.server.base, refs, Boolean(args.json));
211
218
  const requestId = createCliRequestId("req_cli_video");
212
219
  const done = await consumeCoreSse(`${context.server.base}/api/video/generate`, coreBody(args, context, options, references, requestId), args, requestId);
213
220
  if (!done?.filename)
214
221
  die(1, "server did not return a video filename");
215
222
  const filename = String(done.filename);
216
223
  const target = args.out ? String(args.out) : args["out-dir"] ? join(String(args["out-dir"]), filename) : join(config.storage.generatedDir, filename);
217
- const response = await fetch(`${context.server.base}${done.url || `/generated/${encodeURIComponent(filename)}`}`, { signal: AbortSignal.timeout(parseInteger(args.timeout, MCP_VIDEO_TIMEOUT_MS / 1000, "--timeout") * 1000) });
224
+ const response = await fetchServer(context.server.base, String(done.url || `/generated/${encodeURIComponent(filename)}`), { signal: AbortSignal.timeout(parseInteger(args.timeout, MCP_VIDEO_TIMEOUT_MS / 1000, "--timeout") * 1000) });
218
225
  if (!response.ok)
219
226
  die(1, `failed to download video: HTTP ${response.status}`);
220
227
  await writeBuffer(target, Buffer.from(await response.arrayBuffer()));
@@ -357,7 +364,7 @@ function mcpMediaInputs(args, context, roles) {
357
364
  };
358
365
  }
359
366
  async function downloadMcpVideo(serverBase, url, target) {
360
- const response = await fetch(`${serverBase}${url}`);
367
+ const response = await fetchServer(serverBase, url);
361
368
  if (!response.ok)
362
369
  die(1, `failed to download video: HTTP ${response.status}`);
363
370
  await writeBuffer(target, Buffer.from(await response.arrayBuffer()));
@@ -391,7 +398,8 @@ async function runMcpVideo(argv, args, context) {
391
398
  }
392
399
  catch (error) {
393
400
  const typed = error;
394
- fail({ json: Boolean(args.json), code: typed.code ?? "MCP_GENERATION_FAILED", message: typed.message, exitCode: 1 });
401
+ fail({ json: Boolean(args.json), code: typed.code ?? "MCP_GENERATION_FAILED", message: typed.message,
402
+ exitCode: exitCodeForError(error) === 4 ? 4 : 1 });
395
403
  }
396
404
  }
397
405
  export async function runVideoGenerate(argv, args, prompt) {
package/config.js CHANGED
@@ -8,17 +8,29 @@
8
8
  // `config.json` is loaded once at module import. Mutating the file at runtime
9
9
  // requires a server restart (same as env vars).
10
10
  //
11
- // Keep this module dependency-free aside from node:* built-ins to avoid
12
- // circular imports with lib/*.
11
+ // Import only pure policy helpers from lib/*; never runtime owners that import config.
13
12
  import { homedir } from "node:os";
14
13
  import { join, dirname } from "node:path";
15
14
  import { fileURLToPath } from "node:url";
16
15
  import { readFileSync, existsSync } from "node:fs";
16
+ import { parsePublicOrigins } from "./lib/localAccessPolicy.js";
17
+ export { SSE_STREAM_POLICY } from "./lib/eventsPolicy.js";
17
18
  import { deriveSupportedImageModels, deriveUnsupportedImageModels } from "./lib/providers/derive.js";
18
19
  import { DEFAULT_PROMPT_BUILDER_MODELS, PROMPT_BUILDER_BACKENDS, PROMPT_BUILDER_MODELS, } from "./lib/promptBuilder/constants.js";
19
20
  // 4.6 rewrites prompts in ways that read worse than 4.3 for this planner's job, which
20
21
  // is judged by the result rather than a benchmark. 4.6 stays selectable below.
21
22
  export const DEFAULT_GROK_PLANNER_MODEL = "grok-4.3";
23
+ export const AGY_PROCESS_POLICY = Object.freeze({ timeoutMs: 360_000, terminateGraceMs: 1000, maxOutputBytes: 1_048_576 });
24
+ export const AGY_ARTIFACT_POLICY = Object.freeze({ maxBytes: 52_428_800, chunkBytes: 65_536 });
25
+ /** Per-process API admission; streaming frames do not consume request slots. */
26
+ export const API_REQUEST_POLICY = Object.freeze({
27
+ windowMs: 60_000, requests: 600, mutations: 120, maxPeers: 4096,
28
+ });
29
+ export const LAN_SECURITY_POLICY = Object.freeze({
30
+ lanSessionTtlMs: 28_800_000, lanMaxSessions: 256,
31
+ lanAuthWindowMs: 60_000, lanAuthMaxFailures: 10,
32
+ lanAuthMaxBuckets: 4096, lanTokenMaxBytes: 4096,
33
+ });
22
34
  export const GROK_PLANNER_MODELS = [
23
35
  DEFAULT_GROK_PLANNER_MODEL,
24
36
  "grok-4.6",
@@ -46,15 +58,21 @@ function loadConfigJson() {
46
58
  continue;
47
59
  try {
48
60
  const raw = readFileSync(p, "utf-8");
49
- return JSON.parse(raw);
61
+ const parsed = JSON.parse(raw);
62
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
63
+ return { values: parsed, source: p };
50
64
  }
51
65
  catch {
52
66
  // ignore malformed config.json; env+defaults still apply
53
67
  }
54
68
  }
55
- return {};
69
+ return { values: {}, source: null };
56
70
  }
57
- const fileCfg = loadConfigJson();
71
+ const loadedFileConfig = loadConfigJson();
72
+ const fileCfg = loadedFileConfig.values;
73
+ export const CONFIG_SOURCE_FILE = loadedFileConfig.source;
74
+ const publicOriginsEnv = env.IMA2_PUBLIC_ORIGINS;
75
+ const publicOriginsFile = fileCfg.server?.publicOrigins;
58
76
  function firstDefined(...vals) {
59
77
  return vals.find((v) => v !== undefined && v !== "");
60
78
  }
@@ -92,11 +110,29 @@ export function defaultLogLevelForEnv(runtimeEnv = env) {
92
110
  return runtimeEnv.IMA2_DEV === "1" ? "debug" : "info";
93
111
  }
94
112
  export const config = {
113
+ security: LAN_SECURITY_POLICY,
114
+ diagnostics: {
115
+ keyTimeoutMs: Math.min(30000, pickPositiveInt(env.IMA2_DIAGNOSTIC_KEY_TIMEOUT_MS, fileCfg.diagnostics?.keyTimeoutMs, 5000)),
116
+ runtimeTimeoutMs: Math.min(30000, pickPositiveInt(env.IMA2_DIAGNOSTIC_RUNTIME_TIMEOUT_MS, fileCfg.diagnostics?.runtimeTimeoutMs, 1500)),
117
+ },
95
118
  server: {
96
119
  // Accept both IMA2_PORT and legacy PORT.
97
120
  port: pickInt(firstDefined(env.IMA2_PORT, env.PORT), fileCfg.server?.port, 3333),
98
121
  host: pickStr(env.IMA2_HOST, fileCfg.server?.host, "127.0.0.1"),
99
122
  lanToken: env.IMA2_LAN_TOKEN || "",
123
+ // Lazy validation permits config rm to repair a bad file-layer value.
124
+ // Access policy snapshots this before listening; inputs stay import-time fixed.
125
+ get publicOrigins() {
126
+ try {
127
+ return parsePublicOrigins(publicOriginsEnv === undefined
128
+ ? (publicOriginsFile === undefined ? [] : publicOriginsFile) : JSON.parse(publicOriginsEnv));
129
+ }
130
+ catch {
131
+ throw Object.assign(new Error("IMA2_PUBLIC_ORIGINS must be an array of exact HTTP(S) origins."), {
132
+ code: "INVALID_PUBLIC_ORIGINS", status: 400,
133
+ });
134
+ }
135
+ },
100
136
  bodyLimit: pickStr(env.IMA2_BODY_LIMIT, fileCfg.server?.bodyLimit, "50mb"),
101
137
  },
102
138
  limits: {
package/docs/API.md CHANGED
@@ -8,15 +8,63 @@ Base URL:
8
8
  http://localhost:3333
9
9
  ```
10
10
 
11
+ ## Local and LAN access
12
+
13
+ The configured bind determines access mode. Loopback keeps single-user no-token
14
+ use; a non-loopback bind requires `IMA2_LAN_TOKEN`, including requests arriving
15
+ from a loopback proxy. LAN protects both `/api` and `/generated` before file,
16
+ Range or conditional-response processing. UI/static assets remain public.
17
+
18
+ | Endpoint | Contract |
19
+ |---|---|
20
+ | `GET /api/auth/lan/session` | No-store `{mode, authenticated, expiresAt}`; no token/cookie value returned |
21
+ | `POST /api/auth/lan/session` | Same-origin `Origin`, JSON `{}`, valid `x-ima2-token`; 204 and an opaque HttpOnly/SameSite=Strict cookie in LAN mode |
22
+ | `DELETE /api/auth/lan/session` | Same-origin `Origin` and presented cookie; revoke it and return 204 |
23
+
24
+ Sessions are in memory, origin-bound and expire after an absolute eight hours.
25
+ Restart/rotation invalidates them. Settings → General → Studio session signs out;
26
+ sign-out/expiry closes admitted streams but does not cancel accepted generation.
27
+ Reauthentication does not automatically submit a waiting generation again.
28
+ The browser removes a supplied `?token=...` before loading App and never stores
29
+ the token in local/sessionStorage; entering it in the sign-in form avoids putting
30
+ it in initial URL/access logs. API clients can use `x-ima2-token`; legacy query
31
+ tokens remain accepted. Header → query → cookie precedence applies. Malformed
32
+ duplicates are rejected; an explicit invalid credential cannot fall back to a cookie.
33
+
34
+ Serving Host/Origin must match the actual listener or exact configured
35
+ `server.publicOrigins` / `IMA2_PUBLIC_ORIGINS` JSON array (maximum 16). No wildcards,
36
+ userinfo, paths, query or fragments. This is a serving-origin list, not CORS
37
+ permission: cross-origin and same-site sibling-origin requests remain rejected.
38
+ Forwarded headers do not establish trust. The case-compatible exact GET MCP OAuth
39
+ callback retains its existing state/PKCE check; admin stop still needs its nonce.
40
+
41
+ For Vite development, use `http://127.0.0.1:5173` and explicitly include that origin
42
+ on the backend (`IMA2_PUBLIC_ORIGINS='["http://127.0.0.1:5173"]'`). The dev proxy
43
+ preserves Host. If opening `localhost` instead, list that exact origin too; local
44
+ mode canonicalizes it to `127.0.0.1`, while LAN mode preserves the entered origin.
45
+
46
+ LAN media uses `private, no-store, max-age=0`, no ETag/Last-Modified, and
47
+ same-origin resource policy. JSON sidecars and outside-root links are not served;
48
+ SVG retains its restrictive CSP. Previously cached/downloaded copies cannot be
49
+ revoked: purge operator-controlled proxy caches during upgrade. HTTP LAN is for
50
+ trusted networks only; use TLS/VPN on untrusted networks. HTTPS sessions use Secure
51
+ cookies. A malicious process on the operator host is outside this boundary.
52
+
53
+ Wrong/missing LAN credentials return `401 LAN_TOKEN_REQUIRED`; invalid Host/Origin
54
+ returns 403. Bootstrap is limited to ten failed attempts per socket peer per minute
55
+ and shares the app API request budget. Respect `Retry-After` on 429; a full
56
+ 256-session store returns 503 instead of evicting users. Tokens are bounded to
57
+ 4096 UTF-8 bytes. Access errors contain fixed messages, never supplied secrets.
58
+
11
59
  ## Provider Policy
12
60
 
13
61
  Image generation supports OAuth, API-key, Grok, and Gemini (`agy` and `gemini-api`) providers.
14
62
 
15
63
  - `provider: "oauth"` uses the local Codex OAuth proxy.
16
64
  - `provider: "api"` uses the OpenAI Responses API with the hosted `image_generation` tool.
17
- - `provider: "grok"` uses the bundled progrok xAI proxy. Classic, Node, and Agent generation run mandatory xAI Web Search through `/v1/responses`, then run a `grok-4.3` planner call with a forced local `generate_image` function, then ima2 executes xAI `/v1/images/generations`. `grok-4.6` and `grok-4.5` remain selectable overrides. If reference images, a Node parent image, or an Agent current image are attached, the final step switches to xAI `/v1/images/edits` so image-to-image context is preserved.
65
+ - `provider: "grok"` uses the bundled progrok xAI proxy. Classic, Node and multimode image generation honor `webSearchEnabled: false` (Node also honors `searchMode: "off"`): this skips `/v1/responses` search, not the configured planner's forced `generate_image` call. Planner default is `grok-4.3`; `grok-4.6` and `grok-4.5` remain selectable overrides. References select `/v1/images/edits` instead of `/v1/images/generations`. Agent's omitted-option search default is unchanged.
18
66
  - `provider: "agy"` spawns the Antigravity CLI (`agy -p`) to generate images via Google Gemini's `default_api:generate_image` tool. Model is `nano-banana-2`. Output is fixed at 1024×1024 JPEG. Max 3 reference images (i2i). No web search, quality, size, or mask controls. Multimode returns a single image. Video is unsupported (`AGY_VIDEO_UNSUPPORTED`).
19
- - `provider: "grok-api"` uses a direct xAI API key instead of the bundled progrok OAuth proxy. Same pipeline as `grok` (Web Search → planner → `/v1/images/generations`), same aspect ratio and resolution options. Requires an xAI API key configured via the web UI key management or `XAI_API_KEY` env var. Also supports video generation.
67
+ - `provider: "grok-api"` uses a direct xAI API key with the same image search/planner/image semantics. Missing or blank image credentials are refused with `GROK_API_KEY_MISSING` before admission, never changed to the proxy lane. Video remains a separate path. Configure the key in Settings or `XAI_API_KEY`.
20
68
  - `provider: "gemini-api"` calls the Google Generative Language API directly (or Vertex AI with a service account JSON). Supports models `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). Supports variable aspect ratios (1:1 through 21:9) and four resolution tiers (512px, 1K, 2K, 4K) on both auth paths — the direct API path sends `generation_config.response_format.image` (snake_case) while the Vertex AI endpoint (`aiplatform.googleapis.com`) sends `generationConfig.imageConfig` (camelCase). With `size: "auto"` the image config is omitted entirely and the model decides ratio/size. Auth: `GEMINI_API_KEY` env var, web UI key management (`/api/keys/gemini`), or a Vertex AI service account JSON (`VERTEX_SERVICE_ACCOUNT_JSON` or `/api/keys/vertex`). When both Vertex credentials and an API key are configured, Vertex takes priority. The chosen auth mode (`apikey` or `vertex`) persists to `~/.ima2/config.json` as `geminiAuthMode` and is restored on server startup. Per-model cost: `nano-banana-2` (Flash): 512=$0.001, 1K=$0.003, 2K=$0.004, 4K=$0.006; `nano-banana-pro`: 1K=$0.007, 2K=$0.007, 4K=$0.013. No web search or mask controls.
21
69
  - `provider: "nai"` calls NovelAI text-to-image generation with one of `nai-diffusion-5-full`, `nai-diffusion-5-curated`, `nai-diffusion-4-5-full`, or `nai-diffusion-4-5-curated`. It accepts the provider-native request fields documented below and returns a ZIP that ima2 decodes to PNG. References, edits, and masks are explicitly refused.
22
70
  - API-key generation covers classic generate, edit, mask-guided edit, multimode, and node generation.
@@ -27,8 +75,77 @@ Image generation supports OAuth, API-key, Grok, and Gemini (`agy` and `gemini-ap
27
75
  Grok video generation uses `POST /api/video/generate` (SSE). See the Video
28
76
  Generation section below for the full endpoint specification.
29
77
 
78
+ ### Grok image artifact retrieval
79
+
80
+ Returned image URLs require HTTPS and a conservative numeric-address policy.
81
+ Private/link-local/shared/loopback/documentation/transition destinations are
82
+ rejected by default; every DNS answer and redirect hop is validated, and the
83
+ connection uses only those validated addresses. No default re-resolution,
84
+ connection pooling, API credential, cookie or referrer forwarding occurs.
85
+
86
+ The exact server-configured proxy origin is a local exception only when it is
87
+ the initial artifact origin. Leaving it permanently removes that exception for
88
+ the chain: public→private-proxy and proxy→public→private-proxy cannot regain trust.
89
+ At most five redirects and50MiB are accepted; the single30s default deadline
90
+ covers DNS, retries, redirects and streamed bytes. An omitted/false Content-Length
91
+ does not bypass the streamed cap. HTTP GET may retry under the existing policy;
92
+ billable image POST is not automatically retried.
93
+
94
+ The downloader reports `GROK_IMAGE_DOWNLOAD_FAILED`/502 for policy/body failures,
95
+ `GROK_IMAGE_TIMEOUT`/504 for deadline, and `GENERATION_CANCELED`/499 for caller
96
+ abort; existing surface error normalization still applies. Errors do not expose
97
+ raw returned URL/query credentials. This restricted policy is not an exhaustive
98
+ public-address/routability guarantee and does not change video or MCP URL policy.
99
+
100
+ ### Agy artifact reads and cleanup
101
+
102
+ Reported and fallback artifact paths must be absolute regular files within the
103
+ canonical `.gemini`, `.cache`, or system temporary roots. Intentionally relocated
104
+ root directories remain supported; missing roots never broaden the boundary.
105
+ Leaf symlinks, paths escaping those roots, unusable file identities and detected
106
+ path/file replacement are rejected. The same checks apply to parsed result paths
107
+ and fallback discovery; filename matching alone is not acceptance.
108
+
109
+ The reader enforces an inclusive 50MiB limit before and during descriptor reads,
110
+ using bounded blocks even when individual reads are short. A successful receipt
111
+ authorizes cleanup only for that same checked file; replacements and unrelated
112
+ siblings are left alone. Direct reader/operation cancellation returns
113
+ `GENERATION_CANCELED`/499 after pending file I/O and descriptor close settle;
114
+ existing node normalization can expose `INVALID_REQUEST`/499 instead.
115
+ Policy-rejected artifacts are not deleted. The reader and Agy operation add no
116
+ retries, but caller policies remain unchanged: reference-free node requests can
117
+ retry once after retryable artifact errors, starting another generation attempt.
118
+
119
+ `AGY_PATH_REJECTED`/502 covers the path/identity policy; `AGY_ARTIFACT_TOO_LARGE`/502
120
+ covers overflow, and `AGY_ARTIFACT_NOT_FOUND`/502 covers missing reported files.
121
+ Existing generation normalization may expose `code: UNKNOWN` while retaining
122
+ the original `rawCode` and `errorClass: INTERNAL_STATE_ERROR`.
123
+ Windows uses explicit path/identity checks without POSIX no-follow open flags.
124
+ This is not an atomic filesystem sandbox against another same-user process, a
125
+ hardlink-provenance guarantee, or a process-wide memory ceiling.
126
+
30
127
  ## Health And Status
31
128
 
129
+ ### Core provider surface metadata
130
+
131
+ `GET /api/capabilities` includes `providerSurfaces[coreProviderId][surface]`.
132
+ `GET /api/models` includes the same `surfaces` on each core lane; MCP lanes
133
+ retain their existing model capabilities. Surfaces are `generate`, `edit`,
134
+ `multimode`, `node`, and `video`.
135
+
136
+ Each record contains `supported`, `references`, `mask`, `streaming`, and
137
+ `catalogAccess` (`static` or `runtime`). These are application capability facts,
138
+ not credential/liveness or entitlement checks. `references` means the surface
139
+ can accept image input; a Comfy workflow still needs the appropriate binding.
140
+ `streaming` describes partial-image delivery on Node/Multimode, not lifecycle SSE.
141
+
142
+ NovelAI's generate/node/multimode entries are supported without references;
143
+ edit/video are unsupported. Its models explicitly declare generation support,
144
+ not fictitious edit support. Comfy is runtime-catalog backed: empty static
145
+ model IDs do not disable its generate/edit/video surfaces, while node/multimode
146
+ remain unsupported. Local capabilities expose the structural map without
147
+ inventing a runtime `lanes` availability result.
148
+
32
149
  | Method | Path | Notes |
33
150
  |---|---|---|
34
151
  | `GET` | `/api/health` | Server health, version, paths, provider policy |
@@ -230,23 +347,23 @@ enabled alpha are V5-only. The four exact image models are
230
347
  text-to-image only: `NAI_REF_UNSUPPORTED`, `NAI_EDIT_UNSUPPORTED`, and
231
348
  `NAI_MASK_UNSUPPORTED` fail closed rather than discarding input.
232
349
 
233
- When `provider` is `"grok"`, supported models are `grok-imagine-image` and
234
- `grok-imagine-image-quality`. The server uses `grok-4.5` as the search/planner
235
- model by default (`IMA2_GROK_PLANNER_MODEL`) and times the mandatory search and
350
+ When `provider` is `"grok"`, supported models include `grok-imagine-image-2.0`,
351
+ `grok-imagine-image` and `grok-imagine-image-quality`. The server uses `grok-4.3`
352
+ as the configured search/planner default (`IMA2_GROK_PLANNER_MODEL`) and times search and
236
353
  planner steps separately from the image call (`IMA2_GROK_PLANNER_TIMEOUT_MS`).
237
354
  For `n > 1`, search and planning run once and the planned prompt is reused for
238
- the image requests. Successful Grok classic generations report one mandatory
239
- web-search call in metadata.
355
+ the image requests. Search-disabled classic generations report zero search calls;
356
+ otherwise the shared plan reports one logical search call.
240
357
 
241
358
  If `references` are present on a Grok classic request, ima2 still performs the
242
- mandatory search and `grok-4.5` planning phases. The planner receives the
359
+ configured planning phase, with search only when enabled. The planner receives the
243
360
  reference images as multimodal `image_url` inputs, and its forced
244
361
  `generate_image.prompt` argument is instructed to be English-only except for
245
362
  exact visible text requested by the user. The final image call then uses xAI
246
363
  `/v1/images/edits` with the same reference images instead of
247
364
  `/v1/images/generations`. This keeps image-to-image/reference context alive
248
- through the three-phase pipeline. xAI currently documents up to three source
249
- images for image editing, so Grok classic requests with more than three
365
+ through the pipeline. The server permits up to three source images for these
366
+ Grok classic requests; more than three
250
367
  references return `GROK_REF_TOO_MANY`.
251
368
 
252
369
  Grok size mapping:
@@ -280,9 +397,11 @@ With `provider: "grok"`, edit requests are sent to xAI `/v1/images/edits`
280
397
  through the bundled progrok proxy. Masked Grok edits are rejected before
281
398
  upstream with `GROK_MASK_UNSUPPORTED`.
282
399
 
283
- Grok multimode currently sends each image request directly to xAI Images API
284
- with the mapped `aspect_ratio`/`resolution`; the mandatory search + planner
285
- pipeline is limited to classic `/api/generate`.
400
+ Grok multimode plans each attempted image in order and performs search only when
401
+ enabled. Sparse successes retain their original attempt index: a failed first
402
+ attempt cannot make the final sweep persist the second image twice. Identical
403
+ bytes at different successful indices remain separate outputs. The internal
404
+ originalIndexes array is not added to request/SSE/history/sidecar schemas.
286
405
 
287
406
  ### `POST /api/node/generate`
288
407
 
@@ -310,7 +429,7 @@ Body fields:
310
429
 
311
430
  When `parentNodeId` is present, the server loads the stored parent node image and uses the edit path. Node-local references are allowed on both root and child/edit nodes; for child/edit nodes the parent image is sent first, then references, then the text prompt.
312
431
 
313
- With `provider: "grok"`, Node Mode uses the same xAI search + `grok-4.5` planner + Images API pipeline as classic generation. A parent node image, `externalSrc`, or extra references are passed to the planner and then to xAI `/v1/images/edits`; otherwise the final call uses `/v1/images/generations`. Grok Node requests are capped at three total input images, counting the parent/current image plus references, and return `GROK_REF_TOO_MANY` before upstream when that limit is exceeded. `quality: "high"` promotes the final image model to `grok-imagine-image-quality`.
432
+ Grok Node Mode uses the configured planner and Images API, with search suppressed by `searchMode: "off"` or `webSearchEnabled: false`. A parent node image, `externalSrc`, or extra references are passed to the planner and then to `/v1/images/edits`; otherwise the final call uses `/v1/images/generations`. The server caps total input images at three, counting parent/current image plus references, and returns `GROK_REF_TOO_MANY` before upstream when exceeded. Existing quality-model resolution is unchanged.
314
433
 
315
434
  The route can stream Server-Sent Events when the client sends `Accept: text/event-stream`. Possible events include `phase`, `partial`, `done`, and `error`. Alternatively, send `{ "async": true, "requestId": "req_xxx" }` in the body to receive `202 { requestId }` immediately and follow progress on `GET /api/events` (see Events section).
316
435
 
@@ -489,7 +608,7 @@ Grok prompt surfaces used by video APIs:
489
608
 
490
609
  | Code | Meaning |
491
610
  |---|---|
492
- | `VIDEO_PROVIDER_UNSUPPORTED` | Provider is not `"grok"` |
611
+ | `VIDEO_PROVIDER_UNSUPPORTED` | Provider is not `"grok"` or `"grok-api"` |
493
612
  | `PROMPT_REQUIRED` | Empty or missing prompt |
494
613
  | `INVALID_GROK_VIDEO_MODEL` | Model not in valid set |
495
614
  | `INVALID_VIDEO_RESOLUTION` | Resolution is not 480p/720p/1080p, or 1080p was requested outside `grok-imagine-video-1.5` prompt-only T2V / I2V |
@@ -498,6 +617,24 @@ Grok prompt surfaces used by video APIs:
498
617
  | `GROK_VIDEO_REF_TOO_MANY` | More than 7 reference images |
499
618
  | `GROK_VIDEO_FAILED` | Upstream xAI video generation failed |
500
619
  | `GROK_VIDEO_FRAME_FAILED` | Server could not extract the parent video's last frame |
620
+ | `GROK_VIDEO_DOWNLOAD_FAILED` | Download-stage HTTP, size, MIME, empty-body, MP4-prefix or read failure (502) |
621
+ | `GROK_VIDEO_TIMEOUT` | The download's own non-resetting timeout expired (504) |
622
+ | `GENERATION_CANCELED` | Caller cancellation observed while downloading, including custom reasons (499) |
623
+
624
+ Completed Grok video artifacts are consumed incrementally with an inclusive
625
+ **100 MiB** byte cap. Declared size and actual streamed bytes are checked before
626
+ retaining an overflowing chunk; invalid/empty/aborted bodies do not reach caller
627
+ persistence. Accepted MIME is MP4 or octet-stream (missing MIME defaults to MP4),
628
+ and the existing minimum-length/`ftyp` prefix check is retained. This is not full
629
+ MP4 decoding or a100MiB process-memory ceiling: fetch buffers, chunk overhead and
630
+ downstream copies are separate. Safe GET retry ends at response headers; body
631
+ failure never starts a new billed generation.
632
+
633
+ The video destination policy is unchanged: initial HTTPS or the existing literal
634
+ HTTP-loopback allowlist, with fetch's default redirect behavior. It does not gain
635
+ the image downloader's DNS/private-address/per-hop pinning policy. Do not treat
636
+ HTTPS alone or these body bounds as an SSRF/security guarantee. Download error
637
+ status appears in the existing JSON or SSE/terminal envelope for each caller.
501
638
 
502
639
  ### `POST /api/video/edit`
503
640
 
@@ -751,7 +888,8 @@ Registered only when `config.features.cardNews` is true (`routes/cardNews.ts`).
751
888
  | `AGY_QUOTA_EXHAUSTED` | Gemini API quota exhausted (rate limit) |
752
889
  | `AGY_PARSE_FAILED` | Could not parse artifact path from agy output |
753
890
  | `AGY_ARTIFACT_NOT_FOUND` | Agy reported an artifact path that does not exist |
754
- | `AGY_PATH_REJECTED` | Agy artifact path was outside allowed directories |
891
+ | `AGY_PATH_REJECTED` | Agy artifact path, file type, or identity failed the allowed-root policy |
892
+ | `AGY_ARTIFACT_TOO_LARGE` | Agy artifact exceeded the inclusive 50MiB read limit |
755
893
  | `AGY_VIDEO_UNSUPPORTED` | Video generation is not supported by the Gemini (agy) provider |
756
894
  | `AGY_MASK_UNSUPPORTED` | Mask-based editing is not supported by the Gemini (agy) provider |
757
895
  | `AGY_REF_TOO_MANY` | Too many reference images for agy (max 3) |
package/docs/CLI.md CHANGED
@@ -12,6 +12,7 @@ For a quick start, see the [main README](../README.md). For endpoint mapping, se
12
12
  | `ima2 setup` / `ima2 login` | Reconfigure saved auth (interactive) |
13
13
  | `ima2 status` | Show config and OAuth status |
14
14
  | `ima2 doctor` | Diagnose Node, package, config, and auth |
15
+ | `ima2 doctor --installation --json` | Offline package checks without config, account or network access |
15
16
  | `ima2 doctor image-probe [--json]` | Run live sanitized Responses image probes for `EMPTY_RESPONSE` support |
16
17
  | `ima2 open` | Open the web UI in a browser |
17
18
  | `ima2 grok login/status/models/proxy` | Manage the bundled progrok runtime used by the Grok provider |
@@ -24,10 +25,40 @@ These work on most client commands:
24
25
 
25
26
  | Flag | Meaning |
26
27
  |---|---|
27
- | `--server <url>` | Override server discovery (default uses `~/.ima2/server.json`, falls back to `IMA2_SERVER` env) |
28
+ | `--server <url>` | Explicit server origin; overrides `IMA2_SERVER`, then credential-free advertise/default discovery |
28
29
  | `--json` | Emit machine-readable JSON instead of human-formatted output |
29
30
  | `-h`, `--help` | Show subcommand help |
30
31
 
32
+ ## LAN server authentication
33
+
34
+ Export your private `IMA2_LAN_TOKEN`, then select the known server explicitly:
35
+
36
+ ```bash
37
+ IMA2_SERVER=http://192.168.1.20:3333 ima2 ping --json
38
+ ima2 models --server https://studio.example --json
39
+ ```
40
+
41
+ The token goes only to the selected normalized HTTP(S) origin, never to arbitrary
42
+ advertise entries, redirects or external returned media. Do not put credentials,
43
+ query strings or non-root paths in `--server` / `IMA2_SERVER`. No token flag or
44
+ cookie jar is used. Auth/forbidden failures exit 4 with a safe code; an unreachable
45
+ explicit server exits 3. Explicit targets never silently fall back to local data.
46
+ `--local`/`--offline` options keep their documented local behavior.
47
+
48
+ For a proxy or changed published port, configure exact origins and restart:
49
+
50
+ ```bash
51
+ ima2 config set server.publicOrigins '["https://studio.example"]' --yes
52
+ ima2 config rm server.publicOrigins --yes
53
+ ```
54
+
55
+ Invalid origin values are rejected without echoing them. Removal can recover an
56
+ invalid primary or legacy file-layer value while preserving other settings; the
57
+ edited full object is written privately to the primary path and the legacy file
58
+ is retained. An invalid environment override must be unset or corrected separately.
59
+ Config listings/get redact nested credentials; `config path` identifies the file.
60
+ See [LAN HTTP/session and transport limits](API.md#local-and-lan-access).
61
+
31
62
  ## Agent discovery
32
63
 
33
64
  Agents should start from the packaged skill and capability commands instead of guessing from scattered help text.
@@ -278,6 +309,39 @@ download fields such as `ok`, `path`, and `filename`. `ima2 video continue
278
309
 
279
310
  ## Diagnostics
280
311
 
312
+ `ima2 doctor --json` prints one JSON document with `schemaVersion: 1`, `mode`,
313
+ `checks` and `summary`. Each check has a stable code, severity, evidence category
314
+ and a fixed message/action. Exit0 means no failed checks; warnings alone do not
315
+ fail. Exit1 means a prerequisite failed. Invalid flag combinations exit2 with
316
+ stderr only. Standard mode retains provider-auth checks; missing OAuth can fail
317
+ standard doctor even if a different provider is selected.
318
+
319
+ Use `ima2 doctor --installation --json` to check only the package Node engine,
320
+ runtime dependencies/CLI entries, in-memory SQLite binding, packaged skills and UI
321
+ build. It does not load account/config files, repair anything, build the UI or make
322
+ network requests. Missing optional provider credentials are not installation errors.
323
+ It cannot combine with `--bundle`, `--verify-keys` or `--runtime`.
324
+
325
+ `ima2 doctor --bundle --json` retains the legacy version/node/platform/hostnameHash/
326
+ lanes fields and adds safe checks/summary. The compatibility hostname hash is not
327
+ an anonymity guarantee. Machine reports omit arbitrary diagnostic text, raw paths,
328
+ URLs, response bodies and credential values.
329
+
330
+ `--verify-keys` explicitly checks registry-declared non-generating authentication
331
+ endpoints. Configured/present is not verified, and successful authentication does
332
+ not prove generation capability or balance. Authentication rejection, rate limit,
333
+ upstream failure, network failure and timeout have different codes.
334
+
335
+ `--runtime http://127.0.0.1:3333` explicitly checks only that loopback origin's
336
+ `/api/health`; localhost and IPv6 loopback are also accepted. No credentials,
337
+ redirects, arbitrary paths, query strings or server discovery are used. A version
338
+ match proves a local response, not provider success. Authorization-required health
339
+ responses direct the operator to the configured authorized client.
340
+
341
+ Timeouts default to5000ms for key checks and1500ms for runtime health. Optional
342
+ `IMA2_DIAGNOSTIC_KEY_TIMEOUT_MS` and `IMA2_DIAGNOSTIC_RUNTIME_TIMEOUT_MS` override
343
+ them, capped at30000ms. Response cleanup remains inside the deadline.
344
+
281
345
  `ima2 doctor image-probe` runs live Responses probes that help classify image
282
346
  generation failures such as `EMPTY_RESPONSE`. It is intended for support
283
347
  bundles, especially when OAuth is green but a simple prompt produces no image.
package/docs/DOCKER.md CHANGED
@@ -5,11 +5,13 @@ web UI and server from source and runs the Express server directly.
5
5
 
6
6
  ## Quick start
7
7
 
8
+ First export your own strong, private `IMA2_LAN_TOKEN`. Do not deploy a sample token.
9
+
8
10
  ```bash
9
11
  docker build -t ima2-gen .
10
12
  docker run -d --name ima2 \
11
13
  -p 3333:3333 \
12
- -e IMA2_LAN_TOKEN=change-me \
14
+ -e IMA2_LAN_TOKEN \
13
15
  -v ima2-data:/data \
14
16
  ima2-gen
15
17
  ```
@@ -17,11 +19,12 @@ docker run -d --name ima2 \
17
19
  Or with compose:
18
20
 
19
21
  ```bash
20
- IMA2_LAN_TOKEN=change-me docker compose up -d
22
+ docker compose up -d
21
23
  ```
22
24
 
23
- Then open `http://localhost:3333/?token=change-me` (or send the token as an
24
- `x-ima2-token` header on API calls).
25
+ Then open `http://localhost:3333/` and enter the token in the studio sign-in form.
26
+ The browser uses a short-lived HttpOnly session for APIs, images, videos and SSE.
27
+ CLI clients use the token environment variable with an explicit server origin.
25
28
 
26
29
  ## Why `IMA2_LAN_TOKEN` is required
27
30
 
@@ -38,6 +41,7 @@ container exits at startup with a clear error — that is by design, not a bug.
38
41
  | `IMA2_LAN_TOKEN` | — (required) | API access token for non-loopback bind |
39
42
  | `IMA2_PORT` | `3333` | Server port |
40
43
  | `IMA2_HOST` | `0.0.0.0` | Bind host |
44
+ | `IMA2_PUBLIC_ORIGINS` | `[]` | JSON array of exact browser/proxy origins when address or published port differs |
41
45
  | `IMA2_CONFIG_DIR` | `/data` | Config + generated output + credentials |
42
46
  | `OPENAI_API_KEY` | — | Optional: API-key image provider |
43
47
  | `GEMINI_API_KEY` | — | Optional: Gemini API provider |
@@ -54,6 +58,17 @@ in `/data`.
54
58
 
55
59
  ## Limitations
56
60
 
61
+ For `-p 8080:3333`, also pass
62
+ `-e IMA2_PUBLIC_ORIGINS='["http://localhost:8080"]'`. Add the exact LAN IP/DNS URL
63
+ used by remote browsers as needed. A TLS proxy must preserve external Host and
64
+ Origin, forward Set-Cookie and avoid caching or logging token queries. Do not use
65
+ a loopback bind behind an external proxy as a substitute for LAN authentication.
66
+ HTTP does not encrypt credentials; use trusted networks or TLS/VPN.
67
+
68
+ LAN generated media is now private/no-store. Old proxy/browser cached copies may
69
+ outlive an upgrade or sign-out; purge caches you control. Restart invalidates browser
70
+ sessions without deleting files. [Full access contract](API.md#local-and-lan-access).
71
+
57
72
  - The image runs the server only. CLI workflows (`ima2 gen`, etc.) can exec
58
73
  into the container (`docker exec -it ima2 node bin/ima2.js …`) but are
59
74
  primarily designed for host installs.