buckyos 0.7.164 → 0.7.168

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/cli/dist/cli.mjs CHANGED
@@ -614,6 +614,8 @@ function collectArgumentPaths(argv) {
614
614
  } else if (["input", "session-token-file", "pikg"].includes(name)) {
615
615
  if (value !== "-")
616
616
  read.push(value);
617
+ } else if (name === "catalog" && module === "pikg") {
618
+ read.push(value);
617
619
  } else if (name === "source" && module === "pikg") {
618
620
  read.push(value);
619
621
  } else if (name === "plan") {
@@ -3331,47 +3333,145 @@ async function gzipFile(source, destination) {
3331
3333
  }
3332
3334
  async function readTarGz(bytes, limit) {
3333
3335
  const tar = await gunzipBytes(bytes, limit);
3334
- const decoder = new TextDecoder("utf-8", { fatal: true });
3335
- const text2 = (offset2, length) => decoder.decode(tar.subarray(offset2, offset2 + length)).replace(/\0.*$/s, "");
3336
3336
  const files = [];
3337
3337
  const seen = /* @__PURE__ */ new Set();
3338
3338
  let offset = 0;
3339
3339
  while (offset + BLOCK_SIZE <= tar.length && tar.subarray(offset, offset + BLOCK_SIZE).some(Boolean)) {
3340
- let raw;
3341
- let size;
3342
- let mode;
3343
- try {
3344
- const prefix = text2(offset + 257, 6) === "ustar" ? text2(offset + 345, 155) : "";
3345
- raw = prefix ? `${prefix}/${text2(offset, 100)}` : text2(offset, 100);
3346
- size = Number.parseInt(text2(offset + 124, 12).trim() || "0", 8);
3347
- mode = Number.parseInt(text2(offset + 100, 8).trim() || "0", 8);
3348
- } catch {
3349
- throw invalidZip("payload", "tar header is not valid UTF-8");
3350
- }
3351
- const type = String.fromCharCode(tar[offset + 156] || 48);
3352
- const path = raw.replace(/^(?:\.\/)+/, "").replace(/\/$/, "");
3340
+ const header = parseTarHeader(tar.subarray(offset, offset + BLOCK_SIZE));
3353
3341
  const start = offset + BLOCK_SIZE;
3354
- if (!Number.isSafeInteger(size) || size < 0 || start + size > tar.length) {
3355
- throw invalidZip("payload", `tar entry is truncated: ${raw}`);
3356
- }
3357
- offset = start + Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE;
3358
- if (type === "5" && (path === "" || path === "."))
3359
- continue;
3360
- if (!path || path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.includes("\\") || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
3361
- throw invalidZip("payload", `unsafe tar entry path: ${JSON.stringify(raw)}`);
3342
+ if (start + header.size > tar.length) {
3343
+ throw invalidZip("payload", `tar entry is truncated: ${header.path}`);
3362
3344
  }
3363
- if (type === "5")
3345
+ offset = start + Math.ceil(header.size / BLOCK_SIZE) * BLOCK_SIZE;
3346
+ if (header.kind !== "file")
3364
3347
  continue;
3365
- if (type !== "0") {
3366
- throw invalidZip("payload", `tar entry is not a regular file or directory: ${path}`);
3367
- }
3368
- if (seen.has(path))
3369
- throw invalidZip("payload", `duplicate tar entry: ${path}`);
3370
- seen.add(path);
3371
- files.push({ path, executable: (mode & 73) !== 0, bytes: tar.slice(start, start + size) });
3348
+ if (seen.has(header.path))
3349
+ throw invalidZip("payload", `duplicate tar entry: ${header.path}`);
3350
+ seen.add(header.path);
3351
+ files.push({ path: header.path, executable: header.executable, bytes: tar.slice(start, start + header.size) });
3372
3352
  }
3373
3353
  return files;
3374
3354
  }
3355
+ function parseTarHeader(block) {
3356
+ const decoder = new TextDecoder("utf-8", { fatal: true });
3357
+ const text2 = (offset, length) => decoder.decode(block.subarray(offset, offset + length)).replace(/\0.*$/s, "");
3358
+ let raw;
3359
+ let size;
3360
+ let mode;
3361
+ try {
3362
+ const prefix = text2(257, 6) === "ustar" ? text2(345, 155) : "";
3363
+ raw = prefix ? `${prefix}/${text2(0, 100)}` : text2(0, 100);
3364
+ size = Number.parseInt(text2(124, 12).trim() || "0", 8);
3365
+ mode = Number.parseInt(text2(100, 8).trim() || "0", 8);
3366
+ } catch {
3367
+ throw invalidZip("payload", "tar header is not valid UTF-8");
3368
+ }
3369
+ if (!Number.isSafeInteger(size) || size < 0) {
3370
+ throw invalidZip("payload", `tar entry is truncated: ${raw}`);
3371
+ }
3372
+ const type = String.fromCharCode(block[156] || 48);
3373
+ const path = raw.replace(/^(?:\.\/)+/, "").replace(/\/$/, "");
3374
+ if (type === "5" && (path === "" || path === ".")) {
3375
+ return { path, kind: "root", size, executable: false };
3376
+ }
3377
+ if (!path || path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.includes("\\") || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
3378
+ throw invalidZip("payload", `unsafe tar entry path: ${JSON.stringify(raw)}`);
3379
+ }
3380
+ if (type !== "5" && type !== "0") {
3381
+ throw invalidZip("payload", `tar entry is not a regular file or directory: ${path}`);
3382
+ }
3383
+ return { path, kind: type === "5" ? "directory" : "file", size, executable: (mode & 73) !== 0 };
3384
+ }
3385
+ async function listTarGzStream(gzip, limit, capture2) {
3386
+ const listing = { files: /* @__PURE__ */ new Map(), directories: /* @__PURE__ */ new Set(), captured: /* @__PURE__ */ new Map() };
3387
+ const header = new Uint8Array(BLOCK_SIZE);
3388
+ let headerFill = 0;
3389
+ let skip = 0;
3390
+ let body;
3391
+ let ended = false;
3392
+ let total = 0;
3393
+ try {
3394
+ const stream = gzip.pipeThrough(
3395
+ new DecompressionStream("gzip")
3396
+ );
3397
+ for await (const chunk of stream) {
3398
+ total += chunk.length;
3399
+ if (total > limit)
3400
+ throw invalidZip("limits", "payload expands beyond the size limit");
3401
+ let offset = 0;
3402
+ while (offset < chunk.length && !ended) {
3403
+ if (skip > 0) {
3404
+ const take2 = Math.min(skip, chunk.length - offset);
3405
+ if (body && body.fill < body.bytes.length) {
3406
+ const part = Math.min(take2, body.bytes.length - body.fill);
3407
+ body.bytes.set(chunk.subarray(offset, offset + part), body.fill);
3408
+ body.fill += part;
3409
+ if (body.fill === body.bytes.length) {
3410
+ listing.captured.set(body.path, body.bytes);
3411
+ body = void 0;
3412
+ }
3413
+ }
3414
+ skip -= take2;
3415
+ offset += take2;
3416
+ continue;
3417
+ }
3418
+ const take = Math.min(BLOCK_SIZE - headerFill, chunk.length - offset);
3419
+ header.set(chunk.subarray(offset, offset + take), headerFill);
3420
+ headerFill += take;
3421
+ offset += take;
3422
+ if (headerFill < BLOCK_SIZE)
3423
+ break;
3424
+ headerFill = 0;
3425
+ if (!header.some(Boolean)) {
3426
+ ended = true;
3427
+ break;
3428
+ }
3429
+ const entry = parseTarHeader(header);
3430
+ skip = Math.ceil(entry.size / BLOCK_SIZE) * BLOCK_SIZE;
3431
+ if (entry.kind === "directory")
3432
+ listing.directories.add(entry.path);
3433
+ if (entry.kind !== "file")
3434
+ continue;
3435
+ if (listing.files.has(entry.path)) {
3436
+ throw invalidZip("payload", `duplicate tar entry: ${entry.path}`);
3437
+ }
3438
+ listing.files.set(entry.path, { size: entry.size, executable: entry.executable });
3439
+ if (capture2(entry.path, entry.size)) {
3440
+ if (entry.size === 0)
3441
+ listing.captured.set(entry.path, new Uint8Array());
3442
+ else
3443
+ body = { path: entry.path, bytes: new Uint8Array(entry.size), fill: 0 };
3444
+ }
3445
+ }
3446
+ }
3447
+ } catch (error) {
3448
+ if (error instanceof ToolError)
3449
+ throw error;
3450
+ throw invalidZip("compression", "payload is not a valid gzip stream");
3451
+ }
3452
+ if (skip > 0 || headerFill > 0 || body)
3453
+ throw invalidZip("payload", "tar stream is truncated");
3454
+ return listing;
3455
+ }
3456
+ function fileReadableStream(path) {
3457
+ let file;
3458
+ return new ReadableStream({
3459
+ async pull(controller) {
3460
+ file ??= await getHost().open(path, { read: true });
3461
+ const buffer = new Uint8Array(256 * 1024);
3462
+ const read = await file.read(buffer);
3463
+ if (read === null) {
3464
+ await file.close();
3465
+ controller.close();
3466
+ return;
3467
+ }
3468
+ controller.enqueue(buffer.subarray(0, read));
3469
+ },
3470
+ async cancel() {
3471
+ await file?.close();
3472
+ }
3473
+ });
3474
+ }
3375
3475
  async function gunzipBytes(bytes, limit) {
3376
3476
  const stream = new ReadableStream({
3377
3477
  start(controller) {
@@ -4163,11 +4263,10 @@ function derivedSelector(key) {
4163
4263
  web: {},
4164
4264
  agent: {},
4165
4265
  agent_skills: {},
4166
- agent_tools: {},
4167
4266
  aiws: {},
4168
4267
  script: {}
4169
4268
  };
4170
- return selectors[key];
4269
+ return selectors[key] ?? extToolsPlatformOfKey(key)?.selector;
4171
4270
  }
4172
4271
  function canonicalSelector(value, label) {
4173
4272
  if (value === void 0)
@@ -4505,6 +4604,168 @@ async function verifyAiwsPayload(tarGz) {
4505
4604
  templates: checked.manifest.workspace_templates.map((t) => ({ id: t.id, title: t.title, digest: t.digest, requires: t.requires }))
4506
4605
  };
4507
4606
  }
4607
+ const EXT_TOOLS_APP_TYPE = "ext_tools";
4608
+ const EXT_TOOLS_MANIFEST_FILE = "ext-manifest.json";
4609
+ const EXT_TOOLS_MANIFEST_SCHEMA = "buckyos.ext_tools/1";
4610
+ const MAX_EXT_TOOLS_PAYLOAD_BYTES = 64 * 1024 * 1024 * 1024;
4611
+ const MAX_EXT_TOOLS_LAUNCHER_BYTES = 1024 * 1024;
4612
+ const EXT_TOOLS_ID = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
4613
+ const EXT_TOOLS_COMMAND = /^[A-Za-z0-9_+-][A-Za-z0-9._+-]*$/;
4614
+ const EXT_TOOLS_WINDOWS_SUFFIXES = ["", ".exe", ".cmd", ".bat", ".ps1"];
4615
+ const BUILD_MACHINE_PATH = /(?:^|[\s"'=:(])(\/home\/|\/Users\/|\/root\/|\/tmp\/|\/var\/folders\/|\/opt\/buckyos\/|[A-Za-z]:\\\\?Users\\)/;
4616
+ const EXT_TOOLS_PLATFORMS = [
4617
+ { name: "linux-x86_64", key: "ext_tools_linux_x86_64", selector: { os: "linux", arch: "x86_64" } },
4618
+ { name: "linux-aarch64", key: "ext_tools_linux_aarch64", selector: { os: "linux", arch: "aarch64" } },
4619
+ { name: "darwin-arm64", key: "ext_tools_darwin_arm64", selector: { os: "macos", arch: "aarch64" } },
4620
+ { name: "darwin-x86_64", key: "ext_tools_darwin_x86_64", selector: { os: "macos", arch: "x86_64" } },
4621
+ { name: "windows-x86_64", key: "ext_tools_windows_x86_64", selector: { os: "windows", arch: "x86_64" } }
4622
+ ];
4623
+ function extToolsPlatformOfKey(key) {
4624
+ return EXT_TOOLS_PLATFORMS.find((platform) => platform.key === key);
4625
+ }
4626
+ function extToolsManifestErrors(value) {
4627
+ if (!value || typeof value !== "object" || Array.isArray(value))
4628
+ return ["must be a JSON object"];
4629
+ const manifest = value;
4630
+ const errors = [];
4631
+ const allowed = ["schema", "id", "version", "requires", "conflicts", "provides", "platforms", "size_bytes", "license", "desc"];
4632
+ for (const key of Object.keys(manifest))
4633
+ if (!allowed.includes(key))
4634
+ errors.push(`${key} is not a field of the manifest`);
4635
+ if (manifest.schema !== EXT_TOOLS_MANIFEST_SCHEMA)
4636
+ errors.push(`schema must be ${EXT_TOOLS_MANIFEST_SCHEMA}`);
4637
+ if (typeof manifest.id !== "string" || !EXT_TOOLS_ID.test(manifest.id))
4638
+ errors.push("id must be lowercase letters, digits and -");
4639
+ if (typeof manifest.version !== "string" || !manifest.version.trim())
4640
+ errors.push("version is required");
4641
+ if (!Array.isArray(manifest.provides) || manifest.provides.length === 0)
4642
+ errors.push("provides must not be empty");
4643
+ const commands = /* @__PURE__ */ new Set();
4644
+ for (const raw of Array.isArray(manifest.provides) ? manifest.provides : []) {
4645
+ const entry = raw ?? {};
4646
+ const command = entry.command;
4647
+ if (typeof command !== "string" || !EXT_TOOLS_COMMAND.test(command)) {
4648
+ errors.push(`provides: ${JSON.stringify(command)} is not a command name`);
4649
+ continue;
4650
+ }
4651
+ if (commands.has(command))
4652
+ errors.push(`provides: command ${command} is listed twice`);
4653
+ commands.add(command);
4654
+ if (typeof entry.version_probe !== "string" || !entry.version_probe.trim())
4655
+ errors.push(`provides: ${command} has no version_probe`);
4656
+ const smoke = entry.smoke;
4657
+ if (smoke !== void 0 && (typeof smoke !== "string" || !isSafeRelativePath(smoke))) {
4658
+ errors.push(`provides: the smoke of ${command} must be a relative path inside the payload`);
4659
+ }
4660
+ }
4661
+ if (!Array.isArray(manifest.platforms) || manifest.platforms.length === 0)
4662
+ errors.push("platforms must not be empty");
4663
+ for (const platform of Array.isArray(manifest.platforms) ? manifest.platforms : []) {
4664
+ if (!EXT_TOOLS_PLATFORMS.some((known) => known.name === platform))
4665
+ errors.push(`unknown platform ${JSON.stringify(platform)}`);
4666
+ }
4667
+ for (const raw of Array.isArray(manifest.requires) ? manifest.requires : manifest.requires === void 0 ? [] : [null]) {
4668
+ const required = raw ?? {};
4669
+ if (typeof required.id !== "string" || !EXT_TOOLS_ID.test(required.id) || required.id === manifest.id || typeof required.version !== "string") {
4670
+ errors.push(`requires: ${JSON.stringify(raw)} is not another package's {id, version}`);
4671
+ }
4672
+ }
4673
+ if (manifest.conflicts !== void 0 && !(Array.isArray(manifest.conflicts) && manifest.conflicts.every((id) => typeof id === "string" && id !== manifest.id))) {
4674
+ errors.push("conflicts must list other packages by id");
4675
+ }
4676
+ return errors;
4677
+ }
4678
+ function isSafeRelativePath(path) {
4679
+ return !path.includes("\\") && !path.startsWith("/") && path.split("/").every((part) => part && part !== "." && part !== "..");
4680
+ }
4681
+ function parseExtToolsManifest(bytes) {
4682
+ let value;
4683
+ try {
4684
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
4685
+ } catch (error) {
4686
+ throw invalid("ext_tools", `${EXT_TOOLS_MANIFEST_FILE} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
4687
+ }
4688
+ const errors = extToolsManifestErrors(value);
4689
+ if (errors.length > 0)
4690
+ throw invalid("ext_tools", `${EXT_TOOLS_MANIFEST_FILE}: ${errors.join("; ")}`);
4691
+ const manifest = value;
4692
+ manifest.requires ??= [];
4693
+ manifest.conflicts ??= [];
4694
+ return manifest;
4695
+ }
4696
+ function extToolsPayloadErrors(manifest, platform, tree, buildPaths = []) {
4697
+ const errors = [];
4698
+ const windows = platform.startsWith("windows-");
4699
+ const suffixes = windows ? EXT_TOOLS_WINDOWS_SUFFIXES : [""];
4700
+ for (const path of [...tree.files.keys(), ...tree.directories]) {
4701
+ const [top, second] = path.split("/");
4702
+ const isTop = !path.includes("/");
4703
+ const known = top === EXT_TOOLS_MANIFEST_FILE && isTop || top === "bin" || top === "smoke" || top === "store";
4704
+ if (!known)
4705
+ errors.push(`${path}: a package only holds ${EXT_TOOLS_MANIFEST_FILE}, bin/, smoke/ and store/${manifest.id}/`);
4706
+ if (top === "store" && second !== void 0 && second !== manifest.id)
4707
+ errors.push(`${path}: a package only writes store/${manifest.id}/`);
4708
+ }
4709
+ const declared = new Set(manifest.provides.map((provided) => provided.command));
4710
+ for (const provided of manifest.provides) {
4711
+ const launcher = suffixes.map((suffix) => tree.files.get(`bin/${provided.command}${suffix}`)).find(Boolean);
4712
+ if (!launcher)
4713
+ errors.push(`bin/${provided.command}: the launcher of a provided command is missing`);
4714
+ else if (!windows && !launcher.executable)
4715
+ errors.push(`bin/${provided.command}: the launcher is not executable`);
4716
+ if (provided.smoke && !tree.files.has(provided.smoke))
4717
+ errors.push(`${provided.smoke}: the smoke script of ${provided.command} is missing`);
4718
+ }
4719
+ for (const path of tree.files.keys()) {
4720
+ if (!path.startsWith("bin/"))
4721
+ continue;
4722
+ const name = path.slice("bin/".length);
4723
+ const command = suffixes.map((suffix) => suffix && name.endsWith(suffix) ? name.slice(0, -suffix.length) : name).find((candidate) => declared.has(candidate));
4724
+ if (!command)
4725
+ errors.push(`${path}: not in provides, so nothing knows this package provides it`);
4726
+ }
4727
+ const decoder = new TextDecoder();
4728
+ for (const [path, bytes] of tree.launchers) {
4729
+ const text2 = decoder.decode(bytes);
4730
+ const named = buildPaths.find((dir) => text2.includes(dir));
4731
+ if (named)
4732
+ errors.push(`${path}: names ${named} on the machine that built it`);
4733
+ else if (BUILD_MACHINE_PATH.test(text2))
4734
+ errors.push(`${path}: names an absolute path of the machine that built it (${BUILD_MACHINE_PATH.exec(text2)?.[1]})`);
4735
+ }
4736
+ return errors;
4737
+ }
4738
+ function assertExtToolsOf(appDoc, manifest, platform, platforms) {
4739
+ if (manifest.id !== appDoc.name)
4740
+ throw invalid("ext_tools", `${EXT_TOOLS_MANIFEST_FILE} id ${manifest.id} is not the App name ${appDoc.name}`);
4741
+ if (manifest.version !== appDoc.version)
4742
+ throw invalid("ext_tools", `${EXT_TOOLS_MANIFEST_FILE} version ${manifest.version} is not the App version ${appDoc.version}`);
4743
+ if (!manifest.platforms.includes(platform))
4744
+ throw invalid("ext_tools", `${EXT_TOOLS_MANIFEST_FILE} does not list the payload's platform ${platform}`);
4745
+ const missing = manifest.platforms.filter((name) => !platforms.includes(name));
4746
+ if (missing.length > 0)
4747
+ throw invalid("ext_tools", `${EXT_TOOLS_MANIFEST_FILE} lists platforms without a payload: ${missing.join(", ")}`);
4748
+ }
4749
+ async function verifyExtToolsPayload(gzip, key, appDoc) {
4750
+ const platform = extToolsPlatformOfKey(key);
4751
+ if (!platform)
4752
+ throw invalid("appdoc", `an ext_tools package has no sub package ${key}`);
4753
+ const listing = await listTarGzStream(
4754
+ gzip,
4755
+ MAX_EXT_TOOLS_PAYLOAD_BYTES,
4756
+ (path, size) => path === EXT_TOOLS_MANIFEST_FILE || path.startsWith("bin/") && size <= MAX_EXT_TOOLS_LAUNCHER_BYTES
4757
+ );
4758
+ const raw = listing.captured.get(EXT_TOOLS_MANIFEST_FILE);
4759
+ if (!raw)
4760
+ throw invalid("ext_tools", `${key}: ${EXT_TOOLS_MANIFEST_FILE} is not at the root of the payload`);
4761
+ const manifest = parseExtToolsManifest(raw);
4762
+ assertExtToolsOf(appDoc, manifest, platform.name, Object.keys(expectObject$3(appDoc.pkg_list, "APPDOC.pkg_list")).flatMap((k) => extToolsPlatformOfKey(k)?.name ?? []));
4763
+ const launchers = new Map([...listing.captured].filter(([path]) => path.startsWith("bin/")));
4764
+ const errors = extToolsPayloadErrors(manifest, platform.name, { files: listing.files, directories: listing.directories, launchers });
4765
+ if (errors.length > 0)
4766
+ throw invalid("ext_tools", `${key}: ${errors.join("; ")}`);
4767
+ return manifest;
4768
+ }
4508
4769
  async function validateSnapshot(distDir, appDoc, packageMeta) {
4509
4770
  const payloads = /* @__PURE__ */ new Map();
4510
4771
  for (const entryValue of Object.values(packageMeta.content_index)) {
@@ -4518,6 +4779,10 @@ async function validateSnapshot(distDir, appDoc, packageMeta) {
4518
4779
  for (const sub of validated.subpackages)
4519
4780
  await verifyAiwsPayload(await getHost().readFile(join$1(distDir, sub.payload_path)));
4520
4781
  }
4782
+ if (appDoc.app_type === EXT_TOOLS_APP_TYPE) {
4783
+ for (const sub of validated.subpackages)
4784
+ await verifyExtToolsPayload(fileReadableStream(join$1(distDir, sub.payload_path)), sub.key, appDoc);
4785
+ }
4521
4786
  return validated;
4522
4787
  }
4523
4788
  async function packSnapshot(distDir, destination, appDoc, packageMeta) {
@@ -4628,10 +4893,18 @@ async function inspectPikg(path) {
4628
4893
  const sub = validated.subpackages[0];
4629
4894
  aiws2 = await verifyAiwsPayload(await readZipEntry(archive, archive.byName.get(sub.payload_path), MAX_AIWS_PAYLOAD_BYTES));
4630
4895
  }
4896
+ let extTools;
4897
+ if (appDoc.app_type === EXT_TOOLS_APP_TYPE) {
4898
+ for (const sub of validated.subpackages) {
4899
+ const manifest = await verifyExtToolsPayload(zipEntryStream(archive.path, archive.byName.get(sub.payload_path)), sub.key, appDoc);
4900
+ extTools ??= { ...manifest };
4901
+ }
4902
+ }
4631
4903
  const pikgDigest = await digestFile(path);
4632
4904
  return {
4633
4905
  schema_version: 1,
4634
4906
  ...aiws2 ? { aiws: aiws2 } : {},
4907
+ ...extTools ? { ext_tools: extTools } : {},
4635
4908
  protocol: PACKAGE_META_SCHEMA,
4636
4909
  pikg_path: resolve$2(path),
4637
4910
  size: pikgDigest.size,
@@ -4928,7 +5201,7 @@ function validateAppDocShape(appDoc) {
4928
5201
  }
4929
5202
  if (appDoc.doc_type !== "app")
4930
5203
  throw invalid("appdoc", "APPDOC.doc_type must be app");
4931
- if (!["service", "dapp", "web", "agent", "agent_skills", "aiws"].includes(String(appDoc.app_type))) {
5204
+ if (!["service", "dapp", "web", "agent", "agent_skills", "aiws", EXT_TOOLS_APP_TYPE].includes(String(appDoc.app_type))) {
4932
5205
  throw invalid("appdoc", "APPDOC.app_type is invalid");
4933
5206
  }
4934
5207
  if (appDoc.name !== void 0)
@@ -4989,6 +5262,16 @@ function validateAppDocShape(appDoc) {
4989
5262
  if (appDoc.app_type === "aiws" && Object.keys(pkgList).join() !== AIWS_SUB_PACKAGE) {
4990
5263
  throw invalid("appdoc", "an aiws package must contain only the aiws sub package");
4991
5264
  }
5265
+ if (appDoc.app_type === EXT_TOOLS_APP_TYPE) {
5266
+ expectNonEmptyString(appDoc.name, "APPDOC.name");
5267
+ const unknown = Object.keys(pkgList).find((key) => !extToolsPlatformOfKey(key));
5268
+ if (unknown)
5269
+ throw invalid("appdoc", `an ext_tools package must contain only ext_tools_<os>_<arch> sub packages, not ${unknown}`);
5270
+ } else {
5271
+ const stray = Object.keys(pkgList).find((key) => extToolsPlatformOfKey(key));
5272
+ if (stray)
5273
+ throw invalid("appdoc", `${stray} belongs to an ext_tools package`);
5274
+ }
4992
5275
  deriveNamespaceForPackage(appDoc);
4993
5276
  }
4994
5277
  function deriveNamespaceForPackage(appDoc) {
@@ -5242,7 +5525,7 @@ function validateContentHandlers(value, permissions) {
5242
5525
  }
5243
5526
  return value;
5244
5527
  }
5245
- const PACKAGE_VERSION = "0.7.164";
5528
+ const PACKAGE_VERSION = "0.7.168";
5246
5529
  const TOOL_VERSION = PACKAGE_VERSION;
5247
5530
  const SDK_VERSION = PACKAGE_VERSION;
5248
5531
  const PROTOCOL_VERSION = "1";
@@ -5260,14 +5543,17 @@ const SHA256_ID = /^sha256:[0-9a-f]{64}$/;
5260
5543
  const OBJECT_ID = /^(?:appdoc|pkg):[0-9a-f]{64}$/;
5261
5544
  const SAFE_GENERATED_FILE = /^[A-Za-z0-9._-]+$/;
5262
5545
  const OBJECT_OUTPUT$c = { type: "object", additionalProperties: true };
5263
- const APP_KINDS = ["static-web", "script", "docker", "agent-skills", "aiws-controls"];
5546
+ const APP_KINDS = ["static-web", "script", "docker", "agent-skills", "aiws-controls", "ext-tools"];
5264
5547
  const PATH_KINDS = {
5265
5548
  "static-web": { key: "web", category: "web", selectorType: "static" },
5266
5549
  script: { key: "script", category: "dapp", selectorType: "single" },
5267
5550
  "agent-skills": { key: "agent_skills", category: "agent_skills", selectorType: "single" },
5268
5551
  // AI Workspace controls: one bundle (aiws-manifest.json + controls/<dir>/), installed into the AI Workspace library
5269
- "aiws-controls": { key: "aiws", category: "aiws", selectorType: "single" }
5552
+ "aiws-controls": { key: "aiws", category: "aiws", selectorType: "single" },
5553
+ // Extension tools: ext-manifest.json + payload/<platform>/, one sub package per platform, enabled for the Zone
5554
+ "ext-tools": { key: "ext_tools", category: "ext_tools", selectorType: "single" }
5270
5555
  };
5556
+ const EXT_TOOLS_LAUNCHER_LIMIT = 1024 * 1024;
5271
5557
  function packageEnvironmentQualifier(key, selector) {
5272
5558
  const effective = selector ?? derivedSelector(key);
5273
5559
  const os = effective?.os;
@@ -5299,9 +5585,19 @@ function createPikgModule(dependencies = {}) {
5299
5585
  positionals: [
5300
5586
  { name: "meta_dir", description: "dapp_meta directory", required: false }
5301
5587
  ],
5588
+ options: [
5589
+ {
5590
+ name: "catalog",
5591
+ description: "ext-tools: a directory of other packages (<id>/ext-manifest.json) to check requires and same-named commands against",
5592
+ type: "string"
5593
+ }
5594
+ ],
5302
5595
  inputSchema: {
5303
5596
  type: "object",
5304
- properties: { meta_dir: { type: "string", minLength: 1 } },
5597
+ properties: {
5598
+ meta_dir: { type: "string", minLength: 1 },
5599
+ catalog: { type: "string", minLength: 1 }
5600
+ },
5305
5601
  additionalProperties: false
5306
5602
  },
5307
5603
  outputSchema: OBJECT_OUTPUT$c,
@@ -5311,7 +5607,11 @@ function createPikgModule(dependencies = {}) {
5311
5607
  requiresSession: false,
5312
5608
  execution: "local",
5313
5609
  networkAccess: false,
5314
- examples: ["buckyos pikg doctor", "buckyos pikg doctor ./dapp_meta"],
5610
+ examples: [
5611
+ "buckyos pikg doctor",
5612
+ "buckyos pikg doctor ./dapp_meta",
5613
+ "buckyos pikg doctor ./dapp_meta --catalog ../publish/exttool"
5614
+ ],
5315
5615
  handler: async (ctx, input) => await doctorCommand(ctx, input)
5316
5616
  },
5317
5617
  {
@@ -5369,7 +5669,8 @@ function createPikgModule(dependencies = {}) {
5369
5669
  "buckyos --non-interactive pikg init . --owner did:bns:root --kind static-web --source ./web/dist",
5370
5670
  "buckyos --non-interactive pikg init . --owner did:bns:root --kind agent-skills --source ./skills",
5371
5671
  "buckyos --non-interactive pikg init . --owner did:bns:alice --kind aiws-controls --source ./aiws",
5372
- "buckyos --non-interactive pikg init . --owner did:bns:alice --kind aiws-controls --source ./aiws --composite"
5672
+ "buckyos --non-interactive pikg init . --owner did:bns:alice --kind aiws-controls --source ./aiws --composite",
5673
+ "buckyos --non-interactive pikg init . --name ext-media --owner did:bns:root --kind ext-tools --source ./ext-media"
5373
5674
  ],
5374
5675
  handler: async (ctx, input) => await initCommand(ctx, input, docker)
5375
5676
  },
@@ -5527,6 +5828,10 @@ async function doctorCommand(ctx, input) {
5527
5828
  status: missingSources.length === 0 ? "ok" : "missing",
5528
5829
  missing: missingSources
5529
5830
  });
5831
+ if (app.categories[0] === PATH_KINDS["ext-tools"].category) {
5832
+ const catalog = optionalInputString(input, "catalog");
5833
+ checks.push(...await extToolsChecks(app, pikg, metaDir, catalog ? resolveFromCwd$1(ctx, catalog) : void 0));
5834
+ }
5530
5835
  } catch (error) {
5531
5836
  checks.push({
5532
5837
  name: "metadata_schema",
@@ -5540,7 +5845,7 @@ async function doctorCommand(ctx, input) {
5540
5845
  const profile = profileName ? await ctx.configStore.readProfile(profileName) : void 0;
5541
5846
  const host2 = getHost();
5542
5847
  return {
5543
- healthy: checks.every((check) => check.status === "ok"),
5848
+ healthy: checks.every((check) => check.status === "ok" || check.status === "skipped"),
5544
5849
  tool_version: TOOL_VERSION,
5545
5850
  sdk_version: SDK_VERSION,
5546
5851
  protocol_version: PROTOCOL_VERSION,
@@ -5591,7 +5896,25 @@ async function initCommand(ctx, input, docker) {
5591
5896
  let sourceValue;
5592
5897
  let selector;
5593
5898
  let sourceSummary2;
5594
- if (kind === "docker") {
5899
+ if (kind === "ext-tools") {
5900
+ const platform = hostExtToolsPlatform();
5901
+ const absoluteSource = resolve$1(projectDir, source);
5902
+ subpackageKey = platform.key;
5903
+ selector = platform.selector;
5904
+ sourceValue = {
5905
+ type: "path",
5906
+ path: toPortablePath(relative(join(projectDir, "dapp_meta"), join(absoluteSource, "payload", platform.name)))
5907
+ };
5908
+ sourceSummary2 = { type: "path", path: displaySource(projectDir, absoluteSource), platform: platform.name };
5909
+ try {
5910
+ await getHost().lstat(join(absoluteSource, EXT_TOOLS_MANIFEST_FILE));
5911
+ } catch (error) {
5912
+ if (!isHostError(error, "NotFound"))
5913
+ throw error;
5914
+ await writeExtToolsSkeleton(absoluteSource, name, version, platform.name);
5915
+ sourceSummary2.skeleton = true;
5916
+ }
5917
+ } else if (kind === "docker") {
5595
5918
  const image = await docker.inspect(source);
5596
5919
  const arch = normalizeArch$1(image.architecture);
5597
5920
  subpackageKey = arch === "x86_64" ? "amd64_docker_image" : arch === "aarch64" ? "aarch64_docker_image" : (() => {
@@ -5696,6 +6019,221 @@ async function writeAiwsSkeleton(dir, ownerDid, name, composite) {
5696
6019
  await host2.writeTextFile(join(dir, AIWS_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
5697
6020
  `);
5698
6021
  }
6022
+ function hostExtToolsPlatform() {
6023
+ const host2 = getHost();
6024
+ const os = host2.platform === "darwin" ? "macos" : host2.platform === "win32" ? "windows" : host2.platform;
6025
+ const arch = normalizeArch$1(host2.arch);
6026
+ const platform = EXT_TOOLS_PLATFORMS.find((known) => known.selector.os === os && known.selector.arch === arch);
6027
+ if (!platform) {
6028
+ throw new UsageError("UNSUPPORTED_PLATFORM", `extension packages have no payload for ${os}/${arch}`);
6029
+ }
6030
+ return platform;
6031
+ }
6032
+ async function writeExtToolsSkeleton(dir, id, version, platform) {
6033
+ const host2 = getHost();
6034
+ const manifest = {
6035
+ schema: EXT_TOOLS_MANIFEST_SCHEMA,
6036
+ id,
6037
+ version,
6038
+ requires: [],
6039
+ conflicts: [],
6040
+ provides: [{ command: id, version_probe: `${id} --version`, smoke: `smoke/${id}.sh` }],
6041
+ platforms: [platform],
6042
+ license: "",
6043
+ desc: ""
6044
+ };
6045
+ const text2 = `${JSON.stringify(manifest, null, 2)}
6046
+ `;
6047
+ const payload = join(dir, "payload", platform);
6048
+ for (const sub of ["bin", "smoke", join("store", id)])
6049
+ await host2.mkdir(join(payload, sub), { recursive: true });
6050
+ await host2.writeTextFile(join(dir, EXT_TOOLS_MANIFEST_FILE), text2);
6051
+ await host2.writeTextFile(join(payload, EXT_TOOLS_MANIFEST_FILE), text2);
6052
+ const windows = platform.startsWith("windows-");
6053
+ await host2.writeTextFile(
6054
+ join(payload, "bin", windows ? `${id}.cmd` : id),
6055
+ windows ? `@echo off\r
6056
+ echo ${id} ${version}\r
6057
+ ` : `#!/bin/sh
6058
+ echo "${id} ${version}"
6059
+ `,
6060
+ { mode: 493 }
6061
+ );
6062
+ await host2.writeTextFile(join(payload, "smoke", `${id}.sh`), `#!/bin/sh
6063
+ set -eu
6064
+ ${id} --version >/dev/null
6065
+ `, { mode: 493 });
6066
+ }
6067
+ async function readExtToolsManifestFile(path) {
6068
+ let text2;
6069
+ try {
6070
+ text2 = await getHost().readTextFile(path);
6071
+ } catch (error) {
6072
+ if (isHostError(error, "NotFound"))
6073
+ return { text: "", errors: ["not found"] };
6074
+ throw error;
6075
+ }
6076
+ let value;
6077
+ try {
6078
+ value = JSON.parse(text2);
6079
+ } catch {
6080
+ return { text: text2, errors: ["not valid JSON"] };
6081
+ }
6082
+ const errors = extToolsManifestErrors(value);
6083
+ if (errors.length > 0)
6084
+ return { text: text2, errors };
6085
+ const manifest = value;
6086
+ manifest.requires ??= [];
6087
+ manifest.conflicts ??= [];
6088
+ return { text: text2, manifest, errors };
6089
+ }
6090
+ async function readExtToolsPayloadTree(root) {
6091
+ const host2 = getHost();
6092
+ const tree = { files: /* @__PURE__ */ new Map(), directories: /* @__PURE__ */ new Set(), launchers: /* @__PURE__ */ new Map() };
6093
+ const errors = [];
6094
+ const realRoot = await host2.realPath(root);
6095
+ const walk = async (directory, prefix) => {
6096
+ const children = await host2.readDir(directory);
6097
+ for (const child of children) {
6098
+ const archivePath = prefix ? `${prefix}/${child.name}` : child.name;
6099
+ const path = join(directory, child.name);
6100
+ const link = await host2.lstat(path);
6101
+ let physical = path;
6102
+ if (link.isSymlink) {
6103
+ try {
6104
+ physical = await host2.realPath(path);
6105
+ } catch (error) {
6106
+ errors.push(`${archivePath}: ${isHostError(error, "NotFound") ? "a dangling symlink" : "a symlink out of the payload"}`);
6107
+ continue;
6108
+ }
6109
+ const inside = relative(realRoot, physical);
6110
+ if (inside === ".." || inside.startsWith(`..${host2.path.sep}`) || isAbsolute$1(inside)) {
6111
+ errors.push(`${archivePath}: a symlink out of the payload`);
6112
+ continue;
6113
+ }
6114
+ }
6115
+ const info = link.isSymlink ? await host2.stat(physical) : link;
6116
+ if (info.isDirectory) {
6117
+ tree.directories.add(archivePath);
6118
+ await walk(physical, archivePath);
6119
+ } else if (info.isFile) {
6120
+ tree.files.set(archivePath, { executable: ((info.mode ?? 0) & 73) !== 0 });
6121
+ if (archivePath.startsWith("bin/") && info.size <= EXT_TOOLS_LAUNCHER_LIMIT) {
6122
+ tree.launchers.set(archivePath, await host2.readFile(physical));
6123
+ }
6124
+ } else {
6125
+ errors.push(`${archivePath}: not a regular file`);
6126
+ }
6127
+ }
6128
+ };
6129
+ await walk(realRoot, "");
6130
+ return { tree, errors };
6131
+ }
6132
+ async function extToolsChecks(app, pikg, metaDir, catalogDir) {
6133
+ const checks = [];
6134
+ const platforms = /* @__PURE__ */ new Map();
6135
+ const stray = [];
6136
+ for (const [key, subpackage] of Object.entries(pikg.sub_pkgs)) {
6137
+ const platform = extToolsPlatformOfKey(key);
6138
+ if (!platform || subpackage.source.type !== "path") {
6139
+ stray.push(key);
6140
+ continue;
6141
+ }
6142
+ platforms.set(platform.name, resolve$1(metaDir, subpackage.source.path));
6143
+ }
6144
+ const sources = [...platforms.values()];
6145
+ const source = sources.length > 0 ? dirname(dirname(sources[0])) : dirname(metaDir);
6146
+ const master = await readExtToolsManifestFile(join(source, EXT_TOOLS_MANIFEST_FILE));
6147
+ const manifestErrors = [...master.errors];
6148
+ if (stray.length > 0)
6149
+ manifestErrors.push(`sub packages must be ext_tools_<os>_<arch> from a directory: ${stray.join(", ")}`);
6150
+ const manifest = master.manifest;
6151
+ if (manifest) {
6152
+ if (manifest.id !== app.name)
6153
+ manifestErrors.push(`id ${manifest.id} is not the App name ${app.name}`);
6154
+ if (manifest.version !== app.version)
6155
+ manifestErrors.push(`version ${manifest.version} is not the App version ${app.version}`);
6156
+ const without = manifest.platforms.filter((name) => !platforms.has(name));
6157
+ if (without.length > 0)
6158
+ manifestErrors.push(`platforms without a sub package: ${without.join(", ")}`);
6159
+ const unlisted = [...platforms.keys()].filter((name) => !manifest.platforms.includes(name));
6160
+ if (unlisted.length > 0)
6161
+ manifestErrors.push(`sub packages of platforms it does not list: ${unlisted.join(", ")}`);
6162
+ }
6163
+ checks.push({
6164
+ name: "ext_tools_manifest",
6165
+ status: manifestErrors.length === 0 ? "ok" : "error",
6166
+ path: join(source, EXT_TOOLS_MANIFEST_FILE),
6167
+ ...manifestErrors.length > 0 ? { errors: manifestErrors } : {}
6168
+ });
6169
+ if (!manifest)
6170
+ return checks;
6171
+ for (const [platform, dir] of platforms) {
6172
+ const errors = [];
6173
+ try {
6174
+ const { tree, errors: walkErrors } = await readExtToolsPayloadTree(dir);
6175
+ errors.push(...walkErrors);
6176
+ const copy = await readExtToolsManifestFile(join(dir, EXT_TOOLS_MANIFEST_FILE));
6177
+ if (copy.errors.length > 0)
6178
+ errors.push(`${EXT_TOOLS_MANIFEST_FILE}: ${copy.errors.join("; ")}`);
6179
+ else if (copy.text !== master.text)
6180
+ errors.push(`${EXT_TOOLS_MANIFEST_FILE}: differs from the package's manifest`);
6181
+ errors.push(...extToolsPayloadErrors(manifest, platform, tree, [source, metaDir]));
6182
+ } catch (error) {
6183
+ if (!isHostError(error, "NotFound"))
6184
+ throw error;
6185
+ errors.push(`${dir}: not found`);
6186
+ }
6187
+ checks.push({
6188
+ name: "ext_tools_payload",
6189
+ platform,
6190
+ status: errors.length === 0 ? "ok" : "error",
6191
+ path: dir,
6192
+ ...errors.length > 0 ? { errors } : {}
6193
+ });
6194
+ }
6195
+ if (!catalogDir) {
6196
+ checks.push({
6197
+ name: "ext_tools_catalog",
6198
+ status: "skipped",
6199
+ detail: "no --catalog: requires and commands other packages also provide are not checked"
6200
+ });
6201
+ return checks;
6202
+ }
6203
+ const others = /* @__PURE__ */ new Map();
6204
+ const catalogErrors = [];
6205
+ for (const entry of await getHost().readDir(catalogDir)) {
6206
+ if (entry.isFile)
6207
+ continue;
6208
+ const candidate = await readExtToolsManifestFile(join(catalogDir, entry.name, EXT_TOOLS_MANIFEST_FILE));
6209
+ if (candidate.errors.includes("not found"))
6210
+ continue;
6211
+ if (!candidate.manifest)
6212
+ catalogErrors.push(`${entry.name}: ${candidate.errors.join("; ")}`);
6213
+ else if (candidate.manifest.id !== manifest.id)
6214
+ others.set(candidate.manifest.id, candidate.manifest);
6215
+ }
6216
+ for (const required of manifest.requires) {
6217
+ if (!others.has(required.id))
6218
+ catalogErrors.push(`requires ${required.id}: not in the catalog`);
6219
+ }
6220
+ const commands = new Set(manifest.provides.map((provided) => provided.command));
6221
+ for (const other of others.values()) {
6222
+ if (manifest.conflicts.includes(other.id) || other.conflicts.includes(manifest.id))
6223
+ continue;
6224
+ const shared = other.provides.map((provided) => provided.command).filter((command) => commands.has(command));
6225
+ if (shared.length > 0)
6226
+ catalogErrors.push(`${other.id} also provides ${shared.join(", ")}: one of the two must list the other in conflicts`);
6227
+ }
6228
+ checks.push({
6229
+ name: "ext_tools_catalog",
6230
+ status: catalogErrors.length === 0 ? "ok" : "error",
6231
+ path: catalogDir,
6232
+ packages: [...others.keys()],
6233
+ ...catalogErrors.length > 0 ? { errors: catalogErrors } : {}
6234
+ });
6235
+ return checks;
6236
+ }
5699
6237
  async function buildCommand$1(ctx, input, docker, now) {
5700
6238
  const metaInput = optionalInputString(input, "meta_dir") ?? "./dapp_meta";
5701
6239
  const metaDir = await realDirectory(resolveFromCwd$1(ctx, metaInput), "dapp_meta");
@@ -7131,7 +7669,7 @@ function publishCommand() {
7131
7669
  property: "app_type",
7132
7670
  description: "App type; defaults to the template app_type",
7133
7671
  type: "string",
7134
- enum: ["dapp", "web", "agent"]
7672
+ enum: ["dapp", "web", "agent", "ext_tools"]
7135
7673
  }
7136
7674
  ],
7137
7675
  inputSchema: {
@@ -7139,7 +7677,7 @@ function publishCommand() {
7139
7677
  properties: {
7140
7678
  local_dir: { type: "string", minLength: 1 },
7141
7679
  app_doc: { type: "string", minLength: 1 },
7142
- app_type: { type: "string", enum: ["dapp", "web", "agent"] }
7680
+ app_type: { type: "string", enum: ["dapp", "web", "agent", "ext_tools"] }
7143
7681
  },
7144
7682
  required: ["local_dir", "app_doc"],
7145
7683
  additionalProperties: false
@@ -7151,7 +7689,8 @@ function publishCommand() {
7151
7689
  requiresSession: true,
7152
7690
  examples: [
7153
7691
  "buckyos app publish /srv/build/demo-web --app-doc ./demo-web.appdoc.json",
7154
- "buckyos app publish /srv/build/demo-agent --app-doc ./demo-agent.appdoc.json --app-type agent"
7692
+ "buckyos app publish /srv/build/demo-agent --app-doc ./demo-agent.appdoc.json --app-type agent",
7693
+ "buckyos app publish /srv/build/ext-media --app-doc ./dapp_dist/APPDOC.json"
7155
7694
  ],
7156
7695
  handler: async (ctx, input) => {
7157
7696
  const localDir = expectString(input, "local_dir");
@@ -7227,6 +7766,12 @@ async function installApp(ctx, input, dependencies) {
7227
7766
  try {
7228
7767
  let inspection = submittedPlan ? await recomputePlan(ctx, source, input, submittedPlan) : await inspectSource(ctx, source, input);
7229
7768
  verifyPikgBinding(source, submittedPlan ?? inspection.plan);
7769
+ if (inspection.plan.app_doc?.app_type === "ext_tools") {
7770
+ throw new ToolError(
7771
+ "EXT_TOOLS_NOT_AN_APP",
7772
+ "an extension tool package is not installed as an App: publish it to the Repo with `buckyos app publish`, then enable it for the Zone on the management page"
7773
+ );
7774
+ }
7230
7775
  const app = expectObject$1(inspection.plan.app, "inspection.plan.app");
7231
7776
  const appDid = expectString(app, "did");
7232
7777
  const plannedOwner = submittedPlan ? expectString(submittedPlan, "owner_user_id") : void 0;