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.
- package/.env.example +10 -0
- package/CHANGELOG.md +14 -0
- package/README.md +15 -2
- package/bin/commands/capabilities.js +5 -3
- package/bin/commands/config.js +15 -4
- package/bin/commands/defaults.js +7 -5
- package/bin/commands/doctor.js +88 -39
- package/bin/commands/gen.js +21 -4
- package/bin/commands/models.js +2 -2
- package/bin/commands/ping.js +1 -1
- package/bin/commands/prompt.js +5 -3
- package/bin/commands/service.js +32 -4
- package/bin/commands/tools.js +19 -3
- package/bin/commands/upscale.js +18 -4
- package/bin/commands/video.js +37 -50
- package/bin/ima2.js +28 -9
- package/bin/lib/characterResolve.js +2 -1
- package/bin/lib/client.js +155 -20
- package/bin/lib/config-store.js +40 -7
- package/bin/lib/doctor-bundle.js +6 -2
- package/bin/lib/doctor-checks.js +13 -29
- package/bin/lib/doctor-media.js +3 -3
- package/bin/lib/doctor-providers.js +48 -35
- package/bin/lib/doctor-report.js +78 -0
- package/bin/lib/doctor-runtime.js +173 -0
- package/bin/lib/mcpJob.js +19 -7
- package/bin/lib/output.js +3 -0
- package/bin/lib/platform.js +12 -9
- package/bin/lib/recover-output.js +9 -2
- package/bin/lib/sse.js +9 -1
- package/bin/lib/videoMcp.js +16 -8
- package/config.js +41 -5
- package/docs/API.md +154 -16
- package/docs/CLI.md +65 -1
- package/docs/DOCKER.md +19 -4
- package/docs/DOCKER.zh-CN.md +11 -4
- package/docs/DOCKER.zh-TW.md +11 -4
- package/docs/FAQ.md +13 -0
- package/docs/NPX_QUICKSTART.md +6 -0
- package/docs/README.ja.md +15 -2
- package/docs/README.ko.md +15 -2
- package/docs/README.zh-CN.md +15 -2
- package/docs/README.zh-TW.md +15 -2
- package/docs/migration/runtime-test-inventory.md +79 -8
- package/lib/agyArtifact.js +82 -0
- package/lib/agyArtifactRead.js +249 -0
- package/lib/agyImageAdapter.js +2 -329
- package/lib/agyProcess.js +122 -0
- package/lib/apiRequestBudget.js +48 -0
- package/lib/assetLifecycle.js +42 -9
- package/lib/canvasVersionStore.js +46 -14
- package/lib/capabilities.js +7 -1
- package/lib/configKeys.js +2 -0
- package/lib/errors/providerMap.js +26 -0
- package/lib/eventBus.js +10 -6
- package/lib/eventsPolicy.js +9 -0
- package/lib/geminiApiImageAdapter.js +1 -214
- package/lib/generatePipeline.js +39 -165
- package/lib/generatedMediaAccess.js +74 -0
- package/lib/generationErrors.js +3 -22
- package/lib/grokImageAdapter.js +2 -353
- package/lib/grokImageCore.js +1 -57
- package/lib/grokImageDownload.js +159 -0
- package/lib/grokImageDownloadPolicy.js +124 -0
- package/lib/grokImagePlanner.js +301 -0
- package/lib/grokMultimodeAdapter.js +1 -75
- package/lib/grokVideoDownload.js +137 -34
- package/lib/inflight.js +112 -112
- package/lib/jobs/terminalStore.js +45 -0
- package/lib/lanSessionStore.js +131 -0
- package/lib/localAccessPolicy.js +180 -0
- package/lib/localLanAccess.js +281 -0
- package/lib/logger.js +22 -13
- package/lib/mcp/downloadMediaResult.js +72 -78
- package/lib/multimodePipeline.js +39 -126
- package/lib/nodeGeneration.js +44 -101
- package/lib/nodeStore.js +25 -11
- package/lib/nodeValidation.js +5 -1
- package/lib/pinnedHttpGet.js +174 -0
- package/lib/promptImport/githubFolder.js +32 -18
- package/lib/promptImport/githubSource.js +14 -10
- package/lib/promptImport/parsePromptCandidates.js +32 -2
- package/lib/providers/adapters/agyOperations.js +179 -0
- package/lib/providers/adapters/comfy.js +1 -0
- package/lib/providers/adapters/geminiOperations.js +214 -0
- package/lib/providers/adapters/googleExecution.js +97 -0
- package/lib/providers/adapters/grokExecution.js +137 -0
- package/lib/providers/adapters/grokMultimodeOperations.js +83 -0
- package/lib/providers/adapters/grokOperations.js +63 -0
- package/lib/providers/adapters/openaiExecution.js +118 -0
- package/lib/providers/adapters/openaiOperations.js +201 -0
- package/lib/providers/adapters/openaiTypes.js +1 -0
- package/lib/providers/derive.js +4 -0
- package/lib/providers/deriveCore.js +2 -7
- package/lib/providers/execution/admission.js +25 -0
- package/lib/providers/execution/index.js +34 -0
- package/lib/providers/execution/legacy.js +17 -0
- package/lib/providers/execution/legacyClassic.js +66 -0
- package/lib/providers/execution/legacyEdit.js +30 -0
- package/lib/providers/execution/legacyMultimode.js +34 -0
- package/lib/providers/execution/legacyNode.js +41 -0
- package/lib/providers/execution/types.js +1 -0
- package/lib/providers/registry.js +19 -14
- package/lib/providers/surfaceSupport.js +22 -0
- package/lib/responsesImageAdapter.js +1 -389
- package/lib/responsesTransport.js +190 -0
- package/lib/spriteJobEvents.js +20 -2
- package/lib/ssePublish.js +5 -4
- package/lib/videoExtendI2vOperation.js +76 -0
- package/package.json +5 -2
- package/routes/edit.js +31 -123
- package/routes/events.js +140 -63
- package/routes/mcpMedia.js +10 -2
- package/routes/models.js +28 -25
- package/routes/videoExtended.js +9 -68
- package/server.js +24 -46
- package/ui/dist/.vite/manifest.json +90 -63
- package/ui/dist/assets/{AgentWorkspace-PbBytTeV.js → AgentWorkspace-CL3MLv3l.js} +2 -2
- package/ui/dist/assets/App-B9cEM6dJ.js +9 -0
- package/ui/dist/assets/AssetGenWorkspace-swWPvXrx.js +2 -0
- package/ui/dist/assets/AssetsWorkspace-CRVeYU2F.js +1 -0
- package/ui/dist/assets/{CardNewsWorkspace-BS3jKOja.js → CardNewsWorkspace-BAOxWLKf.js} +2 -2
- package/ui/dist/assets/GenerationRequestLogPanel-DZtYZm-D.js +1 -0
- package/ui/dist/assets/HomeWorkspace-BeZ1s0Jm.js +1 -0
- package/ui/dist/assets/KeyingPanel-ZyBSXioh.js +1 -0
- package/ui/dist/assets/NodeCanvas-BvvcCErf.js +7 -0
- package/ui/dist/assets/{PromptBuilderPanel-D97A4pwB.js → PromptBuilderPanel-DBycmShA.js} +2 -2
- package/ui/dist/assets/PromptImportDialog-ChvMv7ek.js +2 -0
- package/ui/dist/assets/PromptImportDiscoverySection-60py7whH.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-CCRh_-_7.js +1 -0
- package/ui/dist/assets/PromptLibraryPanel-BFWQO8OT.js +2 -0
- package/ui/dist/assets/SettingsWorkspace-B5Ghsgdj.js +1 -0
- package/ui/dist/assets/SpriteRecipeWorkspace-DRWdkmwU.js +1 -0
- package/ui/dist/assets/index-7ngEOVxt.js +5 -0
- package/ui/dist/assets/index-B5jy7ePg.css +1 -0
- package/ui/dist/assets/index-gxh_02DQ.js +22 -0
- package/ui/dist/assets/{pptxgen.es-D4pedbwM.js → pptxgen.es-OUNhQLEV.js} +1 -1
- package/ui/dist/assets/promptBuilderStore-BTUUzbBT.js +1 -0
- package/ui/dist/assets/useAgentDialogFocus-DSc2qY8p.js +1 -0
- package/ui/dist/index.html +3 -2
- package/ui/dist/assets/AssetGenWorkspace-B-TGlvVb.js +0 -2
- package/ui/dist/assets/AssetsWorkspace-LnGaW151.js +0 -1
- package/ui/dist/assets/GenerationRequestLogPanel-DuDWD_ic.js +0 -1
- package/ui/dist/assets/HomeWorkspace-DVP7UG6O.js +0 -1
- package/ui/dist/assets/KeyingPanel-5bRUFQaq.js +0 -1
- package/ui/dist/assets/NodeCanvas-BBEpERTb.js +0 -7
- package/ui/dist/assets/PromptImportDialog-C89MN5JY.js +0 -2
- package/ui/dist/assets/PromptImportDiscoverySection-BgRfDOs2.js +0 -1
- package/ui/dist/assets/PromptImportFolderSection-zTMyriT1.js +0 -1
- package/ui/dist/assets/PromptLibraryPanel-BD96qZu_.js +0 -2
- package/ui/dist/assets/SettingsWorkspace-BAOk3X_8.js +0 -1
- package/ui/dist/assets/SpriteRecipeWorkspace-ClIM5QRo.js +0 -1
- package/ui/dist/assets/index-0fpfY8vu.css +0 -1
- package/ui/dist/assets/index-BFciPGTn.js +0 -5
- package/ui/dist/assets/index-BbykeXR9.js +0 -30
- package/ui/dist/assets/promptBuilderStore-Do7M8g7J.js +0 -1
- package/ui/dist/assets/useAgentDialogFocus-B6-QgEVI.js +0 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { AGY_PROCESS_POLICY } from "../config.js";
|
|
3
|
+
import { buildAgyPathEnv, resolveAgyBin } from "./agyCli.js";
|
|
4
|
+
export function agyError(message, status, code) {
|
|
5
|
+
const err = new Error(message);
|
|
6
|
+
err.status = status;
|
|
7
|
+
err.code = code;
|
|
8
|
+
return err;
|
|
9
|
+
}
|
|
10
|
+
class AgyProcessLifetime {
|
|
11
|
+
child;
|
|
12
|
+
signal;
|
|
13
|
+
resolve;
|
|
14
|
+
reject;
|
|
15
|
+
stdout = "";
|
|
16
|
+
stderr = "";
|
|
17
|
+
settled = false;
|
|
18
|
+
reason;
|
|
19
|
+
timer;
|
|
20
|
+
graceTimer;
|
|
21
|
+
constructor(child, signal, resolve, reject) {
|
|
22
|
+
this.child = child;
|
|
23
|
+
this.signal = signal;
|
|
24
|
+
this.resolve = resolve;
|
|
25
|
+
this.reject = reject;
|
|
26
|
+
}
|
|
27
|
+
start(prompt) {
|
|
28
|
+
this.child.stdout.on("data", this.onStdout);
|
|
29
|
+
this.child.stderr.on("data", this.onStderr);
|
|
30
|
+
this.child.on("error", this.onError);
|
|
31
|
+
this.child.once("close", this.finish);
|
|
32
|
+
this.child.stdin.on("error", this.onStdinError);
|
|
33
|
+
this.timer = setTimeout(() => {
|
|
34
|
+
this.terminate(agyError("Agy generation timed out", 504, "AGY_TIMEOUT"));
|
|
35
|
+
}, AGY_PROCESS_POLICY.timeoutMs);
|
|
36
|
+
this.signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
37
|
+
if (this.signal?.aborted) {
|
|
38
|
+
this.onAbort();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.child.stdin.write(prompt);
|
|
42
|
+
this.child.stdin.end();
|
|
43
|
+
}
|
|
44
|
+
onStdout = (chunk) => {
|
|
45
|
+
if (this.stdout.length < AGY_PROCESS_POLICY.maxOutputBytes)
|
|
46
|
+
this.stdout += chunk.toString();
|
|
47
|
+
};
|
|
48
|
+
onStderr = (chunk) => {
|
|
49
|
+
if (this.stderr.length < AGY_PROCESS_POLICY.maxOutputBytes)
|
|
50
|
+
this.stderr += chunk.toString();
|
|
51
|
+
};
|
|
52
|
+
onStdinError = () => {
|
|
53
|
+
// Preserve ignored stdin errors; the child error/close owns the result.
|
|
54
|
+
};
|
|
55
|
+
onAbort = () => {
|
|
56
|
+
this.terminate(agyError("Generation canceled", 499, "GENERATION_CANCELED"));
|
|
57
|
+
};
|
|
58
|
+
terminate(reason) {
|
|
59
|
+
if (this.settled || this.reason)
|
|
60
|
+
return;
|
|
61
|
+
this.reason = reason;
|
|
62
|
+
clearTimeout(this.timer);
|
|
63
|
+
// Arm before TERM so even a synchronous close clears the grace timer.
|
|
64
|
+
this.graceTimer = setTimeout(() => {
|
|
65
|
+
if (!this.settled)
|
|
66
|
+
this.child.kill("SIGKILL");
|
|
67
|
+
}, AGY_PROCESS_POLICY.terminateGraceMs);
|
|
68
|
+
this.child.kill("SIGTERM");
|
|
69
|
+
}
|
|
70
|
+
onError = (err) => {
|
|
71
|
+
if (this.settled || this.reason)
|
|
72
|
+
return;
|
|
73
|
+
const bin = resolveAgyBin();
|
|
74
|
+
const hint = err.code === "ENOENT"
|
|
75
|
+
? `. "${bin}" was not found — install agy or set IMA2_AGY_BIN=/absolute/path/to/agy`
|
|
76
|
+
: "";
|
|
77
|
+
this.reason = agyError(`Agy process error: ${err.message}${hint}`, 502, "AGY_PROCESS_ERROR");
|
|
78
|
+
};
|
|
79
|
+
finish = (code) => {
|
|
80
|
+
if (this.settled)
|
|
81
|
+
return;
|
|
82
|
+
this.settled = true;
|
|
83
|
+
clearTimeout(this.timer);
|
|
84
|
+
clearTimeout(this.graceTimer);
|
|
85
|
+
this.signal?.removeEventListener("abort", this.onAbort);
|
|
86
|
+
this.child.stdout.removeListener("data", this.onStdout);
|
|
87
|
+
this.child.stderr.removeListener("data", this.onStderr);
|
|
88
|
+
this.child.stdin.removeListener("error", this.onStdinError);
|
|
89
|
+
this.child.removeListener("error", this.onError);
|
|
90
|
+
this.child.removeListener("close", this.finish);
|
|
91
|
+
if (this.reason) {
|
|
92
|
+
this.reject(this.reason);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (code !== 0 && !this.stdout.trim()) {
|
|
96
|
+
this.reject(agyError(`Agy exited with code ${code}: ${this.stderr.slice(0, 200)}`, 502, "AGY_PROCESS_ERROR"));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.resolve({ stdout: this.stdout, stderr: this.stderr });
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function spawnAgy(prompt, signal) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
if (signal?.aborted) {
|
|
105
|
+
reject(agyError("Generation canceled", 499, "GENERATION_CANCELED"));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const child = spawn(resolveAgyBin(), ["-p", "-"], {
|
|
109
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
110
|
+
env: {
|
|
111
|
+
PATH: buildAgyPathEnv(),
|
|
112
|
+
HOME: process.env.HOME,
|
|
113
|
+
USERPROFILE: process.env.USERPROFILE,
|
|
114
|
+
TMPDIR: process.env.TMPDIR,
|
|
115
|
+
TEMP: process.env.TEMP,
|
|
116
|
+
LANG: process.env.LANG,
|
|
117
|
+
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
new AgyProcessLifetime(child, signal, resolve, reject).start(prompt);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Express routes are case-insensitive by default; do not protect /apix as /api. */
|
|
2
|
+
export function isApiRequestPath(path) {
|
|
3
|
+
return /^\/api(?:\/|$)/i.test(path);
|
|
4
|
+
}
|
|
5
|
+
function rejectBudget(res, expires, now) {
|
|
6
|
+
res.setHeader("Retry-After", String(Math.max(1, Math.ceil((expires - now) / 1000))));
|
|
7
|
+
return res.status(429).json({
|
|
8
|
+
error: { code: "API_RATE_LIMITED", message: "Too many API requests; retry after the indicated delay" },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/** App-owned windows, bounded memory and socket identity (never proxy headers). */
|
|
12
|
+
export function createApiRequestBudget(policy, clock = Date.now) {
|
|
13
|
+
const peers = new Map();
|
|
14
|
+
function prune(now) {
|
|
15
|
+
let earliest = now + policy.windowMs;
|
|
16
|
+
for (const [peer, window] of peers) {
|
|
17
|
+
if (window.expires <= now)
|
|
18
|
+
peers.delete(peer);
|
|
19
|
+
else
|
|
20
|
+
earliest = Math.min(earliest, window.expires);
|
|
21
|
+
}
|
|
22
|
+
return earliest;
|
|
23
|
+
}
|
|
24
|
+
return function apiRequestBudget(req, res, next) {
|
|
25
|
+
if (!isApiRequestPath(req.path))
|
|
26
|
+
return next();
|
|
27
|
+
const now = clock();
|
|
28
|
+
const peer = req.socket.remoteAddress ?? "unknown-peer";
|
|
29
|
+
let window = peers.get(peer);
|
|
30
|
+
if (!window || window.expires <= now) {
|
|
31
|
+
if (!window && peers.size >= policy.maxPeers) {
|
|
32
|
+
const earliest = prune(now);
|
|
33
|
+
if (peers.size >= policy.maxPeers)
|
|
34
|
+
return rejectBudget(res, earliest, now);
|
|
35
|
+
}
|
|
36
|
+
window = { expires: now + policy.windowMs, requests: 0, mutations: 0 };
|
|
37
|
+
peers.set(peer, window);
|
|
38
|
+
}
|
|
39
|
+
const mutation = !["GET", "HEAD", "OPTIONS"].includes(req.method);
|
|
40
|
+
if (window.requests >= policy.requests || (mutation && window.mutations >= policy.mutations)) {
|
|
41
|
+
return rejectBudget(res, window.expires, now);
|
|
42
|
+
}
|
|
43
|
+
window.requests++;
|
|
44
|
+
if (mutation)
|
|
45
|
+
window.mutations++;
|
|
46
|
+
return next();
|
|
47
|
+
};
|
|
48
|
+
}
|
package/lib/assetLifecycle.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getDb } from "./db.js";
|
|
2
2
|
import { mkdir, rename, unlink, lstat, realpath } from "fs/promises";
|
|
3
|
-
import { resolve, sep } from "path";
|
|
3
|
+
import { basename, dirname, join, resolve, sep } from "path";
|
|
4
4
|
import { moveToSystemTrash } from "./systemTrash.js";
|
|
5
5
|
import { config } from "../config.js";
|
|
6
6
|
export function resolveInGenerated(rootDir, relPath) {
|
|
@@ -19,7 +19,8 @@ export function resolveInGenerated(rootDir, relPath) {
|
|
|
19
19
|
}
|
|
20
20
|
const baseDir = resolve(config.storage.generatedDir);
|
|
21
21
|
const target = resolve(baseDir, relPath);
|
|
22
|
-
|
|
22
|
+
const prefix = baseDir.endsWith(sep) ? baseDir : baseDir + sep;
|
|
23
|
+
if (target === baseDir || !target.startsWith(prefix)) {
|
|
23
24
|
const err = new Error("filename escapes generated/");
|
|
24
25
|
err.status = 400;
|
|
25
26
|
err.code = "INVALID_FILENAME";
|
|
@@ -28,7 +29,16 @@ export function resolveInGenerated(rootDir, relPath) {
|
|
|
28
29
|
return target;
|
|
29
30
|
}
|
|
30
31
|
export async function assertRegularGeneratedPath(path) {
|
|
31
|
-
|
|
32
|
+
try {
|
|
33
|
+
await regularFileWithin(path, config.storage.generatedDir);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Existing assets only; output filenames use the separate lexical resolver. */
|
|
40
|
+
async function regularFileWithin(path, root) {
|
|
41
|
+
const baseDir = await realpath(resolve(root));
|
|
32
42
|
const stat = await lstat(path);
|
|
33
43
|
if (stat.isSymbolicLink()) {
|
|
34
44
|
const err = new Error("symbolic links are not valid assets");
|
|
@@ -36,13 +46,18 @@ export async function assertRegularGeneratedPath(path) {
|
|
|
36
46
|
err.code = "INVALID_FILENAME";
|
|
37
47
|
throw err;
|
|
38
48
|
}
|
|
49
|
+
if (!stat.isFile()) {
|
|
50
|
+
throw Object.assign(new Error("only regular files are valid assets"), { status: 400, code: "INVALID_FILENAME" });
|
|
51
|
+
}
|
|
39
52
|
const canonical = await realpath(path);
|
|
40
|
-
|
|
53
|
+
const prefix = baseDir.endsWith(sep) ? baseDir : baseDir + sep;
|
|
54
|
+
if (canonical === baseDir || !canonical.startsWith(prefix)) {
|
|
41
55
|
const err = new Error("filename escapes generated/");
|
|
42
56
|
err.status = 400;
|
|
43
57
|
err.code = "INVALID_FILENAME";
|
|
44
58
|
throw err;
|
|
45
59
|
}
|
|
60
|
+
return canonical;
|
|
46
61
|
}
|
|
47
62
|
function nodesReferencingFilename(filename) {
|
|
48
63
|
// The client stores imageUrl as `/generated/<encoded filename>` in node data JSON.
|
|
@@ -117,7 +132,7 @@ export async function trashAsset(rootDir, filename) {
|
|
|
117
132
|
trashMethod = "internal";
|
|
118
133
|
const trashDir = resolve(config.storage.trashDir);
|
|
119
134
|
await mkdir(trashDir, { recursive: true });
|
|
120
|
-
const trashId = `${Date.now()}_${
|
|
135
|
+
const trashId = `${Date.now()}_${basename(src)}`;
|
|
121
136
|
for (const p of paths) {
|
|
122
137
|
const dest = resolve(trashDir, p.endsWith(".json") ? `${trashId}.json` : trashId);
|
|
123
138
|
await rename(p, dest);
|
|
@@ -165,16 +180,34 @@ export async function deleteAssetPermanent(rootDir, filename) {
|
|
|
165
180
|
};
|
|
166
181
|
}
|
|
167
182
|
export async function restoreAsset(rootDir, trashId, originalFilename) {
|
|
168
|
-
void rootDir;
|
|
169
183
|
const trashDir = resolve(config.storage.trashDir);
|
|
170
184
|
const src = resolve(trashDir, trashId);
|
|
171
|
-
|
|
185
|
+
const prefix = trashDir.endsWith(sep) ? trashDir : trashDir + sep;
|
|
186
|
+
if (src === trashDir || !src.startsWith(prefix)) {
|
|
172
187
|
const err = new Error("invalid trashId");
|
|
173
188
|
err.status = 400;
|
|
189
|
+
err.code = "INVALID_FILENAME";
|
|
174
190
|
throw err;
|
|
175
191
|
}
|
|
192
|
+
const canonicalSrc = await regularFileWithin(src, trashDir);
|
|
193
|
+
let canonicalSidecar;
|
|
194
|
+
try {
|
|
195
|
+
canonicalSidecar = await regularFileWithin(`${src}.json`, trashDir);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (error.code !== "ENOENT")
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
176
201
|
const dst = resolveInGenerated(rootDir, originalFilename);
|
|
177
|
-
await
|
|
178
|
-
|
|
202
|
+
const generatedRoot = await realpath(config.storage.generatedDir);
|
|
203
|
+
const parent = await realpath(dirname(dst));
|
|
204
|
+
const generatedPrefix = generatedRoot.endsWith(sep) ? generatedRoot : generatedRoot + sep;
|
|
205
|
+
if (parent !== generatedRoot && !parent.startsWith(generatedPrefix)) {
|
|
206
|
+
throw Object.assign(new Error("restore destination escapes generated/"), { status: 400, code: "INVALID_FILENAME" });
|
|
207
|
+
}
|
|
208
|
+
const canonicalDst = join(parent, basename(dst));
|
|
209
|
+
await rename(canonicalSrc, canonicalDst);
|
|
210
|
+
if (canonicalSidecar)
|
|
211
|
+
await rename(canonicalSidecar, `${canonicalDst}.json`);
|
|
179
212
|
return { ok: true };
|
|
180
213
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { mkdir, writeFile,
|
|
2
|
-
import {
|
|
3
|
-
import { basename, join, normalize, parse } from "path";
|
|
1
|
+
import { mkdir, writeFile, readFile, lstat, realpath } from "fs/promises";
|
|
2
|
+
import { basename, dirname, resolve, parse, sep } from "path";
|
|
4
3
|
import { randomBytes } from "crypto";
|
|
5
4
|
import sharp from "sharp";
|
|
6
5
|
import { embedImageMetadataBestEffort } from "./imageMetadataStore.js";
|
|
@@ -34,12 +33,20 @@ function assertSafeFilename(filename) {
|
|
|
34
33
|
}
|
|
35
34
|
function safeSourceBase(sourceFilename) {
|
|
36
35
|
const parsed = parse(basename(String(sourceFilename || "image")));
|
|
37
|
-
|
|
36
|
+
const safe = parsed.name.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
37
|
+
let start = 0;
|
|
38
|
+
let end = safe.length;
|
|
39
|
+
while (start < end && safe[start] === "-")
|
|
40
|
+
start++;
|
|
41
|
+
while (end > start && safe[end - 1] === "-")
|
|
42
|
+
end--;
|
|
43
|
+
return safe.slice(start, Math.min(end, start + 80)) || "image";
|
|
38
44
|
}
|
|
39
45
|
function ensureInsideGeneratedDir(generatedDir, filename) {
|
|
40
|
-
const full =
|
|
41
|
-
const root =
|
|
42
|
-
|
|
46
|
+
const full = resolve(generatedDir, filename);
|
|
47
|
+
const root = resolve(generatedDir);
|
|
48
|
+
const prefix = root.endsWith(sep) ? root : root + sep;
|
|
49
|
+
if (full === root || !full.startsWith(prefix)) {
|
|
43
50
|
const err = new Error("Canvas version path escapes generated directory");
|
|
44
51
|
err.status = 400;
|
|
45
52
|
err.code = "CANVAS_VERSION_PATH_ESCAPE";
|
|
@@ -47,6 +54,28 @@ function ensureInsideGeneratedDir(generatedDir, filename) {
|
|
|
47
54
|
}
|
|
48
55
|
return full;
|
|
49
56
|
}
|
|
57
|
+
/** Existing leaves must be regular files; only a new output leaf may be absent. */
|
|
58
|
+
async function checkedCanvasPath(generatedDir, filename, allowMissing = false) {
|
|
59
|
+
const lexical = ensureInsideGeneratedDir(generatedDir, filename);
|
|
60
|
+
const root = await realpath(generatedDir);
|
|
61
|
+
const parent = await realpath(dirname(lexical));
|
|
62
|
+
const prefix = root.endsWith(sep) ? root : root + sep;
|
|
63
|
+
if (parent !== root && !parent.startsWith(prefix)) {
|
|
64
|
+
throw Object.assign(new Error("Canvas path escapes generated directory"), { status: 400, code: "CANVAS_VERSION_PATH_ESCAPE" });
|
|
65
|
+
}
|
|
66
|
+
const full = resolve(parent, basename(lexical));
|
|
67
|
+
try {
|
|
68
|
+
const stat = await lstat(full);
|
|
69
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
70
|
+
throw Object.assign(new Error("Canvas path must be a regular file"), { status: 400, code: "CANVAS_VERSION_PATH_ESCAPE" });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (!allowMissing || error.code !== "ENOENT")
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
return full;
|
|
78
|
+
}
|
|
50
79
|
function makeCanvasFilename(sourceFilename) {
|
|
51
80
|
const stamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
|
|
52
81
|
const rand = randomBytes(3).toString("hex");
|
|
@@ -54,12 +83,13 @@ function makeCanvasFilename(sourceFilename) {
|
|
|
54
83
|
}
|
|
55
84
|
async function writeCanvasPng(ctx, filename, buffer, meta) {
|
|
56
85
|
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
57
|
-
const full =
|
|
86
|
+
const full = await checkedCanvasPath(ctx.config.storage.generatedDir, filename, true);
|
|
87
|
+
const sidecar = await checkedCanvasPath(ctx.config.storage.generatedDir, `${filename}.json`, true);
|
|
58
88
|
const embedded = await embedImageMetadataBestEffort(buffer, "png", meta, {
|
|
59
89
|
version: ctx.packageVersion,
|
|
60
90
|
});
|
|
61
91
|
await writeFile(full, embedded.buffer);
|
|
62
|
-
await writeFile(
|
|
92
|
+
await writeFile(sidecar, JSON.stringify(meta)).catch(() => { });
|
|
63
93
|
invalidateHistoryIndex();
|
|
64
94
|
}
|
|
65
95
|
async function readGeneratedMetadata(ctx, filename) {
|
|
@@ -67,7 +97,8 @@ async function readGeneratedMetadata(ctx, filename) {
|
|
|
67
97
|
return null;
|
|
68
98
|
try {
|
|
69
99
|
const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, basename(filename));
|
|
70
|
-
|
|
100
|
+
const sidecar = await checkedCanvasPath(ctx.config.storage.generatedDir, `${full}.json`);
|
|
101
|
+
return JSON.parse(await readFile(sidecar, "utf8"));
|
|
71
102
|
}
|
|
72
103
|
catch {
|
|
73
104
|
return null;
|
|
@@ -149,8 +180,9 @@ export async function createCanvasVersion(ctx, input) {
|
|
|
149
180
|
export async function updateCanvasVersion(ctx, filename, input) {
|
|
150
181
|
assertSafeFilename(filename);
|
|
151
182
|
assertPngBuffer(input.buffer);
|
|
152
|
-
|
|
153
|
-
|
|
183
|
+
await checkedCanvasPath(ctx.config.storage.generatedDir, filename).catch((cause) => {
|
|
184
|
+
if (cause.code !== "ENOENT")
|
|
185
|
+
throw cause;
|
|
154
186
|
const err = new Error("Canvas version not found");
|
|
155
187
|
err.status = 404;
|
|
156
188
|
err.code = "CANVAS_VERSION_NOT_FOUND";
|
|
@@ -185,7 +217,6 @@ export async function updateCanvasVersion(ctx, filename, input) {
|
|
|
185
217
|
export async function recordCanvasAnnotationBake(ctx, filename, snapshot, annotationOnly) {
|
|
186
218
|
assertSafeFilename(filename);
|
|
187
219
|
assertAnnotationSnapshot(snapshot);
|
|
188
|
-
const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
|
|
189
220
|
const previousMeta = await readGeneratedMetadata(ctx, filename);
|
|
190
221
|
if (!previousMeta) {
|
|
191
222
|
const err = new Error("Canvas version not found");
|
|
@@ -193,6 +224,7 @@ export async function recordCanvasAnnotationBake(ctx, filename, snapshot, annota
|
|
|
193
224
|
err.code = "CANVAS_VERSION_NOT_FOUND";
|
|
194
225
|
throw err;
|
|
195
226
|
}
|
|
227
|
+
const full = await checkedCanvasPath(ctx.config.storage.generatedDir, filename);
|
|
196
228
|
const buffer = await readFile(full);
|
|
197
229
|
const meta = {
|
|
198
230
|
...previousMeta,
|
|
@@ -224,7 +256,7 @@ export async function revertCanvasAnnotations(ctx, filename) {
|
|
|
224
256
|
err.code = "CANVAS_ANNOTATIONS_NOT_BAKED";
|
|
225
257
|
throw err;
|
|
226
258
|
}
|
|
227
|
-
const sourceFull =
|
|
259
|
+
const sourceFull = await checkedCanvasPath(ctx.config.storage.generatedDir, basename(sourceFilename));
|
|
228
260
|
const cleanPng = await sharp(await readFile(sourceFull)).png().toBuffer();
|
|
229
261
|
const snapshot = previousMeta.annotationSnapshot ?? null;
|
|
230
262
|
const annotationOnly = Boolean(previousMeta.annotationOnly);
|
package/lib/capabilities.js
CHANGED
|
@@ -6,7 +6,8 @@ import { loadAllBundledSnapshots } from "./mcp/snapshotStore.js";
|
|
|
6
6
|
import { KEY_TO_ENV, WRITABLE_CONFIG_KEYS } from "./configKeys.js";
|
|
7
7
|
import { DEFAULT_IMAGE_QUALITY, VALID_IMAGE_QUALITIES } from "./oauthNormalize.js";
|
|
8
8
|
import { MAX_REF2V_REFERENCES, MAX_REFERENCE_AUDIOS, MAX_VIDEO_DURATION, MIN_VIDEO_DURATION } from "./imageModels.js";
|
|
9
|
-
import { deriveProviderIds } from "./providers/derive.js";
|
|
9
|
+
import { deriveProviderIds, getProviderSurfaceSupport } from "./providers/derive.js";
|
|
10
|
+
import { PROVIDER_SURFACES } from "./providers/surfaceSupport.js";
|
|
10
11
|
const VALID_MODES = ["auto", "direct"];
|
|
11
12
|
const VALID_PROVIDERS = ["auto", ...deriveProviderIds()];
|
|
12
13
|
const AGENT_COMMANDS = [
|
|
@@ -39,6 +40,11 @@ export function buildIma2Capabilities({ appConfig = runtimeConfigDefault, packag
|
|
|
39
40
|
server,
|
|
40
41
|
version: packageVersion,
|
|
41
42
|
...(lanes ? { lanes } : {}),
|
|
43
|
+
providerSurfaces: Object.fromEntries(deriveProviderIds().map((id) => [id,
|
|
44
|
+
Object.fromEntries(PROVIDER_SURFACES.map((surface) => [surface,
|
|
45
|
+
getProviderSurfaceSupport(id, surface),
|
|
46
|
+
])),
|
|
47
|
+
])),
|
|
42
48
|
commands: AGENT_COMMANDS,
|
|
43
49
|
defaults: {
|
|
44
50
|
oauth: {
|
package/lib/configKeys.js
CHANGED
|
@@ -28,6 +28,7 @@ export const WRITABLE_CONFIG_KEYS = new Set([
|
|
|
28
28
|
"storage.generatedDirName",
|
|
29
29
|
"server.port",
|
|
30
30
|
"server.host",
|
|
31
|
+
"server.publicOrigins",
|
|
31
32
|
"server.bodyLimit",
|
|
32
33
|
"oauth.proxyPort",
|
|
33
34
|
"oauth.statusTimeoutMs",
|
|
@@ -49,6 +50,7 @@ export const KEY_TO_ENV = {
|
|
|
49
50
|
"features.cardNews": "IMA2_CARD_NEWS",
|
|
50
51
|
"server.port": "IMA2_PORT",
|
|
51
52
|
"server.host": "IMA2_HOST",
|
|
53
|
+
"server.publicOrigins": "IMA2_PUBLIC_ORIGINS",
|
|
52
54
|
"server.bodyLimit": "IMA2_BODY_LIMIT",
|
|
53
55
|
"oauth.proxyPort": "IMA2_OAUTH_PROXY_PORT",
|
|
54
56
|
"storage.generatedDir": "IMA2_GENERATED_DIR",
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
import { RESPONSE_DIAGNOSTIC_CODES } from "../responsesErrors.js";
|
|
2
|
+
export function statusForErrorCode(code, fallback = 500) {
|
|
3
|
+
if (code === "GROK_API_KEY_MISSING")
|
|
4
|
+
return 401;
|
|
5
|
+
if (code === "OAUTH_UNAVAILABLE" || code === "NETWORK_FAILED")
|
|
6
|
+
return 503;
|
|
7
|
+
if (code === "AUTH_CHATGPT_EXPIRED" || code === "AUTH_API_KEY_INVALID")
|
|
8
|
+
return 401;
|
|
9
|
+
if (code === "API_KEY_REQUIRED")
|
|
10
|
+
return 401;
|
|
11
|
+
if (code === "UPSTREAM_5XX")
|
|
12
|
+
return 502;
|
|
13
|
+
if (code === "RESPONSES_STREAM_ERROR")
|
|
14
|
+
return 502;
|
|
15
|
+
if (code === "OAUTH_IMAGE_TIMEOUT")
|
|
16
|
+
return 504;
|
|
17
|
+
if (code === "INVALID_REQUEST")
|
|
18
|
+
return 400;
|
|
19
|
+
if (RESPONSE_DIAGNOSTIC_CODES.has(code))
|
|
20
|
+
return 422;
|
|
21
|
+
if (code === "SAFETY_REFUSAL" || code === "MODERATION_REFUSED" || code === "moderation_blocked")
|
|
22
|
+
return 422;
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
1
25
|
export const PROVIDER_ERROR_MAP = {
|
|
2
26
|
MINIMAX_API_KEY_MISSING: "AUTH_INVALID",
|
|
3
27
|
MINIMAX_AUTH_FAILED: "AUTH_INVALID",
|
|
@@ -43,6 +67,7 @@ export const PROVIDER_ERROR_MAP = {
|
|
|
43
67
|
GEMINI_API_SAFETY_BLOCKED: "CONTENT_REJECTED",
|
|
44
68
|
GEMINI_API_UPSTREAM_ERROR: "NETWORK_FAILURE",
|
|
45
69
|
GEMINI_API_MASK_UNSUPPORTED: "CAPABILITY_UNSUPPORTED",
|
|
70
|
+
GROK_API_KEY_MISSING: "AUTH_INVALID",
|
|
46
71
|
GROK_AUTH_FAILED: "AUTH_INVALID",
|
|
47
72
|
GROK_BAD_REQUEST: "CAPABILITY_UNSUPPORTED",
|
|
48
73
|
GROK_EMPTY_RESPONSE: "INTERNAL_STATE_ERROR",
|
|
@@ -78,6 +103,7 @@ export const PROVIDER_ERROR_MAP = {
|
|
|
78
103
|
GROK_MASK_UNSUPPORTED: "CAPABILITY_UNSUPPORTED",
|
|
79
104
|
GROK_REF_TOO_MANY: "CAPABILITY_UNSUPPORTED",
|
|
80
105
|
AGY_ARTIFACT_NOT_FOUND: "INTERNAL_STATE_ERROR",
|
|
106
|
+
AGY_ARTIFACT_TOO_LARGE: "INTERNAL_STATE_ERROR",
|
|
81
107
|
AGY_GENERATION_FAILED: "INTERNAL_STATE_ERROR",
|
|
82
108
|
AGY_MALFORMED_RESULT: "INTERNAL_STATE_ERROR",
|
|
83
109
|
AGY_PARSE_FAILED: "INTERNAL_STATE_ERROR",
|
package/lib/eventBus.js
CHANGED
|
@@ -102,14 +102,18 @@ export function subscribe(listener) {
|
|
|
102
102
|
export function replayOldestId() {
|
|
103
103
|
return ring.length > 0 ? ring[0]?.id ?? null : null;
|
|
104
104
|
}
|
|
105
|
-
/**
|
|
105
|
+
/** Reads the process cursor without allocating an event. */
|
|
106
|
+
export function latestEventId() {
|
|
107
|
+
return seq;
|
|
108
|
+
}
|
|
109
|
+
/** True for an evicted cursor or a cursor ahead of this process. */
|
|
106
110
|
export function hasReplayGap(lastEventId) {
|
|
107
|
-
if (lastEventId
|
|
108
|
-
return false;
|
|
109
|
-
const oldest = ring[0]?.id;
|
|
110
|
-
if (oldest === undefined)
|
|
111
|
+
if (!Number.isSafeInteger(lastEventId) || lastEventId < 0)
|
|
111
112
|
return false;
|
|
112
|
-
|
|
113
|
+
if (lastEventId > seq)
|
|
114
|
+
return true;
|
|
115
|
+
const oldest = replayOldestId();
|
|
116
|
+
return oldest !== null && lastEventId < oldest - 1;
|
|
113
117
|
}
|
|
114
118
|
export function replaySince(lastEventId) {
|
|
115
119
|
const idx = ring.findIndex(e => e.id > lastEventId);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Maximum time one accepted SSE write may wait for drain. */
|
|
2
|
+
export const SSE_STREAM_POLICY = Object.freeze({ drainTimeoutMs: 15_000 });
|
|
3
|
+
/** Parse the replay cursor header without accepting numeric coercions or overflow. */
|
|
4
|
+
export function parseEventCursor(value) {
|
|
5
|
+
if (value === null || !/^\d+$/.test(value))
|
|
6
|
+
return null;
|
|
7
|
+
const cursor = Number(value);
|
|
8
|
+
return Number.isSafeInteger(cursor) ? cursor : null;
|
|
9
|
+
}
|