@hasna/skills 0.5.3 → 0.5.5
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 +72 -7
- package/bin/index.js +1429 -209
- package/bin/mcp.js +1545 -40
- package/bin/migrate.js +13 -4
- package/bin/server.js +81 -19
- package/bin/worker.js +37 -7
- package/dist/cli/commands/private-publications.d.ts +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +756 -24
- package/dist/lib/fleet-credentials.d.ts +6 -1
- package/dist/lib/home-adoption.d.ts +2 -7
- package/dist/lib/mcp-contracts.d.ts +1 -0
- package/dist/lib/private-publication-customer.d.ts +10 -0
- package/dist/lib/private-publication-recovery.d.ts +43 -0
- package/dist/lib/remote-account.d.ts +5 -0
- package/dist/lib/remote-auth.d.ts +3 -0
- package/dist/lib/remote-client.d.ts +9 -2
- package/dist/lib/remote-private-publications.d.ts +74 -0
- package/dist/lib/remote-quote-errors.d.ts +12 -0
- package/dist/mcp/private-publication-tools.d.ts +2 -0
- package/dist/sdk/execution/dispatchers/ecs.d.ts +25 -14
- package/dist/sdk/index.d.ts +4 -1
- package/dist/sdk/index.js +938 -164
- package/dist/server/types.d.ts +2 -0
- package/package.json +1 -1
package/bin/mcp.js
CHANGED
|
@@ -5586,7 +5586,7 @@ var package_default;
|
|
|
5586
5586
|
var init_package = __esm(() => {
|
|
5587
5587
|
package_default = {
|
|
5588
5588
|
name: "@hasna/skills",
|
|
5589
|
-
version: "0.5.
|
|
5589
|
+
version: "0.5.5",
|
|
5590
5590
|
description: "Skills library for AI coding agents",
|
|
5591
5591
|
type: "module",
|
|
5592
5592
|
bin: {
|
|
@@ -6507,6 +6507,7 @@ __export(exports_fleet_credentials, {
|
|
|
6507
6507
|
skillsCredentialOrReason: () => skillsCredentialOrReason,
|
|
6508
6508
|
skillsCredentialFiles: () => skillsCredentialFiles,
|
|
6509
6509
|
skillsCredentialFilePath: () => skillsCredentialFilePath,
|
|
6510
|
+
skillsApiRequestUrl: () => skillsApiRequestUrl,
|
|
6510
6511
|
selectsSkillsLocalMode: () => selectsSkillsLocalMode,
|
|
6511
6512
|
resolveSkillsFleet: () => resolveSkillsFleet,
|
|
6512
6513
|
resolveSkillsConnection: () => resolveSkillsConnection,
|
|
@@ -6547,7 +6548,9 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
6547
6548
|
throw new SkillsFleetCredentialError("A Skills API URL must use HTTPS (or loopback HTTP), without credentials, query or fragment", "INVALID_API_URL");
|
|
6548
6549
|
}
|
|
6549
6550
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
6550
|
-
if (
|
|
6551
|
+
if (url.origin === "https://api.hasna.com" && pathname === "/skills/v1") {
|
|
6552
|
+
url.pathname = "/skills";
|
|
6553
|
+
} else if (pathname === "/api" || pathname === "/api/v1") {
|
|
6551
6554
|
url.pathname = "/";
|
|
6552
6555
|
} else if (pathname.endsWith("/api/v1")) {
|
|
6553
6556
|
url.pathname = pathname.slice(0, -"/api/v1".length) || "/";
|
|
@@ -6556,6 +6559,21 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
6556
6559
|
}
|
|
6557
6560
|
return url.toString().replace(/\/+$/, "");
|
|
6558
6561
|
}
|
|
6562
|
+
function skillsApiRequestUrl(apiUrl, route) {
|
|
6563
|
+
const origin = normalizeSkillsApiOrigin(apiUrl);
|
|
6564
|
+
if (!route.startsWith("/api/") || route.includes("#") || route.includes("\\")) {
|
|
6565
|
+
throw new SkillsFleetCredentialError("Invalid Skills API route", "INVALID_API_URL");
|
|
6566
|
+
}
|
|
6567
|
+
if (origin === "https://api.hasna.com/skills") {
|
|
6568
|
+
if (route === "/api/auth/whoami")
|
|
6569
|
+
return `${origin}/v1/auth/whoami`;
|
|
6570
|
+
if (!route.startsWith("/api/v1/")) {
|
|
6571
|
+
throw new SkillsFleetCredentialError("The internal Skills gateway has no established login contract yet. Select an explicitly configured instance with supported authentication.", "GATEWAY_AUTH_UNAVAILABLE");
|
|
6572
|
+
}
|
|
6573
|
+
return `${origin}${route.slice("/api".length)}`;
|
|
6574
|
+
}
|
|
6575
|
+
return `${origin}${route}`;
|
|
6576
|
+
}
|
|
6559
6577
|
function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
6560
6578
|
const declared = SKILLS_API_URL_ENV_KEYS.filter((key) => env[key] !== undefined).map((key) => ({ key, value: env[key] }));
|
|
6561
6579
|
for (const entry of declared) {
|
|
@@ -23273,6 +23291,186 @@ function hashBundleFiles(files) {
|
|
|
23273
23291
|
hash.update(part);
|
|
23274
23292
|
return hash.digest("hex");
|
|
23275
23293
|
}
|
|
23294
|
+
async function hashBundleFilesCooperatively(files, check2) {
|
|
23295
|
+
const hash = createHash(CONTENT_HASH_ALGORITHM);
|
|
23296
|
+
let bytesSinceYield = 0;
|
|
23297
|
+
for (const part of bundleHashParts(files)) {
|
|
23298
|
+
for (let offset = 0;offset < part.byteLength; offset += 64 * 1024) {
|
|
23299
|
+
check2();
|
|
23300
|
+
const chunk = part.subarray(offset, offset + 64 * 1024);
|
|
23301
|
+
hash.update(chunk);
|
|
23302
|
+
bytesSinceYield += chunk.byteLength;
|
|
23303
|
+
if (bytesSinceYield >= 256 * 1024) {
|
|
23304
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
23305
|
+
bytesSinceYield = 0;
|
|
23306
|
+
}
|
|
23307
|
+
}
|
|
23308
|
+
}
|
|
23309
|
+
check2();
|
|
23310
|
+
return hash.digest("hex");
|
|
23311
|
+
}
|
|
23312
|
+
function invalidContent(message = "Invalid content hash input") {
|
|
23313
|
+
throw new ContentHashInputError("CONTENT_HASH_INVALID", message);
|
|
23314
|
+
}
|
|
23315
|
+
function contentLimit(message) {
|
|
23316
|
+
throw new ContentHashInputError("CONTENT_HASH_LIMIT", message);
|
|
23317
|
+
}
|
|
23318
|
+
function contentRecord(value, allowed) {
|
|
23319
|
+
if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
23320
|
+
invalidContent();
|
|
23321
|
+
const result = Object.create(null);
|
|
23322
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
23323
|
+
if (typeof key !== "string" || !allowed.includes(key))
|
|
23324
|
+
invalidContent();
|
|
23325
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
23326
|
+
if (!descriptor || !("value" in descriptor))
|
|
23327
|
+
invalidContent("Accessor content hash input is unsupported");
|
|
23328
|
+
result[key] = descriptor.value;
|
|
23329
|
+
}
|
|
23330
|
+
return result;
|
|
23331
|
+
}
|
|
23332
|
+
function contentOptions(options) {
|
|
23333
|
+
const record3 = contentRecord(options, ["limits", "signal"]);
|
|
23334
|
+
const limits = { ...CONTENT_HASH_LIMITS };
|
|
23335
|
+
if (record3.limits !== undefined) {
|
|
23336
|
+
const supplied = contentRecord(record3.limits, Object.keys(limits));
|
|
23337
|
+
for (const key of Object.keys(supplied)) {
|
|
23338
|
+
const value = supplied[key];
|
|
23339
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > limits[key])
|
|
23340
|
+
contentLimit("Invalid content hash limit");
|
|
23341
|
+
limits[key] = value;
|
|
23342
|
+
}
|
|
23343
|
+
}
|
|
23344
|
+
if (record3.signal !== undefined && !(record3.signal instanceof AbortSignal))
|
|
23345
|
+
invalidContent("Invalid content hash signal");
|
|
23346
|
+
return { limits, signal: record3.signal };
|
|
23347
|
+
}
|
|
23348
|
+
function snapshotContentEntries(entries, limits, check2) {
|
|
23349
|
+
if (!Array.isArray(entries))
|
|
23350
|
+
invalidContent("Content hash entries must be an array");
|
|
23351
|
+
if (entries.length > limits.entries)
|
|
23352
|
+
contentLimit("Content hash entry limit exceeded");
|
|
23353
|
+
if (Reflect.ownKeys(entries).length !== entries.length + 1)
|
|
23354
|
+
invalidContent("Invalid content hash entry array");
|
|
23355
|
+
const snapshot = [];
|
|
23356
|
+
const paths = new SkillEntryPaths;
|
|
23357
|
+
let rawBytes = 0;
|
|
23358
|
+
for (let index = 0;index < entries.length; index++) {
|
|
23359
|
+
check2();
|
|
23360
|
+
const descriptor = Object.getOwnPropertyDescriptor(entries, String(index));
|
|
23361
|
+
if (!descriptor || !("value" in descriptor))
|
|
23362
|
+
invalidContent("Invalid content hash entry array");
|
|
23363
|
+
const entry = contentRecord(descriptor.value, ["path", "bytes", "mode"]);
|
|
23364
|
+
if (typeof entry.path !== "string" || typeof entry.mode !== "number" || !Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 511)
|
|
23365
|
+
invalidContent("Invalid regular-file content hash entry");
|
|
23366
|
+
paths.add(entry.path, limits.pathBytes, invalidContent, () => contentLimit("Content hash path limit exceeded"));
|
|
23367
|
+
if (!(entry.bytes instanceof Uint8Array) || !ArrayBuffer.isView(entry.bytes))
|
|
23368
|
+
invalidContent("Content hash entry requires bytes");
|
|
23369
|
+
const size = byteLengthOf.call(entry.bytes);
|
|
23370
|
+
if (!(bufferOf.call(entry.bytes) instanceof ArrayBuffer))
|
|
23371
|
+
invalidContent("Shared content hash bytes are unsupported");
|
|
23372
|
+
if (size > limits.fileBytes || rawBytes + size > limits.rawBytes)
|
|
23373
|
+
contentLimit("Content hash raw byte limit exceeded");
|
|
23374
|
+
if (entry.path === "skill.json" && size > limits.manifestBytes)
|
|
23375
|
+
contentLimit("Content hash manifest byte limit exceeded");
|
|
23376
|
+
rawBytes += size;
|
|
23377
|
+
const bytes = new Uint8Array(new ArrayBuffer(size));
|
|
23378
|
+
bytes.set(entry.bytes);
|
|
23379
|
+
snapshot.push({ path: entry.path, bytes, mode: entry.mode });
|
|
23380
|
+
}
|
|
23381
|
+
check2();
|
|
23382
|
+
return snapshot;
|
|
23383
|
+
}
|
|
23384
|
+
function coveredContentPath(path) {
|
|
23385
|
+
const segments = path.split("/");
|
|
23386
|
+
if (!HASH_COVERAGE.includes(segments[0]))
|
|
23387
|
+
return false;
|
|
23388
|
+
return !segments.slice(1).some((segment, index) => excludedHashEntry(segment, index < segments.length - 2));
|
|
23389
|
+
}
|
|
23390
|
+
function boundedManifest(raw, maxDepth) {
|
|
23391
|
+
let parsed;
|
|
23392
|
+
try {
|
|
23393
|
+
parsed = JSON.parse(raw);
|
|
23394
|
+
} catch {
|
|
23395
|
+
return;
|
|
23396
|
+
}
|
|
23397
|
+
const pending = [{ value: parsed, depth: 1 }];
|
|
23398
|
+
while (pending.length) {
|
|
23399
|
+
const { value, depth } = pending.pop();
|
|
23400
|
+
if (!value || typeof value !== "object")
|
|
23401
|
+
continue;
|
|
23402
|
+
if (depth > maxDepth)
|
|
23403
|
+
contentLimit("Content hash manifest depth limit exceeded");
|
|
23404
|
+
for (const child of Object.values(value))
|
|
23405
|
+
pending.push({ value: child, depth: depth + 1 });
|
|
23406
|
+
}
|
|
23407
|
+
return parsed;
|
|
23408
|
+
}
|
|
23409
|
+
async function hashContentEntries(entries, options) {
|
|
23410
|
+
const { limits, signal } = contentOptions(options);
|
|
23411
|
+
const deadline = performance.now() + limits.timeoutMs;
|
|
23412
|
+
let terminal;
|
|
23413
|
+
const abort = () => {
|
|
23414
|
+
terminal ??= new ContentHashInputError("CONTENT_HASH_ABORTED", "Content hashing aborted");
|
|
23415
|
+
};
|
|
23416
|
+
const timer = setTimeout(() => {
|
|
23417
|
+
terminal ??= new ContentHashInputError("CONTENT_HASH_TIMEOUT", "Content hashing deadline exceeded");
|
|
23418
|
+
}, limits.timeoutMs);
|
|
23419
|
+
const check2 = () => {
|
|
23420
|
+
if (signal?.aborted)
|
|
23421
|
+
abort();
|
|
23422
|
+
if (terminal)
|
|
23423
|
+
throw terminal;
|
|
23424
|
+
if (performance.now() >= deadline)
|
|
23425
|
+
throw new ContentHashInputError("CONTENT_HASH_TIMEOUT", "Content hashing deadline exceeded");
|
|
23426
|
+
};
|
|
23427
|
+
try {
|
|
23428
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
23429
|
+
check2();
|
|
23430
|
+
const snapshot = snapshotContentEntries(entries, limits, check2);
|
|
23431
|
+
const normalized = [];
|
|
23432
|
+
let normalizedBytes = 0;
|
|
23433
|
+
let manifest;
|
|
23434
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
23435
|
+
for (const entry of snapshot) {
|
|
23436
|
+
check2();
|
|
23437
|
+
if (!coveredContentPath(entry.path))
|
|
23438
|
+
continue;
|
|
23439
|
+
if (entry.path === "skill.json")
|
|
23440
|
+
manifest = boundedManifest(new TextDecoder().decode(entry.bytes), limits.manifestDepth);
|
|
23441
|
+
const file = normalizeBundleFile(entry.path, entry.bytes);
|
|
23442
|
+
check2();
|
|
23443
|
+
if (file.content.byteLength > limits.normalizedFileBytes || normalizedBytes + file.content.byteLength > limits.normalizedBytes)
|
|
23444
|
+
contentLimit("Content hash normalized byte limit exceeded");
|
|
23445
|
+
normalizedBytes += file.content.byteLength;
|
|
23446
|
+
normalized.push(file);
|
|
23447
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
23448
|
+
}
|
|
23449
|
+
normalized.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
23450
|
+
check2();
|
|
23451
|
+
return { hash: await hashBundleFilesCooperatively(normalized, check2), manifest };
|
|
23452
|
+
} catch (error2) {
|
|
23453
|
+
if (error2 instanceof ContentHashInputError)
|
|
23454
|
+
throw error2;
|
|
23455
|
+
throw new ContentHashInputError("CONTENT_HASH_INVALID", "Invalid content hash input");
|
|
23456
|
+
} finally {
|
|
23457
|
+
clearTimeout(timer);
|
|
23458
|
+
signal?.removeEventListener("abort", abort);
|
|
23459
|
+
}
|
|
23460
|
+
}
|
|
23461
|
+
async function verifyContentHashFromEntries(entries, options = {}) {
|
|
23462
|
+
const { hash, manifest } = await hashContentEntries(entries, options);
|
|
23463
|
+
const provenance = manifest && typeof manifest === "object" && !Array.isArray(manifest) ? manifest.provenance : undefined;
|
|
23464
|
+
const value = provenance && typeof provenance === "object" && !Array.isArray(provenance) ? provenance.content_hash : undefined;
|
|
23465
|
+
if (value !== undefined && typeof value !== "string")
|
|
23466
|
+
invalidContent("Invalid content hash declaration");
|
|
23467
|
+
const declaredHash = value?.trim() || undefined;
|
|
23468
|
+
if (!declaredHash)
|
|
23469
|
+
return { declared: false, valid: false };
|
|
23470
|
+
if (!/^[a-f0-9]{64}$/.test(declaredHash))
|
|
23471
|
+
return { declared: true, valid: false, declaredHash };
|
|
23472
|
+
return { declared: true, valid: hash === declaredHash, declaredHash, computedHash: hash };
|
|
23473
|
+
}
|
|
23276
23474
|
function verifyContentHash(skillPath, manifest) {
|
|
23277
23475
|
const declaredHash = manifest?.provenance?.content_hash?.trim() || undefined;
|
|
23278
23476
|
if (!declaredHash)
|
|
@@ -23288,7 +23486,7 @@ function verifyContentHash(skillPath, manifest) {
|
|
|
23288
23486
|
computedHash
|
|
23289
23487
|
};
|
|
23290
23488
|
}
|
|
23291
|
-
var CONTENT_HASH_ALGORITHM = "sha256", HASH_EXCLUDE_DIRS, HASH_COVERAGE, CONTENT_HASH_LIMITS, typedArrayPrototype, byteLengthOf, bufferOf;
|
|
23489
|
+
var CONTENT_HASH_ALGORITHM = "sha256", HASH_EXCLUDE_DIRS, HASH_COVERAGE, CONTENT_HASH_LIMITS, ContentHashInputError, typedArrayPrototype, byteLengthOf, bufferOf;
|
|
23292
23490
|
var init_skill_hash = __esm(() => {
|
|
23293
23491
|
HASH_EXCLUDE_DIRS = new Set([".git", "node_modules", "dist", "build", ".turbo"]);
|
|
23294
23492
|
HASH_COVERAGE = [
|
|
@@ -23313,6 +23511,14 @@ var init_skill_hash = __esm(() => {
|
|
|
23313
23511
|
manifestDepth: 64,
|
|
23314
23512
|
timeoutMs: 5000
|
|
23315
23513
|
});
|
|
23514
|
+
ContentHashInputError = class ContentHashInputError extends Error {
|
|
23515
|
+
code;
|
|
23516
|
+
constructor(code, message) {
|
|
23517
|
+
super(message);
|
|
23518
|
+
this.code = code;
|
|
23519
|
+
this.name = "ContentHashInputError";
|
|
23520
|
+
}
|
|
23521
|
+
};
|
|
23316
23522
|
typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
|
|
23317
23523
|
byteLengthOf = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength").get;
|
|
23318
23524
|
bufferOf = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer").get;
|
|
@@ -24898,7 +25104,14 @@ function getConfiguredApiUrl(env = process.env) {
|
|
|
24898
25104
|
}
|
|
24899
25105
|
function buildSkillsApiUrl(apiUrl, endpoint = "/skills") {
|
|
24900
25106
|
const url = new URL(apiUrl);
|
|
25107
|
+
if (url.origin === "https://api.hasna.com" && /^\/skills\/(?:api\/)?v1\/skills\/?$/.test(url.pathname)) {
|
|
25108
|
+
url.pathname = "/skills";
|
|
25109
|
+
apiUrl = url.toString();
|
|
25110
|
+
}
|
|
24901
25111
|
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
25112
|
+
if (normalizeSkillsApiOrigin(apiUrl) === "https://api.hasna.com/skills") {
|
|
25113
|
+
return skillsApiRequestUrl(apiUrl, `/api/v1${cleanEndpoint}`);
|
|
25114
|
+
}
|
|
24902
25115
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
24903
25116
|
const apiBase = /\/api(?:\/v1)?\/skills$/.test(pathname) ? pathname.slice(0, -"/skills".length) : pathname;
|
|
24904
25117
|
if (/\/api(?:\/v1)?$/.test(apiBase)) {
|
|
@@ -25800,7 +26013,7 @@ var MCP_CONTRACT_SCHEMA_VERSION = 1, stringSchema = (description) => ({
|
|
|
25800
26013
|
type: "array",
|
|
25801
26014
|
items,
|
|
25802
26015
|
...description ? { description } : {}
|
|
25803
|
-
}), skillNameInput, optionalAgentInput, scopeInput, runInputSchema, runArgsSchema, errorSchema, skillSummarySchema, toolPrimitiveSummarySchema, skillToolDependencySchema, validationMessageSchema, validationOutputSchema, installOutputSchema, runOutputSchema, toolContracts, remoteCustomerContracts, contracts, resourceContracts;
|
|
26016
|
+
}), skillNameInput, optionalAgentInput, scopeInput, runInputSchema, runArgsSchema, errorSchema, skillSummarySchema, toolPrimitiveSummarySchema, skillToolDependencySchema, validationMessageSchema, validationOutputSchema, installOutputSchema, runOutputSchema, toolContracts, remoteCustomerContracts, publicationUuidSchema, publicationVerification, privatePublicationContracts, contracts, resourceContracts;
|
|
25804
26017
|
var init_mcp_contracts = __esm(() => {
|
|
25805
26018
|
init_remote_customer_operations();
|
|
25806
26019
|
skillNameInput = stringSchema("skill name or alias.");
|
|
@@ -26306,7 +26519,7 @@ var init_mcp_contracts = __esm(() => {
|
|
|
26306
26519
|
name: "run_skill",
|
|
26307
26520
|
title: "Run Skill",
|
|
26308
26521
|
description: "Run a skill locally or through a configured remote runner. Returns compact stdout/stderr previews and run summaries by default; pass detail:true for full records.",
|
|
26309
|
-
params: ["name", "input?", "args?", "detail?", "remote?", "maxCredits?", "maxCostCents?", "idempotency_key?", "files?"],
|
|
26522
|
+
params: ["name", "input?", "args?", "detail?", "remote?", "maxCredits?", "maxCostCents?", "quoteReceipt?", "idempotency_key?", "files?"],
|
|
26310
26523
|
category: "execution",
|
|
26311
26524
|
sideEffects: "local-process-or-remote-run",
|
|
26312
26525
|
stable: true,
|
|
@@ -26318,6 +26531,7 @@ var init_mcp_contracts = __esm(() => {
|
|
|
26318
26531
|
remote: { type: "boolean", description: "Use the configured server catalog." },
|
|
26319
26532
|
maxCredits: { type: "integer", minimum: 0, description: "Maximum explicitly approved integer credits; omitted permits only free remote runs." },
|
|
26320
26533
|
maxCostCents: { type: "integer", minimum: 0, description: "Legacy alias for maxCredits; both must agree." },
|
|
26534
|
+
quoteReceipt: { type: "string", minLength: 1, maxLength: 4096, description: "Opaque approved quote receipt, at most 4096 UTF-8 bytes. Preserve it and the quoted input/args unchanged; never refresh after confirmation." },
|
|
26321
26535
|
idempotency_key: { type: "string", pattern: "^[A-Za-z0-9._:-]{1,128}$", description: "Stable retry key for the same remote submission." },
|
|
26322
26536
|
files: { type: "array", maxItems: 10, items: objectSchema({ name: stringSchema("Safe basename"), base64: { type: "string", maxLength: 1398104 }, contentType: stringSchema("MIME type") }, ["name", "base64"]), description: "Inline remote inputs, at most 1 MiB combined." }
|
|
26323
26537
|
}, ["name"]),
|
|
@@ -26601,12 +26815,17 @@ var init_mcp_contracts = __esm(() => {
|
|
|
26601
26815
|
name: "quote_skill",
|
|
26602
26816
|
title: "Quote Remote Skill",
|
|
26603
26817
|
description: "Get a server credit quote without submitting a run.",
|
|
26604
|
-
params: ["name", "input?", "args?"],
|
|
26818
|
+
params: ["name", "input?", "args?", "files?"],
|
|
26605
26819
|
category: "execution",
|
|
26606
26820
|
sideEffects: "none",
|
|
26607
26821
|
stable: true,
|
|
26608
|
-
inputSchema: objectSchema({
|
|
26609
|
-
|
|
26822
|
+
inputSchema: objectSchema({
|
|
26823
|
+
name: skillNameInput,
|
|
26824
|
+
input: runInputSchema,
|
|
26825
|
+
args: runArgsSchema,
|
|
26826
|
+
files: { type: "array", maxItems: 10, items: objectSchema({ name: stringSchema("Safe basename"), base64: { type: "string", maxLength: 1398104 }, contentType: stringSchema("MIME type") }, ["name", "base64"]), description: "Same inline files to submit after approval, at most 1 MiB combined." }
|
|
26827
|
+
}, ["name"]),
|
|
26828
|
+
outputSchema: objectSchema({ skill: stringSchema("Canonical server skill."), pricing: objectSchema({}, [], "Quoted integer credits.", true), quoteReceipt: { type: "string", minLength: 1, maxLength: 4096, description: "Opaque server quote binding, at most 4096 UTF-8 bytes; preserve verbatim for approval." } }, ["skill", "pricing"], undefined, true)
|
|
26610
26829
|
});
|
|
26611
26830
|
remoteCustomerContracts.push({
|
|
26612
26831
|
name: "download_run_artifact",
|
|
@@ -26619,7 +26838,47 @@ var init_mcp_contracts = __esm(() => {
|
|
|
26619
26838
|
inputSchema: objectSchema({ run_id: stringSchema("Run identifier."), artifact_id: stringSchema("Artifact identifier.") }, ["run_id", "artifact_id"]),
|
|
26620
26839
|
outputSchema: objectSchema({ id: stringSchema("Artifact identifier."), fileName: stringSchema("Artifact file name."), base64: stringSchema("Verified bytes."), sha256: stringSchema("SHA256 digest."), byteSize: { type: "integer", minimum: 0 } }, ["id", "fileName", "base64", "sha256", "byteSize"])
|
|
26621
26840
|
});
|
|
26622
|
-
|
|
26841
|
+
publicationUuidSchema = { type: "string", pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" };
|
|
26842
|
+
publicationVerification = {
|
|
26843
|
+
email: { type: "string", format: "email", maxLength: 254 },
|
|
26844
|
+
code: { type: "string", pattern: "^\\d{6}$" },
|
|
26845
|
+
userId: publicationUuidSchema,
|
|
26846
|
+
membershipId: publicationUuidSchema,
|
|
26847
|
+
recoveryDirectory: { type: "string", maxLength: 4096, description: "Absolute host-local recovery directory without symbolic links." }
|
|
26848
|
+
};
|
|
26849
|
+
privatePublicationContracts = [
|
|
26850
|
+
{ name: "publish_private_skill", title: "Publish private skill", extras: {
|
|
26851
|
+
directory: { type: "string", maxLength: 4096, description: "Absolute local skill source directory." },
|
|
26852
|
+
skillId: publicationUuidSchema,
|
|
26853
|
+
expectedCurrentVersionId: { oneOf: [publicationUuidSchema, { type: "null" }] },
|
|
26854
|
+
idempotencyKey: publicationUuidSchema,
|
|
26855
|
+
confirm: { const: true },
|
|
26856
|
+
waitMs: { type: "integer", minimum: 0, maximum: 300000 }
|
|
26857
|
+
}, required: ["directory", "skillId", "expectedCurrentVersionId", "confirm"] },
|
|
26858
|
+
{ name: "get_private_publication", title: "Get private publication", extras: {}, required: [] },
|
|
26859
|
+
{ name: "resume_private_publication", title: "Resume private publication", extras: { confirm: { const: true }, waitMs: { type: "integer", minimum: 0, maximum: 300000 } }, required: ["confirm"] },
|
|
26860
|
+
{ name: "cancel_private_publication", title: "Cancel private publication", extras: { confirm: { const: true } }, required: ["confirm"] }
|
|
26861
|
+
].map((operation) => ({
|
|
26862
|
+
name: operation.name,
|
|
26863
|
+
title: operation.title,
|
|
26864
|
+
description: "Manage private source publication with fresh workspace verification and durable host-local recovery. Upload consent and current version comparison are explicit; private execution remains unavailable.",
|
|
26865
|
+
params: [...Object.keys(publicationVerification), ...Object.keys(operation.extras)],
|
|
26866
|
+
category: "storage",
|
|
26867
|
+
sideEffects: "filesystem",
|
|
26868
|
+
stable: true,
|
|
26869
|
+
inputSchema: objectSchema({ ...publicationVerification, ...operation.extras }, [...Object.keys(publicationVerification), ...operation.required]),
|
|
26870
|
+
outputSchema: objectSchema({
|
|
26871
|
+
recoveryDirectory: { type: "string" },
|
|
26872
|
+
skillId: publicationUuidSchema,
|
|
26873
|
+
intentId: { oneOf: [publicationUuidSchema, { type: "null" }] },
|
|
26874
|
+
state: { type: "string" },
|
|
26875
|
+
versionId: { oneOf: [publicationUuidSchema, { type: "null" }] },
|
|
26876
|
+
committed: { type: "boolean" },
|
|
26877
|
+
executionEnabled: { const: false },
|
|
26878
|
+
nextAction: { type: "string" }
|
|
26879
|
+
}, ["recoveryDirectory", "skillId", "intentId", "state", "versionId", "committed", "executionEnabled", "nextAction"])
|
|
26880
|
+
}));
|
|
26881
|
+
contracts = [...toolContracts, ...remoteCustomerContracts, ...privatePublicationContracts].sort((a, b) => a.name.localeCompare(b.name));
|
|
26623
26882
|
resourceContracts = [
|
|
26624
26883
|
{
|
|
26625
26884
|
uri: "skills://mcp/contracts",
|
|
@@ -28321,8 +28580,17 @@ function creditCount(value) {
|
|
|
28321
28580
|
}
|
|
28322
28581
|
return value;
|
|
28323
28582
|
}
|
|
28583
|
+
function runQuoteReceipt(value) {
|
|
28584
|
+
if (value === undefined)
|
|
28585
|
+
return;
|
|
28586
|
+
if (typeof value !== "string" || !value.length || Buffer.byteLength(value, "utf8") > 4096) {
|
|
28587
|
+
throw new Error("Invalid quote receipt");
|
|
28588
|
+
}
|
|
28589
|
+
return value;
|
|
28590
|
+
}
|
|
28324
28591
|
function parseRemoteRunQuote(value) {
|
|
28325
28592
|
const quote = object4(value);
|
|
28593
|
+
runQuoteReceipt(quote.quoteReceipt);
|
|
28326
28594
|
const pricing = object4(quote.pricing);
|
|
28327
28595
|
if (typeof quote.skill !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(quote.skill))
|
|
28328
28596
|
throw new Error("Invalid quoted skill");
|
|
@@ -28424,6 +28692,72 @@ function parseUpdatedWorkspace(value) {
|
|
|
28424
28692
|
return { organization: { id: organization2.id, slug: organization2.slug, name: organization2.name } };
|
|
28425
28693
|
}
|
|
28426
28694
|
|
|
28695
|
+
// src/lib/remote-quote-errors.ts
|
|
28696
|
+
async function readQuoteUnavailableCode(response) {
|
|
28697
|
+
const maximum = 16 * 1024;
|
|
28698
|
+
const length = response.headers.get("content-length");
|
|
28699
|
+
if (response.status !== 503 || response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json" || length !== null && (!/^\d+$/.test(length) || Number(length) > maximum)) {
|
|
28700
|
+
response.body?.cancel().catch(() => {});
|
|
28701
|
+
return null;
|
|
28702
|
+
}
|
|
28703
|
+
const reader = response.body?.getReader();
|
|
28704
|
+
if (!reader)
|
|
28705
|
+
return null;
|
|
28706
|
+
let timer;
|
|
28707
|
+
let deadlineExceeded = false;
|
|
28708
|
+
const expired = Symbol("quote body deadline");
|
|
28709
|
+
const deadline = new Promise((resolve2) => {
|
|
28710
|
+
timer = setTimeout(() => {
|
|
28711
|
+
deadlineExceeded = true;
|
|
28712
|
+
resolve2(expired);
|
|
28713
|
+
reader.cancel().catch(() => {});
|
|
28714
|
+
}, 1500);
|
|
28715
|
+
});
|
|
28716
|
+
const chunks = [];
|
|
28717
|
+
let size = 0;
|
|
28718
|
+
try {
|
|
28719
|
+
while (true) {
|
|
28720
|
+
const next = await Promise.race([reader.read(), deadline]);
|
|
28721
|
+
if (next === expired || deadlineExceeded)
|
|
28722
|
+
return null;
|
|
28723
|
+
if (next.done)
|
|
28724
|
+
break;
|
|
28725
|
+
size += next.value.byteLength;
|
|
28726
|
+
if (size > maximum)
|
|
28727
|
+
return null;
|
|
28728
|
+
chunks.push(next.value);
|
|
28729
|
+
}
|
|
28730
|
+
const bytes = new Uint8Array(size);
|
|
28731
|
+
let offset = 0;
|
|
28732
|
+
for (const chunk of chunks) {
|
|
28733
|
+
bytes.set(chunk, offset);
|
|
28734
|
+
offset += chunk.byteLength;
|
|
28735
|
+
}
|
|
28736
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
28737
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
28738
|
+
return null;
|
|
28739
|
+
const code = value.code;
|
|
28740
|
+
return typeof code === "string" && Object.hasOwn(quoteUnavailableMessages, code) ? code : null;
|
|
28741
|
+
} catch {
|
|
28742
|
+
return null;
|
|
28743
|
+
} finally {
|
|
28744
|
+
clearTimeout(timer);
|
|
28745
|
+
reader.cancel().catch(() => {});
|
|
28746
|
+
reader.releaseLock();
|
|
28747
|
+
}
|
|
28748
|
+
}
|
|
28749
|
+
var quoteUnavailableMessages;
|
|
28750
|
+
var init_remote_quote_errors = __esm(() => {
|
|
28751
|
+
quoteUnavailableMessages = Object.freeze({
|
|
28752
|
+
HOSTED_PROVIDER_UNAVAILABLE: "Hosted execution is temporarily unavailable on this Skills instance.",
|
|
28753
|
+
HOSTED_CONNECTORS_UNAVAILABLE: "Hosted connector execution is unavailable on this Skills instance.",
|
|
28754
|
+
SKILL_IMPLEMENTATION_UNAVAILABLE: "This skill has no hosted execution implementation.",
|
|
28755
|
+
HOSTED_PRICING_UNAVAILABLE: "Hosted execution is unavailable while this skill's pricing is reviewed.",
|
|
28756
|
+
RUNTIME_ALLOWLIST_REQUIRED: "Hosted execution is unavailable until this Skills instance enables its skill catalog.",
|
|
28757
|
+
RUNTIME_SKILL_NOT_ALLOWED: "This skill is not enabled for hosted execution on this Skills instance."
|
|
28758
|
+
});
|
|
28759
|
+
});
|
|
28760
|
+
|
|
28427
28761
|
// src/lib/remote-client.ts
|
|
28428
28762
|
var exports_remote_client = {};
|
|
28429
28763
|
__export(exports_remote_client, {
|
|
@@ -28434,6 +28768,7 @@ __export(exports_remote_client, {
|
|
|
28434
28768
|
RemoteSkillsClient: () => RemoteSkillsClient,
|
|
28435
28769
|
RemoteRouteUnsupportedError: () => RemoteRouteUnsupportedError,
|
|
28436
28770
|
RemoteRequestError: () => RemoteRequestError,
|
|
28771
|
+
RemoteQuoteUnavailableError: () => RemoteQuoteUnavailableError,
|
|
28437
28772
|
RemoteCapabilityUnavailableError: () => RemoteCapabilityUnavailableError
|
|
28438
28773
|
});
|
|
28439
28774
|
|
|
@@ -28446,7 +28781,7 @@ class RemoteSkillsClient {
|
|
|
28446
28781
|
this.apiUrl = normalizeSkillsApiOrigin(apiUrl);
|
|
28447
28782
|
}
|
|
28448
28783
|
async request(path, options) {
|
|
28449
|
-
return fetch(
|
|
28784
|
+
return fetch(skillsApiRequestUrl(this.apiUrl, path), {
|
|
28450
28785
|
...options,
|
|
28451
28786
|
redirect: "error",
|
|
28452
28787
|
credentials: "omit",
|
|
@@ -28469,6 +28804,11 @@ class RemoteSkillsClient {
|
|
|
28469
28804
|
throw new RemoteRouteUnsupportedError(routePath, response.status, this.apiUrl);
|
|
28470
28805
|
}
|
|
28471
28806
|
if (!response.ok) {
|
|
28807
|
+
if (opts.quoteRefusal && options?.method === "POST" && /^\/api\/v1\/skills\/[^/?#]+\/quote$/.test(routePath) && response.status === 503) {
|
|
28808
|
+
const code = await readQuoteUnavailableCode(response);
|
|
28809
|
+
if (code)
|
|
28810
|
+
throw new RemoteQuoteUnavailableError(routePath, code);
|
|
28811
|
+
}
|
|
28472
28812
|
if (path === "/api/v1/billing/checkout" && options?.method === "POST" && response.status === 503 && await responseBodyCarriesCode(response, ["SUBSCRIPTION_CHECKOUT_UNAVAILABLE"])) {
|
|
28473
28813
|
throw new RemoteCapabilityUnavailableError;
|
|
28474
28814
|
}
|
|
@@ -28501,6 +28841,7 @@ class RemoteSkillsClient {
|
|
|
28501
28841
|
return { status: res.status, body };
|
|
28502
28842
|
}
|
|
28503
28843
|
async submitRun(slug, input, args, approval = {}) {
|
|
28844
|
+
const quoteReceipt = runQuoteReceipt(approval.quoteReceipt);
|
|
28504
28845
|
if (approval.idempotencyKey !== undefined && !/^[A-Za-z0-9._:-]{1,128}$/.test(approval.idempotencyKey))
|
|
28505
28846
|
throw new Error("Idempotency key must be 1-128 URL-safe characters");
|
|
28506
28847
|
if (approval.maxCostCents !== undefined)
|
|
@@ -28517,16 +28858,21 @@ class RemoteSkillsClient {
|
|
|
28517
28858
|
...approval.maxCredits !== undefined ? { maxCredits: approval.maxCredits } : {},
|
|
28518
28859
|
...approval.maxCostCents !== undefined ? { maxCostCents: approval.maxCostCents } : {},
|
|
28519
28860
|
...approval.idempotencyKey !== undefined ? { idempotencyKey: approval.idempotencyKey } : {},
|
|
28520
|
-
...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {}
|
|
28861
|
+
...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {},
|
|
28862
|
+
...quoteReceipt !== undefined ? { quoteReceipt } : {}
|
|
28521
28863
|
})
|
|
28522
28864
|
});
|
|
28865
|
+
if (!res.ok) {
|
|
28866
|
+
res.body?.cancel().catch(() => {});
|
|
28867
|
+
throw new RemoteRequestError(`/api/v1/runs/${encodeURIComponent(slug)}`, res.status);
|
|
28868
|
+
}
|
|
28523
28869
|
return normalizeRemoteSkillRunContract(await res.json(), slug);
|
|
28524
28870
|
}
|
|
28525
|
-
async quoteRun(slug, input = {}, args = []) {
|
|
28871
|
+
async quoteRun(slug, input = {}, args = [], files) {
|
|
28526
28872
|
const response = await this.requestNewRoute(`/api/v1/skills/${encodeURIComponent(slug)}/quote`, {
|
|
28527
28873
|
method: "POST",
|
|
28528
|
-
body: JSON.stringify({ input, args })
|
|
28529
|
-
});
|
|
28874
|
+
body: JSON.stringify({ input, args, ...files === undefined ? {} : { files } })
|
|
28875
|
+
}, { quoteRefusal: true });
|
|
28530
28876
|
return parseRemoteRunQuote(await response.json());
|
|
28531
28877
|
}
|
|
28532
28878
|
getCapabilities() {
|
|
@@ -28541,17 +28887,24 @@ class RemoteSkillsClient {
|
|
|
28541
28887
|
return this.capabilities;
|
|
28542
28888
|
}
|
|
28543
28889
|
async submitQuotedRun(slug, input = {}, args = [], approval = {}) {
|
|
28890
|
+
runQuoteReceipt(approval.quoteReceipt);
|
|
28891
|
+
({ input, args, approval } = JSON.parse(JSON.stringify({ input, args, approval })));
|
|
28544
28892
|
const maximum = creditCount(approval.maxCredits ?? approval.maxCostCents ?? 0);
|
|
28545
28893
|
if (approval.maxCostCents !== undefined && approval.maxCostCents !== maximum)
|
|
28546
28894
|
throw new Error("Credit approval fields disagree");
|
|
28547
|
-
const quote = await this.quoteRun(slug, input, args);
|
|
28548
|
-
if (quote.pricing.costCents > maximum)
|
|
28895
|
+
const quote = approval.quoteReceipt === undefined ? await this.quoteRun(slug, input, args, approval.inputFiles?.length ? approval.inputFiles : undefined) : undefined;
|
|
28896
|
+
if (quote && quote.pricing.costCents > maximum)
|
|
28549
28897
|
throw new RemoteCreditApprovalError(quote.pricing.costCents, maximum);
|
|
28550
28898
|
const capabilities = await this.getCapabilities();
|
|
28551
28899
|
if (!capabilities.capabilities.includes("runs.submit") || capabilities.billing?.boundedRunApproval !== true || capabilities.billing.unit !== "credits") {
|
|
28552
28900
|
throw new Error("The configured server does not support bounded credit approval; refusing remote submission");
|
|
28553
28901
|
}
|
|
28554
|
-
return this.submitRun(quote
|
|
28902
|
+
return this.submitRun(quote?.skill ?? slug, input, args, {
|
|
28903
|
+
...approval,
|
|
28904
|
+
maxCredits: maximum,
|
|
28905
|
+
maxCostCents: maximum,
|
|
28906
|
+
...(quote?.quoteReceipt ?? approval.quoteReceipt) === undefined ? {} : { quoteReceipt: quote?.quoteReceipt ?? approval.quoteReceipt }
|
|
28907
|
+
});
|
|
28555
28908
|
}
|
|
28556
28909
|
async getIdentity() {
|
|
28557
28910
|
return (await this.requestNewRoute("/api/auth/whoami")).json();
|
|
@@ -28839,6 +29192,10 @@ class RemoteSkillsClient {
|
|
|
28839
29192
|
return { id: artifactId, fileName: String(artifact.fileName ?? artifactId), bytes, byteSize: bytes.byteLength, sha256: artifact.sha256 };
|
|
28840
29193
|
}
|
|
28841
29194
|
async submitQuotedRunWithFiles(slug, input, args, files, approval = {}) {
|
|
29195
|
+
runQuoteReceipt(approval.quoteReceipt);
|
|
29196
|
+
({ input, args, approval } = JSON.parse(JSON.stringify({ input, args, approval })));
|
|
29197
|
+
describeRemoteFiles(files);
|
|
29198
|
+
files = files.map((file) => ({ name: file.name, contentType: file.contentType, bytes: new Uint8Array(file.bytes) }));
|
|
28842
29199
|
const inputFiles = describeRemoteFiles(files);
|
|
28843
29200
|
if (files.length && !(await this.getCapabilities()).capabilities.includes("runs.uploads"))
|
|
28844
29201
|
throw new Error("The configured server does not support input uploads");
|
|
@@ -28902,7 +29259,7 @@ class RemoteSkillsClient {
|
|
|
28902
29259
|
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
28903
29260
|
if (ifMatch)
|
|
28904
29261
|
headers["If-Match"] = ifMatch;
|
|
28905
|
-
return fetch(
|
|
29262
|
+
return fetch(skillsApiRequestUrl(this.apiUrl, "/api/v1/skills"), {
|
|
28906
29263
|
method: "POST",
|
|
28907
29264
|
headers,
|
|
28908
29265
|
body: form,
|
|
@@ -29125,7 +29482,7 @@ async function createRemoteSkillsClient(env = process.env) {
|
|
|
29125
29482
|
function createRemoteSkillsClientReadOnly(env = process.env) {
|
|
29126
29483
|
return createRemoteSkillsClient(env);
|
|
29127
29484
|
}
|
|
29128
|
-
var RemoteRouteUnsupportedError, RemoteRequestError, RemoteWorkspaceMemberError, RemoteWorkspaceSelectionError, RemoteCapabilityUnavailableError, INVALID_SKILL_VERSION_RESPONSE = "Remote skill version payload did not match the expected contract.";
|
|
29485
|
+
var RemoteRouteUnsupportedError, RemoteRequestError, RemoteQuoteUnavailableError, RemoteWorkspaceMemberError, RemoteWorkspaceSelectionError, RemoteCapabilityUnavailableError, INVALID_SKILL_VERSION_RESPONSE = "Remote skill version payload did not match the expected contract.";
|
|
29129
29486
|
var init_remote_client = __esm(() => {
|
|
29130
29487
|
init_remote_invitations();
|
|
29131
29488
|
init_remote_workspace_leave();
|
|
@@ -29136,6 +29493,7 @@ var init_remote_client = __esm(() => {
|
|
|
29136
29493
|
init_fleet_credentials();
|
|
29137
29494
|
init_remote_account();
|
|
29138
29495
|
init_remote_files();
|
|
29496
|
+
init_remote_quote_errors();
|
|
29139
29497
|
RemoteRouteUnsupportedError = class RemoteRouteUnsupportedError extends Error {
|
|
29140
29498
|
path;
|
|
29141
29499
|
status;
|
|
@@ -29158,6 +29516,17 @@ var init_remote_client = __esm(() => {
|
|
|
29158
29516
|
this.name = "RemoteRequestError";
|
|
29159
29517
|
}
|
|
29160
29518
|
};
|
|
29519
|
+
RemoteQuoteUnavailableError = class RemoteQuoteUnavailableError extends RemoteRequestError {
|
|
29520
|
+
code;
|
|
29521
|
+
constructor(path, code) {
|
|
29522
|
+
super(path, 503);
|
|
29523
|
+
this.code = code;
|
|
29524
|
+
if (!Object.hasOwn(quoteUnavailableMessages, code))
|
|
29525
|
+
throw new Error("Unknown quote refusal code");
|
|
29526
|
+
this.name = "RemoteQuoteUnavailableError";
|
|
29527
|
+
this.message = quoteUnavailableMessages[code];
|
|
29528
|
+
}
|
|
29529
|
+
};
|
|
29161
29530
|
RemoteWorkspaceMemberError = class RemoteWorkspaceMemberError extends RemoteRequestError {
|
|
29162
29531
|
code;
|
|
29163
29532
|
constructor(path, code) {
|
|
@@ -29395,11 +29764,12 @@ function registerOperationTools(server) {
|
|
|
29395
29764
|
detail: exports_external.boolean().optional(),
|
|
29396
29765
|
maxCostCents: exports_external.number().int().min(0).max(2147483647).optional().describe("Maximum integer credits explicitly approved by the user for a remote run; omitted permits only free runs"),
|
|
29397
29766
|
maxCredits: exports_external.number().int().min(0).max(2147483647).optional(),
|
|
29767
|
+
quoteReceipt: exports_external.string().min(1).max(4096).refine((value) => Buffer.byteLength(value, "utf8") <= 4096, "Quote receipt exceeds 4096 UTF-8 bytes").optional().describe("Opaque receipt from the approved quote; send unchanged with the same input, args and files. Never refresh after confirmation."),
|
|
29398
29768
|
remote: exports_external.boolean().optional().describe("Use the configured server catalog, including skills not installed locally"),
|
|
29399
29769
|
idempotency_key: exports_external.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe("Reuse for the same approved submission after an interrupted response"),
|
|
29400
29770
|
files: exports_external.array(exports_external.object({ name: exports_external.string(), base64: exports_external.string().max(1398104), contentType: exports_external.string().optional() })).max(10).optional().describe("Inline remote inputs, at most 1 MiB combined; use CLI or SDK for larger files")
|
|
29401
29771
|
}
|
|
29402
|
-
}, async ({ name, input, args, detail, maxCostCents, maxCredits, remote, idempotency_key, files }) => {
|
|
29772
|
+
}, async ({ name, input, args, detail, maxCostCents, maxCredits, quoteReceipt, remote, idempotency_key, files }) => {
|
|
29403
29773
|
const skill = remote ? { name, serverOwned: true } : getSkill(name);
|
|
29404
29774
|
if (!skill) {
|
|
29405
29775
|
return mcpError("SKILL_NOT_FOUND", `Skill '${name}' not found`, findSimilarSkills(name));
|
|
@@ -29440,7 +29810,7 @@ function registerOperationTools(server) {
|
|
|
29440
29810
|
try {
|
|
29441
29811
|
const { RemoteSkillsClient: RemoteSkillsClient2 } = await Promise.resolve().then(() => (init_remote_client(), exports_remote_client));
|
|
29442
29812
|
const client = new RemoteSkillsClient2(routing.apiKey, routing.apiOrigin);
|
|
29443
|
-
const run = await client.submitQuotedRunWithFiles(skillName, runInput, runArgs, inputFiles, { maxCredits, maxCostCents, idempotencyKey: idempotency_key ?? runContext.record.id });
|
|
29813
|
+
const run = await client.submitQuotedRunWithFiles(skillName, runInput, runArgs, inputFiles, { maxCredits, maxCostCents, quoteReceipt, idempotencyKey: idempotency_key ?? runContext.record.id });
|
|
29444
29814
|
if (run.error) {
|
|
29445
29815
|
writeRunLogs(runContext, "", String(run.error) + `
|
|
29446
29816
|
`);
|
|
@@ -30434,7 +30804,7 @@ async function requestInvitationEmail(origin, action, input) {
|
|
|
30434
30804
|
}
|
|
30435
30805
|
const body = JSON.stringify({ invitationId: value.invitationId, token: value.token, challengeId: value.challengeId, ...action === "accept" ? { code: value.code } : {} });
|
|
30436
30806
|
try {
|
|
30437
|
-
const response = await fetch(
|
|
30807
|
+
const response = await fetch(skillsApiRequestUrl(target, `/api/v1/account/invitations/email-${action}`), {
|
|
30438
30808
|
method: "POST",
|
|
30439
30809
|
headers: { "Content-Type": "application/json" },
|
|
30440
30810
|
body,
|
|
@@ -30509,14 +30879,318 @@ var init_remote_invitation_recovery = __esm(() => {
|
|
|
30509
30879
|
};
|
|
30510
30880
|
});
|
|
30511
30881
|
|
|
30882
|
+
// src/lib/remote-private-publications.ts
|
|
30883
|
+
import { createHash as createHash5 } from "crypto";
|
|
30884
|
+
function checkedPublicationDeclaration(value) {
|
|
30885
|
+
if (!exact(value, ["idempotencyKey", "version", "expectedCurrentVersionId", "manifestText", "archiveSha256", "archiveByteSize"]) || !publicationUuid(value.idempotencyKey) || !(value.expectedCurrentVersionId === null || publicationUuid(value.expectedCurrentVersionId)) || typeof value.version !== "string" || !value.version || value.version.length > 128 || /[\p{Cc}\p{Cs}]/u.test(value.version) || typeof value.manifestText !== "string" || Buffer.byteLength(value.manifestText) > 16384 || !hash(value.archiveSha256) || !Number.isSafeInteger(value.archiveByteSize) || Number(value.archiveByteSize) < 1 || Number(value.archiveByteSize) > PRIVATE_PUBLICATION_MAX_BYTES)
|
|
30886
|
+
return bad();
|
|
30887
|
+
try {
|
|
30888
|
+
const manifest = JSON.parse(value.manifestText);
|
|
30889
|
+
if (!record7(manifest) || manifest.version !== value.version || validatePortableManifestContract(manifest, { strict: true }).length || !hash(manifest.provenance?.content_hash))
|
|
30890
|
+
return bad();
|
|
30891
|
+
} catch {
|
|
30892
|
+
return bad();
|
|
30893
|
+
}
|
|
30894
|
+
return Object.freeze({ ...value });
|
|
30895
|
+
}
|
|
30896
|
+
function checkedPublicationView(value, skillId, intentId, declaration) {
|
|
30897
|
+
if (!exact(value, ["id", "skillId", "version", "expectedCurrentVersionId", "archiveSha256", "archiveByteSize", "state", "expiresAt", "createdAt", "versionId"]) || !publicationUuid(value.id) || value.skillId !== skillId || intentId !== undefined && value.id !== intentId || typeof value.version !== "string" || !value.version || value.version.length > 128 || /[\p{Cc}\p{Cs}]/u.test(value.version) || !(value.expectedCurrentVersionId === null || publicationUuid(value.expectedCurrentVersionId)) || !hash(value.archiveSha256) || !Number.isSafeInteger(value.archiveByteSize) || Number(value.archiveByteSize) < 1 || Number(value.archiveByteSize) > PRIVATE_PUBLICATION_MAX_BYTES || typeof value.state !== "string" || !["awaiting_upload", "queued", "verifying", "needs_attention", "committed", "rejected", "cancelled", "expired"].includes(value.state) || !date4(value.expiresAt) || !date4(value.createdAt) || !(value.versionId === null || publicationUuid(value.versionId)) || value.state === "committed" && value.versionId === null)
|
|
30898
|
+
return invalid3();
|
|
30899
|
+
if (declaration && ["version", "expectedCurrentVersionId", "archiveSha256", "archiveByteSize"].some((k) => value[k] !== declaration[k]))
|
|
30900
|
+
return invalid3();
|
|
30901
|
+
return Object.freeze({ ...value });
|
|
30902
|
+
}
|
|
30903
|
+
async function boundedJson(url, init, token, mutation, budget = 15000, signal) {
|
|
30904
|
+
const controller = new AbortController;
|
|
30905
|
+
let reader;
|
|
30906
|
+
let response;
|
|
30907
|
+
const timeout = new PrivatePublicationError("PUBLICATION_UNCONFIRMED", "No confirmed publication result. Inspect the saved intent before retrying.", mutation);
|
|
30908
|
+
let timer;
|
|
30909
|
+
let abort;
|
|
30910
|
+
try {
|
|
30911
|
+
return await Promise.race([(async () => {
|
|
30912
|
+
if (signal?.aborted)
|
|
30913
|
+
throw timeout;
|
|
30914
|
+
response = await fetch(url, {
|
|
30915
|
+
...init,
|
|
30916
|
+
redirect: "error",
|
|
30917
|
+
credentials: "omit",
|
|
30918
|
+
signal: controller.signal,
|
|
30919
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }
|
|
30920
|
+
});
|
|
30921
|
+
const length = response.headers.get("content-length");
|
|
30922
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) > 65536))
|
|
30923
|
+
return invalid3();
|
|
30924
|
+
reader = response.body?.getReader();
|
|
30925
|
+
const chunks = [];
|
|
30926
|
+
let size = 0;
|
|
30927
|
+
if (reader)
|
|
30928
|
+
while (true) {
|
|
30929
|
+
const part = await reader.read();
|
|
30930
|
+
if (part.done)
|
|
30931
|
+
break;
|
|
30932
|
+
size += part.value.byteLength;
|
|
30933
|
+
if (size > 65536)
|
|
30934
|
+
return invalid3();
|
|
30935
|
+
chunks.push(part.value);
|
|
30936
|
+
}
|
|
30937
|
+
if (controller.signal.aborted)
|
|
30938
|
+
throw timeout;
|
|
30939
|
+
const bytes = Buffer.concat(chunks);
|
|
30940
|
+
let body;
|
|
30941
|
+
try {
|
|
30942
|
+
body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
30943
|
+
} catch {
|
|
30944
|
+
return invalid3();
|
|
30945
|
+
}
|
|
30946
|
+
if (!response.ok) {
|
|
30947
|
+
const code = record7(body) && typeof body.code === "string" && Object.hasOwn(failures, body.code) ? body.code : null;
|
|
30948
|
+
if (code && failures[code][0] === response.status)
|
|
30949
|
+
throw new PrivatePublicationError(code, failures[code][1], mutation && code === "PUBLICATION_UNCERTAIN", response.status);
|
|
30950
|
+
throw new PrivatePublicationError("PUBLICATION_REQUEST_FAILED", "The publication request was refused. Inspect its status before retrying.", mutation && response.status >= 500, response.status);
|
|
30951
|
+
}
|
|
30952
|
+
return body;
|
|
30953
|
+
})(), new Promise((_, reject) => {
|
|
30954
|
+
abort = () => {
|
|
30955
|
+
controller.abort();
|
|
30956
|
+
reject(timeout);
|
|
30957
|
+
};
|
|
30958
|
+
timer = setTimeout(abort, Math.max(1, Math.min(15000, budget)));
|
|
30959
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
30960
|
+
})]);
|
|
30961
|
+
} catch (error2) {
|
|
30962
|
+
if (error2 instanceof PrivatePublicationError) {
|
|
30963
|
+
if (mutation && error2.code === "INVALID_PUBLICATION_RESPONSE")
|
|
30964
|
+
throw timeout;
|
|
30965
|
+
throw error2;
|
|
30966
|
+
}
|
|
30967
|
+
throw timeout;
|
|
30968
|
+
} finally {
|
|
30969
|
+
if (timer)
|
|
30970
|
+
clearTimeout(timer);
|
|
30971
|
+
if (abort)
|
|
30972
|
+
signal?.removeEventListener("abort", abort);
|
|
30973
|
+
controller.abort();
|
|
30974
|
+
if (reader)
|
|
30975
|
+
reader.cancel().catch(() => {});
|
|
30976
|
+
else
|
|
30977
|
+
response?.body?.cancel().catch(() => {});
|
|
30978
|
+
}
|
|
30979
|
+
}
|
|
30980
|
+
|
|
30981
|
+
class RemotePrivatePublicationsClient {
|
|
30982
|
+
apiOrigin;
|
|
30983
|
+
organizationId;
|
|
30984
|
+
userId;
|
|
30985
|
+
membershipId;
|
|
30986
|
+
#token;
|
|
30987
|
+
constructor(apiUrl, session) {
|
|
30988
|
+
const checked = parseWorkspaceSession(session, { userId: session?.user?.id, membershipId: session?.user?.membershipId });
|
|
30989
|
+
if (checked.user.role === "viewer")
|
|
30990
|
+
throw new PrivatePublicationError("PUBLICATION_FORBIDDEN", failures.PUBLICATION_FORBIDDEN[1]);
|
|
30991
|
+
this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
|
|
30992
|
+
this.#token = checked.token;
|
|
30993
|
+
this.organizationId = checked.organization.id;
|
|
30994
|
+
this.userId = checked.user.id;
|
|
30995
|
+
this.membershipId = checked.user.membershipId;
|
|
30996
|
+
Object.freeze(this);
|
|
30997
|
+
}
|
|
30998
|
+
async getCapability(options = {}) {
|
|
30999
|
+
if (options.timeoutMs !== undefined && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 15000))
|
|
31000
|
+
return bad();
|
|
31001
|
+
const response = await boundedJson(skillsApiRequestUrl(this.apiOrigin, "/api/v1/capabilities"), {}, this.#token, false, options.timeoutMs, options.signal);
|
|
31002
|
+
const p = record7(response) && response.privatePublishing;
|
|
31003
|
+
if (!record7(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p.contractVersion !== 1 || typeof p.enabled !== "boolean" || p.authentication !== "interactive-session" || p.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p.uploadMaxTtlSeconds !== 300 || p.executionEnabled !== false)
|
|
31004
|
+
throw new PrivatePublicationError("PUBLICATION_CONTRACT_UNAVAILABLE", "This server does not support the hosted private publication contract.");
|
|
31005
|
+
return Object.freeze({ ...p });
|
|
31006
|
+
}
|
|
31007
|
+
async#gate(enabled, options = {}) {
|
|
31008
|
+
if (!(await this.getCapability(options)).enabled && enabled)
|
|
31009
|
+
throw new PrivatePublicationError("PUBLICATION_CAPABILITY_UNAVAILABLE", failures.PUBLICATION_CAPABILITY_UNAVAILABLE[1]);
|
|
31010
|
+
}
|
|
31011
|
+
#path(skillId, intentId) {
|
|
31012
|
+
if (!publicationUuid(skillId) || intentId !== undefined && !publicationUuid(intentId))
|
|
31013
|
+
return bad();
|
|
31014
|
+
return skillsApiRequestUrl(this.apiOrigin, `/api/v1/skills/${skillId}/publication-uploads${intentId ? `/${intentId}` : ""}`);
|
|
31015
|
+
}
|
|
31016
|
+
async#view(path, method, skillId, intentId, declaration, options = {}) {
|
|
31017
|
+
const value = await boundedJson(path, { method, ...method === "GET" ? {} : { body: JSON.stringify(declaration ?? {}) } }, this.#token, method !== "GET", options.timeoutMs, options.signal);
|
|
31018
|
+
try {
|
|
31019
|
+
if (!record7(value) || !Object.keys(value).every((k) => k === "upload" || k === "changed") || value.changed !== undefined && typeof value.changed !== "boolean")
|
|
31020
|
+
return invalid3();
|
|
31021
|
+
return checkedPublicationView(value.upload, skillId, intentId, declaration);
|
|
31022
|
+
} catch (error2) {
|
|
31023
|
+
if (method !== "GET")
|
|
31024
|
+
throw new PrivatePublicationError("PUBLICATION_UNCONFIRMED", "The publication result could not be confirmed. Reconcile the saved intent before another action.", true);
|
|
31025
|
+
throw error2;
|
|
31026
|
+
}
|
|
31027
|
+
}
|
|
31028
|
+
async begin(skillId, input) {
|
|
31029
|
+
const path = this.#path(skillId), declaration = checkedPublicationDeclaration(input);
|
|
31030
|
+
await this.#gate(true);
|
|
31031
|
+
return this.#view(path, "POST", skillId, undefined, declaration);
|
|
31032
|
+
}
|
|
31033
|
+
async get(skillId, intentId, options = {}) {
|
|
31034
|
+
const path = this.#path(skillId, intentId), until = Date.now() + (options.timeoutMs ?? 15000);
|
|
31035
|
+
await this.#gate(false, options);
|
|
31036
|
+
return this.#view(path, "GET", skillId, intentId, undefined, { ...options, timeoutMs: Math.max(1, until - Date.now()) });
|
|
31037
|
+
}
|
|
31038
|
+
async finalize(skillId, intentId) {
|
|
31039
|
+
const path = this.#path(skillId, intentId);
|
|
31040
|
+
await this.#gate(true);
|
|
31041
|
+
return this.#view(`${path}/finalize`, "POST", skillId, intentId);
|
|
31042
|
+
}
|
|
31043
|
+
async cancel(skillId, intentId) {
|
|
31044
|
+
const path = this.#path(skillId, intentId);
|
|
31045
|
+
await this.#gate(false);
|
|
31046
|
+
return this.#view(path, "DELETE", skillId, intentId);
|
|
31047
|
+
}
|
|
31048
|
+
async upload(skillId, intent, bytes) {
|
|
31049
|
+
const captured = checkedPublicationView(intent, skillId), path = this.#path(skillId, captured.id);
|
|
31050
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength !== captured.archiveByteSize)
|
|
31051
|
+
return bad();
|
|
31052
|
+
const owned = Buffer.from(bytes);
|
|
31053
|
+
if (captured.state !== "awaiting_upload" || owned.byteLength !== captured.archiveByteSize || publicationSha256(owned) !== captured.archiveSha256)
|
|
31054
|
+
return bad();
|
|
31055
|
+
await this.#gate(true);
|
|
31056
|
+
const value = await boundedJson(`${path}/upload-url`, { method: "POST", body: "{}" }, this.#token, false);
|
|
31057
|
+
const upload = this.#upload(value, captured);
|
|
31058
|
+
const controller = new AbortController;
|
|
31059
|
+
let timer;
|
|
31060
|
+
const uncertain = () => new PrivatePublicationError("PUBLICATION_UPLOAD_UNCONFIRMED", "Upload acceptance is uncertain. Resume this saved intent to finalize and inspect it; do not upload again.", true);
|
|
31061
|
+
try {
|
|
31062
|
+
await Promise.race([(async () => {
|
|
31063
|
+
const response = await fetch(upload.uploadUrl, { method: "PUT", headers: upload.headers, body: owned, redirect: "error", credentials: "omit", signal: controller.signal });
|
|
31064
|
+
response.body?.cancel().catch(() => {});
|
|
31065
|
+
if (response.status !== 200 || controller.signal.aborted)
|
|
31066
|
+
throw uncertain();
|
|
31067
|
+
})(), new Promise((_, reject) => {
|
|
31068
|
+
timer = setTimeout(() => {
|
|
31069
|
+
controller.abort();
|
|
31070
|
+
reject(uncertain());
|
|
31071
|
+
}, 30000);
|
|
31072
|
+
})]);
|
|
31073
|
+
} catch {
|
|
31074
|
+
throw uncertain();
|
|
31075
|
+
} finally {
|
|
31076
|
+
if (timer)
|
|
31077
|
+
clearTimeout(timer);
|
|
31078
|
+
controller.abort();
|
|
31079
|
+
}
|
|
31080
|
+
}
|
|
31081
|
+
#upload(value, intent) {
|
|
31082
|
+
if (!exact(value, ["upload"]) || !exact(value.upload, ["method", "uploadUrl", "headers", "expiresAt"]))
|
|
31083
|
+
return invalid3();
|
|
31084
|
+
const p = value.upload;
|
|
31085
|
+
if (p.method !== "PUT" || typeof p.uploadUrl !== "string" || p.uploadUrl.length > 8192 || /[\x00-\x20\x7f]/.test(p.uploadUrl) || !date4(p.expiresAt) || Date.parse(p.expiresAt) - Date.now() < 1000 || Date.parse(p.expiresAt) - Date.now() > 300000 || Date.parse(p.expiresAt) > Date.parse(intent.expiresAt) || !exact(p.headers, ["content-type", "content-length", "x-amz-checksum-sha256", "x-amz-expected-bucket-owner"]) || p.headers["content-type"] !== "application/gzip" || p.headers["content-length"] !== String(intent.archiveByteSize) || p.headers["x-amz-checksum-sha256"] !== Buffer.from(intent.archiveSha256, "hex").toString("base64") || typeof p.headers["x-amz-expected-bucket-owner"] !== "string" || !/^\d{12}$/.test(p.headers["x-amz-expected-bucket-owner"]))
|
|
31086
|
+
return invalid3();
|
|
31087
|
+
let url;
|
|
31088
|
+
try {
|
|
31089
|
+
url = new URL(p.uploadUrl);
|
|
31090
|
+
} catch {
|
|
31091
|
+
return invalid3();
|
|
31092
|
+
}
|
|
31093
|
+
if (url.protocol !== "https:" || url.port || url.username || url.password || url.hash || !/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]\.s3\.[a-z]{2}(?:-[a-z]+)+-[1-9]\.amazonaws\.com$/.test(url.hostname) || url.pathname !== `/private-publication-staging/${this.organizationId}/${intent.id}/bundle.tgz` || !url.search || url.searchParams.get("X-Amz-Algorithm") !== "AWS4-HMAC-SHA256" || url.searchParams.get("X-Amz-SignedHeaders") !== "content-length;content-type;host;x-amz-checksum-sha256;x-amz-expected-bucket-owner" || !/^[a-f0-9]{64}$/.test(url.searchParams.get("X-Amz-Signature") ?? ""))
|
|
31094
|
+
return invalid3();
|
|
31095
|
+
const queryKeys = ["X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Date", "X-Amz-Expires", "X-Amz-Security-Token", "X-Amz-Signature", "X-Amz-SignedHeaders"];
|
|
31096
|
+
if ([...url.searchParams.keys()].sort().join(",") !== queryKeys.sort().join(","))
|
|
31097
|
+
return invalid3();
|
|
31098
|
+
const issued = url.searchParams.get("X-Amz-Date"), ttl = url.searchParams.get("X-Amz-Expires"), credential = url.searchParams.get("X-Amz-Credential");
|
|
31099
|
+
const timestamp3 = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z$/.exec(issued);
|
|
31100
|
+
const region = url.hostname.split(".s3.")[1].split(".amazonaws.com")[0];
|
|
31101
|
+
if (!timestamp3 || !/^[1-9]\d{0,2}$/.test(ttl) || Number(ttl) > 300 || !/^[A-Z0-9]{16,128}\//.test(credential) || credential.split("/").slice(1).join("/") !== `${issued.slice(0, 8)}/${region}/s3/aws4_request` || !/^[\x21-\x7e]{1,4096}$/.test(url.searchParams.get("X-Amz-Security-Token")))
|
|
31102
|
+
return invalid3();
|
|
31103
|
+
const issuedAt = Date.parse(`${timestamp3[1]}-${timestamp3[2]}-${timestamp3[3]}T${timestamp3[4]}:${timestamp3[5]}:${timestamp3[6]}Z`);
|
|
31104
|
+
if (!Number.isFinite(issuedAt) || issuedAt > Date.now() + 1000 || issuedAt + Number(ttl) * 1000 !== Date.parse(p.expiresAt))
|
|
31105
|
+
return invalid3();
|
|
31106
|
+
return { method: "PUT", uploadUrl: url.href, expiresAt: p.expiresAt, headers: Object.freeze({ ...p.headers }) };
|
|
31107
|
+
}
|
|
31108
|
+
async wait(skillId, intentId, options = {}) {
|
|
31109
|
+
const timeout = options.timeoutMs ?? 60000;
|
|
31110
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0 || timeout > 300000)
|
|
31111
|
+
return bad();
|
|
31112
|
+
const until = Date.now() + timeout;
|
|
31113
|
+
let previous;
|
|
31114
|
+
while (true) {
|
|
31115
|
+
if (options.signal?.aborted)
|
|
31116
|
+
throw new PrivatePublicationError("PUBLICATION_WAIT_ABORTED", "Stopped waiting. The server publication continues; inspect the saved intent.");
|
|
31117
|
+
let view;
|
|
31118
|
+
try {
|
|
31119
|
+
view = await this.get(skillId, intentId, { timeoutMs: timeout === 0 ? 15000 : Math.max(1, Math.min(15000, until - Date.now())), signal: options.signal });
|
|
31120
|
+
} catch (error2) {
|
|
31121
|
+
if (previous && Date.now() >= until && !options.signal?.aborted)
|
|
31122
|
+
return previous;
|
|
31123
|
+
throw error2;
|
|
31124
|
+
}
|
|
31125
|
+
previous = view;
|
|
31126
|
+
if (!["queued", "verifying"].includes(view.state) || Date.now() >= until)
|
|
31127
|
+
return view;
|
|
31128
|
+
await new Promise((resolve2) => {
|
|
31129
|
+
const timer = setTimeout(done, Math.min(1000, until - Date.now()));
|
|
31130
|
+
function done() {
|
|
31131
|
+
clearTimeout(timer);
|
|
31132
|
+
options.signal?.removeEventListener("abort", done);
|
|
31133
|
+
resolve2();
|
|
31134
|
+
}
|
|
31135
|
+
options.signal?.addEventListener("abort", done, { once: true });
|
|
31136
|
+
});
|
|
31137
|
+
}
|
|
31138
|
+
}
|
|
31139
|
+
}
|
|
31140
|
+
var PRIVATE_PUBLICATION_MAX_BYTES, failures, PrivatePublicationError, bad = () => {
|
|
31141
|
+
throw new PrivatePublicationError("INVALID_PUBLICATION_INPUT", "Invalid publication input or recovery data.");
|
|
31142
|
+
}, invalid3 = () => {
|
|
31143
|
+
throw new PrivatePublicationError("INVALID_PUBLICATION_RESPONSE", "The server returned an invalid publication result.");
|
|
31144
|
+
}, publicationUuid = (v) => typeof v === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(v), hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v), record7 = (v) => !!v && typeof v === "object" && !Array.isArray(v), exact = (v, keys) => record7(v) && Object.keys(v).sort().join(",") === keys.sort().join(","), date4 = (v) => typeof v === "string" && /^\d{4}-\d\d-\d\dT[0-9:.]+(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(v) && Number.isFinite(Date.parse(v)), publicationSha256 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
|
|
31145
|
+
var init_remote_private_publications = __esm(() => {
|
|
31146
|
+
init_fleet_credentials();
|
|
31147
|
+
init_remote_workspace_selection();
|
|
31148
|
+
init_skill_contract();
|
|
31149
|
+
PRIVATE_PUBLICATION_MAX_BYTES = 16 * 1024 * 1024;
|
|
31150
|
+
failures = {
|
|
31151
|
+
INVALID_REQUEST: [400, "The publication request is invalid."],
|
|
31152
|
+
SESSION_EXPIRED: [401, "Sign in again to manage this publication."],
|
|
31153
|
+
ACCOUNT_UNAVAILABLE: [403, "The account is unavailable."],
|
|
31154
|
+
INTERACTIVE_SESSION_REQUIRED: [403, "An interactive account session is required."],
|
|
31155
|
+
PUBLICATION_FORBIDDEN: [403, "This session cannot manage the publication."],
|
|
31156
|
+
PUBLICATION_ENTITLEMENT_REQUIRED: [403, "The workspace is not entitled to publish private skills."],
|
|
31157
|
+
PUBLICATION_UNAVAILABLE: [404, "The publication is unavailable to this session."],
|
|
31158
|
+
MANIFEST_NAME_MISMATCH: [409, "The manifest name does not match the selected skill."],
|
|
31159
|
+
IDEMPOTENCY_CONFLICT: [409, "This request key already identifies different publication bytes. Use the saved recovery directory."],
|
|
31160
|
+
CURRENT_VERSION_CHANGED: [409, "The current version changed. Inspect it before explicitly starting another publication."],
|
|
31161
|
+
VERSION_EXISTS: [409, "This version already exists."],
|
|
31162
|
+
VERSION_RESERVED: [409, "This version is reserved by another publication."],
|
|
31163
|
+
PUBLICATION_COMMITTED: [409, "The publication is already committed."],
|
|
31164
|
+
PUBLICATION_UPLOAD_UNAVAILABLE: [409, "This publication cannot receive another upload."],
|
|
31165
|
+
PUBLICATION_LIMIT: [429, "The workspace publication limit has been reached."],
|
|
31166
|
+
PUBLICATION_BUSY: [503, "The publication is busy. Reconcile the saved intent before retrying."],
|
|
31167
|
+
PUBLICATION_UNCERTAIN: [503, "The publication outcome is uncertain. Reconcile the saved intent."],
|
|
31168
|
+
PUBLICATION_CAPABILITY_UNAVAILABLE: [503, "Private publishing is not enabled on this server."],
|
|
31169
|
+
PUBLICATION_SIGNING_UNAVAILABLE: [503, "Upload authorization is temporarily unavailable. Keep the same intent."]
|
|
31170
|
+
};
|
|
31171
|
+
PrivatePublicationError = class PrivatePublicationError extends Error {
|
|
31172
|
+
code;
|
|
31173
|
+
uncertain;
|
|
31174
|
+
status;
|
|
31175
|
+
constructor(code, message, uncertain = false, status) {
|
|
31176
|
+
super(message);
|
|
31177
|
+
this.code = code;
|
|
31178
|
+
this.uncertain = uncertain;
|
|
31179
|
+
this.status = status;
|
|
31180
|
+
this.name = "PrivatePublicationError";
|
|
31181
|
+
}
|
|
31182
|
+
};
|
|
31183
|
+
});
|
|
31184
|
+
|
|
30512
31185
|
// src/lib/remote-auth.ts
|
|
30513
31186
|
async function requestAuthApi(instance, path, options) {
|
|
30514
31187
|
const url = normalizeSkillsApiOrigin(instance);
|
|
30515
31188
|
const safeUrl = url;
|
|
30516
|
-
const
|
|
31189
|
+
const requestUrl = skillsApiRequestUrl(url, path);
|
|
31190
|
+
const endpoint = `${(options?.method || "GET").toUpperCase()} ${requestUrl}`;
|
|
30517
31191
|
let res;
|
|
30518
31192
|
try {
|
|
30519
|
-
res = await fetch(
|
|
31193
|
+
res = await fetch(requestUrl, {
|
|
30520
31194
|
...options,
|
|
30521
31195
|
redirect: "error",
|
|
30522
31196
|
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
@@ -30531,10 +31205,10 @@ async function requestAuthApi(instance, path, options) {
|
|
|
30531
31205
|
const text2 = await res.text();
|
|
30532
31206
|
const body = text2 ? parseJsonBody(text2) : {};
|
|
30533
31207
|
if (!res.ok) {
|
|
30534
|
-
const
|
|
30535
|
-
const detail = typeof
|
|
30536
|
-
const error2 = typeof
|
|
30537
|
-
const code = typeof
|
|
31208
|
+
const record8 = isRecord5(body) ? body : {};
|
|
31209
|
+
const detail = typeof record8.detail === "string" ? record8.detail : undefined;
|
|
31210
|
+
const error2 = typeof record8.error === "string" ? record8.error : undefined;
|
|
31211
|
+
const code = typeof record8.code === "string" ? record8.code : undefined;
|
|
30538
31212
|
throw new HostedApiError(detail || error2 || `${res.status} ${res.statusText}`, {
|
|
30539
31213
|
status: res.status,
|
|
30540
31214
|
code,
|
|
@@ -30568,6 +31242,10 @@ class RemoteSkillsAuthClient {
|
|
|
30568
31242
|
constructor(apiUrl) {
|
|
30569
31243
|
this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
|
|
30570
31244
|
}
|
|
31245
|
+
async openPrivatePublications(email3, code, context) {
|
|
31246
|
+
const origin = this.apiOrigin, captured = workspaceContext(context);
|
|
31247
|
+
return new RemotePrivatePublicationsClient(origin, await this.switchWorkspace(email3, code, captured));
|
|
31248
|
+
}
|
|
30571
31249
|
requestInvitationEmailChallenge(input) {
|
|
30572
31250
|
return requestInvitationEmail(this.apiOrigin, "challenge", input);
|
|
30573
31251
|
}
|
|
@@ -30615,9 +31293,10 @@ class RemoteSkillsAuthClient {
|
|
|
30615
31293
|
const apiOrigin = this.apiOrigin;
|
|
30616
31294
|
if (typeof email3 !== "string" || !email3.includes("@") || typeof code !== "string" || !/^\d{6}$/.test(code))
|
|
30617
31295
|
throw new Error("Fresh email and six-digit verification code are required to manage this account");
|
|
31296
|
+
const requestUrl = skillsApiRequestUrl(apiOrigin, "/api/auth/verify");
|
|
30618
31297
|
let response;
|
|
30619
31298
|
try {
|
|
30620
|
-
response = await fetch(
|
|
31299
|
+
response = await fetch(requestUrl, {
|
|
30621
31300
|
method: "POST",
|
|
30622
31301
|
redirect: "error",
|
|
30623
31302
|
credentials: "omit",
|
|
@@ -30711,6 +31390,7 @@ class RemoteSkillsAuthClient {
|
|
|
30711
31390
|
var MAX_ERROR_DETAIL_LENGTH = 200, HostedApiError;
|
|
30712
31391
|
var init_remote_auth = __esm(() => {
|
|
30713
31392
|
init_remote_invitation_recovery();
|
|
31393
|
+
init_remote_private_publications();
|
|
30714
31394
|
init_remote_invitations();
|
|
30715
31395
|
init_remote_workspace_leave();
|
|
30716
31396
|
init_remote_workspace_selection();
|
|
@@ -30913,9 +31593,824 @@ var init_remote_invitation_tools = __esm(() => {
|
|
|
30913
31593
|
init_helpers();
|
|
30914
31594
|
});
|
|
30915
31595
|
|
|
31596
|
+
// src/lib/private-publication-customer.ts
|
|
31597
|
+
async function privatePublicationSession(email3, code, requested) {
|
|
31598
|
+
const target = await captureProfileWorkspace("Manage private publications");
|
|
31599
|
+
const context = requested ?? target.context;
|
|
31600
|
+
if (!context || !publicationUuid(context.userId) || !publicationUuid(context.membershipId) || target.context && (context.userId !== target.context.userId || context.membershipId !== target.context.membershipId))
|
|
31601
|
+
throw new PrivatePublicationError("PUBLICATION_CONTEXT_REQUIRED", "Provide the observed user and membership IDs, or select an enrolled workspace profile.");
|
|
31602
|
+
target.unchanged();
|
|
31603
|
+
const client = await new RemoteSkillsAuthClient(target.origin).openPrivatePublications(email3, code, context);
|
|
31604
|
+
target.unchanged();
|
|
31605
|
+
return client;
|
|
31606
|
+
}
|
|
31607
|
+
function privatePublicationCustomerError(error2) {
|
|
31608
|
+
return error2 instanceof PrivatePublicationError ? { error: error2.message, code: error2.code, uncertain: error2.uncertain } : { error: "The publication action could not be confirmed. Preserve the recovery directory and inspect the same intent; credentials were not changed.", code: "PUBLICATION_ACTION_UNCONFIRMED", uncertain: true };
|
|
31609
|
+
}
|
|
31610
|
+
var init_private_publication_customer = __esm(() => {
|
|
31611
|
+
init_remote_auth();
|
|
31612
|
+
init_remote_private_publications();
|
|
31613
|
+
init_workspace_profile();
|
|
31614
|
+
});
|
|
31615
|
+
|
|
31616
|
+
// src/lib/skill-bundle.ts
|
|
31617
|
+
import { createHash as createHash6 } from "crypto";
|
|
31618
|
+
import { readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync11 } from "fs";
|
|
31619
|
+
import { join as join19, relative as relative4 } from "path";
|
|
31620
|
+
import { createGunzip } from "zlib";
|
|
31621
|
+
function isDotenvFile(lower) {
|
|
31622
|
+
if (lower === ".env" || lower.startsWith(".env."))
|
|
31623
|
+
return true;
|
|
31624
|
+
if (lower === "env")
|
|
31625
|
+
return true;
|
|
31626
|
+
if (lower.startsWith("env.")) {
|
|
31627
|
+
const extension = lower.slice(lower.lastIndexOf(".") + 1);
|
|
31628
|
+
return !NON_DOTENV_EXTENSIONS.has(extension);
|
|
31629
|
+
}
|
|
31630
|
+
return false;
|
|
31631
|
+
}
|
|
31632
|
+
function isCredentialFile(name) {
|
|
31633
|
+
const lower = name.toLowerCase();
|
|
31634
|
+
if (ENV_TEMPLATE_NAMES.has(lower))
|
|
31635
|
+
return false;
|
|
31636
|
+
if (isDotenvFile(lower))
|
|
31637
|
+
return true;
|
|
31638
|
+
if (CREDENTIAL_FILENAMES.has(lower))
|
|
31639
|
+
return true;
|
|
31640
|
+
return CREDENTIAL_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
31641
|
+
}
|
|
31642
|
+
function ownBytes(view) {
|
|
31643
|
+
const source = view instanceof ArrayBuffer ? new Uint8Array(view) : view;
|
|
31644
|
+
const out = new Uint8Array(new ArrayBuffer(source.byteLength));
|
|
31645
|
+
out.set(source);
|
|
31646
|
+
return out;
|
|
31647
|
+
}
|
|
31648
|
+
function sha256Hex(bytes) {
|
|
31649
|
+
return createHash6("sha256").update(bytes).digest("hex");
|
|
31650
|
+
}
|
|
31651
|
+
function collectSkillBundleEntries(dir) {
|
|
31652
|
+
const entries = [];
|
|
31653
|
+
walk(dir, dir, entries);
|
|
31654
|
+
return entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
31655
|
+
}
|
|
31656
|
+
function walk(root, current, out) {
|
|
31657
|
+
for (const entry of readdirSync10(current, { withFileTypes: true })) {
|
|
31658
|
+
const absolute = join19(current, entry.name);
|
|
31659
|
+
const rel = relative4(root, absolute).split("\\").join("/");
|
|
31660
|
+
const isRootLevel = !rel.includes("/");
|
|
31661
|
+
if (ANY_SEGMENT_EXCLUDES.has(entry.name.toLowerCase()))
|
|
31662
|
+
continue;
|
|
31663
|
+
if (isRootLevel && ROOT_EXCLUDES.has(entry.name.toLowerCase()))
|
|
31664
|
+
continue;
|
|
31665
|
+
if (isRootLevel && TOOL_SIDECAR_FILENAMES.has(entry.name.toLowerCase()))
|
|
31666
|
+
continue;
|
|
31667
|
+
if (entry.name.startsWith("._"))
|
|
31668
|
+
continue;
|
|
31669
|
+
if (entry.isSymbolicLink())
|
|
31670
|
+
continue;
|
|
31671
|
+
if (entry.isDirectory()) {
|
|
31672
|
+
walk(root, absolute, out);
|
|
31673
|
+
continue;
|
|
31674
|
+
}
|
|
31675
|
+
if (!entry.isFile())
|
|
31676
|
+
continue;
|
|
31677
|
+
if (isCredentialFile(entry.name))
|
|
31678
|
+
continue;
|
|
31679
|
+
const stats = statSync11(absolute);
|
|
31680
|
+
out.push({
|
|
31681
|
+
path: rel,
|
|
31682
|
+
bytes: ownBytes(readFileSync16(absolute)),
|
|
31683
|
+
mode: stats.mode & 64 ? 493 : 420
|
|
31684
|
+
});
|
|
31685
|
+
}
|
|
31686
|
+
}
|
|
31687
|
+
function packSkillBundle(dir, options = {}) {
|
|
31688
|
+
const entries = collectSkillBundleEntries(dir);
|
|
31689
|
+
if (entries.length === 0) {
|
|
31690
|
+
throw new Error(`Nothing to pack: ${dir} contains no files after exclusions (.git, node_modules, dist, .env)`);
|
|
31691
|
+
}
|
|
31692
|
+
const unpackedByteSize = entries.reduce((sum, entry) => sum + entry.bytes.byteLength, 0);
|
|
31693
|
+
const max = options.maxUnpackedBytes ?? 0;
|
|
31694
|
+
if (max > 0 && unpackedByteSize > max) {
|
|
31695
|
+
throw new Error(`Skill sources are ${unpackedByteSize} bytes, over the ${max} byte limit. Remove build output or large fixtures.`);
|
|
31696
|
+
}
|
|
31697
|
+
const tar = writeTar(entries);
|
|
31698
|
+
const bytes = canonicalGzip(tar);
|
|
31699
|
+
return {
|
|
31700
|
+
bytes,
|
|
31701
|
+
sha256: sha256Hex(bytes),
|
|
31702
|
+
fileCount: entries.length,
|
|
31703
|
+
unpackedByteSize,
|
|
31704
|
+
paths: entries.map((entry) => entry.path)
|
|
31705
|
+
};
|
|
31706
|
+
}
|
|
31707
|
+
function canonicalGzip(tar) {
|
|
31708
|
+
const bytes = ownBytes(Bun.gzipSync(tar, { level: 6 }));
|
|
31709
|
+
if (bytes.byteLength >= 10) {
|
|
31710
|
+
bytes[4] = 0;
|
|
31711
|
+
bytes[5] = 0;
|
|
31712
|
+
bytes[6] = 0;
|
|
31713
|
+
bytes[7] = 0;
|
|
31714
|
+
bytes[9] = 255;
|
|
31715
|
+
}
|
|
31716
|
+
return bytes;
|
|
31717
|
+
}
|
|
31718
|
+
function writeTar(entries) {
|
|
31719
|
+
const blocks = [];
|
|
31720
|
+
for (const entry of entries) {
|
|
31721
|
+
blocks.push(ustarHeader(entry));
|
|
31722
|
+
blocks.push(entry.bytes);
|
|
31723
|
+
const remainder = entry.bytes.byteLength % BLOCK;
|
|
31724
|
+
if (remainder !== 0)
|
|
31725
|
+
blocks.push(new Uint8Array(new ArrayBuffer(BLOCK - remainder)));
|
|
31726
|
+
}
|
|
31727
|
+
blocks.push(new Uint8Array(new ArrayBuffer(BLOCK * 2)));
|
|
31728
|
+
return concat(blocks);
|
|
31729
|
+
}
|
|
31730
|
+
function ustarHeader(entry) {
|
|
31731
|
+
const header = new Uint8Array(new ArrayBuffer(BLOCK));
|
|
31732
|
+
const encoder = new TextEncoder;
|
|
31733
|
+
const put = (offset, length, value) => {
|
|
31734
|
+
const encoded = encoder.encode(value);
|
|
31735
|
+
if (encoded.byteLength > length) {
|
|
31736
|
+
throw new Error(`Cannot pack '${entry.path}': field does not fit in a ustar header (${encoded.byteLength} > ${length})`);
|
|
31737
|
+
}
|
|
31738
|
+
header.set(encoded, offset);
|
|
31739
|
+
};
|
|
31740
|
+
if (encoder.encode(entry.path).byteLength > 100) {
|
|
31741
|
+
throw new Error(`Cannot pack '${entry.path}': path is longer than the 100 bytes a ustar header holds`);
|
|
31742
|
+
}
|
|
31743
|
+
put(0, 100, entry.path);
|
|
31744
|
+
put(100, 8, `${entry.mode.toString(8).padStart(7, "0")}\x00`);
|
|
31745
|
+
put(108, 8, "0000000\x00");
|
|
31746
|
+
put(116, 8, "0000000\x00");
|
|
31747
|
+
put(124, 12, `${entry.bytes.byteLength.toString(8).padStart(11, "0")}\x00`);
|
|
31748
|
+
put(136, 12, `${0 .toString(8).padStart(11, "0")}\x00`);
|
|
31749
|
+
put(148, 8, " ");
|
|
31750
|
+
put(156, 1, "0");
|
|
31751
|
+
put(257, 6, "ustar\x00");
|
|
31752
|
+
put(263, 2, "00");
|
|
31753
|
+
let checksum = 0;
|
|
31754
|
+
for (const byte of header)
|
|
31755
|
+
checksum += byte;
|
|
31756
|
+
put(148, 8, `${checksum.toString(8).padStart(6, "0")}\x00 `);
|
|
31757
|
+
return header;
|
|
31758
|
+
}
|
|
31759
|
+
function concat(chunks) {
|
|
31760
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
31761
|
+
const merged = new Uint8Array(new ArrayBuffer(total));
|
|
31762
|
+
let offset = 0;
|
|
31763
|
+
for (const chunk of chunks) {
|
|
31764
|
+
merged.set(chunk, offset);
|
|
31765
|
+
offset += chunk.byteLength;
|
|
31766
|
+
}
|
|
31767
|
+
return merged;
|
|
31768
|
+
}
|
|
31769
|
+
function invalidBundle(message) {
|
|
31770
|
+
throw new SkillBundleInspectionError("BUNDLE_INVALID", message);
|
|
31771
|
+
}
|
|
31772
|
+
function inspectionLimits(options) {
|
|
31773
|
+
const limits = { ...SKILL_BUNDLE_INSPECTION_LIMITS };
|
|
31774
|
+
for (const key of Object.keys(options.limits ?? {})) {
|
|
31775
|
+
if (!Object.hasOwn(limits, key))
|
|
31776
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Unknown bundle limit");
|
|
31777
|
+
const field = key;
|
|
31778
|
+
const value = options.limits[field];
|
|
31779
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > limits[field]) {
|
|
31780
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle limits must be positive integers within the hard ceilings");
|
|
31781
|
+
}
|
|
31782
|
+
limits[field] = value;
|
|
31783
|
+
}
|
|
31784
|
+
return limits;
|
|
31785
|
+
}
|
|
31786
|
+
async function inspectSkillBundle(bundle, options = {}) {
|
|
31787
|
+
const signal = options.signal;
|
|
31788
|
+
const limits = inspectionLimits(options);
|
|
31789
|
+
const deadline = performance.now() + limits.timeoutMs;
|
|
31790
|
+
const check2 = () => {
|
|
31791
|
+
if (signal?.aborted)
|
|
31792
|
+
throw new SkillBundleInspectionError("BUNDLE_ABORTED", "Bundle inspection aborted");
|
|
31793
|
+
if (performance.now() >= deadline)
|
|
31794
|
+
throw new SkillBundleInspectionError("BUNDLE_TIMEOUT", "Bundle inspection deadline exceeded");
|
|
31795
|
+
};
|
|
31796
|
+
check2();
|
|
31797
|
+
if (bundle.byteLength > limits.compressedBytes)
|
|
31798
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Compressed bundle exceeds byte limit");
|
|
31799
|
+
const snapshot = ownBytes(bundle);
|
|
31800
|
+
check2();
|
|
31801
|
+
const sha2562 = sha256Hex(snapshot);
|
|
31802
|
+
check2();
|
|
31803
|
+
const parser = new BoundedTarReader(limits, check2);
|
|
31804
|
+
const streamOptions = { chunkSize: 16 * 1024, highWaterMark: 16 * 1024 };
|
|
31805
|
+
const decoder = createGunzip(streamOptions);
|
|
31806
|
+
let terminalError;
|
|
31807
|
+
const stop = (code) => {
|
|
31808
|
+
terminalError ??= new SkillBundleInspectionError(code, code === "BUNDLE_ABORTED" ? "Bundle inspection aborted" : "Bundle inspection deadline exceeded");
|
|
31809
|
+
decoder.destroy(terminalError);
|
|
31810
|
+
};
|
|
31811
|
+
const onAbort = () => stop("BUNDLE_ABORTED");
|
|
31812
|
+
const timer = setTimeout(() => stop("BUNDLE_TIMEOUT"), Math.max(1, deadline - performance.now()));
|
|
31813
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
31814
|
+
let decompressedByteSize = 0;
|
|
31815
|
+
let bytesSinceYield = 0;
|
|
31816
|
+
try {
|
|
31817
|
+
check2();
|
|
31818
|
+
decoder.end(snapshot);
|
|
31819
|
+
for await (const chunk of decoder) {
|
|
31820
|
+
check2();
|
|
31821
|
+
decompressedByteSize += chunk.byteLength;
|
|
31822
|
+
if (decompressedByteSize > limits.decompressedBytes)
|
|
31823
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Decompressed bundle exceeds byte limit");
|
|
31824
|
+
parser.push(chunk);
|
|
31825
|
+
bytesSinceYield += chunk.byteLength;
|
|
31826
|
+
if (bytesSinceYield >= 256 * 1024) {
|
|
31827
|
+
await new Promise((resolve2) => setTimeout(resolve2, 0));
|
|
31828
|
+
bytesSinceYield = 0;
|
|
31829
|
+
check2();
|
|
31830
|
+
}
|
|
31831
|
+
}
|
|
31832
|
+
check2();
|
|
31833
|
+
const entries = parser.finish();
|
|
31834
|
+
return {
|
|
31835
|
+
entries,
|
|
31836
|
+
sha256: sha2562,
|
|
31837
|
+
compressedByteSize: snapshot.byteLength,
|
|
31838
|
+
decompressedByteSize,
|
|
31839
|
+
unpackedByteSize: parser.fileBytes,
|
|
31840
|
+
fileCount: entries.length
|
|
31841
|
+
};
|
|
31842
|
+
} catch (error2) {
|
|
31843
|
+
if (terminalError)
|
|
31844
|
+
throw terminalError;
|
|
31845
|
+
if (error2 instanceof SkillBundleInspectionError)
|
|
31846
|
+
throw error2;
|
|
31847
|
+
throw new SkillBundleInspectionError("BUNDLE_INVALID", "Invalid or truncated gzip bundle");
|
|
31848
|
+
} finally {
|
|
31849
|
+
clearTimeout(timer);
|
|
31850
|
+
signal?.removeEventListener("abort", onAbort);
|
|
31851
|
+
decoder.destroy();
|
|
31852
|
+
}
|
|
31853
|
+
}
|
|
31854
|
+
|
|
31855
|
+
class BoundedTarReader {
|
|
31856
|
+
limits;
|
|
31857
|
+
check;
|
|
31858
|
+
header = new Uint8Array(BLOCK);
|
|
31859
|
+
headerOffset = 0;
|
|
31860
|
+
pending;
|
|
31861
|
+
bodyOffset = 0;
|
|
31862
|
+
padding = 0;
|
|
31863
|
+
zeroBlocks = 0;
|
|
31864
|
+
entries = [];
|
|
31865
|
+
paths = new SkillEntryPaths;
|
|
31866
|
+
fileBytes = 0;
|
|
31867
|
+
constructor(limits, check2) {
|
|
31868
|
+
this.limits = limits;
|
|
31869
|
+
this.check = check2;
|
|
31870
|
+
}
|
|
31871
|
+
push(chunk) {
|
|
31872
|
+
let offset = 0;
|
|
31873
|
+
while (offset < chunk.byteLength) {
|
|
31874
|
+
this.check();
|
|
31875
|
+
if (this.pending) {
|
|
31876
|
+
const count = Math.min(this.pending.bytes.byteLength - this.bodyOffset, chunk.byteLength - offset);
|
|
31877
|
+
this.pending.bytes.set(chunk.subarray(offset, offset + count), this.bodyOffset);
|
|
31878
|
+
offset += count;
|
|
31879
|
+
this.bodyOffset += count;
|
|
31880
|
+
if (this.bodyOffset === this.pending.bytes.byteLength) {
|
|
31881
|
+
this.entries.push(this.pending);
|
|
31882
|
+
this.pending = undefined;
|
|
31883
|
+
}
|
|
31884
|
+
} else if (this.padding) {
|
|
31885
|
+
const count = Math.min(this.padding, chunk.byteLength - offset);
|
|
31886
|
+
if (chunk.subarray(offset, offset + count).some((byte) => byte !== 0))
|
|
31887
|
+
invalidBundle("Nonzero tar body padding");
|
|
31888
|
+
offset += count;
|
|
31889
|
+
this.padding -= count;
|
|
31890
|
+
} else {
|
|
31891
|
+
const count = Math.min(BLOCK - this.headerOffset, chunk.byteLength - offset);
|
|
31892
|
+
this.header.set(chunk.subarray(offset, offset + count), this.headerOffset);
|
|
31893
|
+
offset += count;
|
|
31894
|
+
this.headerOffset += count;
|
|
31895
|
+
if (this.headerOffset === BLOCK) {
|
|
31896
|
+
this.readHeader();
|
|
31897
|
+
this.headerOffset = 0;
|
|
31898
|
+
}
|
|
31899
|
+
}
|
|
31900
|
+
}
|
|
31901
|
+
}
|
|
31902
|
+
finish() {
|
|
31903
|
+
this.check();
|
|
31904
|
+
if (this.pending || this.padding || this.headerOffset || this.zeroBlocks < 2)
|
|
31905
|
+
invalidBundle("Truncated tar bundle");
|
|
31906
|
+
return this.entries;
|
|
31907
|
+
}
|
|
31908
|
+
readHeader() {
|
|
31909
|
+
this.check();
|
|
31910
|
+
const h = this.header;
|
|
31911
|
+
if (h.every((byte) => byte === 0)) {
|
|
31912
|
+
this.zeroBlocks++;
|
|
31913
|
+
return;
|
|
31914
|
+
}
|
|
31915
|
+
if (this.zeroBlocks)
|
|
31916
|
+
invalidBundle("Nonzero tar data after terminator");
|
|
31917
|
+
if (this.entries.length >= this.limits.entries)
|
|
31918
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle entry limit exceeded");
|
|
31919
|
+
let checksum = 0;
|
|
31920
|
+
for (let i = 0;i < BLOCK; i++)
|
|
31921
|
+
checksum += i >= 148 && i < 156 ? 32 : h[i];
|
|
31922
|
+
if (tarOctal(h.subarray(148, 156)) !== checksum)
|
|
31923
|
+
invalidBundle("Invalid tar header checksum");
|
|
31924
|
+
if (new TextDecoder().decode(h.subarray(257, 265)) !== "ustar\x00" + "00")
|
|
31925
|
+
invalidBundle("Unsupported tar format");
|
|
31926
|
+
if (h[156] !== 48 && h[156] !== 0 || h.subarray(157, 257).some((b) => b !== 0) || h.subarray(345).some((b) => b !== 0))
|
|
31927
|
+
invalidBundle("Unsupported tar entry or path prefix");
|
|
31928
|
+
const mode = tarOctal(h.subarray(100, 108));
|
|
31929
|
+
if (mode > 511)
|
|
31930
|
+
invalidBundle("Unsupported tar permission bits");
|
|
31931
|
+
tarOctal(h.subarray(108, 116));
|
|
31932
|
+
tarOctal(h.subarray(116, 124));
|
|
31933
|
+
tarOctal(h.subarray(136, 148));
|
|
31934
|
+
const size = tarOctal(h.subarray(124, 136));
|
|
31935
|
+
if (size > this.limits.fileBytes || this.fileBytes + size > this.limits.decompressedBytes) {
|
|
31936
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle file byte limit exceeded");
|
|
31937
|
+
}
|
|
31938
|
+
const name = h.subarray(0, 100);
|
|
31939
|
+
const end = name.indexOf(0);
|
|
31940
|
+
if (end !== -1 && name.subarray(end).some((b) => b !== 0))
|
|
31941
|
+
invalidBundle("Invalid tar path padding");
|
|
31942
|
+
const raw = end === -1 ? name : name.subarray(0, end);
|
|
31943
|
+
if (raw.byteLength > this.limits.pathBytes)
|
|
31944
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle path byte limit exceeded");
|
|
31945
|
+
let path;
|
|
31946
|
+
try {
|
|
31947
|
+
path = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw);
|
|
31948
|
+
} catch {
|
|
31949
|
+
return invalidBundle("Invalid UTF-8 bundle path");
|
|
31950
|
+
}
|
|
31951
|
+
this.paths.add(path, this.limits.pathBytes, invalidBundle, () => {
|
|
31952
|
+
throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle path byte limit exceeded");
|
|
31953
|
+
});
|
|
31954
|
+
this.fileBytes += size;
|
|
31955
|
+
this.pending = { path, mode, bytes: new Uint8Array(new ArrayBuffer(size)) };
|
|
31956
|
+
this.bodyOffset = 0;
|
|
31957
|
+
this.padding = (BLOCK - size % BLOCK) % BLOCK;
|
|
31958
|
+
if (!size) {
|
|
31959
|
+
this.entries.push(this.pending);
|
|
31960
|
+
this.pending = undefined;
|
|
31961
|
+
}
|
|
31962
|
+
}
|
|
31963
|
+
}
|
|
31964
|
+
function tarOctal(field) {
|
|
31965
|
+
const text2 = new TextDecoder().decode(field);
|
|
31966
|
+
if (!/^[0-7]+[\0 ]*$/.test(text2))
|
|
31967
|
+
invalidBundle("Invalid tar octal field");
|
|
31968
|
+
const value = Number.parseInt(text2, 8);
|
|
31969
|
+
if (!Number.isSafeInteger(value))
|
|
31970
|
+
invalidBundle("Tar integer is out of range");
|
|
31971
|
+
return value;
|
|
31972
|
+
}
|
|
31973
|
+
var BLOCK = 512, ANY_SEGMENT_EXCLUDES, ROOT_EXCLUDES, TOOL_SIDECAR_FILENAMES, CREDENTIAL_FILENAMES, CREDENTIAL_EXTENSIONS, ENV_TEMPLATE_NAMES, NON_DOTENV_EXTENSIONS, SKILL_BUNDLE_INSPECTION_LIMITS, SkillBundleInspectionError;
|
|
31974
|
+
var init_skill_bundle = __esm(() => {
|
|
31975
|
+
ANY_SEGMENT_EXCLUDES = new Set([
|
|
31976
|
+
".git",
|
|
31977
|
+
".ds_store",
|
|
31978
|
+
".system",
|
|
31979
|
+
"node_modules",
|
|
31980
|
+
".aws",
|
|
31981
|
+
".ssh",
|
|
31982
|
+
".gnupg",
|
|
31983
|
+
".docker"
|
|
31984
|
+
]);
|
|
31985
|
+
ROOT_EXCLUDES = new Set(["dist", "build", ".turbo"]);
|
|
31986
|
+
TOOL_SIDECAR_FILENAMES = new Set([".hasna-skills.json"]);
|
|
31987
|
+
CREDENTIAL_FILENAMES = new Set([
|
|
31988
|
+
".npmrc",
|
|
31989
|
+
".pypirc",
|
|
31990
|
+
".netrc",
|
|
31991
|
+
".envrc",
|
|
31992
|
+
".pgpass",
|
|
31993
|
+
".git-credentials",
|
|
31994
|
+
"credentials",
|
|
31995
|
+
"id_rsa",
|
|
31996
|
+
"id_dsa",
|
|
31997
|
+
"id_ecdsa",
|
|
31998
|
+
"id_ed25519"
|
|
31999
|
+
]);
|
|
32000
|
+
CREDENTIAL_EXTENSIONS = [".pem", ".key", ".p12", ".pfx", ".keystore", ".jks"];
|
|
32001
|
+
ENV_TEMPLATE_NAMES = new Set([".env.example", ".env.sample", ".env.template", ".env.dist"]);
|
|
32002
|
+
NON_DOTENV_EXTENSIONS = new Set([
|
|
32003
|
+
"ts",
|
|
32004
|
+
"tsx",
|
|
32005
|
+
"mts",
|
|
32006
|
+
"cts",
|
|
32007
|
+
"js",
|
|
32008
|
+
"jsx",
|
|
32009
|
+
"mjs",
|
|
32010
|
+
"cjs",
|
|
32011
|
+
"json",
|
|
32012
|
+
"jsonc",
|
|
32013
|
+
"json5",
|
|
32014
|
+
"py",
|
|
32015
|
+
"rb",
|
|
32016
|
+
"go",
|
|
32017
|
+
"rs",
|
|
32018
|
+
"java",
|
|
32019
|
+
"kt",
|
|
32020
|
+
"kts",
|
|
32021
|
+
"swift",
|
|
32022
|
+
"c",
|
|
32023
|
+
"h",
|
|
32024
|
+
"cc",
|
|
32025
|
+
"cpp",
|
|
32026
|
+
"hpp",
|
|
32027
|
+
"cs",
|
|
32028
|
+
"php",
|
|
32029
|
+
"sh",
|
|
32030
|
+
"bash",
|
|
32031
|
+
"zsh",
|
|
32032
|
+
"fish",
|
|
32033
|
+
"ps1",
|
|
32034
|
+
"bat",
|
|
32035
|
+
"cmd",
|
|
32036
|
+
"lua",
|
|
32037
|
+
"pl",
|
|
32038
|
+
"pm",
|
|
32039
|
+
"r",
|
|
32040
|
+
"jl",
|
|
32041
|
+
"dart",
|
|
32042
|
+
"ex",
|
|
32043
|
+
"exs",
|
|
32044
|
+
"scala",
|
|
32045
|
+
"clj",
|
|
32046
|
+
"cljs",
|
|
32047
|
+
"groovy",
|
|
32048
|
+
"gradle",
|
|
32049
|
+
"vb",
|
|
32050
|
+
"fs",
|
|
32051
|
+
"yaml",
|
|
32052
|
+
"yml",
|
|
32053
|
+
"toml",
|
|
32054
|
+
"ini",
|
|
32055
|
+
"xml",
|
|
32056
|
+
"csv",
|
|
32057
|
+
"tsv",
|
|
32058
|
+
"html",
|
|
32059
|
+
"htm",
|
|
32060
|
+
"css",
|
|
32061
|
+
"scss",
|
|
32062
|
+
"sass",
|
|
32063
|
+
"less",
|
|
32064
|
+
"md",
|
|
32065
|
+
"mdx",
|
|
32066
|
+
"txt",
|
|
32067
|
+
"rst",
|
|
32068
|
+
"adoc",
|
|
32069
|
+
"sql",
|
|
32070
|
+
"graphql",
|
|
32071
|
+
"gql",
|
|
32072
|
+
"proto",
|
|
32073
|
+
"d",
|
|
32074
|
+
"png",
|
|
32075
|
+
"jpg",
|
|
32076
|
+
"jpeg",
|
|
32077
|
+
"gif",
|
|
32078
|
+
"svg",
|
|
32079
|
+
"webp",
|
|
32080
|
+
"ico",
|
|
32081
|
+
"bmp",
|
|
32082
|
+
"pdf",
|
|
32083
|
+
"woff",
|
|
32084
|
+
"woff2",
|
|
32085
|
+
"ttf",
|
|
32086
|
+
"otf",
|
|
32087
|
+
"eot",
|
|
32088
|
+
"mp3",
|
|
32089
|
+
"mp4",
|
|
32090
|
+
"wav",
|
|
32091
|
+
"webm",
|
|
32092
|
+
"zip",
|
|
32093
|
+
"gz",
|
|
32094
|
+
"tar",
|
|
32095
|
+
"wasm"
|
|
32096
|
+
]);
|
|
32097
|
+
SKILL_BUNDLE_INSPECTION_LIMITS = Object.freeze({
|
|
32098
|
+
compressedBytes: 16 * 1024 * 1024,
|
|
32099
|
+
decompressedBytes: 64 * 1024 * 1024,
|
|
32100
|
+
entries: 1024,
|
|
32101
|
+
fileBytes: 16 * 1024 * 1024,
|
|
32102
|
+
pathBytes: 100,
|
|
32103
|
+
timeoutMs: 5000
|
|
32104
|
+
});
|
|
32105
|
+
SkillBundleInspectionError = class SkillBundleInspectionError extends Error {
|
|
32106
|
+
code;
|
|
32107
|
+
constructor(code, message) {
|
|
32108
|
+
super(message);
|
|
32109
|
+
this.code = code;
|
|
32110
|
+
this.name = "SkillBundleInspectionError";
|
|
32111
|
+
}
|
|
32112
|
+
};
|
|
32113
|
+
});
|
|
32114
|
+
|
|
32115
|
+
// src/lib/private-publication-recovery.ts
|
|
32116
|
+
import { constants as constants3, closeSync as closeSync4, fsyncSync, fstatSync as fstatSync4, lstatSync as lstatSync5, mkdirSync as mkdirSync11, openSync as openSync4, readSync as readSync2, realpathSync as realpathSync2, renameSync as renameSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
|
|
32117
|
+
import { dirname as dirname8, isAbsolute as isAbsolute4, join as join20, resolve as resolve2 } from "path";
|
|
32118
|
+
import { randomUUID } from "crypto";
|
|
32119
|
+
function safeDirectory(directory) {
|
|
32120
|
+
if (!isAbsolute4(directory) || directory !== resolve2(directory) || realpathSync2(directory) !== directory)
|
|
32121
|
+
return fail2();
|
|
32122
|
+
for (let path = directory;; path = dirname8(path)) {
|
|
32123
|
+
const stat2 = lstatSync5(path);
|
|
32124
|
+
if (!stat2.isDirectory() || stat2.isSymbolicLink())
|
|
32125
|
+
return fail2();
|
|
32126
|
+
if (path === dirname8(path))
|
|
32127
|
+
break;
|
|
32128
|
+
}
|
|
32129
|
+
const own = lstatSync5(directory);
|
|
32130
|
+
if ((own.mode & 63) !== 0 || process.getuid && own.uid !== process.getuid())
|
|
32131
|
+
return fail2();
|
|
32132
|
+
return { dev: own.dev, ino: own.ino };
|
|
32133
|
+
}
|
|
32134
|
+
function unchangedDirectory(directory, identity) {
|
|
32135
|
+
const now = safeDirectory(directory);
|
|
32136
|
+
if (now.dev !== identity.dev || now.ino !== identity.ino)
|
|
32137
|
+
return fail2();
|
|
32138
|
+
}
|
|
32139
|
+
function readOwned(directory, name, max) {
|
|
32140
|
+
const identity = safeDirectory(directory), file = join20(directory, name);
|
|
32141
|
+
const fd = openSync4(file, constants3.O_RDONLY | constants3.O_NOFOLLOW);
|
|
32142
|
+
try {
|
|
32143
|
+
const stat2 = fstatSync4(fd);
|
|
32144
|
+
if (!stat2.isFile() || stat2.nlink !== 1 || stat2.size > max || stat2.size < 1 || (stat2.mode & 63) !== 0 || process.getuid && stat2.uid !== process.getuid())
|
|
32145
|
+
return fail2();
|
|
32146
|
+
const buffer = Buffer.alloc(Math.min(max, stat2.size) + 1);
|
|
32147
|
+
let length = 0;
|
|
32148
|
+
while (length < buffer.length) {
|
|
32149
|
+
const count = readSync2(fd, buffer, length, buffer.length - length, length);
|
|
32150
|
+
if (count === 0)
|
|
32151
|
+
break;
|
|
32152
|
+
length += count;
|
|
32153
|
+
}
|
|
32154
|
+
const bytes = buffer.subarray(0, length), after = fstatSync4(fd);
|
|
32155
|
+
unchangedDirectory(directory, identity);
|
|
32156
|
+
if (bytes.length !== stat2.size || stat2.size !== after.size || stat2.mtimeMs !== after.mtimeMs || stat2.ctimeMs !== after.ctimeMs)
|
|
32157
|
+
return fail2();
|
|
32158
|
+
return bytes;
|
|
32159
|
+
} finally {
|
|
32160
|
+
closeSync4(fd);
|
|
32161
|
+
}
|
|
32162
|
+
}
|
|
32163
|
+
function writeOwned(directory, name, bytes) {
|
|
32164
|
+
const fd = openSync4(join20(directory, name), constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
|
|
32165
|
+
try {
|
|
32166
|
+
writeFileSync10(fd, bytes);
|
|
32167
|
+
fsyncSync(fd);
|
|
32168
|
+
} finally {
|
|
32169
|
+
closeSync4(fd);
|
|
32170
|
+
}
|
|
32171
|
+
}
|
|
32172
|
+
function save(directory, value) {
|
|
32173
|
+
const identity = safeDirectory(directory), temporary = `.receipt-${randomUUID()}.json`;
|
|
32174
|
+
writeOwned(directory, temporary, JSON.stringify(value) + `
|
|
32175
|
+
`);
|
|
32176
|
+
unchangedDirectory(directory, identity);
|
|
32177
|
+
renameSync4(join20(directory, temporary), join20(directory, "receipt.json"));
|
|
32178
|
+
const fd = openSync4(directory, constants3.O_RDONLY | constants3.O_NOFOLLOW);
|
|
32179
|
+
try {
|
|
32180
|
+
fsyncSync(fd);
|
|
32181
|
+
} finally {
|
|
32182
|
+
closeSync4(fd);
|
|
32183
|
+
}
|
|
32184
|
+
}
|
|
32185
|
+
function bind(client, receipt) {
|
|
32186
|
+
if (["apiOrigin", "organizationId", "userId", "membershipId"].some((k) => client[k] !== receipt[k]))
|
|
32187
|
+
throw new PrivatePublicationError("PUBLICATION_IDENTITY_CHANGED", "The fresh session does not match the recovery directory's server, account and membership.");
|
|
32188
|
+
}
|
|
32189
|
+
async function locked(directory, action) {
|
|
32190
|
+
const identity = safeDirectory(directory), file = join20(directory, "operation.lock");
|
|
32191
|
+
let fd;
|
|
32192
|
+
try {
|
|
32193
|
+
fd = openSync4(file, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
|
|
32194
|
+
} catch {
|
|
32195
|
+
throw new PrivatePublicationError("PUBLICATION_RECOVERY_BUSY", "Another operation holds this recovery directory. If it crashed, confirm that process has stopped before explicitly removing operation.lock and resuming.");
|
|
32196
|
+
}
|
|
32197
|
+
const lock = fstatSync4(fd);
|
|
32198
|
+
try {
|
|
32199
|
+
writeFileSync10(fd, JSON.stringify({ pid: process.pid }) + `
|
|
32200
|
+
`);
|
|
32201
|
+
fsyncSync(fd);
|
|
32202
|
+
return await action();
|
|
32203
|
+
} finally {
|
|
32204
|
+
closeSync4(fd);
|
|
32205
|
+
unchangedDirectory(directory, identity);
|
|
32206
|
+
const now = lstatSync5(file);
|
|
32207
|
+
if (now.dev !== lock.dev || now.ino !== lock.ino || !now.isFile() || now.isSymbolicLink())
|
|
32208
|
+
fail2();
|
|
32209
|
+
unlinkSync2(file);
|
|
32210
|
+
}
|
|
32211
|
+
}
|
|
32212
|
+
function readPrivatePublicationRecovery(directory) {
|
|
32213
|
+
try {
|
|
32214
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readOwned(directory, "receipt.json", 65536)));
|
|
32215
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== ["contractVersion", "apiOrigin", "organizationId", "userId", "membershipId", "skillId", "declaration", "phase", "intent"].sort().join(",") || value.contractVersion !== 1 || ![value.organizationId, value.userId, value.membershipId, value.skillId].every(publicationUuid) || typeof value.apiOrigin !== "string" || normalizeSkillsApiOrigin(value.apiOrigin) !== value.apiOrigin || !["prepared", "begin_uncertain", "awaiting_upload", "upload_uncertain", "uploaded", "finalize_uncertain", "observed"].includes(value.phase))
|
|
32216
|
+
return fail2();
|
|
32217
|
+
value.declaration = checkedPublicationDeclaration(value.declaration);
|
|
32218
|
+
if (value.intent !== null)
|
|
32219
|
+
value.intent = checkedPublicationView(value.intent, value.skillId, undefined, value.declaration);
|
|
32220
|
+
if (value.intent === null && !["prepared", "begin_uncertain"].includes(value.phase))
|
|
32221
|
+
return fail2();
|
|
32222
|
+
const bytes = readOwned(directory, "bundle.tgz", PRIVATE_PUBLICATION_MAX_BYTES);
|
|
32223
|
+
if (bytes.length !== value.declaration.archiveByteSize || publicationSha256(bytes) !== value.declaration.archiveSha256)
|
|
32224
|
+
return fail2();
|
|
32225
|
+
return { receipt: value, bytes };
|
|
32226
|
+
} catch {
|
|
32227
|
+
return fail2();
|
|
32228
|
+
}
|
|
32229
|
+
}
|
|
32230
|
+
async function preparePrivatePublication(client, sourceDirectory, recoveryDirectory, input) {
|
|
32231
|
+
if (!publicationUuid(input.skillId) || !(input.expectedCurrentVersionId === null || publicationUuid(input.expectedCurrentVersionId)) || input.idempotencyKey !== undefined && !publicationUuid(input.idempotencyKey))
|
|
32232
|
+
return fail2();
|
|
32233
|
+
const packed = packSkillBundle(sourceDirectory, { maxUnpackedBytes: 32 * 1024 * 1024 });
|
|
32234
|
+
const inspected = await inspectSkillBundle(packed.bytes, { limits: { compressedBytes: PRIVATE_PUBLICATION_MAX_BYTES } });
|
|
32235
|
+
const manifest = inspected.entries.find((entry) => entry.path === "skill.json");
|
|
32236
|
+
if (!manifest || manifest.bytes.length > 16384 || !(await verifyContentHashFromEntries(inspected.entries)).valid)
|
|
32237
|
+
throw new PrivatePublicationError("PUBLICATION_BUNDLE_INVALID", "The skill must have a valid skill.json with its current content hash. Validate the skill before publishing.");
|
|
32238
|
+
const manifestText = new TextDecoder("utf-8", { fatal: true }).decode(manifest.bytes);
|
|
32239
|
+
const declaration = checkedPublicationDeclaration({
|
|
32240
|
+
idempotencyKey: input.idempotencyKey ?? randomUUID(),
|
|
32241
|
+
version: JSON.parse(manifestText).version,
|
|
32242
|
+
expectedCurrentVersionId: input.expectedCurrentVersionId,
|
|
32243
|
+
manifestText,
|
|
32244
|
+
archiveSha256: inspected.sha256,
|
|
32245
|
+
archiveByteSize: packed.bytes.length
|
|
32246
|
+
});
|
|
32247
|
+
const receipt = {
|
|
32248
|
+
contractVersion: 1,
|
|
32249
|
+
apiOrigin: client.apiOrigin,
|
|
32250
|
+
organizationId: client.organizationId,
|
|
32251
|
+
userId: client.userId,
|
|
32252
|
+
membershipId: client.membershipId,
|
|
32253
|
+
skillId: input.skillId,
|
|
32254
|
+
declaration,
|
|
32255
|
+
phase: "prepared",
|
|
32256
|
+
intent: null
|
|
32257
|
+
};
|
|
32258
|
+
if (!isAbsolute4(recoveryDirectory) || resolve2(recoveryDirectory) !== recoveryDirectory || realpathSync2(dirname8(recoveryDirectory)) !== dirname8(recoveryDirectory))
|
|
32259
|
+
return fail2();
|
|
32260
|
+
mkdirSync11(recoveryDirectory, { mode: 448 });
|
|
32261
|
+
safeDirectory(recoveryDirectory);
|
|
32262
|
+
const parent = openSync4(dirname8(recoveryDirectory), constants3.O_RDONLY | constants3.O_NOFOLLOW);
|
|
32263
|
+
try {
|
|
32264
|
+
fsyncSync(parent);
|
|
32265
|
+
} finally {
|
|
32266
|
+
closeSync4(parent);
|
|
32267
|
+
}
|
|
32268
|
+
writeOwned(recoveryDirectory, "bundle.tgz", packed.bytes);
|
|
32269
|
+
save(recoveryDirectory, receipt);
|
|
32270
|
+
return receipt;
|
|
32271
|
+
}
|
|
32272
|
+
function privatePublicationResult(directory, receipt) {
|
|
32273
|
+
const state = receipt.intent?.state ?? receipt.phase, committed = state === "committed";
|
|
32274
|
+
const nextAction = committed ? "The version is published. Private execution remains unavailable." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
|
|
32275
|
+
return {
|
|
32276
|
+
recoveryDirectory: directory,
|
|
32277
|
+
skillId: receipt.skillId,
|
|
32278
|
+
intentId: receipt.intent?.id ?? null,
|
|
32279
|
+
state,
|
|
32280
|
+
versionId: receipt.intent?.versionId ?? null,
|
|
32281
|
+
committed,
|
|
32282
|
+
executionEnabled: false,
|
|
32283
|
+
nextAction
|
|
32284
|
+
};
|
|
32285
|
+
}
|
|
32286
|
+
async function continuePrivatePublication(client, directory, options) {
|
|
32287
|
+
if (options.confirm !== true)
|
|
32288
|
+
throw new PrivatePublicationError("PUBLICATION_CONFIRM_REQUIRED", "Explicit upload confirmation is required.");
|
|
32289
|
+
if (options.waitMs !== undefined && (!Number.isSafeInteger(options.waitMs) || options.waitMs < 0 || options.waitMs > 300000))
|
|
32290
|
+
return fail2();
|
|
32291
|
+
return locked(directory, () => continueLocked(client, directory, options));
|
|
32292
|
+
}
|
|
32293
|
+
async function continueLocked(client, directory, options) {
|
|
32294
|
+
const { receipt, bytes } = readPrivatePublicationRecovery(directory);
|
|
32295
|
+
bind(client, receipt);
|
|
32296
|
+
if (!receipt.intent) {
|
|
32297
|
+
receipt.phase = "begin_uncertain";
|
|
32298
|
+
save(directory, receipt);
|
|
32299
|
+
receipt.intent = await client.begin(receipt.skillId, receipt.declaration);
|
|
32300
|
+
receipt.phase = "awaiting_upload";
|
|
32301
|
+
save(directory, receipt);
|
|
32302
|
+
} else {
|
|
32303
|
+
receipt.intent = checkedPublicationView(await client.get(receipt.skillId, receipt.intent.id), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
32304
|
+
save(directory, receipt);
|
|
32305
|
+
}
|
|
32306
|
+
if (receipt.intent.state === "awaiting_upload") {
|
|
32307
|
+
if (receipt.phase === "awaiting_upload") {
|
|
32308
|
+
receipt.phase = "upload_uncertain";
|
|
32309
|
+
save(directory, receipt);
|
|
32310
|
+
try {
|
|
32311
|
+
await client.upload(receipt.skillId, receipt.intent, bytes);
|
|
32312
|
+
} catch (error2) {
|
|
32313
|
+
if (error2 instanceof PrivatePublicationError && !error2.uncertain) {
|
|
32314
|
+
receipt.phase = "awaiting_upload";
|
|
32315
|
+
save(directory, receipt);
|
|
32316
|
+
}
|
|
32317
|
+
throw error2;
|
|
32318
|
+
}
|
|
32319
|
+
receipt.phase = "uploaded";
|
|
32320
|
+
save(directory, receipt);
|
|
32321
|
+
}
|
|
32322
|
+
receipt.phase = "finalize_uncertain";
|
|
32323
|
+
save(directory, receipt);
|
|
32324
|
+
receipt.intent = checkedPublicationView(await client.finalize(receipt.skillId, receipt.intent.id), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
32325
|
+
receipt.phase = "observed";
|
|
32326
|
+
save(directory, receipt);
|
|
32327
|
+
}
|
|
32328
|
+
if (options.waitMs !== undefined && options.waitMs > 0 && ["queued", "verifying"].includes(receipt.intent.state)) {
|
|
32329
|
+
receipt.intent = checkedPublicationView(await client.wait(receipt.skillId, receipt.intent.id, { timeoutMs: options.waitMs }), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
32330
|
+
receipt.phase = "observed";
|
|
32331
|
+
save(directory, receipt);
|
|
32332
|
+
}
|
|
32333
|
+
return privatePublicationResult(directory, receipt);
|
|
32334
|
+
}
|
|
32335
|
+
async function inspectPrivatePublication(client, directory, cancel = false) {
|
|
32336
|
+
return locked(directory, () => inspectLocked(client, directory, cancel));
|
|
32337
|
+
}
|
|
32338
|
+
async function inspectLocked(client, directory, cancel) {
|
|
32339
|
+
const { receipt } = readPrivatePublicationRecovery(directory);
|
|
32340
|
+
bind(client, receipt);
|
|
32341
|
+
if (receipt.intent) {
|
|
32342
|
+
receipt.intent = checkedPublicationView(await (cancel ? client.cancel(receipt.skillId, receipt.intent.id) : client.get(receipt.skillId, receipt.intent.id)), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
32343
|
+
save(directory, receipt);
|
|
32344
|
+
} else if (cancel)
|
|
32345
|
+
throw new PrivatePublicationError("PUBLICATION_INTENT_UNKNOWN", "Reconcile the saved begin request with publication resume before cancelling its intent.");
|
|
32346
|
+
return privatePublicationResult(directory, receipt);
|
|
32347
|
+
}
|
|
32348
|
+
var fail2 = () => {
|
|
32349
|
+
throw new PrivatePublicationError("PUBLICATION_RECOVERY_INVALID", "The recovery directory is invalid or changed. Preserve it and inspect the existing intent; do not start a replacement automatically.");
|
|
32350
|
+
};
|
|
32351
|
+
var init_private_publication_recovery = __esm(() => {
|
|
32352
|
+
init_skill_bundle();
|
|
32353
|
+
init_skill_hash();
|
|
32354
|
+
init_remote_private_publications();
|
|
32355
|
+
init_fleet_credentials();
|
|
32356
|
+
});
|
|
32357
|
+
|
|
32358
|
+
// src/mcp/private-publication-tools.ts
|
|
32359
|
+
import { isAbsolute as isAbsolute5 } from "path";
|
|
32360
|
+
function registerPrivatePublicationTools(server) {
|
|
32361
|
+
const uuid6 = exports_external.string().regex(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/);
|
|
32362
|
+
const directory = exports_external.string().max(4096).refine(isAbsolute5, "Use an absolute local directory");
|
|
32363
|
+
const verification = { email: exports_external.string().email().max(254), code: exports_external.string().regex(/^\d{6}$/), userId: uuid6, membershipId: uuid6, recoveryDirectory: directory };
|
|
32364
|
+
const description = "Manage a private source publication using fresh workspace-bound verification and a host-local recovery directory. Publishing requires explicit UUID current-version comparison and upload consent. Resume reuses immutable saved bytes and intent; an uncertain PUT is finalized for server verification, never uploaded twice. Sessions and signed URLs are not returned or persisted. Private execution is unavailable. Host request history may contain the supplied verification code.";
|
|
32365
|
+
const handler = (action) => async (value) => {
|
|
32366
|
+
try {
|
|
32367
|
+
const client = await privatePublicationSession(String(value.email), String(value.code), { userId: String(value.userId), membershipId: String(value.membershipId) });
|
|
32368
|
+
const recovery = String(value.recoveryDirectory);
|
|
32369
|
+
if (action === "publish")
|
|
32370
|
+
await preparePrivatePublication(client, String(value.directory), recovery, { skillId: String(value.skillId), expectedCurrentVersionId: value.expectedCurrentVersionId, idempotencyKey: value.idempotencyKey });
|
|
32371
|
+
const result = action === "publish" || action === "resume" ? await continuePrivatePublication(client, recovery, { confirm: true, waitMs: value.waitMs }) : await inspectPrivatePublication(client, recovery, action === "cancel");
|
|
32372
|
+
return mcpJson(result);
|
|
32373
|
+
} catch (error2) {
|
|
32374
|
+
const result = privatePublicationCustomerError(error2);
|
|
32375
|
+
return mcpError(result.code, result.error);
|
|
32376
|
+
}
|
|
32377
|
+
};
|
|
32378
|
+
server.registerTool("publish_private_skill", {
|
|
32379
|
+
title: "Publish private skill",
|
|
32380
|
+
description,
|
|
32381
|
+
annotations: { destructiveHint: true, readOnlyHint: false, idempotentHint: false },
|
|
32382
|
+
inputSchema: exports_external.object({ ...verification, directory, skillId: uuid6, expectedCurrentVersionId: uuid6.nullable(), idempotencyKey: uuid6.optional(), confirm: exports_external.literal(true), waitMs: exports_external.number().int().min(0).max(300000).optional() }).strict()
|
|
32383
|
+
}, handler("publish"));
|
|
32384
|
+
server.registerTool("get_private_publication", {
|
|
32385
|
+
title: "Get private publication",
|
|
32386
|
+
description,
|
|
32387
|
+
annotations: { destructiveHint: false, readOnlyHint: true, idempotentHint: true },
|
|
32388
|
+
inputSchema: exports_external.object(verification).strict()
|
|
32389
|
+
}, handler("get"));
|
|
32390
|
+
server.registerTool("resume_private_publication", {
|
|
32391
|
+
title: "Resume private publication",
|
|
32392
|
+
description,
|
|
32393
|
+
annotations: { destructiveHint: true, readOnlyHint: false, idempotentHint: true },
|
|
32394
|
+
inputSchema: exports_external.object({ ...verification, confirm: exports_external.literal(true), waitMs: exports_external.number().int().min(0).max(300000).optional() }).strict()
|
|
32395
|
+
}, handler("resume"));
|
|
32396
|
+
server.registerTool("cancel_private_publication", {
|
|
32397
|
+
title: "Cancel private publication",
|
|
32398
|
+
description,
|
|
32399
|
+
annotations: { destructiveHint: true, readOnlyHint: false, idempotentHint: true },
|
|
32400
|
+
inputSchema: exports_external.object({ ...verification, confirm: exports_external.literal(true) }).strict()
|
|
32401
|
+
}, handler("cancel"));
|
|
32402
|
+
}
|
|
32403
|
+
var init_private_publication_tools = __esm(() => {
|
|
32404
|
+
init_zod();
|
|
32405
|
+
init_private_publication_customer();
|
|
32406
|
+
init_private_publication_recovery();
|
|
32407
|
+
init_helpers();
|
|
32408
|
+
});
|
|
32409
|
+
|
|
30916
32410
|
// src/mcp/remote-customer-tools.ts
|
|
30917
32411
|
function registerRemoteCustomerTools(server) {
|
|
30918
32412
|
registerRemoteInvitationTools(server);
|
|
32413
|
+
registerPrivatePublicationTools(server);
|
|
30919
32414
|
const memberRole = exports_external.enum(["owner", "admin", "member", "viewer"]);
|
|
30920
32415
|
const memberInput = {
|
|
30921
32416
|
membershipId: exports_external.string().regex(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/),
|
|
@@ -31033,8 +32528,16 @@ function registerRemoteCustomerTools(server) {
|
|
|
31033
32528
|
server.registerTool("quote_skill", {
|
|
31034
32529
|
title: "Quote Remote Skill",
|
|
31035
32530
|
description: "Get the configured server's credit quote without submitting a run.",
|
|
31036
|
-
inputSchema: {
|
|
31037
|
-
|
|
32531
|
+
inputSchema: {
|
|
32532
|
+
name: exports_external.string(),
|
|
32533
|
+
input: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
|
|
32534
|
+
args: exports_external.array(exports_external.string()).optional(),
|
|
32535
|
+
files: exports_external.array(exports_external.object({ name: exports_external.string(), base64: exports_external.string().max(1398104), contentType: exports_external.string().optional() })).max(10).optional().describe("The same inline files to submit after approval; quoted descriptors bind their exact bytes, names and types.")
|
|
32536
|
+
}
|
|
32537
|
+
}, ({ name, input, args, files }) => callRemote((client) => {
|
|
32538
|
+
const descriptors = describeRemoteFiles(decodeRemoteFiles(files ?? []));
|
|
32539
|
+
return client.quoteRun(name, input, args, descriptors.length ? descriptors : undefined);
|
|
32540
|
+
}));
|
|
31038
32541
|
server.registerTool("download_run_artifact", {
|
|
31039
32542
|
title: "Download Verified Run Artifact",
|
|
31040
32543
|
description: "Return verified artifact bytes as base64 (at most 1 MiB); use the CLI for larger files.",
|
|
@@ -31068,6 +32571,7 @@ async function freshAccount(action, operation) {
|
|
|
31068
32571
|
}
|
|
31069
32572
|
var init_remote_customer_tools = __esm(() => {
|
|
31070
32573
|
init_remote_invitation_tools();
|
|
32574
|
+
init_private_publication_tools();
|
|
31071
32575
|
init_zod();
|
|
31072
32576
|
init_remote_auth();
|
|
31073
32577
|
init_workspace_profile();
|
|
@@ -31075,6 +32579,7 @@ var init_remote_customer_tools = __esm(() => {
|
|
|
31075
32579
|
init_remote_client();
|
|
31076
32580
|
init_remote_workspace_leave();
|
|
31077
32581
|
init_helpers();
|
|
32582
|
+
init_remote_files();
|
|
31078
32583
|
});
|
|
31079
32584
|
|
|
31080
32585
|
// src/mcp/server.ts
|
|
@@ -31488,7 +32993,7 @@ var RequestError, toRequestError = (e) => {
|
|
|
31488
32993
|
});
|
|
31489
32994
|
if (!chunk) {
|
|
31490
32995
|
if (i === 1) {
|
|
31491
|
-
await new Promise((
|
|
32996
|
+
await new Promise((resolve3) => setTimeout(resolve3));
|
|
31492
32997
|
maxReadCount = 3;
|
|
31493
32998
|
continue;
|
|
31494
32999
|
}
|
|
@@ -32323,9 +33828,9 @@ data:
|
|
|
32323
33828
|
const initRequest = messages.find((m) => isInitializeRequest(m));
|
|
32324
33829
|
const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
|
|
32325
33830
|
if (this._enableJsonResponse) {
|
|
32326
|
-
return new Promise((
|
|
33831
|
+
return new Promise((resolve3) => {
|
|
32327
33832
|
this._streamMapping.set(streamId, {
|
|
32328
|
-
resolveJson:
|
|
33833
|
+
resolveJson: resolve3,
|
|
32329
33834
|
cleanup: () => {
|
|
32330
33835
|
this._streamMapping.delete(streamId);
|
|
32331
33836
|
}
|
|
@@ -32694,19 +34199,19 @@ async function connectMcpForNode() {
|
|
|
32694
34199
|
return { server: server2, transport };
|
|
32695
34200
|
}
|
|
32696
34201
|
function readBody(req) {
|
|
32697
|
-
return new Promise((
|
|
34202
|
+
return new Promise((resolve3, reject) => {
|
|
32698
34203
|
const chunks = [];
|
|
32699
34204
|
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
32700
34205
|
req.on("end", () => {
|
|
32701
34206
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
32702
34207
|
if (!raw.trim()) {
|
|
32703
|
-
|
|
34208
|
+
resolve3(undefined);
|
|
32704
34209
|
return;
|
|
32705
34210
|
}
|
|
32706
34211
|
try {
|
|
32707
|
-
|
|
34212
|
+
resolve3(JSON.parse(raw));
|
|
32708
34213
|
} catch {
|
|
32709
|
-
|
|
34214
|
+
resolve3(undefined);
|
|
32710
34215
|
}
|
|
32711
34216
|
});
|
|
32712
34217
|
req.on("error", reject);
|
|
@@ -32748,9 +34253,9 @@ async function startSkillsMcpHttpServer(options = {}) {
|
|
|
32748
34253
|
}
|
|
32749
34254
|
}
|
|
32750
34255
|
});
|
|
32751
|
-
await new Promise((
|
|
34256
|
+
await new Promise((resolve3, reject) => {
|
|
32752
34257
|
httpServer.once("error", reject);
|
|
32753
|
-
httpServer.listen(port, hostname2, () =>
|
|
34258
|
+
httpServer.listen(port, hostname2, () => resolve3());
|
|
32754
34259
|
});
|
|
32755
34260
|
const address = httpServer.address();
|
|
32756
34261
|
const listenPort = typeof address === "object" && address ? address.port : port;
|