@prismer/runtime 2.0.0 → 2.0.1
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/CHANGELOG.md +50 -0
- package/dist/cli.cjs +560 -31
- package/dist/cli.js +568 -39
- package/dist/index.cjs +562 -31
- package/dist/index.d.cts +142 -65
- package/dist/index.d.ts +142 -65
- package/dist/index.js +573 -43
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -465,7 +465,7 @@ var require_package = __commonJS({
|
|
|
465
465
|
"package.json"(exports2, module2) {
|
|
466
466
|
module2.exports = {
|
|
467
467
|
name: "@prismer/runtime",
|
|
468
|
-
version: "2.0.
|
|
468
|
+
version: "2.0.1",
|
|
469
469
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
470
470
|
type: "module",
|
|
471
471
|
main: "dist/index.js",
|
|
@@ -502,6 +502,7 @@ var require_package = __commonJS({
|
|
|
502
502
|
"better-sqlite3": "^11.8.0",
|
|
503
503
|
commander: "^12.1.0",
|
|
504
504
|
qrcode: "^1.5.4",
|
|
505
|
+
undici: "^7.24.0",
|
|
505
506
|
ws: "^8.19.0",
|
|
506
507
|
yaml: "^2.8.2",
|
|
507
508
|
zod: "^3.23.8"
|
|
@@ -738,6 +739,7 @@ __export(src_exports, {
|
|
|
738
739
|
currentSchemaVersion: () => currentSchemaVersion,
|
|
739
740
|
deriveWsUrl: () => deriveWsUrl,
|
|
740
741
|
envelope: () => envelope,
|
|
742
|
+
extractHttpUrls: () => extractHttpUrls,
|
|
741
743
|
getRoleTemplate: () => getRoleTemplate,
|
|
742
744
|
handleAgentMessageDispatch: () => handleAgentMessageDispatch,
|
|
743
745
|
handleDispatch: () => handleDispatch,
|
|
@@ -2946,8 +2948,11 @@ function envelope(type, payload, requestId) {
|
|
|
2946
2948
|
|
|
2947
2949
|
// src/asset-cache.ts
|
|
2948
2950
|
var import_node_crypto2 = require("crypto");
|
|
2951
|
+
var import_node_dns = require("dns");
|
|
2949
2952
|
var import_node_fs5 = require("fs");
|
|
2953
|
+
var import_node_net = require("net");
|
|
2950
2954
|
var import_node_path6 = require("path");
|
|
2955
|
+
var import_undici = require("undici");
|
|
2951
2956
|
function rowToCached(row) {
|
|
2952
2957
|
return {
|
|
2953
2958
|
contentHash: row.content_hash,
|
|
@@ -3068,6 +3073,73 @@ var AssetCache = class {
|
|
|
3068
3073
|
pin: false
|
|
3069
3074
|
};
|
|
3070
3075
|
}
|
|
3076
|
+
/**
|
|
3077
|
+
* Fetch a public http(s) URL, content-hash dedup, cache locally.
|
|
3078
|
+
*
|
|
3079
|
+
* Mirrors `getOrFetch` (cache key = sha256 of body bytes), but the input
|
|
3080
|
+
* is a URL whose hash we don't know up front. The fetcher applies:
|
|
3081
|
+
*
|
|
3082
|
+
* - SSRF guard (resolve hostname, reject loopback / RFC1918 / link-local
|
|
3083
|
+
* / cloud-metadata / IPv6 ULA + link-local). Cross-host redirects
|
|
3084
|
+
* re-validate the new host before each hop.
|
|
3085
|
+
* - manual redirect handling (max 3 hops).
|
|
3086
|
+
* - hard timeout (default 15 s, override via env
|
|
3087
|
+
* `PRISMER_URL_FETCH_TIMEOUT_MS`).
|
|
3088
|
+
* - streaming body read with abort-at-limit (default 5 MiB, override
|
|
3089
|
+
* via env `PRISMER_URL_FETCH_MAX_BYTES`). Truncated bodies are NOT
|
|
3090
|
+
* cached.
|
|
3091
|
+
* - non-2xx → throws (caller should leave the URL in place and emit
|
|
3092
|
+
* an `error` observation).
|
|
3093
|
+
*
|
|
3094
|
+
* On success returns { cached, finalUrl, durationMs }; the caller can
|
|
3095
|
+
* pin / unpin the hash like any other asset.
|
|
3096
|
+
*/
|
|
3097
|
+
async getOrFetchUrl(url, opts) {
|
|
3098
|
+
const started = Date.now();
|
|
3099
|
+
const maxBytes = opts?.maxBytes ?? defaultMaxBytes();
|
|
3100
|
+
const timeoutMs = opts?.timeoutMs ?? defaultTimeoutMs();
|
|
3101
|
+
const maxRedirects = opts?.maxRedirects ?? 3;
|
|
3102
|
+
const userAgent = opts?.userAgent ?? "prismer-daemon";
|
|
3103
|
+
const { body, finalUrl, mime } = await fetchUrlWithGuards(url, {
|
|
3104
|
+
signal: opts?.signal,
|
|
3105
|
+
maxBytes,
|
|
3106
|
+
timeoutMs,
|
|
3107
|
+
maxRedirects,
|
|
3108
|
+
userAgent
|
|
3109
|
+
});
|
|
3110
|
+
const hash = sha256(body);
|
|
3111
|
+
const existing = this.get(hash);
|
|
3112
|
+
if (existing) {
|
|
3113
|
+
return { cached: existing, finalUrl, durationMs: Date.now() - started };
|
|
3114
|
+
}
|
|
3115
|
+
const localPath = this.pathFor(hash);
|
|
3116
|
+
if (!(0, import_node_fs5.existsSync)((0, import_node_path6.dirname)(localPath))) {
|
|
3117
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path6.dirname)(localPath), { recursive: true });
|
|
3118
|
+
}
|
|
3119
|
+
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
3120
|
+
(0, import_node_fs5.writeFileSync)(tmpPath, body);
|
|
3121
|
+
(0, import_node_fs5.renameSync)(tmpPath, localPath);
|
|
3122
|
+
const now = Date.now();
|
|
3123
|
+
this.db.prepare(
|
|
3124
|
+
`INSERT OR REPLACE INTO cached_assets
|
|
3125
|
+
(content_hash, size_bytes, mime, local_path, fetched_at, last_used_at, pin)
|
|
3126
|
+
VALUES (?, ?, ?, ?, ?, ?, 0)`
|
|
3127
|
+
).run(hash, body.length, mime, localPath, now, now);
|
|
3128
|
+
this.evictIfOver();
|
|
3129
|
+
return {
|
|
3130
|
+
cached: {
|
|
3131
|
+
contentHash: hash,
|
|
3132
|
+
sizeBytes: body.length,
|
|
3133
|
+
mime,
|
|
3134
|
+
localPath,
|
|
3135
|
+
fetchedAt: now,
|
|
3136
|
+
lastUsedAt: now,
|
|
3137
|
+
pin: false
|
|
3138
|
+
},
|
|
3139
|
+
finalUrl,
|
|
3140
|
+
durationMs: now - started
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3071
3143
|
/** Insert an already-on-disk asset into the cache (e.g., asset just produced by adapter). */
|
|
3072
3144
|
registerLocal(hash, localPath, mime) {
|
|
3073
3145
|
const actualHash = sha256(readFileBuffer(localPath));
|
|
@@ -3119,6 +3191,184 @@ function sha256(buf) {
|
|
|
3119
3191
|
function readFileBuffer(path9) {
|
|
3120
3192
|
return (0, import_node_fs5.readFileSync)(path9);
|
|
3121
3193
|
}
|
|
3194
|
+
var DEFAULT_URL_FETCH_MAX_BYTES = 5 * 1024 * 1024;
|
|
3195
|
+
var DEFAULT_URL_FETCH_TIMEOUT_MS = 15e3;
|
|
3196
|
+
function defaultMaxBytes() {
|
|
3197
|
+
const v = Number(process.env.PRISMER_URL_FETCH_MAX_BYTES);
|
|
3198
|
+
return Number.isFinite(v) && v > 0 ? v : DEFAULT_URL_FETCH_MAX_BYTES;
|
|
3199
|
+
}
|
|
3200
|
+
function defaultTimeoutMs() {
|
|
3201
|
+
const v = Number(process.env.PRISMER_URL_FETCH_TIMEOUT_MS);
|
|
3202
|
+
return Number.isFinite(v) && v > 0 ? v : DEFAULT_URL_FETCH_TIMEOUT_MS;
|
|
3203
|
+
}
|
|
3204
|
+
var urlFetchDeps = {};
|
|
3205
|
+
function activeFetch() {
|
|
3206
|
+
return urlFetchDeps.fetch ?? globalThis.fetch;
|
|
3207
|
+
}
|
|
3208
|
+
async function activeResolveHostname(host) {
|
|
3209
|
+
if (urlFetchDeps.resolveHostname) return urlFetchDeps.resolveHostname(host);
|
|
3210
|
+
if ((0, import_node_net.isIP)(host)) return [host];
|
|
3211
|
+
const res = await import_node_dns.promises.lookup(host, { all: true, verbatim: true });
|
|
3212
|
+
return res.map((r) => r.address);
|
|
3213
|
+
}
|
|
3214
|
+
function isForbiddenIp(ip) {
|
|
3215
|
+
if ((0, import_node_net.isIPv4)(ip)) {
|
|
3216
|
+
const parts = ip.split(".").map((n) => Number(n));
|
|
3217
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isFinite(p) || p < 0 || p > 255)) {
|
|
3218
|
+
return true;
|
|
3219
|
+
}
|
|
3220
|
+
const [a = 0, b = 0] = parts;
|
|
3221
|
+
if (a === 127) return true;
|
|
3222
|
+
if (a === 10) return true;
|
|
3223
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
3224
|
+
if (a === 192 && b === 168) return true;
|
|
3225
|
+
if (a === 169 && b === 254) return true;
|
|
3226
|
+
if (a === 0) return true;
|
|
3227
|
+
return false;
|
|
3228
|
+
}
|
|
3229
|
+
if ((0, import_node_net.isIPv6)(ip)) {
|
|
3230
|
+
const lower = ip.toLowerCase();
|
|
3231
|
+
if (lower === "::1" || lower === "0:0:0:0:0:0:0:1") return true;
|
|
3232
|
+
if (lower === "::" || lower === "0:0:0:0:0:0:0:0") return true;
|
|
3233
|
+
if (/^fe[89ab][0-9a-f]?:/.test(lower)) return true;
|
|
3234
|
+
if (/^f[cd][0-9a-f]{2}:/.test(lower)) return true;
|
|
3235
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(lower);
|
|
3236
|
+
if (mapped) return isForbiddenIp(mapped[1]);
|
|
3237
|
+
return false;
|
|
3238
|
+
}
|
|
3239
|
+
return true;
|
|
3240
|
+
}
|
|
3241
|
+
async function assertHostAllowed(hostname5) {
|
|
3242
|
+
let ips;
|
|
3243
|
+
try {
|
|
3244
|
+
ips = await activeResolveHostname(hostname5);
|
|
3245
|
+
} catch (err) {
|
|
3246
|
+
throw new Error(`hostname resolution failed for ${hostname5}: ${err.message}`);
|
|
3247
|
+
}
|
|
3248
|
+
if (ips.length === 0) {
|
|
3249
|
+
throw new Error(`no IPs resolved for ${hostname5}`);
|
|
3250
|
+
}
|
|
3251
|
+
for (const ip of ips) {
|
|
3252
|
+
if (isForbiddenIp(ip)) {
|
|
3253
|
+
throw new Error(`SSRF guard: ${hostname5} \u2192 ${ip} is in a forbidden range`);
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
return ips[0];
|
|
3257
|
+
}
|
|
3258
|
+
async function fetchUrlWithGuards(rawUrl, opts) {
|
|
3259
|
+
let currentUrl;
|
|
3260
|
+
try {
|
|
3261
|
+
currentUrl = new URL(rawUrl);
|
|
3262
|
+
} catch {
|
|
3263
|
+
throw new Error(`invalid URL: ${rawUrl}`);
|
|
3264
|
+
}
|
|
3265
|
+
for (let hop = 0; hop <= opts.maxRedirects; hop += 1) {
|
|
3266
|
+
if (currentUrl.protocol !== "http:" && currentUrl.protocol !== "https:") {
|
|
3267
|
+
throw new Error(`unsupported scheme: ${currentUrl.protocol}`);
|
|
3268
|
+
}
|
|
3269
|
+
const pinnedIp = await assertHostAllowed(currentUrl.hostname);
|
|
3270
|
+
const controller = new AbortController();
|
|
3271
|
+
const timer = setTimeout(() => controller.abort(new Error("timeout")), opts.timeoutMs);
|
|
3272
|
+
if (opts.signal) {
|
|
3273
|
+
if (opts.signal.aborted) controller.abort(opts.signal.reason);
|
|
3274
|
+
else opts.signal.addEventListener("abort", () => controller.abort(opts.signal.reason), { once: true });
|
|
3275
|
+
}
|
|
3276
|
+
const usingMockedFetch = urlFetchDeps.fetch !== void 0;
|
|
3277
|
+
const dispatcher = usingMockedFetch ? void 0 : new import_undici.Agent({
|
|
3278
|
+
connect: {
|
|
3279
|
+
lookup: (_hostname, _options, cb) => {
|
|
3280
|
+
cb(null, pinnedIp, (0, import_node_net.isIPv6)(pinnedIp) ? 6 : 4);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
try {
|
|
3285
|
+
let res;
|
|
3286
|
+
try {
|
|
3287
|
+
res = await activeFetch()(currentUrl, {
|
|
3288
|
+
method: "GET",
|
|
3289
|
+
redirect: "manual",
|
|
3290
|
+
headers: { "User-Agent": opts.userAgent, Accept: "*/*" },
|
|
3291
|
+
signal: controller.signal,
|
|
3292
|
+
// `dispatcher` is undici-specific; Node's built-in fetch (which
|
|
3293
|
+
// IS undici under the hood) accepts it via this option.
|
|
3294
|
+
...dispatcher ? { dispatcher } : {}
|
|
3295
|
+
});
|
|
3296
|
+
} catch (err) {
|
|
3297
|
+
clearTimeout(timer);
|
|
3298
|
+
throw new Error(`fetch failed: ${err.message}`);
|
|
3299
|
+
}
|
|
3300
|
+
if (res.status >= 300 && res.status < 400) {
|
|
3301
|
+
clearTimeout(timer);
|
|
3302
|
+
const loc = res.headers.get("location");
|
|
3303
|
+
if (!loc) throw new Error(`redirect ${res.status} with no Location header`);
|
|
3304
|
+
try {
|
|
3305
|
+
currentUrl = new URL(loc, currentUrl);
|
|
3306
|
+
} catch {
|
|
3307
|
+
throw new Error(`invalid redirect Location: ${loc}`);
|
|
3308
|
+
}
|
|
3309
|
+
try {
|
|
3310
|
+
await res.arrayBuffer();
|
|
3311
|
+
} catch {
|
|
3312
|
+
}
|
|
3313
|
+
if (hop === opts.maxRedirects) {
|
|
3314
|
+
throw new Error(`too many redirects (>${opts.maxRedirects})`);
|
|
3315
|
+
}
|
|
3316
|
+
continue;
|
|
3317
|
+
}
|
|
3318
|
+
if (!res.ok) {
|
|
3319
|
+
clearTimeout(timer);
|
|
3320
|
+
throw new Error(`HTTP ${res.status}`);
|
|
3321
|
+
}
|
|
3322
|
+
const contentLength = Number(res.headers.get("content-length"));
|
|
3323
|
+
if (Number.isFinite(contentLength) && contentLength > opts.maxBytes) {
|
|
3324
|
+
clearTimeout(timer);
|
|
3325
|
+
controller.abort(new Error("content-length exceeds limit"));
|
|
3326
|
+
throw new Error(`body too large: content-length=${contentLength} > ${opts.maxBytes}`);
|
|
3327
|
+
}
|
|
3328
|
+
const chunks = [];
|
|
3329
|
+
let total = 0;
|
|
3330
|
+
const reader = res.body?.getReader();
|
|
3331
|
+
try {
|
|
3332
|
+
if (reader) {
|
|
3333
|
+
for (; ; ) {
|
|
3334
|
+
const { done, value } = await reader.read();
|
|
3335
|
+
if (done) break;
|
|
3336
|
+
if (value) {
|
|
3337
|
+
total += value.byteLength;
|
|
3338
|
+
if (total > opts.maxBytes) {
|
|
3339
|
+
controller.abort(new Error("body exceeds limit"));
|
|
3340
|
+
try {
|
|
3341
|
+
await reader.cancel();
|
|
3342
|
+
} catch {
|
|
3343
|
+
}
|
|
3344
|
+
throw new Error(`body too large: read ${total} > ${opts.maxBytes}`);
|
|
3345
|
+
}
|
|
3346
|
+
chunks.push(value);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
} else {
|
|
3350
|
+
const ab = await res.arrayBuffer();
|
|
3351
|
+
if (ab.byteLength > opts.maxBytes) {
|
|
3352
|
+
throw new Error(`body too large: ${ab.byteLength} > ${opts.maxBytes}`);
|
|
3353
|
+
}
|
|
3354
|
+
chunks.push(new Uint8Array(ab));
|
|
3355
|
+
total = ab.byteLength;
|
|
3356
|
+
}
|
|
3357
|
+
} finally {
|
|
3358
|
+
clearTimeout(timer);
|
|
3359
|
+
}
|
|
3360
|
+
const body = Buffer.concat(chunks.map((c) => Buffer.from(c.buffer, c.byteOffset, c.byteLength)), total);
|
|
3361
|
+
return {
|
|
3362
|
+
body,
|
|
3363
|
+
finalUrl: currentUrl.toString(),
|
|
3364
|
+
mime: res.headers.get("content-type")
|
|
3365
|
+
};
|
|
3366
|
+
} finally {
|
|
3367
|
+
await dispatcher?.close().catch(() => void 0);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
throw new Error(`too many redirects (>${opts.maxRedirects})`);
|
|
3371
|
+
}
|
|
3122
3372
|
|
|
3123
3373
|
// src/daemon/asset/mirror.ts
|
|
3124
3374
|
var import_node_fs6 = require("fs");
|
|
@@ -3371,6 +3621,8 @@ var ParseClaimController = class {
|
|
|
3371
3621
|
// src/uri-resolver.ts
|
|
3372
3622
|
var URI_REGEX_WORKSPACE = /prismer:\/\/workspace\/([^/\s]+)\/(asset|file)\/([^\s)\]]+)/g;
|
|
3373
3623
|
var URI_REGEX_LEGACY = /prismer:\/\/(?!workspace\/)([^/\s]+)\/(asset|file)\/([^\s)\]]+)/g;
|
|
3624
|
+
var HTTP_URL_REGEX = /https?:\/\/[^\s<>"'`]+/g;
|
|
3625
|
+
var URL_TRAILING_PUNCT_RE = /[,.;:!?)\]}'"]+$/;
|
|
3374
3626
|
function parseUris(text) {
|
|
3375
3627
|
if (!text) return [];
|
|
3376
3628
|
const out = [];
|
|
@@ -3433,12 +3685,20 @@ var UriResolver = class {
|
|
|
3433
3685
|
return cached.localPath;
|
|
3434
3686
|
}
|
|
3435
3687
|
/**
|
|
3436
|
-
* Walk a string, replace every `prismer://(asset|file)/...`
|
|
3437
|
-
*
|
|
3688
|
+
* Walk a string, replace every `prismer://(asset|file)/...` and
|
|
3689
|
+
* `https://…` / `http://…` URL with `file://<localPath>`.
|
|
3690
|
+
*
|
|
3691
|
+
* Unrecognized URIs pass through unchanged. Returns rewritten text, the
|
|
3692
|
+
* list of pinned hashes, and one observation per http(s) URL the resolver
|
|
3693
|
+
* attempted (success + error both surface) so the dispatch can include
|
|
3694
|
+
* them in `reply.assetObservability`.
|
|
3695
|
+
*
|
|
3696
|
+
* `urlCache` lets the caller dedupe URL fetches across multiple rewrite
|
|
3697
|
+
* calls within a single dispatch (e.g. prompt + each context entry).
|
|
3698
|
+
* Pass the same Map instance to every rewrite() / rewriteAll() call.
|
|
3438
3699
|
*/
|
|
3439
3700
|
async rewrite(text, opts) {
|
|
3440
3701
|
const uris = parseUris(text);
|
|
3441
|
-
if (uris.length === 0) return { text, resolvedHashes: [] };
|
|
3442
3702
|
const replacements = /* @__PURE__ */ new Map();
|
|
3443
3703
|
const resolvedHashes = [];
|
|
3444
3704
|
for (const u of uris) {
|
|
@@ -3461,6 +3721,46 @@ var UriResolver = class {
|
|
|
3461
3721
|
console.warn(`[uri-resolver] failed to resolve ${u.raw}: ${err.message}`);
|
|
3462
3722
|
}
|
|
3463
3723
|
}
|
|
3724
|
+
if (opts?.fetchUrls !== false) {
|
|
3725
|
+
const urlCache = opts?.urlCache;
|
|
3726
|
+
const urls = extractHttpUrls(text);
|
|
3727
|
+
for (const original of urls) {
|
|
3728
|
+
try {
|
|
3729
|
+
let resolution = urlCache?.get(original);
|
|
3730
|
+
if (!resolution) {
|
|
3731
|
+
const { cached, finalUrl, durationMs } = await this.assetCache.getOrFetchUrl(original, {
|
|
3732
|
+
signal: opts?.signal
|
|
3733
|
+
});
|
|
3734
|
+
resolution = { hash: cached.contentHash, localPath: cached.localPath, sizeBytes: cached.sizeBytes, mime: cached.mime, finalUrl, durationMs };
|
|
3735
|
+
urlCache?.set(original, resolution);
|
|
3736
|
+
opts?.urlObservations?.push({
|
|
3737
|
+
contentHash: cached.contentHash,
|
|
3738
|
+
mime: cached.mime,
|
|
3739
|
+
sizeBytes: cached.sizeBytes,
|
|
3740
|
+
strategy: "fetched-https",
|
|
3741
|
+
originalUrl: original,
|
|
3742
|
+
finalUrl,
|
|
3743
|
+
durationMs
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
replacements.set(original, `file://${resolution.localPath}`);
|
|
3747
|
+
resolvedHashes.push(resolution.hash);
|
|
3748
|
+
if (opts?.pin) this.assetCache.pin(resolution.hash);
|
|
3749
|
+
} catch (err) {
|
|
3750
|
+
const message = err.message;
|
|
3751
|
+
console.warn(`[uri-resolver] failed to fetch ${original}: ${message}`);
|
|
3752
|
+
opts?.urlObservations?.push({
|
|
3753
|
+
contentHash: "",
|
|
3754
|
+
mime: null,
|
|
3755
|
+
sizeBytes: null,
|
|
3756
|
+
strategy: "error",
|
|
3757
|
+
originalUrl: original,
|
|
3758
|
+
error: message
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
if (replacements.size === 0) return { text, resolvedHashes };
|
|
3464
3764
|
let rewritten = text;
|
|
3465
3765
|
for (const [raw, sub] of replacements) {
|
|
3466
3766
|
rewritten = rewritten.split(raw).join(sub);
|
|
@@ -3498,6 +3798,27 @@ var UriResolver = class {
|
|
|
3498
3798
|
function dedupe(xs) {
|
|
3499
3799
|
return Array.from(new Set(xs));
|
|
3500
3800
|
}
|
|
3801
|
+
function extractHttpUrls(text) {
|
|
3802
|
+
if (!text) return [];
|
|
3803
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3804
|
+
const out = [];
|
|
3805
|
+
for (const m of text.matchAll(HTTP_URL_REGEX)) {
|
|
3806
|
+
let url = m[0];
|
|
3807
|
+
const punct = URL_TRAILING_PUNCT_RE.exec(url);
|
|
3808
|
+
if (punct) url = url.slice(0, -punct[0].length);
|
|
3809
|
+
if (!url) continue;
|
|
3810
|
+
try {
|
|
3811
|
+
const parsed = new URL(url);
|
|
3812
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") continue;
|
|
3813
|
+
} catch {
|
|
3814
|
+
continue;
|
|
3815
|
+
}
|
|
3816
|
+
if (seen.has(url)) continue;
|
|
3817
|
+
seen.add(url);
|
|
3818
|
+
out.push(url);
|
|
3819
|
+
}
|
|
3820
|
+
return out;
|
|
3821
|
+
}
|
|
3501
3822
|
|
|
3502
3823
|
// src/daemon/dispatch.ts
|
|
3503
3824
|
var import_node_fs8 = require("fs");
|
|
@@ -3522,29 +3843,141 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
3522
3843
|
for (const entry of entries) {
|
|
3523
3844
|
const slug = sanitizeSlug(trimmedStringFrom(entry.skill?.slug) ?? trimmedStringFrom(entry.slug));
|
|
3524
3845
|
const skillId = trimmedStringFrom(entry.skill?.id);
|
|
3525
|
-
|
|
3526
|
-
if (!slug || !content) {
|
|
3846
|
+
if (!slug) {
|
|
3527
3847
|
skipped++;
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3848
|
+
continue;
|
|
3849
|
+
}
|
|
3850
|
+
let files = null;
|
|
3851
|
+
const manifestRaw = entry.skill?.contentManifest;
|
|
3852
|
+
if (typeof manifestRaw === "string" && manifestRaw.trim()) {
|
|
3853
|
+
files = parseManifest(manifestRaw, slug);
|
|
3854
|
+
}
|
|
3855
|
+
if (!files) {
|
|
3856
|
+
const legacyContent = contentStringFrom(entry.skill?.content) ?? contentStringFrom(entry.content);
|
|
3857
|
+
if (legacyContent) {
|
|
3858
|
+
const buf = Buffer.from(legacyContent, "utf8");
|
|
3859
|
+
files = [
|
|
3860
|
+
{
|
|
3861
|
+
path: "SKILL.md",
|
|
3862
|
+
size: buf.byteLength,
|
|
3863
|
+
sha256: sha256Buffer(buf),
|
|
3864
|
+
inline: true,
|
|
3865
|
+
content: buf.toString("base64")
|
|
3866
|
+
}
|
|
3867
|
+
];
|
|
3532
3868
|
}
|
|
3869
|
+
}
|
|
3870
|
+
if (!files || files.length === 0) {
|
|
3871
|
+
skipped++;
|
|
3872
|
+
process.stderr.write(`[daemon] skill sync skipped ${slug}: no manifest or content
|
|
3873
|
+
`);
|
|
3874
|
+
await ackSkillSync(cloud, agentImUserId, { skillId, slug, error: "missing content" }, signal);
|
|
3533
3875
|
continue;
|
|
3534
3876
|
}
|
|
3535
|
-
const revision = sha2562(content);
|
|
3536
3877
|
const skillDir = (0, import_node_path8.join)(skillsRoot, slug);
|
|
3537
3878
|
await import_node_fs7.promises.mkdir(skillDir, { recursive: true });
|
|
3538
|
-
const
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3879
|
+
const localFiles = await walkLocalDir(skillDir);
|
|
3880
|
+
let dirty = false;
|
|
3881
|
+
let perFileFailures = 0;
|
|
3882
|
+
for (const file of files) {
|
|
3883
|
+
if (!isSafeRelativePath(file.path)) {
|
|
3884
|
+
process.stderr.write(`[daemon] skill sync ${slug}: skipping suspicious path "${file.path}"
|
|
3885
|
+
`);
|
|
3886
|
+
perFileFailures++;
|
|
3887
|
+
continue;
|
|
3888
|
+
}
|
|
3889
|
+
const targetPath = (0, import_node_path8.join)(skillDir, file.path);
|
|
3890
|
+
const normalizedTarget = targetPath + (targetPath.endsWith(import_node_path8.sep) ? "" : "");
|
|
3891
|
+
if (normalizedTarget !== skillDir && !normalizedTarget.startsWith(skillDir + import_node_path8.sep)) {
|
|
3892
|
+
process.stderr.write(`[daemon] skill sync ${slug}: path escapes skillDir "${file.path}"
|
|
3893
|
+
`);
|
|
3894
|
+
perFileFailures++;
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3897
|
+
const existingHash = localFiles.get(file.path);
|
|
3898
|
+
if (existingHash === file.sha256) {
|
|
3899
|
+
localFiles.delete(file.path);
|
|
3900
|
+
continue;
|
|
3901
|
+
}
|
|
3902
|
+
let bytes = null;
|
|
3903
|
+
try {
|
|
3904
|
+
if (file.inline !== false && typeof file.content === "string") {
|
|
3905
|
+
bytes = Buffer.from(file.content, "base64");
|
|
3906
|
+
} else if (typeof file.url === "string" && file.url) {
|
|
3907
|
+
bytes = await downloadUrl(file.url, signal);
|
|
3908
|
+
} else {
|
|
3909
|
+
process.stderr.write(
|
|
3910
|
+
`[daemon] skill sync ${slug}: file ${file.path} has neither inline content nor url
|
|
3911
|
+
`
|
|
3912
|
+
);
|
|
3913
|
+
perFileFailures++;
|
|
3914
|
+
continue;
|
|
3915
|
+
}
|
|
3916
|
+
} catch (err) {
|
|
3917
|
+
process.stderr.write(
|
|
3918
|
+
`[daemon] skill sync ${slug}: failed to fetch ${file.path}: ${err.message}
|
|
3919
|
+
`
|
|
3920
|
+
);
|
|
3921
|
+
perFileFailures++;
|
|
3922
|
+
continue;
|
|
3923
|
+
}
|
|
3924
|
+
const downloadedHash = sha256Buffer(bytes);
|
|
3925
|
+
if (downloadedHash !== file.sha256) {
|
|
3926
|
+
process.stderr.write(
|
|
3927
|
+
`[daemon] skill sync ${slug}: hash mismatch for ${file.path} (expected ${file.sha256}, got ${downloadedHash})
|
|
3928
|
+
`
|
|
3929
|
+
);
|
|
3930
|
+
perFileFailures++;
|
|
3931
|
+
continue;
|
|
3932
|
+
}
|
|
3933
|
+
await import_node_fs7.promises.mkdir((0, import_node_path8.dirname)(targetPath), { recursive: true });
|
|
3934
|
+
await import_node_fs7.promises.writeFile(targetPath, bytes);
|
|
3935
|
+
localFiles.delete(file.path);
|
|
3936
|
+
dirty = true;
|
|
3937
|
+
}
|
|
3938
|
+
for (const orphan of localFiles.keys()) {
|
|
3939
|
+
try {
|
|
3940
|
+
await import_node_fs7.promises.unlink((0, import_node_path8.join)(skillDir, orphan));
|
|
3941
|
+
dirty = true;
|
|
3942
|
+
} catch (err) {
|
|
3943
|
+
if (err.code !== "ENOENT") {
|
|
3944
|
+
process.stderr.write(
|
|
3945
|
+
`[daemon] skill sync ${slug}: failed to remove orphan ${orphan}: ${err.message}
|
|
3946
|
+
`
|
|
3947
|
+
);
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
if (perFileFailures > 0) {
|
|
3952
|
+
skipped++;
|
|
3953
|
+
await ackSkillSync(
|
|
3954
|
+
cloud,
|
|
3955
|
+
agentImUserId,
|
|
3956
|
+
{ skillId, slug, error: `${perFileFailures} file(s) failed` },
|
|
3957
|
+
signal
|
|
3958
|
+
);
|
|
3543
3959
|
continue;
|
|
3544
3960
|
}
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3961
|
+
const localMerkle = computeMerkle(files);
|
|
3962
|
+
const declaredRevision = trimmedStringFrom(entry.skill?.contentManifestRevision);
|
|
3963
|
+
const revision = declaredRevision ?? localMerkle;
|
|
3964
|
+
if (declaredRevision && declaredRevision !== localMerkle) {
|
|
3965
|
+
process.stderr.write(
|
|
3966
|
+
`[daemon] skill sync ${slug}: declared revision ${declaredRevision} != computed ${localMerkle}; using computed
|
|
3967
|
+
`
|
|
3968
|
+
);
|
|
3969
|
+
}
|
|
3970
|
+
if (dirty) {
|
|
3971
|
+
synced++;
|
|
3972
|
+
} else {
|
|
3973
|
+
unchanged++;
|
|
3974
|
+
}
|
|
3975
|
+
await ackSkillSync(
|
|
3976
|
+
cloud,
|
|
3977
|
+
agentImUserId,
|
|
3978
|
+
{ skillId, slug, revision: declaredRevision === localMerkle ? declaredRevision : localMerkle },
|
|
3979
|
+
signal
|
|
3980
|
+
);
|
|
3548
3981
|
}
|
|
3549
3982
|
return { synced, skipped, unchanged };
|
|
3550
3983
|
}
|
|
@@ -3562,9 +3995,96 @@ function resolveSkillsRoot(profile) {
|
|
|
3562
3995
|
}
|
|
3563
3996
|
return null;
|
|
3564
3997
|
}
|
|
3565
|
-
function
|
|
3998
|
+
function sha256Buffer(value) {
|
|
3566
3999
|
return (0, import_node_crypto3.createHash)("sha256").update(value).digest("hex");
|
|
3567
4000
|
}
|
|
4001
|
+
function computeMerkle(files) {
|
|
4002
|
+
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
4003
|
+
const lines = sorted.map((f) => `${f.path}:${f.sha256}`).join("\n");
|
|
4004
|
+
return (0, import_node_crypto3.createHash)("sha256").update(lines).digest("hex");
|
|
4005
|
+
}
|
|
4006
|
+
function parseManifest(raw, slug) {
|
|
4007
|
+
try {
|
|
4008
|
+
const parsed = JSON.parse(raw);
|
|
4009
|
+
const arr = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.files) ? parsed.files : null;
|
|
4010
|
+
if (!Array.isArray(arr)) {
|
|
4011
|
+
process.stderr.write(`[daemon] skill sync ${slug}: contentManifest is not an array
|
|
4012
|
+
`);
|
|
4013
|
+
return null;
|
|
4014
|
+
}
|
|
4015
|
+
const files = [];
|
|
4016
|
+
for (const item of arr) {
|
|
4017
|
+
if (!item || typeof item !== "object") continue;
|
|
4018
|
+
const rec = item;
|
|
4019
|
+
const path9 = typeof rec.path === "string" ? rec.path : null;
|
|
4020
|
+
const sha2563 = typeof rec.sha256 === "string" ? rec.sha256 : null;
|
|
4021
|
+
const size = typeof rec.size === "number" && Number.isFinite(rec.size) && rec.size >= 0 ? Math.floor(rec.size) : null;
|
|
4022
|
+
if (!path9 || !sha2563 || size === null) continue;
|
|
4023
|
+
const content = typeof rec.content === "string" ? rec.content : void 0;
|
|
4024
|
+
const url = typeof rec.url === "string" ? rec.url : void 0;
|
|
4025
|
+
const inline = typeof rec.inline === "boolean" ? rec.inline : content !== void 0 ? true : void 0;
|
|
4026
|
+
files.push({ path: path9, size, sha256: sha2563, inline, content, url });
|
|
4027
|
+
}
|
|
4028
|
+
return files;
|
|
4029
|
+
} catch (err) {
|
|
4030
|
+
process.stderr.write(
|
|
4031
|
+
`[daemon] skill sync ${slug}: invalid contentManifest JSON: ${err.message}
|
|
4032
|
+
`
|
|
4033
|
+
);
|
|
4034
|
+
return null;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
function isSafeRelativePath(p) {
|
|
4038
|
+
if (typeof p !== "string" || !p) return false;
|
|
4039
|
+
if (p.length > 512) return false;
|
|
4040
|
+
if (p.includes("\0")) return false;
|
|
4041
|
+
if (p.startsWith("/") || p.startsWith("\\")) return false;
|
|
4042
|
+
if (/^[A-Za-z]:[\\/]/.test(p)) return false;
|
|
4043
|
+
const segs = p.replace(/\\/g, "/").split("/");
|
|
4044
|
+
for (const seg of segs) {
|
|
4045
|
+
if (seg === "" || seg === "." || seg === "..") return false;
|
|
4046
|
+
}
|
|
4047
|
+
return true;
|
|
4048
|
+
}
|
|
4049
|
+
async function walkLocalDir(root) {
|
|
4050
|
+
const out = /* @__PURE__ */ new Map();
|
|
4051
|
+
async function walk2(dir) {
|
|
4052
|
+
let entries;
|
|
4053
|
+
try {
|
|
4054
|
+
entries = await import_node_fs7.promises.readdir(dir, { withFileTypes: true });
|
|
4055
|
+
} catch (err) {
|
|
4056
|
+
if (err.code === "ENOENT") return;
|
|
4057
|
+
throw err;
|
|
4058
|
+
}
|
|
4059
|
+
for (const ent of entries) {
|
|
4060
|
+
const full = (0, import_node_path8.join)(dir, ent.name);
|
|
4061
|
+
if (ent.isDirectory()) {
|
|
4062
|
+
await walk2(full);
|
|
4063
|
+
} else if (ent.isFile()) {
|
|
4064
|
+
try {
|
|
4065
|
+
const buf = await import_node_fs7.promises.readFile(full);
|
|
4066
|
+
const rel = (0, import_node_path8.relative)(root, full).split(import_node_path8.sep).join("/");
|
|
4067
|
+
out.set(rel, sha256Buffer(buf));
|
|
4068
|
+
} catch {
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
await walk2(root);
|
|
4074
|
+
return out;
|
|
4075
|
+
}
|
|
4076
|
+
async function downloadUrl(url, signal) {
|
|
4077
|
+
const lower = url.toLowerCase();
|
|
4078
|
+
if (!lower.startsWith("http://") && !lower.startsWith("https://")) {
|
|
4079
|
+
throw new Error(`unsupported url scheme: ${url.slice(0, 32)}`);
|
|
4080
|
+
}
|
|
4081
|
+
const res = await fetch(url, { signal });
|
|
4082
|
+
if (!res.ok) {
|
|
4083
|
+
throw new Error(`fetch ${url} failed: HTTP ${res.status}`);
|
|
4084
|
+
}
|
|
4085
|
+
const ab = await res.arrayBuffer();
|
|
4086
|
+
return Buffer.from(ab);
|
|
4087
|
+
}
|
|
3568
4088
|
function normalizeInstalledSkills(data) {
|
|
3569
4089
|
if (Array.isArray(data)) return data.filter(isInstalledSkillEntry);
|
|
3570
4090
|
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
@@ -3684,12 +4204,22 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
3684
4204
|
payload.prompt = hashRefResult.text;
|
|
3685
4205
|
}
|
|
3686
4206
|
}
|
|
3687
|
-
const
|
|
4207
|
+
const urlCache = /* @__PURE__ */ new Map();
|
|
4208
|
+
const urlObservations = [];
|
|
4209
|
+
const rewrittenPrompt = await deps.uriResolver.rewrite(payload.prompt, {
|
|
4210
|
+
pin: true,
|
|
4211
|
+
urlCache,
|
|
4212
|
+
urlObservations
|
|
4213
|
+
});
|
|
3688
4214
|
resolvedHashes.push(...rewrittenPrompt.resolvedHashes);
|
|
3689
4215
|
let rewrittenContext = [];
|
|
3690
4216
|
if (payload.context && payload.context.length > 0) {
|
|
3691
4217
|
const contents = payload.context.map((e) => e.content);
|
|
3692
|
-
const r = await deps.uriResolver.rewriteAll(contents, {
|
|
4218
|
+
const r = await deps.uriResolver.rewriteAll(contents, {
|
|
4219
|
+
pin: true,
|
|
4220
|
+
urlCache,
|
|
4221
|
+
urlObservations
|
|
4222
|
+
});
|
|
3693
4223
|
resolvedHashes.push(...r.resolvedHashes);
|
|
3694
4224
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
3695
4225
|
}
|
|
@@ -3809,7 +4339,7 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
3809
4339
|
} : result.error,
|
|
3810
4340
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
3811
4341
|
metrics: result.metrics,
|
|
3812
|
-
...assetResolution.observability.length > 0 ? { assetObservability: assetResolution.observability } : {}
|
|
4342
|
+
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
3813
4343
|
};
|
|
3814
4344
|
await writeBridgeMetadata(payload.taskId, deps.cloud, result.metadata, deps.signal);
|
|
3815
4345
|
await writeObservabilityMetadata(
|
|
@@ -5020,10 +5550,10 @@ async function walkAndDigest(root, current) {
|
|
|
5020
5550
|
}
|
|
5021
5551
|
if (!st.isFile()) continue;
|
|
5022
5552
|
const buf = await import_node_fs9.promises.readFile(full);
|
|
5023
|
-
const
|
|
5553
|
+
const sha2563 = (0, import_node_crypto4.createHash)("sha256").update(buf).digest("hex");
|
|
5024
5554
|
out.push({
|
|
5025
5555
|
path: rel,
|
|
5026
|
-
sha256:
|
|
5556
|
+
sha256: sha2563,
|
|
5027
5557
|
sizeBytes: st.size,
|
|
5028
5558
|
mtime: Math.floor(st.mtimeMs)
|
|
5029
5559
|
});
|
|
@@ -5788,7 +6318,7 @@ var MemoryStore = class {
|
|
|
5788
6318
|
}
|
|
5789
6319
|
const db = this.requireDb();
|
|
5790
6320
|
const now = Date.now();
|
|
5791
|
-
const contentHash =
|
|
6321
|
+
const contentHash = sha2562(input.content);
|
|
5792
6322
|
const payload = sealPlaintext(input.content);
|
|
5793
6323
|
if (payload.kind !== "inline") {
|
|
5794
6324
|
throw new Error("MemoryStore.write: non-inline payload not yet supported in phase-0");
|
|
@@ -5982,7 +6512,7 @@ var MemoryStore = class {
|
|
|
5982
6512
|
};
|
|
5983
6513
|
}
|
|
5984
6514
|
};
|
|
5985
|
-
function
|
|
6515
|
+
function sha2562(s) {
|
|
5986
6516
|
return (0, import_node_crypto5.createHash)("sha256").update(s, "utf8").digest("hex");
|
|
5987
6517
|
}
|
|
5988
6518
|
|
|
@@ -11246,11 +11776,11 @@ function buildAssetCommand() {
|
|
|
11246
11776
|
}
|
|
11247
11777
|
printAssetDetail("Asset", assetId, body);
|
|
11248
11778
|
}, { code: "asset_get_failed" }));
|
|
11249
|
-
cmd.command("by-hash <sha256>").description("Fetch asset metadata by content hash").option("--workspace-id <id>", "Workspace id (required by the cloud API)").option("--json", "Output machine-readable JSON").action(runAction(async (
|
|
11779
|
+
cmd.command("by-hash <sha256>").description("Fetch asset metadata by content hash").option("--workspace-id <id>", "Workspace id (required by the cloud API)").option("--json", "Output machine-readable JSON").action(runAction(async (sha2563, opts) => {
|
|
11250
11780
|
const cloud = mkCloud();
|
|
11251
11781
|
const query = new URLSearchParams();
|
|
11252
11782
|
if (opts.workspaceId) query.set("wsId", opts.workspaceId);
|
|
11253
|
-
const path9 = `/api/im/assets/by-hash/${encodeURIComponent(
|
|
11783
|
+
const path9 = `/api/im/assets/by-hash/${encodeURIComponent(sha2563)}${query.toString() ? `?${query.toString()}` : ""}`;
|
|
11254
11784
|
const res = await cloud.request("GET", path9);
|
|
11255
11785
|
if (!res.ok) exitWithError(`asset by-hash failed (${res.status}): ${res.error?.message ?? "request failed"}`, { code: res.error?.code ?? "asset_by_hash_failed" });
|
|
11256
11786
|
const body = res.data;
|
|
@@ -11261,7 +11791,7 @@ function buildAssetCommand() {
|
|
|
11261
11791
|
printJson(body ?? null);
|
|
11262
11792
|
return;
|
|
11263
11793
|
}
|
|
11264
|
-
printAssetDetail("Asset hash",
|
|
11794
|
+
printAssetDetail("Asset hash", sha2563, body);
|
|
11265
11795
|
}, { code: "asset_by_hash_failed" }));
|
|
11266
11796
|
return cmd;
|
|
11267
11797
|
}
|
|
@@ -14350,7 +14880,7 @@ async function readResponseError(res) {
|
|
|
14350
14880
|
|
|
14351
14881
|
// src/cli/index.ts
|
|
14352
14882
|
init_ui();
|
|
14353
|
-
var VERSION = "2.0.
|
|
14883
|
+
var VERSION = "2.0.1";
|
|
14354
14884
|
function buildProgram() {
|
|
14355
14885
|
const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
14356
14886
|
program.addCommand(buildBannerCommand());
|
|
@@ -14409,6 +14939,7 @@ async function runCli(argv = process.argv) {
|
|
|
14409
14939
|
currentSchemaVersion,
|
|
14410
14940
|
deriveWsUrl,
|
|
14411
14941
|
envelope,
|
|
14942
|
+
extractHttpUrls,
|
|
14412
14943
|
getRoleTemplate,
|
|
14413
14944
|
handleAgentMessageDispatch,
|
|
14414
14945
|
handleDispatch,
|