ima2-gen 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/bin/commands/service.js +450 -0
- package/bin/commands/stop.js +129 -0
- package/bin/ima2.js +15 -1
- package/bin/lib/serviceTemplates.js +87 -0
- package/docs/API.md +1 -0
- package/docs/migration/runtime-test-inventory.md +3 -1
- package/lib/processControl.js +138 -0
- package/lib/runtimeContext.js +1 -0
- package/package.json +2 -2
- package/routes/admin.js +51 -0
- package/routes/edit.js +21 -0
- package/routes/index.js +2 -0
- package/server.js +12 -3
- package/ui/dist/.vite/manifest.json +32 -32
- package/ui/dist/assets/{AgentWorkspace-fcIXiWV6.js → AgentWorkspace-DAOw-Q1x.js} +1 -1
- package/ui/dist/assets/AssetGenWorkspace-Bhi2OrfW.js +2 -0
- package/ui/dist/assets/AssetsWorkspace-D-pcyPvz.js +1 -0
- package/ui/dist/assets/{CardNewsWorkspace-ltdjG40R.js → CardNewsWorkspace-BETskmU7.js} +1 -1
- package/ui/dist/assets/{GenerationRequestLogPanel-B68EUfUg.js → GenerationRequestLogPanel-sr-wSiOe.js} +1 -1
- package/ui/dist/assets/{HomeWorkspace-BBOG_aNm.js → HomeWorkspace-vjN-A1AR.js} +1 -1
- package/ui/dist/assets/{KeyingPanel-Bv0n_bhk.js → KeyingPanel-D7iozqcS.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-VwdctvAW.js → NodeCanvas-Bu6q2z3V.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-BBMpF_3Q.js → PromptBuilderPanel-DklSQCS_.js} +2 -2
- package/ui/dist/assets/{PromptImportDialog-D4QYbRfH.js → PromptImportDialog-CZwFZDvz.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-Bx7BeTFh.js → PromptImportDiscoverySection-B5Lyhl9X.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-Bwf60URx.js → PromptImportFolderSection-DmMFtYmn.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-mNG2wZkQ.js → PromptLibraryPanel-BHr73ns_.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-DrQ44R-B.js +1 -0
- package/ui/dist/assets/{SpriteRecipeWorkspace-Da9qTQX0.js → SpriteRecipeWorkspace-ChboDVCj.js} +1 -1
- package/ui/dist/assets/index-C4IY6hxw.css +1 -0
- package/ui/dist/assets/index-CVVv0v-b.js +30 -0
- package/ui/dist/assets/index-D1SD3LcV.js +5 -0
- package/ui/dist/assets/{pptxgen.es-BE4CSd5F.js → pptxgen.es-DIQkG3ve.js} +1 -1
- package/ui/dist/assets/{useAgentDialogFocus-R64NV-ZZ.js → useAgentDialogFocus-CqjQmoEH.js} +1 -1
- package/ui/dist/index.html +15 -3
- package/ui/dist/assets/AssetGenWorkspace-CqXyN9at.js +0 -2
- package/ui/dist/assets/AssetsWorkspace-xwHFVOb7.js +0 -1
- package/ui/dist/assets/SettingsWorkspace-DA5tIa0Q.js +0 -1
- package/ui/dist/assets/index-Bv1Yrz8W.js +0 -5
- package/ui/dist/assets/index-C0nfanMr.css +0 -1
- package/ui/dist/assets/index-CxEuSZMC.js +0 -30
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure renderers for background-service artifacts. Kept free of process/fs so
|
|
3
|
+
* contract tests can snapshot them (adversarial audit 260821c: PATH must be
|
|
4
|
+
* baked — launchd/systemd hand jobs a minimal environment and the grok/oauth
|
|
5
|
+
* proxies spawn bare binaries that silently die without the user's PATH).
|
|
6
|
+
*/
|
|
7
|
+
export const LAUNCHD_LABEL = "com.ima2.server";
|
|
8
|
+
export const SYSTEMD_UNIT = "ima2.service";
|
|
9
|
+
function xmlEscape(value) {
|
|
10
|
+
return value
|
|
11
|
+
.replaceAll("&", "&")
|
|
12
|
+
.replaceAll("<", "<")
|
|
13
|
+
.replaceAll(">", ">");
|
|
14
|
+
}
|
|
15
|
+
export function renderLaunchdPlist(input) {
|
|
16
|
+
const env = [
|
|
17
|
+
["IMA2_SERVICE", "1"],
|
|
18
|
+
["PATH", input.pathEnv],
|
|
19
|
+
];
|
|
20
|
+
if (input.configDir)
|
|
21
|
+
env.push(["IMA2_CONFIG_DIR", input.configDir]);
|
|
22
|
+
const envXml = env
|
|
23
|
+
.map(([k, v]) => ` <key>${xmlEscape(k)}</key><string>${xmlEscape(v)}</string>`)
|
|
24
|
+
.join("\n");
|
|
25
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
26
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
27
|
+
<plist version="1.0">
|
|
28
|
+
<dict>
|
|
29
|
+
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
30
|
+
<key>ProgramArguments</key>
|
|
31
|
+
<array>
|
|
32
|
+
<string>${xmlEscape(input.nodePath)}</string>
|
|
33
|
+
<string>${xmlEscape(input.serverJs)}</string>
|
|
34
|
+
</array>
|
|
35
|
+
<key>WorkingDirectory</key><string>${xmlEscape(input.rootDir)}</string>
|
|
36
|
+
<key>RunAtLoad</key><true/>
|
|
37
|
+
<key>KeepAlive</key><true/>
|
|
38
|
+
<key>StandardOutPath</key><string>${xmlEscape(input.logDir)}/service.out.log</string>
|
|
39
|
+
<key>StandardErrorPath</key><string>${xmlEscape(input.logDir)}/service.err.log</string>
|
|
40
|
+
<key>EnvironmentVariables</key>
|
|
41
|
+
<dict>
|
|
42
|
+
${envXml}
|
|
43
|
+
</dict>
|
|
44
|
+
</dict>
|
|
45
|
+
</plist>
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
export function renderSystemdUnit(input) {
|
|
49
|
+
const lines = [
|
|
50
|
+
"[Unit]",
|
|
51
|
+
"Description=ima2-gen local generation server",
|
|
52
|
+
"After=network.target",
|
|
53
|
+
"",
|
|
54
|
+
"[Service]",
|
|
55
|
+
`ExecStart=${input.nodePath} ${input.serverJs}`,
|
|
56
|
+
`WorkingDirectory=${input.rootDir}`,
|
|
57
|
+
"Restart=always",
|
|
58
|
+
"RestartSec=2",
|
|
59
|
+
"Environment=IMA2_SERVICE=1",
|
|
60
|
+
`Environment=PATH=${input.pathEnv}`,
|
|
61
|
+
];
|
|
62
|
+
if (input.configDir)
|
|
63
|
+
lines.push(`Environment=IMA2_CONFIG_DIR=${input.configDir}`);
|
|
64
|
+
lines.push("", "[Install]", "WantedBy=default.target", "");
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* launchctl's trap: `load` can exit 0 while writing "Load failed: ..." to
|
|
69
|
+
* stderr (opencodex hit this in production). Treat that stderr shape as a
|
|
70
|
+
* failure regardless of exit status.
|
|
71
|
+
*/
|
|
72
|
+
export function launchctlOutputIndicatesFailure(stderr) {
|
|
73
|
+
const s = (stderr || "").toLowerCase();
|
|
74
|
+
return s.includes("load failed") || s.includes("bootstrap failed") || s.includes("input/output error");
|
|
75
|
+
}
|
|
76
|
+
/** Paths drifted (nvm/prefix/config-dir move) since install? repair re-renders. */
|
|
77
|
+
export function serviceStateStale(state, current) {
|
|
78
|
+
const issues = [];
|
|
79
|
+
if (state.nodePath !== current.nodePath)
|
|
80
|
+
issues.push(`node moved: ${state.nodePath} -> ${current.nodePath}`);
|
|
81
|
+
if (state.serverJs !== current.serverJs)
|
|
82
|
+
issues.push(`server.js moved: ${state.serverJs} -> ${current.serverJs}`);
|
|
83
|
+
if (current.configDir !== undefined && state.configDir !== current.configDir) {
|
|
84
|
+
issues.push(`config dir moved: ${state.configDir} -> ${current.configDir}`);
|
|
85
|
+
}
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
package/docs/API.md
CHANGED
|
@@ -31,6 +31,7 @@ Generation section below for the full endpoint specification.
|
|
|
31
31
|
| Method | Path | Notes |
|
|
32
32
|
|---|---|---|
|
|
33
33
|
| `GET` | `/api/health` | Server health, version, paths, provider policy |
|
|
34
|
+
| `POST` | `/api/admin/stop` | Clean shutdown (local admin only): requires the boot-generated `X-Ima2-Admin-Nonce` from `~/.ima2/server.json`; any request with an `Origin` header is refused (browser drive-by protection). Responds `202` then self-signals SIGTERM |
|
|
34
35
|
| `GET` | `/api/providers` | Provider availability and runtime ports |
|
|
35
36
|
| `GET` | `/api/oauth/status` | OAuth proxy status and visible models |
|
|
36
37
|
| `GET` | `/api/grok/status` | Bundled progrok status and visible xAI image models |
|
|
@@ -4,7 +4,7 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
|
|
|
4
4
|
|
|
5
5
|
_Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
|
|
6
6
|
|
|
7
|
-
Total:
|
|
7
|
+
Total: 378 (runtime: 174, contract: 204)
|
|
8
8
|
|
|
9
9
|
## Runtime-importing tests
|
|
10
10
|
- `tests/agent-mode-auto-planner-contract.test.ts`
|
|
@@ -146,6 +146,7 @@ Total: 376 (runtime: 172, contract: 204)
|
|
|
146
146
|
- `tests/serve-singleton-contract.test.ts`
|
|
147
147
|
- `tests/serve-ui-build-contract.test.ts`
|
|
148
148
|
- `tests/server-code-preservation.test.ts`
|
|
149
|
+
- `tests/service-command-contract.test.ts`
|
|
149
150
|
- `tests/skill-video-claims-contract.test.ts`
|
|
150
151
|
- `tests/sprite-anchor-policy.test.ts`
|
|
151
152
|
- `tests/sprite-atlas-compose.test.ts`
|
|
@@ -157,6 +158,7 @@ Total: 376 (runtime: 172, contract: 204)
|
|
|
157
158
|
- `tests/sprite-recipe-routes.test.ts`
|
|
158
159
|
- `tests/sprite-recipe-store.test.ts`
|
|
159
160
|
- `tests/star-prompt.test.ts`
|
|
161
|
+
- `tests/stop-command-contract.test.ts`
|
|
160
162
|
- `tests/storage-migration.test.ts`
|
|
161
163
|
- `tests/structured-filename-pipelines.test.ts`
|
|
162
164
|
- `tests/style-sheet.test.ts`
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-control helpers for `ima2 stop` (and service stop paths).
|
|
3
|
+
*
|
|
4
|
+
* Doctrine (adversarial audit 260821c): never kill a pid the advertise file
|
|
5
|
+
* merely CLAIMS — verify identity against the live /api/health response first,
|
|
6
|
+
* because pids get recycled. Graceful (admin API) before signals, SIGTERM
|
|
7
|
+
* before SIGKILL, and a stale advertise file is cleaned, not trusted.
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
export function isProcessAlive(pid) {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Poll until the pid exits or the timeout lapses. CLI context: async is fine. */
|
|
20
|
+
export async function waitForExit(pid, timeoutMs) {
|
|
21
|
+
const deadline = Date.now() + timeoutMs;
|
|
22
|
+
while (Date.now() < deadline) {
|
|
23
|
+
if (!isProcessAlive(pid))
|
|
24
|
+
return true;
|
|
25
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
26
|
+
}
|
|
27
|
+
return !isProcessAlive(pid);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Secondary identity signal for when the HTTP check is unreachable: compare
|
|
31
|
+
* the LIVE process start time against the advertised startedAt. A recycled
|
|
32
|
+
* pid belongs to a process started well after our server did; a hung-but-ours
|
|
33
|
+
* server started (approximately) when the advertise file says. Returns
|
|
34
|
+
* "corroborated" only when the start times agree within tolerance; "recycled"
|
|
35
|
+
* when the live process is provably younger than the advertised boot;
|
|
36
|
+
* "unknown" when ps output cannot be read — callers must REFUSE to kill on
|
|
37
|
+
* "unknown"/"recycled" (audit blocker: never guess).
|
|
38
|
+
*/
|
|
39
|
+
export function corroborateByStartTime(pid, advertisedStartedAt, runPs = defaultPs) {
|
|
40
|
+
if (!advertisedStartedAt || !Number.isFinite(advertisedStartedAt))
|
|
41
|
+
return "unknown";
|
|
42
|
+
if (process.platform === "win32")
|
|
43
|
+
return "unknown";
|
|
44
|
+
const lstart = runPs(pid);
|
|
45
|
+
if (!lstart)
|
|
46
|
+
return "unknown";
|
|
47
|
+
const started = Date.parse(lstart);
|
|
48
|
+
if (!Number.isFinite(started))
|
|
49
|
+
return "unknown";
|
|
50
|
+
// advertise happens moments after process start; allow generous skew.
|
|
51
|
+
const TOLERANCE_MS = 120_000;
|
|
52
|
+
if (Math.abs(started - advertisedStartedAt) <= TOLERANCE_MS)
|
|
53
|
+
return "corroborated";
|
|
54
|
+
return started > advertisedStartedAt + TOLERANCE_MS ? "recycled" : "unknown";
|
|
55
|
+
}
|
|
56
|
+
function defaultPs(pid) {
|
|
57
|
+
try {
|
|
58
|
+
const out = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8" });
|
|
59
|
+
const line = out.trim();
|
|
60
|
+
return line.length > 0 ? line : null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Does the server answering on entry.url/port actually carry entry.pid?
|
|
68
|
+
* "mismatch" means someone else answers there (or the pid was recycled):
|
|
69
|
+
* killing entry.pid would hit an innocent process.
|
|
70
|
+
*/
|
|
71
|
+
export async function verifyServerIdentity(entry, fetchFn = fetch) {
|
|
72
|
+
const base = (entry.url ?? (entry.port ? `http://127.0.0.1:${entry.port}` : null))?.toString().replace(/\/$/, "");
|
|
73
|
+
if (!base || !entry.pid)
|
|
74
|
+
return "unreachable";
|
|
75
|
+
try {
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timer = setTimeout(() => controller.abort(), 1500);
|
|
78
|
+
const r = await fetchFn(`${base}/api/health`, {
|
|
79
|
+
signal: controller.signal,
|
|
80
|
+
headers: { connection: "close" },
|
|
81
|
+
});
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
if (!r.ok)
|
|
84
|
+
return "unreachable";
|
|
85
|
+
const health = (await r.json());
|
|
86
|
+
return health.pid === entry.pid ? "match" : "mismatch";
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return "unreachable";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Ask the server to stop itself via the admin API. Requires the nonce from the
|
|
94
|
+
* advertise file. Note: bin/lib/client.ts carries no LAN token, so on a
|
|
95
|
+
* token-guarded non-loopback bind this degrades (401) to the signal path —
|
|
96
|
+
* that is intended behavior, not an accident.
|
|
97
|
+
*/
|
|
98
|
+
export async function gracefulStop(entry, fetchFn = fetch) {
|
|
99
|
+
const base = (entry.url ?? (entry.port ? `http://127.0.0.1:${entry.port}` : null))?.toString().replace(/\/$/, "");
|
|
100
|
+
if (!base || !entry.adminNonce)
|
|
101
|
+
return false;
|
|
102
|
+
try {
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timer = setTimeout(() => controller.abort(), 2500);
|
|
105
|
+
const r = await fetchFn(`${base}/api/admin/stop`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
headers: { "x-ima2-admin-nonce": entry.adminNonce, connection: "close" },
|
|
109
|
+
});
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
return r.status === 202;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** SIGTERM → wait → SIGKILL → wait. Only ever called on an identity-verified pid. */
|
|
118
|
+
export async function escalateKill(pid, waits = {}) {
|
|
119
|
+
if (!isProcessAlive(pid))
|
|
120
|
+
return "already-dead";
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, "SIGTERM");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return isProcessAlive(pid) ? "failed" : "already-dead";
|
|
126
|
+
}
|
|
127
|
+
if (await waitForExit(pid, waits.termMs ?? 5000))
|
|
128
|
+
return "term";
|
|
129
|
+
try {
|
|
130
|
+
process.kill(pid, "SIGKILL");
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return isProcessAlive(pid) ? "failed" : "term";
|
|
134
|
+
}
|
|
135
|
+
if (await waitForExit(pid, waits.killMs ?? 2000))
|
|
136
|
+
return "kill";
|
|
137
|
+
return "failed";
|
|
138
|
+
}
|
package/lib/runtimeContext.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"packageManager": "npm@11.18.0",
|
|
5
5
|
"description": "Local-first visual generation runtime and studio for people and coding agents, with reproducible image and video workflows across multiple providers.",
|
|
6
6
|
"type": "module",
|
|
@@ -119,5 +119,5 @@
|
|
|
119
119
|
"tsx": "^4.23.12",
|
|
120
120
|
"typescript": "^5.9.3"
|
|
121
121
|
},
|
|
122
|
-
"gitHead": "
|
|
122
|
+
"gitHead": "b7369f8a4c042249dcaa282270421d0faa7ed4fe"
|
|
123
123
|
}
|
package/routes/admin.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
3
|
+
import { logEvent } from "../lib/logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Local admin surface. POST /api/admin/stop shuts the server down cleanly.
|
|
6
|
+
*
|
|
7
|
+
* Threat model: the LAN guard is a pass-through on loopback binds, so without
|
|
8
|
+
* extra auth ANY web page could fire a cross-origin
|
|
9
|
+
* fetch("http://127.0.0.1:3333/api/admin/stop") and kill the server — a
|
|
10
|
+
* remote-triggerable kill switch (adversarial audit 260821c, blocker 1).
|
|
11
|
+
* Two independent gates close that hole:
|
|
12
|
+
*
|
|
13
|
+
* 1. The caller must present the boot-generated admin nonce, which is
|
|
14
|
+
* published only in the advertise file (~/.ima2/server.json). Reading that
|
|
15
|
+
* file requires local filesystem access, which a web page does not have.
|
|
16
|
+
* 2. Any request carrying an Origin header is refused outright. Browser-issued
|
|
17
|
+
* cross-origin fetches always carry Origin; the ima2 CLI never does.
|
|
18
|
+
*
|
|
19
|
+
* Shutdown itself is a self-signal: the SIGTERM handler installed by
|
|
20
|
+
* onShutdown() owns the ONLY complete teardown (unadvertise, proxy children,
|
|
21
|
+
* agent queue, timers, DB close, exit) and its shutdownStarted latch makes the
|
|
22
|
+
* signal idempotent. Calling shutdownServerAndMcp() directly here would strand
|
|
23
|
+
* proxy children and leave a stale advertise file (audit blocker 2).
|
|
24
|
+
*/
|
|
25
|
+
export function registerAdminRoutes(app, ctxRaw) {
|
|
26
|
+
const ctx = requireRuntimeContext(ctxRaw);
|
|
27
|
+
app.post("/api/admin/stop", (req, res) => {
|
|
28
|
+
if (typeof req.headers.origin === "string" && req.headers.origin.length > 0) {
|
|
29
|
+
return res.status(403).json({ error: "admin stop is not callable from a browser context" });
|
|
30
|
+
}
|
|
31
|
+
const nonce = req.headers["x-ima2-admin-nonce"];
|
|
32
|
+
const expected = Buffer.from(ctx.adminNonce);
|
|
33
|
+
const presented = typeof nonce === "string" ? Buffer.from(nonce) : Buffer.alloc(0);
|
|
34
|
+
const valid = expected.length > 0 &&
|
|
35
|
+
presented.length === expected.length &&
|
|
36
|
+
timingSafeEqual(presented, expected);
|
|
37
|
+
if (!valid) {
|
|
38
|
+
return res.status(401).json({ error: "missing or invalid admin nonce" });
|
|
39
|
+
}
|
|
40
|
+
logEvent("admin", "stop_requested", { pid: process.pid });
|
|
41
|
+
res.status(202).json({ ok: true, pid: process.pid, stopping: true });
|
|
42
|
+
setImmediate(() => {
|
|
43
|
+
try {
|
|
44
|
+
process.kill(process.pid, "SIGTERM");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
package/routes/edit.js
CHANGED
|
@@ -18,6 +18,8 @@ import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStart
|
|
|
18
18
|
import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "../lib/generationCancel.js";
|
|
19
19
|
import { logEvent, logError } from "../lib/logger.js";
|
|
20
20
|
import { hasPngAlphaChannel, parsePngInfo } from "../lib/pngInfo.js";
|
|
21
|
+
import { verifyBufferAlpha } from "../lib/imageBackgroundParam.js";
|
|
22
|
+
import { decodeRawForAlpha } from "../lib/alphaDecode.js";
|
|
21
23
|
import { invalidateHistoryIndex } from "../lib/historyIndex.js";
|
|
22
24
|
import { errInfo } from "../lib/errInfo.js";
|
|
23
25
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
@@ -287,6 +289,21 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
287
289
|
const editExt = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" || activeProvider === "atlascloud" || activeProvider === "minimax" ? imageFormatFromMime(editMime) : "png";
|
|
288
290
|
const editBuffer = Buffer.from(resultB64, "base64");
|
|
289
291
|
const createdAt = Date.now();
|
|
292
|
+
// Semantic alpha verification: at least one pixel with alpha < 255.
|
|
293
|
+
// Never trust provider mime for transparency claims (same doctrine as
|
|
294
|
+
// the generate path); a failed decode reports alphaVerified: false.
|
|
295
|
+
let alphaVerified = false;
|
|
296
|
+
let alphaReason = null;
|
|
297
|
+
try {
|
|
298
|
+
const verdict = (await verifyBufferAlpha(editBuffer, decodeRawForAlpha));
|
|
299
|
+
alphaVerified = verdict.hasAlpha === true;
|
|
300
|
+
if (!alphaVerified)
|
|
301
|
+
alphaReason = verdict.reason ?? "undetectable";
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
alphaVerified = false;
|
|
305
|
+
alphaReason = "undetectable";
|
|
306
|
+
}
|
|
290
307
|
const filename = await writeFileUnique(ctx.config.storage.generatedDir, buildFilename({
|
|
291
308
|
model: (activeProvider === "grok" || activeProvider === "grok-api") ? resolveGrokQualityModel(imageModel, quality) : (imageModel || activeProvider),
|
|
292
309
|
size: effectiveSize,
|
|
@@ -312,6 +329,8 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
312
329
|
kind: "edit",
|
|
313
330
|
requestId,
|
|
314
331
|
createdAt,
|
|
332
|
+
alphaVerified,
|
|
333
|
+
alphaReason,
|
|
315
334
|
usage: usage || null,
|
|
316
335
|
webSearchCalls,
|
|
317
336
|
webSearchEnabled,
|
|
@@ -332,6 +351,8 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
332
351
|
elapsed,
|
|
333
352
|
reasoningEffort,
|
|
334
353
|
filename,
|
|
354
|
+
alphaVerified,
|
|
355
|
+
alphaReason,
|
|
335
356
|
usage,
|
|
336
357
|
provider: activeProvider,
|
|
337
358
|
model: activeProvider === "grok" ? resolveGrokQualityModel(imageModel, quality) : imageModel,
|
package/routes/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { registerCapabilitiesRoutes } from "./capabilities.js";
|
|
|
2
2
|
import { registerModelsRoutes } from "./models.js";
|
|
3
3
|
import { registerEventsRoute } from "./events.js";
|
|
4
4
|
import { registerHealthRoutes } from "./health.js";
|
|
5
|
+
import { registerAdminRoutes } from "./admin.js";
|
|
5
6
|
import { registerHistoryRoutes } from "./history.js";
|
|
6
7
|
import { registerAssetsRoutes } from "./assets.js";
|
|
7
8
|
import { registerSpriteRecipeRoutes } from "./spriteRecipes.js";
|
|
@@ -45,6 +46,7 @@ export function configureRoutes(app, ctxRaw) {
|
|
|
45
46
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
46
47
|
registerEventsRoute(app, ctx);
|
|
47
48
|
registerHealthRoutes(app, ctx);
|
|
49
|
+
registerAdminRoutes(app, ctx);
|
|
48
50
|
registerCapabilitiesRoutes(app, ctx);
|
|
49
51
|
registerStorageRoutes(app, ctx);
|
|
50
52
|
registerMetadataRoutes(app, ctx);
|
package/server.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
2
|
import express from "express";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { readFile } from "fs/promises";
|
|
4
|
-
import { existsSync, writeFileSync, unlinkSync, mkdirSync, readFileSync as fsReadFileSync, } from "fs";
|
|
5
|
+
import { existsSync, writeFileSync, unlinkSync, chmodSync, mkdirSync, readFileSync as fsReadFileSync, } from "fs";
|
|
5
6
|
import { dirname, join } from "path";
|
|
6
7
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
7
8
|
import { onShutdown } from "./bin/lib/platform.js";
|
|
@@ -296,6 +297,7 @@ export function buildAdvertisePayload(ctx) {
|
|
|
296
297
|
pid: process.pid,
|
|
297
298
|
startedAt: ctx.startedAt,
|
|
298
299
|
version: ctx.packageVersion,
|
|
300
|
+
adminNonce: ctx.adminNonce,
|
|
299
301
|
backend: {
|
|
300
302
|
configuredPort: Number(ctx.serverConfiguredPort || ctx.config.server.port),
|
|
301
303
|
actualPort: Number(ctx.serverActualPort || ctx.config.server.port),
|
|
@@ -321,8 +323,14 @@ function advertise(ctx) {
|
|
|
321
323
|
if (!ctx.serverActualPort)
|
|
322
324
|
return;
|
|
323
325
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
+
// The payload carries the admin nonce (a kill-switch credential): the file
|
|
327
|
+
// must be owner-only, or any local user on a shared host can stop the
|
|
328
|
+
// server (adversarial review 260821c, blocker 3).
|
|
329
|
+
mkdirSync(dirname(ctx.config.storage.advertiseFile), { recursive: true, mode: 0o700 });
|
|
330
|
+
writeFileSync(ctx.config.storage.advertiseFile, JSON.stringify(buildAdvertisePayload(ctx)), { mode: 0o600 });
|
|
331
|
+
// mode applies only at creation: a crash-survivor file from an older build
|
|
332
|
+
// keeps its old permissions, so re-assert them on every publish.
|
|
333
|
+
chmodSync(ctx.config.storage.advertiseFile, 0o600);
|
|
326
334
|
}
|
|
327
335
|
catch (e) {
|
|
328
336
|
const err = errInfo(e);
|
|
@@ -379,6 +387,7 @@ export async function createRuntimeContext(overrides = {}) {
|
|
|
379
387
|
openai,
|
|
380
388
|
startedAt: overrides.startedAt ?? Date.now(),
|
|
381
389
|
packageVersion: overrides.packageVersion ?? readPackageVersion(),
|
|
390
|
+
adminNonce: randomUUID(),
|
|
382
391
|
xaiApiKey: loadedXaiKey.apiKey ?? undefined,
|
|
383
392
|
xaiApiKeySource: loadedXaiKey.apiKeySource,
|
|
384
393
|
hasXaiApiKey: !!loadedXaiKey.apiKey,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_AssetGenWorkspace-
|
|
3
|
-
"file": "assets/AssetGenWorkspace-
|
|
2
|
+
"_AssetGenWorkspace-Bhi2OrfW.js": {
|
|
3
|
+
"file": "assets/AssetGenWorkspace-Bhi2OrfW.js",
|
|
4
4
|
"name": "AssetGenWorkspace",
|
|
5
5
|
"isDynamicEntry": true,
|
|
6
6
|
"imports": [
|
|
7
7
|
"index.html",
|
|
8
|
-
"_KeyingPanel-
|
|
8
|
+
"_KeyingPanel-D7iozqcS.js"
|
|
9
9
|
],
|
|
10
10
|
"dynamicImports": [
|
|
11
11
|
"src/components/assetgen/SpriteRecipeWorkspace.tsx"
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"file": "assets/AssetGenWorkspace-Dkr80l12.css",
|
|
19
19
|
"src": "_AssetGenWorkspace-Dkr80l12.css"
|
|
20
20
|
},
|
|
21
|
-
"_KeyingPanel-
|
|
22
|
-
"file": "assets/KeyingPanel-
|
|
21
|
+
"_KeyingPanel-D7iozqcS.js": {
|
|
22
|
+
"file": "assets/KeyingPanel-D7iozqcS.js",
|
|
23
23
|
"name": "KeyingPanel",
|
|
24
24
|
"imports": [
|
|
25
25
|
"index.html",
|
|
26
|
-
"_useAgentDialogFocus-
|
|
26
|
+
"_useAgentDialogFocus-CqjQmoEH.js"
|
|
27
27
|
]
|
|
28
28
|
},
|
|
29
29
|
"__vite-browser-external": {
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"src": "__vite-browser-external",
|
|
33
33
|
"isDynamicEntry": true
|
|
34
34
|
},
|
|
35
|
-
"_useAgentDialogFocus-
|
|
36
|
-
"file": "assets/useAgentDialogFocus-
|
|
35
|
+
"_useAgentDialogFocus-CqjQmoEH.js": {
|
|
36
|
+
"file": "assets/useAgentDialogFocus-CqjQmoEH.js",
|
|
37
37
|
"name": "useAgentDialogFocus",
|
|
38
38
|
"imports": [
|
|
39
39
|
"index.html"
|
|
40
40
|
]
|
|
41
41
|
},
|
|
42
42
|
"index.html": {
|
|
43
|
-
"file": "assets/index-
|
|
43
|
+
"file": "assets/index-CVVv0v-b.js",
|
|
44
44
|
"name": "index",
|
|
45
45
|
"src": "index.html",
|
|
46
46
|
"isEntry": true,
|
|
@@ -55,16 +55,16 @@
|
|
|
55
55
|
"src/components/card-news/CardNewsWorkspace.tsx",
|
|
56
56
|
"src/components/agent/AgentWorkspace.tsx",
|
|
57
57
|
"src/components/assets/AssetsWorkspace.tsx",
|
|
58
|
-
"_AssetGenWorkspace-
|
|
58
|
+
"_AssetGenWorkspace-Bhi2OrfW.js",
|
|
59
59
|
"src/components/home/HomeWorkspace.tsx",
|
|
60
60
|
"src/components/PromptLibraryPanel.tsx"
|
|
61
61
|
],
|
|
62
62
|
"css": [
|
|
63
|
-
"assets/index-
|
|
63
|
+
"assets/index-C4IY6hxw.css"
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
66
|
"node_modules/pptxgenjs/dist/pptxgen.es.js": {
|
|
67
|
-
"file": "assets/pptxgen.es-
|
|
67
|
+
"file": "assets/pptxgen.es-DIQkG3ve.js",
|
|
68
68
|
"name": "pptxgen.es",
|
|
69
69
|
"src": "node_modules/pptxgenjs/dist/pptxgen.es.js",
|
|
70
70
|
"isDynamicEntry": true,
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
]
|
|
79
79
|
},
|
|
80
80
|
"src/components/GenerationRequestLogPanel.tsx": {
|
|
81
|
-
"file": "assets/GenerationRequestLogPanel-
|
|
81
|
+
"file": "assets/GenerationRequestLogPanel-sr-wSiOe.js",
|
|
82
82
|
"name": "GenerationRequestLogPanel",
|
|
83
83
|
"src": "src/components/GenerationRequestLogPanel.tsx",
|
|
84
84
|
"isDynamicEntry": true,
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
]
|
|
88
88
|
},
|
|
89
89
|
"src/components/NodeCanvas.tsx": {
|
|
90
|
-
"file": "assets/NodeCanvas-
|
|
90
|
+
"file": "assets/NodeCanvas-Bu6q2z3V.js",
|
|
91
91
|
"name": "NodeCanvas",
|
|
92
92
|
"src": "src/components/NodeCanvas.tsx",
|
|
93
93
|
"isDynamicEntry": true,
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
]
|
|
100
100
|
},
|
|
101
101
|
"src/components/PromptImportDialog.tsx": {
|
|
102
|
-
"file": "assets/PromptImportDialog-
|
|
102
|
+
"file": "assets/PromptImportDialog-CZwFZDvz.js",
|
|
103
103
|
"name": "PromptImportDialog",
|
|
104
104
|
"src": "src/components/PromptImportDialog.tsx",
|
|
105
105
|
"isDynamicEntry": true,
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
]
|
|
113
113
|
},
|
|
114
114
|
"src/components/PromptImportDiscoverySection.tsx": {
|
|
115
|
-
"file": "assets/PromptImportDiscoverySection-
|
|
115
|
+
"file": "assets/PromptImportDiscoverySection-B5Lyhl9X.js",
|
|
116
116
|
"name": "PromptImportDiscoverySection",
|
|
117
117
|
"src": "src/components/PromptImportDiscoverySection.tsx",
|
|
118
118
|
"isDynamicEntry": true,
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
]
|
|
122
122
|
},
|
|
123
123
|
"src/components/PromptImportFolderSection.tsx": {
|
|
124
|
-
"file": "assets/PromptImportFolderSection-
|
|
124
|
+
"file": "assets/PromptImportFolderSection-DmMFtYmn.js",
|
|
125
125
|
"name": "PromptImportFolderSection",
|
|
126
126
|
"src": "src/components/PromptImportFolderSection.tsx",
|
|
127
127
|
"isDynamicEntry": true,
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
]
|
|
131
131
|
},
|
|
132
132
|
"src/components/PromptLibraryPanel.tsx": {
|
|
133
|
-
"file": "assets/PromptLibraryPanel-
|
|
133
|
+
"file": "assets/PromptLibraryPanel-BHr73ns_.js",
|
|
134
134
|
"name": "PromptLibraryPanel",
|
|
135
135
|
"src": "src/components/PromptLibraryPanel.tsx",
|
|
136
136
|
"isDynamicEntry": true,
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
]
|
|
143
143
|
},
|
|
144
144
|
"src/components/SettingsWorkspace.tsx": {
|
|
145
|
-
"file": "assets/SettingsWorkspace-
|
|
145
|
+
"file": "assets/SettingsWorkspace-DrQ44R-B.js",
|
|
146
146
|
"name": "SettingsWorkspace",
|
|
147
147
|
"src": "src/components/SettingsWorkspace.tsx",
|
|
148
148
|
"isDynamicEntry": true,
|
|
@@ -151,43 +151,43 @@
|
|
|
151
151
|
]
|
|
152
152
|
},
|
|
153
153
|
"src/components/agent/AgentWorkspace.tsx": {
|
|
154
|
-
"file": "assets/AgentWorkspace-
|
|
154
|
+
"file": "assets/AgentWorkspace-DAOw-Q1x.js",
|
|
155
155
|
"name": "AgentWorkspace",
|
|
156
156
|
"src": "src/components/agent/AgentWorkspace.tsx",
|
|
157
157
|
"isDynamicEntry": true,
|
|
158
158
|
"imports": [
|
|
159
159
|
"index.html",
|
|
160
|
-
"_useAgentDialogFocus-
|
|
160
|
+
"_useAgentDialogFocus-CqjQmoEH.js"
|
|
161
161
|
]
|
|
162
162
|
},
|
|
163
163
|
"src/components/assetgen/SpriteRecipeWorkspace.tsx": {
|
|
164
|
-
"file": "assets/SpriteRecipeWorkspace-
|
|
164
|
+
"file": "assets/SpriteRecipeWorkspace-ChboDVCj.js",
|
|
165
165
|
"name": "SpriteRecipeWorkspace",
|
|
166
166
|
"src": "src/components/assetgen/SpriteRecipeWorkspace.tsx",
|
|
167
167
|
"isDynamicEntry": true,
|
|
168
168
|
"imports": [
|
|
169
169
|
"index.html",
|
|
170
|
-
"_AssetGenWorkspace-
|
|
171
|
-
"_KeyingPanel-
|
|
172
|
-
"_useAgentDialogFocus-
|
|
170
|
+
"_AssetGenWorkspace-Bhi2OrfW.js",
|
|
171
|
+
"_KeyingPanel-D7iozqcS.js",
|
|
172
|
+
"_useAgentDialogFocus-CqjQmoEH.js"
|
|
173
173
|
]
|
|
174
174
|
},
|
|
175
175
|
"src/components/assets/AssetsWorkspace.tsx": {
|
|
176
|
-
"file": "assets/AssetsWorkspace-
|
|
176
|
+
"file": "assets/AssetsWorkspace-D-pcyPvz.js",
|
|
177
177
|
"name": "AssetsWorkspace",
|
|
178
178
|
"src": "src/components/assets/AssetsWorkspace.tsx",
|
|
179
179
|
"isDynamicEntry": true,
|
|
180
180
|
"imports": [
|
|
181
181
|
"index.html",
|
|
182
|
-
"_KeyingPanel-
|
|
183
|
-
"_useAgentDialogFocus-
|
|
182
|
+
"_KeyingPanel-D7iozqcS.js",
|
|
183
|
+
"_useAgentDialogFocus-CqjQmoEH.js"
|
|
184
184
|
],
|
|
185
185
|
"css": [
|
|
186
186
|
"assets/AssetsWorkspace-BkUjPPKU.css"
|
|
187
187
|
]
|
|
188
188
|
},
|
|
189
189
|
"src/components/canvas-mode/index.ts": {
|
|
190
|
-
"file": "assets/index-
|
|
190
|
+
"file": "assets/index-D1SD3LcV.js",
|
|
191
191
|
"name": "index",
|
|
192
192
|
"src": "src/components/canvas-mode/index.ts",
|
|
193
193
|
"isDynamicEntry": true,
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
]
|
|
200
200
|
},
|
|
201
201
|
"src/components/card-news/CardNewsWorkspace.tsx": {
|
|
202
|
-
"file": "assets/CardNewsWorkspace-
|
|
202
|
+
"file": "assets/CardNewsWorkspace-BETskmU7.js",
|
|
203
203
|
"name": "CardNewsWorkspace",
|
|
204
204
|
"src": "src/components/card-news/CardNewsWorkspace.tsx",
|
|
205
205
|
"isDynamicEntry": true,
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
]
|
|
209
209
|
},
|
|
210
210
|
"src/components/home/HomeWorkspace.tsx": {
|
|
211
|
-
"file": "assets/HomeWorkspace-
|
|
211
|
+
"file": "assets/HomeWorkspace-vjN-A1AR.js",
|
|
212
212
|
"name": "HomeWorkspace",
|
|
213
213
|
"src": "src/components/home/HomeWorkspace.tsx",
|
|
214
214
|
"isDynamicEntry": true,
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
]
|
|
218
218
|
},
|
|
219
219
|
"src/components/prompt-builder/PromptBuilderPanel.tsx": {
|
|
220
|
-
"file": "assets/PromptBuilderPanel-
|
|
220
|
+
"file": "assets/PromptBuilderPanel-DklSQCS_.js",
|
|
221
221
|
"name": "PromptBuilderPanel",
|
|
222
222
|
"src": "src/components/prompt-builder/PromptBuilderPanel.tsx",
|
|
223
223
|
"isDynamicEntry": true,
|