@anna-ai/cli 0.1.14 → 0.1.17
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/dist/{apps-BEJUn9Ws.js → apps-ClgEOdKD.js} +1 -1
- package/dist/bridge-B1vq1oG3.js +3 -0
- package/dist/{bridge-D6YyP9DM.js → bridge-Dffh9JUd.js} +80 -2
- package/dist/cli.js +15 -8
- package/dist/{dev-BfLGxpiT.js → dev-BUetXnfG.js} +37 -10
- package/dist/dev-Bi6rkb1x.js +3 -0
- package/dist/dev-app-cache-3Pfesngr.js +189 -0
- package/dist/dev-app-cache-CZ1UjMz0.js +4 -0
- package/dist/{doctor-B3u0edUg.js → doctor-Dxkx0eqv.js} +1 -1
- package/dist/{executa-dev-BhouP8jh.js → executa-dev-BzhSd_A2.js} +59 -12
- package/dist/host_upload-C_pGOS6p.js +136 -0
- package/dist/image-bwolX7pa.js +131 -0
- package/dist/{runner-Bral1LFW.js → runner-DmGLdat0.js} +44 -1
- package/dist/{server-q6nKCeEV.js → server-6WHNkydc.js} +302 -2
- package/package.json +1 -1
- package/templates/minimal/bundle/app.js +2 -0
- package/templates/minimal/bundle/index.html +1 -2
- package/dist/bridge-C0DWb5eQ.js +0 -3
- package/dist/dev-C81H9c9_.js +0 -3
- package/dist/dev-app-cache-C3D1Sp_V.js +0 -93
- package/dist/dev-app-cache-CZ8lIKiw.js +0 -4
- /package/dist/{sampling-3EfSlDHM.js → sampling-CJUDG-mf.js} +0 -0
- /package/dist/{storage-CnWTZqq_.js → storage-EQJA_0UW.js} +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
//#region src/executa/host_upload.ts
|
|
7
|
+
var HostUploadBridge = class {
|
|
8
|
+
mocks = [];
|
|
9
|
+
cachedSession = null;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.opts = opts;
|
|
12
|
+
if (opts.mode === "mock" && opts.mockFile) {
|
|
13
|
+
const path = resolve(opts.mockFile);
|
|
14
|
+
if (existsSync(path)) for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
15
|
+
if (!line.trim() || line.startsWith("#")) continue;
|
|
16
|
+
try {
|
|
17
|
+
this.mocks.push(JSON.parse(line));
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async call(method, params) {
|
|
23
|
+
if (method !== "host/uploadFile") throw withCode(new Error(`unsupported host method: ${method}`), -32601);
|
|
24
|
+
if (this.opts.mode === "off") throw withCode(new Error("host upload disabled — rerun without `--no-upload`"), -32201);
|
|
25
|
+
if (this.opts.mode === "mock") return this.mockCall(params);
|
|
26
|
+
return this.realCall(params);
|
|
27
|
+
}
|
|
28
|
+
mockCall(params) {
|
|
29
|
+
const mode = String(params.mode ?? "inline");
|
|
30
|
+
const filename = String(params.filename ?? "");
|
|
31
|
+
const candidates = this.mocks.filter((m) => m.ns === "host" && m.method === "uploadFile");
|
|
32
|
+
const matched = candidates.find((m) => (!m.match?.modeEquals || m.match.modeEquals === mode) && (!m.match?.filenameIncludes || filename.includes(m.match.filenameIncludes))) ?? candidates[0];
|
|
33
|
+
if (matched && matched.result) return matched.result;
|
|
34
|
+
if (mode === "inline") {
|
|
35
|
+
const b64 = String(params.content_b64 ?? "");
|
|
36
|
+
const mime = String(params.mime_type ?? "application/octet-stream");
|
|
37
|
+
const url = `data:${mime};base64,${b64}`;
|
|
38
|
+
const r2Key = `mock/${hashShort(filename + ":" + b64)}/${filename || "blob"}`;
|
|
39
|
+
return {
|
|
40
|
+
download_url: url,
|
|
41
|
+
r2_key: r2Key,
|
|
42
|
+
size_bytes: Math.floor(b64.length * .75),
|
|
43
|
+
expires_at: new Date(Date.now() + 3600 * 1e3).toISOString()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (mode === "negotiate") {
|
|
47
|
+
const key = `mock/${hashShort(filename + Date.now())}/${filename || "blob"}`;
|
|
48
|
+
return {
|
|
49
|
+
put_url: `https://mock.local/${key}?signature=mock`,
|
|
50
|
+
headers: { "content-type": String(params.mime_type ?? "") },
|
|
51
|
+
r2_key: key,
|
|
52
|
+
expires_at: new Date(Date.now() + 600 * 1e3).toISOString()
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (mode === "confirm") {
|
|
56
|
+
const key = String(params.r2_key ?? "");
|
|
57
|
+
return {
|
|
58
|
+
download_url: `https://mock.local/${key}`,
|
|
59
|
+
r2_key: key,
|
|
60
|
+
size_bytes: 0,
|
|
61
|
+
expires_at: new Date(Date.now() + 3600 * 1e3).toISOString()
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
throw withCode(new Error(`unknown upload mode: ${mode}`), -32203);
|
|
65
|
+
}
|
|
66
|
+
account() {
|
|
67
|
+
const acc = getAccount(this.opts.account);
|
|
68
|
+
if (!acc) throw withCode(new Error("no PAT on disk — run `anna-app login --host <nexus-url>` first (or pass `--mock-upload <fixture>` / `--no-upload`)"), -32201);
|
|
69
|
+
if (acc.expires_at && acc.expires_at < Math.floor(Date.now() / 1e3)) throw withCode(new Error("PAT expired — run `anna-app login` again"), -32201);
|
|
70
|
+
return acc;
|
|
71
|
+
}
|
|
72
|
+
async mint() {
|
|
73
|
+
if (this.cachedSession && this.cachedSession.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return this.cachedSession;
|
|
74
|
+
const acc = this.account();
|
|
75
|
+
if (!this.opts.appSlug) throw withCode(new Error("host upload bridge has no app_slug — pass `--app-slug <slug>`"), -32203);
|
|
76
|
+
const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/session/mint`;
|
|
77
|
+
const res = await fetch(url, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "content-type": "application/json" },
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
pat: acc.pat,
|
|
82
|
+
kind: "complete",
|
|
83
|
+
app_slug: this.opts.appSlug
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const text = await res.text().catch(() => "");
|
|
88
|
+
throw withCode(new Error(`session.mint failed: HTTP ${res.status} ${text}`), -32207);
|
|
89
|
+
}
|
|
90
|
+
const body = await res.json();
|
|
91
|
+
this.cachedSession = {
|
|
92
|
+
appSessionToken: body.app_session_token,
|
|
93
|
+
expiresAt: Math.floor(Date.now() / 1e3) + (body.expires_in || 600)
|
|
94
|
+
};
|
|
95
|
+
return this.cachedSession;
|
|
96
|
+
}
|
|
97
|
+
async realCall(params) {
|
|
98
|
+
const acc = this.account();
|
|
99
|
+
const session = await this.mint();
|
|
100
|
+
const mode = String(params.mode ?? "inline");
|
|
101
|
+
const path = mode === "negotiate" ? "/api/v1/copilot/app/upload/negotiate" : mode === "confirm" ? "/api/v1/copilot/app/upload/confirm" : "/api/v1/copilot/app/upload";
|
|
102
|
+
const res = await fetch(`${canonicalHost(acc.host)}${path}`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"content-type": "application/json",
|
|
106
|
+
authorization: `Bearer ${session.appSessionToken}`
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify(params)
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const text = await res.text().catch(() => "");
|
|
112
|
+
throw withCode(new Error(`HTTP ${res.status}: ${text}`), httpToUploadCode(res.status));
|
|
113
|
+
}
|
|
114
|
+
return res.json();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function withCode(err, code) {
|
|
118
|
+
err.rpcCode = code;
|
|
119
|
+
return err;
|
|
120
|
+
}
|
|
121
|
+
function hashShort(s) {
|
|
122
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 12);
|
|
123
|
+
}
|
|
124
|
+
function httpToUploadCode(status) {
|
|
125
|
+
if (status === 403) return -32201;
|
|
126
|
+
if (status === 429) return -32202;
|
|
127
|
+
if (status === 400) return -32203;
|
|
128
|
+
if (status === 413) return -32204;
|
|
129
|
+
if (status === 415) return -32205;
|
|
130
|
+
if (status === 404) return -32212;
|
|
131
|
+
if (status === 504) return -32208;
|
|
132
|
+
return -32207;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
export { HostUploadBridge };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region src/executa/image.ts
|
|
6
|
+
var ImageBridge = class {
|
|
7
|
+
mocks = [];
|
|
8
|
+
cachedSession = null;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
if (opts.mode === "mock" && opts.mockFile) {
|
|
12
|
+
const path = resolve(opts.mockFile);
|
|
13
|
+
if (existsSync(path)) for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
14
|
+
if (!line.trim() || line.startsWith("#")) continue;
|
|
15
|
+
try {
|
|
16
|
+
this.mocks.push(JSON.parse(line));
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async generate(req) {
|
|
22
|
+
if (this.opts.mode === "off") throw withCode(new Error("image generation disabled — rerun without `--no-image`"), -32101);
|
|
23
|
+
if (this.opts.mode === "mock") return this.pickMock("generate", req.prompt);
|
|
24
|
+
return this.realCall("/api/v1/copilot/app/image/generate", req);
|
|
25
|
+
}
|
|
26
|
+
async edit(req) {
|
|
27
|
+
if (this.opts.mode === "off") throw withCode(new Error("image edit disabled — rerun without `--no-image`"), -32101);
|
|
28
|
+
if (this.opts.mode === "mock") return this.pickMock("edit", req.prompt);
|
|
29
|
+
return this.realCall("/api/v1/copilot/app/image/edit", req);
|
|
30
|
+
}
|
|
31
|
+
pickMock(method, prompt) {
|
|
32
|
+
const wantedMethod = method === "generate" ? "generate" : "edit";
|
|
33
|
+
const candidates = this.mocks.filter((m) => m.ns === "image" && m.method === wantedMethod);
|
|
34
|
+
const matched = candidates.find((m) => m.match?.promptIncludes && prompt.includes(m.match.promptIncludes)) ?? candidates[0];
|
|
35
|
+
if (matched && matched.result) return normaliseImageResult(matched.result);
|
|
36
|
+
return {
|
|
37
|
+
images: [{
|
|
38
|
+
url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
|
39
|
+
mimeType: "image/png"
|
|
40
|
+
}],
|
|
41
|
+
model: "mock-image-model"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
account() {
|
|
45
|
+
const acc = getAccount(this.opts.account);
|
|
46
|
+
if (!acc) throw withCode(new Error("no PAT on disk — run `anna-app login --host <nexus-url>` first (or pass `--mock-image <fixture>` / `--no-image`)"), -32101);
|
|
47
|
+
if (acc.expires_at && acc.expires_at < Math.floor(Date.now() / 1e3)) throw withCode(new Error("PAT expired — run `anna-app login` again"), -32101);
|
|
48
|
+
return acc;
|
|
49
|
+
}
|
|
50
|
+
async mint() {
|
|
51
|
+
if (this.cachedSession && this.cachedSession.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return this.cachedSession;
|
|
52
|
+
const acc = this.account();
|
|
53
|
+
if (!this.opts.appSlug) throw withCode(new Error("image bridge has no app_slug — pass `--app-slug <slug>` to `anna-app executa dev` so nexus can attribute the spend"), -32104);
|
|
54
|
+
const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/session/mint`;
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
pat: acc.pat,
|
|
60
|
+
kind: "complete",
|
|
61
|
+
app_slug: this.opts.appSlug
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const text = await res.text().catch(() => "");
|
|
66
|
+
throw withCode(new Error(`session.mint failed: HTTP ${res.status} ${text}`), -32103);
|
|
67
|
+
}
|
|
68
|
+
const body = await res.json();
|
|
69
|
+
this.cachedSession = {
|
|
70
|
+
appSessionToken: body.app_session_token,
|
|
71
|
+
expiresAt: Math.floor(Date.now() / 1e3) + (body.expires_in || 600)
|
|
72
|
+
};
|
|
73
|
+
return this.cachedSession;
|
|
74
|
+
}
|
|
75
|
+
async realCall(path, body) {
|
|
76
|
+
const acc = this.account();
|
|
77
|
+
const session = await this.mint();
|
|
78
|
+
const res = await fetch(`${canonicalHost(acc.host)}${path}`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: {
|
|
81
|
+
"content-type": "application/json",
|
|
82
|
+
authorization: `Bearer ${session.appSessionToken}`
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(body)
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const text = await res.text().catch(() => "");
|
|
88
|
+
throw withCode(
|
|
89
|
+
new Error(`HTTP ${res.status}: ${text}`),
|
|
90
|
+
// Map common HTTP statuses to canonical image codes; otherwise
|
|
91
|
+
// let the host's body propagate.
|
|
92
|
+
httpToImageCode(res.status)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const out = await res.json();
|
|
96
|
+
return normaliseImageResult(out);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function withCode(err, code) {
|
|
100
|
+
err.rpcCode = code;
|
|
101
|
+
return err;
|
|
102
|
+
}
|
|
103
|
+
function httpToImageCode(status) {
|
|
104
|
+
if (status === 403) return -32101;
|
|
105
|
+
if (status === 429) return -32102;
|
|
106
|
+
if (status === 400) return -32104;
|
|
107
|
+
if (status === 504) return -32105;
|
|
108
|
+
return -32103;
|
|
109
|
+
}
|
|
110
|
+
function normaliseImageResult(raw) {
|
|
111
|
+
if (!raw || typeof raw !== "object") return {
|
|
112
|
+
images: [],
|
|
113
|
+
model: "unknown"
|
|
114
|
+
};
|
|
115
|
+
const o = raw;
|
|
116
|
+
const images = Array.isArray(o.images) ? o.images.map((img) => ({
|
|
117
|
+
url: String(img.url ?? ""),
|
|
118
|
+
mimeType: img.mimeType ? String(img.mimeType) : void 0,
|
|
119
|
+
width: typeof img.width === "number" ? img.width : void 0,
|
|
120
|
+
height: typeof img.height === "number" ? img.height : void 0
|
|
121
|
+
})) : [];
|
|
122
|
+
return {
|
|
123
|
+
images,
|
|
124
|
+
model: typeof o.model === "string" ? o.model : void 0,
|
|
125
|
+
quota_used: o.quota_used && typeof o.quota_used === "object" ? o.quota_used : void 0,
|
|
126
|
+
_meta: o._meta && typeof o._meta === "object" ? o._meta : void 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
export { ImageBridge };
|
|
@@ -8,7 +8,10 @@ const HOST_INITIALIZE_PARAMS = {
|
|
|
8
8
|
client_capabilities: {
|
|
9
9
|
sampling: {},
|
|
10
10
|
agent: {},
|
|
11
|
-
storage: {}
|
|
11
|
+
storage: {},
|
|
12
|
+
image: {},
|
|
13
|
+
"image.edit": {},
|
|
14
|
+
upload: {}
|
|
12
15
|
},
|
|
13
16
|
client_info: {
|
|
14
17
|
name: "anna-app-cli",
|
|
@@ -248,6 +251,46 @@ var ExecutaRunner = class {
|
|
|
248
251
|
}
|
|
249
252
|
return;
|
|
250
253
|
}
|
|
254
|
+
if (method === "image/generate" || method === "image/edit") {
|
|
255
|
+
if (!this.opts.image) {
|
|
256
|
+
respond({ error: {
|
|
257
|
+
code: -32101,
|
|
258
|
+
message: "image not configured (rerun without `--no-image`, or pass `--mock-image <fixture>` / `--app-slug`)"
|
|
259
|
+
} });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const result = method === "image/generate" ? await this.opts.image.generate(params) : await this.opts.image.edit(params);
|
|
264
|
+
respond({ result });
|
|
265
|
+
} catch (e) {
|
|
266
|
+
const code = e.rpcCode ?? -32103;
|
|
267
|
+
respond({ error: {
|
|
268
|
+
code,
|
|
269
|
+
message: e.message
|
|
270
|
+
} });
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (method === "host/uploadFile") {
|
|
275
|
+
if (!this.opts.hostUpload) {
|
|
276
|
+
respond({ error: {
|
|
277
|
+
code: -32201,
|
|
278
|
+
message: "host upload not configured (rerun without `--no-upload`, or pass `--mock-upload <fixture>` / `--app-slug`)"
|
|
279
|
+
} });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const result = await this.opts.hostUpload.call(method, params);
|
|
284
|
+
respond({ result });
|
|
285
|
+
} catch (e) {
|
|
286
|
+
const code = e.rpcCode ?? -32207;
|
|
287
|
+
respond({ error: {
|
|
288
|
+
code,
|
|
289
|
+
message: e.message
|
|
290
|
+
} });
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
251
294
|
if (method.startsWith("storage/") || method.startsWith("files/")) {
|
|
252
295
|
if (!this.opts.storage) {
|
|
253
296
|
respond({ error: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
|
|
2
|
+
import { BridgeRequestError } from "./bridge-Dffh9JUd.js";
|
|
2
3
|
import { dirname, join, normalize, resolve } from "node:path";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { createReadStream, existsSync, readFileSync, statSync, watch } from "node:fs";
|
|
@@ -12,6 +13,10 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
|
12
13
|
var LlmBridge = class {
|
|
13
14
|
mintedAuto = new Map();
|
|
14
15
|
mintedAgent = new Map();
|
|
16
|
+
/** Single shared storage_token per LlmBridge instance — scope is per
|
|
17
|
+
* ``(user, app)`` server-side, not per-window, so caching one is
|
|
18
|
+
* correct. Re-minted automatically before expiry. */
|
|
19
|
+
mintedStorage = null;
|
|
15
20
|
mocks = [];
|
|
16
21
|
streamCounter = 0;
|
|
17
22
|
constructor(opts) {
|
|
@@ -30,10 +35,30 @@ var LlmBridge = class {
|
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
/** Returns true iff this bridge handles `(ns, method)` (i.e. the harness
|
|
33
|
-
* should NOT forward it to the in-process Python dispatcher).
|
|
38
|
+
* should NOT forward it to the in-process Python dispatcher).
|
|
39
|
+
*
|
|
40
|
+
* Storage handling depends on ``storageMode``:
|
|
41
|
+
* - ``legacy`` (default): the bridge does NOT claim storage; the Python
|
|
42
|
+
* dispatcher implements ``anna.storage.*`` against the in-memory
|
|
43
|
+
* ``runtime_state`` dict. Pre-APS parity, works fully offline.
|
|
44
|
+
* - ``aps``: the bridge claims storage and forwards to real nexus APS
|
|
45
|
+
* via ``/api/v1/storage/*`` with a Bearer ``storage_token``. */
|
|
46
|
+
handles(ns, method) {
|
|
47
|
+
if (ns === "llm" && method === "complete") return true;
|
|
48
|
+
if (ns === "agent" && method.startsWith("session.")) return true;
|
|
49
|
+
if (ns === "image" && (method === "generate" || method === "edit")) return true;
|
|
50
|
+
if (ns === "upload" && (method === "inline" || method === "negotiate" || method === "confirm")) return true;
|
|
51
|
+
if (ns === "storage" && this.opts.storageMode === "aps" && this.opts.mode === "real" && (method === "get" || method === "set" || method === "delete" || method === "list")) return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/** Back-compat static alias — older code paths still call
|
|
55
|
+
* ``LlmBridge.handles(ns, method)`` without an instance. Returns the
|
|
56
|
+
* legacy (no-storage) decision since options aren't available here. */
|
|
34
57
|
static handles(ns, method) {
|
|
35
58
|
if (ns === "llm" && method === "complete") return true;
|
|
36
59
|
if (ns === "agent" && method.startsWith("session.")) return true;
|
|
60
|
+
if (ns === "image" && (method === "generate" || method === "edit")) return true;
|
|
61
|
+
if (ns === "upload" && (method === "inline" || method === "negotiate" || method === "confirm")) return true;
|
|
37
62
|
return false;
|
|
38
63
|
}
|
|
39
64
|
/** Resolve the active account or throw with a friendly message. */
|
|
@@ -90,12 +115,65 @@ var LlmBridge = class {
|
|
|
90
115
|
this.mintedAgent.set(ms.appSessionUuid, ms);
|
|
91
116
|
return ms;
|
|
92
117
|
}
|
|
118
|
+
/** Mint (or reuse) a storage_token for APS forwarding.
|
|
119
|
+
*
|
|
120
|
+
* Server-side ``/dev/storage/mint`` issues a JWT scoped to
|
|
121
|
+
* ``(user, app)`` with ``allowed_scopes ⊆ {user, app, tool}`` and a
|
|
122
|
+
* configurable TTL (default 600s) + ``max_calls`` budget (default 200).
|
|
123
|
+
* We re-mint when within 30s of expiry; budget exhaustion currently
|
|
124
|
+
* surfaces as an HTTP 401/403 which propagates back to the iframe.
|
|
125
|
+
*
|
|
126
|
+
* Per-app, not per-window: the token doesn't carry a window or
|
|
127
|
+
* session uuid — storage scopes are ``user``/``app``/``tool``, not
|
|
128
|
+
* ``window``. Caching one per LlmBridge instance is correct. */
|
|
129
|
+
async mintStorage() {
|
|
130
|
+
const cached = this.mintedStorage;
|
|
131
|
+
if (cached && cached.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return cached;
|
|
132
|
+
const acc = this.account();
|
|
133
|
+
const slug = this.requireAppSlug();
|
|
134
|
+
const body = {
|
|
135
|
+
pat: acc.pat,
|
|
136
|
+
app_slug: slug,
|
|
137
|
+
ttl_seconds: 600,
|
|
138
|
+
max_calls: 200
|
|
139
|
+
};
|
|
140
|
+
const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/storage/mint`;
|
|
141
|
+
const res = await fetch(url, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "content-type": "application/json" },
|
|
144
|
+
body: JSON.stringify(body)
|
|
145
|
+
});
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
const text = await res.text().catch(() => "");
|
|
148
|
+
throw new Error(`storage.mint failed: HTTP ${res.status} ${text}`);
|
|
149
|
+
}
|
|
150
|
+
const payload = await res.json();
|
|
151
|
+
const ms = {
|
|
152
|
+
storageToken: payload.storage_token,
|
|
153
|
+
expiresAt: Math.floor(Date.now() / 1e3) + (payload.expires_in || 600),
|
|
154
|
+
allowedScopes: payload.allowed_scopes ?? [
|
|
155
|
+
"user",
|
|
156
|
+
"app",
|
|
157
|
+
"tool"
|
|
158
|
+
]
|
|
159
|
+
};
|
|
160
|
+
this.mintedStorage = ms;
|
|
161
|
+
return ms;
|
|
162
|
+
}
|
|
93
163
|
/** Throws a friendly error if no appSlug was wired into the bridge. */
|
|
94
164
|
requireAppSlug() {
|
|
95
165
|
if (!this.opts.appSlug) throw new Error("llm bridge has no app_slug — the harness must register the dev app via POST /api/v1/anna-apps/dev/apps/register before minting sessions. (Run `anna-app login` once, then `anna-app dev` will auto-register the manifest slug.)");
|
|
96
166
|
return this.opts.appSlug;
|
|
97
167
|
}
|
|
98
168
|
async callMint(host, body) {
|
|
169
|
+
return this._callMintOnce(
|
|
170
|
+
host,
|
|
171
|
+
body,
|
|
172
|
+
/*retryOnAppNotFound=*/
|
|
173
|
+
true
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
async _callMintOnce(host, body, retryOnAppNotFound) {
|
|
99
177
|
const url = `${canonicalHost(host)}/api/v1/anna-apps/dev/session/mint`;
|
|
100
178
|
const res = await fetch(url, {
|
|
101
179
|
method: "POST",
|
|
@@ -104,6 +182,19 @@ var LlmBridge = class {
|
|
|
104
182
|
});
|
|
105
183
|
if (!res.ok) {
|
|
106
184
|
const text = await res.text().catch(() => "");
|
|
185
|
+
if (retryOnAppNotFound && res.status === 404 && /app slug .* not found/.test(text) && typeof this.opts.onAppSlugNotFound === "function") {
|
|
186
|
+
try {
|
|
187
|
+
await this.opts.onAppSlugNotFound();
|
|
188
|
+
} catch (hookErr) {
|
|
189
|
+
throw new Error(`session.mint failed: HTTP 404 ${text}\n recovery hook failed: ${hookErr.message}`);
|
|
190
|
+
}
|
|
191
|
+
return this._callMintOnce(
|
|
192
|
+
host,
|
|
193
|
+
body,
|
|
194
|
+
/*retryOnAppNotFound=*/
|
|
195
|
+
false
|
|
196
|
+
);
|
|
197
|
+
}
|
|
107
198
|
throw new Error(`session.mint failed: HTTP ${res.status} ${text}`);
|
|
108
199
|
}
|
|
109
200
|
return await res.json();
|
|
@@ -290,6 +381,121 @@ var LlmBridge = class {
|
|
|
290
381
|
};
|
|
291
382
|
}
|
|
292
383
|
}
|
|
384
|
+
if (args.ns === "image" && (args.method === "generate" || args.method === "edit")) {
|
|
385
|
+
const ms = await this.mintComplete(args.windowUuid);
|
|
386
|
+
const result = await this.postJson(`${canonicalHost(acc.host)}/api/v1/copilot/app/image/${args.method}`, ms.appSessionToken, args.args);
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
result
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (args.ns === "upload") {
|
|
393
|
+
const ms = await this.mintComplete(args.windowUuid);
|
|
394
|
+
const path = args.method === "inline" ? "/api/v1/copilot/app/upload" : args.method === "negotiate" ? "/api/v1/copilot/app/upload/negotiate" : args.method === "confirm" ? "/api/v1/copilot/app/upload/confirm" : null;
|
|
395
|
+
if (path != null) {
|
|
396
|
+
const result = await this.postJson(`${canonicalHost(acc.host)}${path}`, ms.appSessionToken, args.args);
|
|
397
|
+
return {
|
|
398
|
+
ok: true,
|
|
399
|
+
result
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (args.ns === "storage") {
|
|
404
|
+
const sm = await this.mintStorage();
|
|
405
|
+
const a = args.args;
|
|
406
|
+
const base = `${canonicalHost(acc.host)}/api/v1/storage`;
|
|
407
|
+
if (args.method === "get") {
|
|
408
|
+
const qs = new URLSearchParams();
|
|
409
|
+
qs.set("key", String(a.key ?? ""));
|
|
410
|
+
if (a.scope) qs.set("scope", String(a.scope));
|
|
411
|
+
const res = await fetch(`${base}/kv?${qs.toString()}`, {
|
|
412
|
+
method: "GET",
|
|
413
|
+
headers: { authorization: `Bearer ${sm.storageToken}` }
|
|
414
|
+
});
|
|
415
|
+
if (res.status === 404) return {
|
|
416
|
+
ok: true,
|
|
417
|
+
result: {
|
|
418
|
+
value: null,
|
|
419
|
+
exists: false
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
if (!res.ok) {
|
|
423
|
+
const text = await res.text().catch(() => "");
|
|
424
|
+
throw new Error(`storage.get HTTP ${res.status}: ${text}`);
|
|
425
|
+
}
|
|
426
|
+
const entry = await res.json();
|
|
427
|
+
if (entry.exists === void 0) entry.exists = true;
|
|
428
|
+
return {
|
|
429
|
+
ok: true,
|
|
430
|
+
result: entry
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (args.method === "set") {
|
|
434
|
+
const qs = new URLSearchParams();
|
|
435
|
+
if (a.scope) qs.set("scope", String(a.scope));
|
|
436
|
+
const body = {
|
|
437
|
+
key: a.key,
|
|
438
|
+
value: a.value
|
|
439
|
+
};
|
|
440
|
+
if (a.if_match !== void 0) body.if_match = a.if_match;
|
|
441
|
+
if (a.metadata !== void 0) body.metadata = a.metadata;
|
|
442
|
+
if (a.tags !== void 0) body.tags = a.tags;
|
|
443
|
+
if (a.ttl_seconds !== void 0) body.ttl_seconds = a.ttl_seconds;
|
|
444
|
+
const res = await fetch(`${base}/kv?${qs.toString()}`, {
|
|
445
|
+
method: "PUT",
|
|
446
|
+
headers: {
|
|
447
|
+
"content-type": "application/json",
|
|
448
|
+
authorization: `Bearer ${sm.storageToken}`
|
|
449
|
+
},
|
|
450
|
+
body: JSON.stringify(body)
|
|
451
|
+
});
|
|
452
|
+
if (!res.ok) {
|
|
453
|
+
const text = await res.text().catch(() => "");
|
|
454
|
+
throw new Error(`storage.set HTTP ${res.status}: ${text}`);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
ok: true,
|
|
458
|
+
result: await res.json()
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (args.method === "delete") {
|
|
462
|
+
const qs = new URLSearchParams();
|
|
463
|
+
qs.set("key", String(a.key ?? ""));
|
|
464
|
+
if (a.if_match !== void 0) qs.set("if_match", String(a.if_match));
|
|
465
|
+
if (a.scope) qs.set("scope", String(a.scope));
|
|
466
|
+
const res = await fetch(`${base}/kv?${qs.toString()}`, {
|
|
467
|
+
method: "DELETE",
|
|
468
|
+
headers: { authorization: `Bearer ${sm.storageToken}` }
|
|
469
|
+
});
|
|
470
|
+
if (!res.ok) {
|
|
471
|
+
const text = await res.text().catch(() => "");
|
|
472
|
+
throw new Error(`storage.delete HTTP ${res.status}: ${text}`);
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
ok: true,
|
|
476
|
+
result: await res.json()
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (args.method === "list") {
|
|
480
|
+
const qs = new URLSearchParams();
|
|
481
|
+
if (a.prefix !== void 0) qs.set("prefix", String(a.prefix));
|
|
482
|
+
if (a.cursor !== void 0) qs.set("cursor", String(a.cursor));
|
|
483
|
+
if (a.limit !== void 0) qs.set("limit", String(a.limit));
|
|
484
|
+
if (a.scope) qs.set("scope", String(a.scope));
|
|
485
|
+
const res = await fetch(`${base}/list?${qs.toString()}`, {
|
|
486
|
+
method: "GET",
|
|
487
|
+
headers: { authorization: `Bearer ${sm.storageToken}` }
|
|
488
|
+
});
|
|
489
|
+
if (!res.ok) {
|
|
490
|
+
const text = await res.text().catch(() => "");
|
|
491
|
+
throw new Error(`storage.list HTTP ${res.status}: ${text}`);
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
ok: true,
|
|
495
|
+
result: await res.json()
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
293
499
|
return {
|
|
294
500
|
ok: false,
|
|
295
501
|
error: {
|
|
@@ -434,6 +640,100 @@ var HarnessServer = class {
|
|
|
434
640
|
this.cfg = cfg;
|
|
435
641
|
this.bridge = bridge;
|
|
436
642
|
this.llmBridge = cfg.llm ? new LlmBridge(cfg.llm) : null;
|
|
643
|
+
const HOST_OUTBOUND_ROUTES = [
|
|
644
|
+
[
|
|
645
|
+
"host.llm.complete",
|
|
646
|
+
"llm",
|
|
647
|
+
"complete"
|
|
648
|
+
],
|
|
649
|
+
[
|
|
650
|
+
"host.agent.session.create",
|
|
651
|
+
"agent",
|
|
652
|
+
"session.create"
|
|
653
|
+
],
|
|
654
|
+
[
|
|
655
|
+
"host.agent.session.run",
|
|
656
|
+
"agent",
|
|
657
|
+
"session.run"
|
|
658
|
+
],
|
|
659
|
+
[
|
|
660
|
+
"host.agent.session.cancel",
|
|
661
|
+
"agent",
|
|
662
|
+
"session.cancel"
|
|
663
|
+
],
|
|
664
|
+
[
|
|
665
|
+
"host.agent.session.history",
|
|
666
|
+
"agent",
|
|
667
|
+
"session.history"
|
|
668
|
+
],
|
|
669
|
+
[
|
|
670
|
+
"host.agent.session.delete",
|
|
671
|
+
"agent",
|
|
672
|
+
"session.delete"
|
|
673
|
+
],
|
|
674
|
+
[
|
|
675
|
+
"host.image.generate",
|
|
676
|
+
"image",
|
|
677
|
+
"generate"
|
|
678
|
+
],
|
|
679
|
+
[
|
|
680
|
+
"host.image.edit",
|
|
681
|
+
"image",
|
|
682
|
+
"edit"
|
|
683
|
+
],
|
|
684
|
+
[
|
|
685
|
+
"host.upload.inline",
|
|
686
|
+
"upload",
|
|
687
|
+
"inline"
|
|
688
|
+
],
|
|
689
|
+
[
|
|
690
|
+
"host.upload.negotiate",
|
|
691
|
+
"upload",
|
|
692
|
+
"negotiate"
|
|
693
|
+
],
|
|
694
|
+
[
|
|
695
|
+
"host.upload.confirm",
|
|
696
|
+
"upload",
|
|
697
|
+
"confirm"
|
|
698
|
+
],
|
|
699
|
+
[
|
|
700
|
+
"host.storage.get",
|
|
701
|
+
"storage",
|
|
702
|
+
"get"
|
|
703
|
+
],
|
|
704
|
+
[
|
|
705
|
+
"host.storage.set",
|
|
706
|
+
"storage",
|
|
707
|
+
"set"
|
|
708
|
+
],
|
|
709
|
+
[
|
|
710
|
+
"host.storage.delete",
|
|
711
|
+
"storage",
|
|
712
|
+
"delete"
|
|
713
|
+
],
|
|
714
|
+
[
|
|
715
|
+
"host.storage.list",
|
|
716
|
+
"storage",
|
|
717
|
+
"list"
|
|
718
|
+
]
|
|
719
|
+
];
|
|
720
|
+
for (const [hostMethod, ns, dispatchMethod] of HOST_OUTBOUND_ROUTES) this.bridge.onRequest(hostMethod, async (params) => {
|
|
721
|
+
if (this.llmBridge == null) throw new BridgeRequestError("llm_disabled", "harness started without an LlmBridge (use --no-llm to suppress this path or `anna-app login` for a real bridge)");
|
|
722
|
+
const out = await this.llmBridge.dispatch({
|
|
723
|
+
windowUuid: this.sessionId ?? "harness",
|
|
724
|
+
ns,
|
|
725
|
+
method: dispatchMethod,
|
|
726
|
+
args: params,
|
|
727
|
+
onEvent: (kind, payload) => {
|
|
728
|
+
this.llmEventQueue.push({
|
|
729
|
+
event: kind,
|
|
730
|
+
payload
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
if (out.ok) return out.result;
|
|
735
|
+
throw new BridgeRequestError(out.error.code, out.error.message);
|
|
736
|
+
});
|
|
437
737
|
}
|
|
438
738
|
async listen() {
|
|
439
739
|
if (this.cfg.executas && this.cfg.executas.length > 0) await this.bridge.call("executas.register", { executas: this.cfg.executas.map((e) => ({
|
|
@@ -577,7 +877,7 @@ var HarnessServer = class {
|
|
|
577
877
|
}
|
|
578
878
|
});
|
|
579
879
|
}
|
|
580
|
-
if (this.llmBridge != null &&
|
|
880
|
+
if (this.llmBridge != null && this.llmBridge.handles(parsed.ns, parsed.method)) {
|
|
581
881
|
const out = await this.llmBridge.dispatch({
|
|
582
882
|
windowUuid: this.sessionId ?? "harness",
|
|
583
883
|
ns: parsed.ns,
|