@awebai/oats 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/resolve.mjs CHANGED
@@ -474,12 +474,29 @@ export function packageRef(id, entry, catalog, remote) {
474
474
  const SAFE_REL = (rel) => typeof rel === "string" && rel && !posix.isAbsolute(rel) && !/^[\\/]/.test(rel) && !/\0/.test(rel) && !rel.split(/[\\/]/).some((p) => p === "..");
475
475
 
476
476
  /** Normalize a manifest-declared relative path ("./skills/x/" → "skills/x"); null when unsafe. */
477
- function declaredPath(rel) {
477
+ export function declaredPath(rel) {
478
478
  if (!SAFE_REL(rel)) return null;
479
479
  const n = posix.normalize(rel).replace(/\/+$/, "");
480
480
  return n === "." || n === "" || n.startsWith("../") ? null : n;
481
481
  }
482
482
 
483
+ /** A relative path no listing of a checkout can hold: absolute, empty, a `.`/`..`/`.git` (any case) or empty
484
+ * component, a trailing slash, a backslash or a NUL. */
485
+ export function unsafeRelPath(path) {
486
+ if (typeof path !== "string" || path === "" || path.startsWith("/") || path.endsWith("/") || /[\\\0]/.test(path)) return true;
487
+ return path.split("/").some((c) => c === "" || c === "." || c === ".." || c.toLowerCase() === ".git");
488
+ }
489
+
490
+ /** A manifest-declared path as the module install reads it (lib/materialize.mjs modulePath: a backslash is a
491
+ * separator, `./` and trailing slashes dropped), and only when the result is a safe POSIX path (unsafeRelPath);
492
+ * else null. What `oats capabilities show` reports for an inject or a skill. */
493
+ export function manifestFilePath(rel) {
494
+ const declared = declaredPath(rel);
495
+ if (!declared) return null;
496
+ const n = posix.normalize(declared.replace(/\\/g, "/")).replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
497
+ return unsafeRelPath(n) ? null : n;
498
+ }
499
+
483
500
  /**
484
501
  * Skills declared by a manifest: each `skills[]` entry is a directory under the capability that either
485
502
  * IS a skill (holds SKILL.md) or holds skill directories (<entry>/<skill>/SKILL.md) — the same reading
@@ -525,14 +542,31 @@ async function enumerateSkills({ remote, ref, commit, dir, manifest, listing, mo
525
542
  * would), commands and hooks. `skills` is null when its declared skills cannot be listed (a spawn of it
526
543
  * would refuse; the listing does not). */
527
544
  export async function capabilityProvides({ ref, commit, dir, manifest, remote: injected, remoteOptions }) {
528
- const remote = remoteOf({ remote: injected, remoteOptions });
529
545
  const keys = (o) => (isObject(o) ? Object.keys(o).sort(byCodepoint) : []);
530
- let skills;
546
+ const { skills } = await capabilitySkills({ ref, commit, dir, manifest, remote: injected, remoteOptions });
547
+ return { skills: skills && skills.map((s) => s.name), commands: keys(manifest.commands), hooks: keys(manifest.hooks) };
548
+ }
549
+
550
+ /** A capability's skills as a spawn enumerates them (enumerateSkills), sorted by name in codepoint order:
551
+ * → { skills: [{ name, path }] (path relative to the capability directory), problem: null }, or
552
+ * { skills: null, problem } when the declared skills cannot be enumerated — `problem` the oats error a spawn
553
+ * would meet (`missingCode`: E_CAPABILITY_MISSING for a member, E_PACKAGE_MANIFEST for a package), or a skill
554
+ * path that is not safe (manifestFilePath). Paths are as the install reads them. The one reading of
555
+ * `manifest.skills` behind `oats capabilities` (capabilityProvides) and `oats capabilities show`. */
556
+ export async function capabilitySkills({ ref, commit, dir, manifest, remote: injected, remoteOptions, missingCode = "E_CAPABILITY_MISSING" }) {
557
+ const remote = remoteOf({ remote: injected, remoteOptions });
558
+ const missing = (raw, why, text) => fail(missingCode, `${manifest.capability} ${text}`, { capability: manifest.capability, skill: raw, why });
531
559
  try {
532
- const missing = (raw, why, text) => fail("E_CAPABILITY_MISSING", text, { skill: raw, why });
533
- skills = (await enumerateSkills({ remote, ref, commit, dir, manifest, moduleName: manifest.capability, missing })).map((x) => x.name).sort(byCodepoint);
534
- } catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) skills = null; else throw e; }
535
- return { skills, commands: keys(manifest.commands), hooks: keys(manifest.hooks) };
560
+ const skills = [];
561
+ for (const { name, path } of await enumerateSkills({ remote, ref, commit, dir, manifest, moduleName: manifest.capability, missing })) {
562
+ // A skill path is reported as the install reads it, and only when it is a safe POSIX path (a `.git`
563
+ // directory the remote serves is one a spawn's fetch refuses).
564
+ const safe = manifestFilePath(path);
565
+ if (!safe) throw missing(path, "unsafe", `declares skill ${show(name)} at ${show(path)}, which is not a safe path inside the capability`);
566
+ skills.push({ name, path: safe });
567
+ }
568
+ return { skills: skills.sort((a, b) => byCodepoint(a.name, b.name)), problem: null };
569
+ } catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return { skills: null, problem: e }; throw e; }
536
570
  }
537
571
 
538
572
  /** The capability manifests of a locked package, read at its locked commit (feature desktop-facts). */
@@ -543,6 +577,20 @@ export async function lockedPackageCapabilities(id, entry, { catalog = null, rem
543
577
  return { ref, capabilities };
544
578
  }
545
579
 
580
+ /** The lock must describe the tree it names: the capability list a package's manifests declare at the locked
581
+ * commit (`capabilities`, readPackageManifests) must equal the lock entry's, and must hold `name`, else
582
+ * E_PACKAGE_INTEGRITY. → that capability { name, dir, manifest }. Spawn's check (resolveSoul), shared with
583
+ * `oats capabilities show`. */
584
+ export function lockedCapability(name, id, entry, capabilities, details = { capability: name, id, version: entry.version, commit: entry.commit, path: entry.path }) {
585
+ const listed = capabilities.map((c) => c.name).sort(), locked = [...entry.capabilities].sort(); // validateLock guarantees the array
586
+ if (listed.length !== locked.length || listed.some((c, i) => c !== locked[i])) {
587
+ throw fail("E_PACKAGE_INTEGRITY", `${name}: the lock says package ${id} v${entry.version} provides [${locked.join(", ")}], but ${entry.path}/oats-package.json at ${short(entry.commit)} declares [${listed.join(", ")}]`, { ...details, why: "capabilities", listed, locked });
588
+ }
589
+ const cap = capabilities.find((c) => c.name === name);
590
+ if (!cap) throw fail("E_PACKAGE_INTEGRITY", `${name}: the lock says package ${id} v${entry.version} provides it, but ${entry.path}/oats-package.json at ${short(entry.commit)} does not`, { ...details, listed: capabilities.map((c) => c.name) });
591
+ return cap;
592
+ }
593
+
546
594
  /* ───────────────────────────── resolveSoul ────────────────────────────── */
547
595
 
548
596
  /**
@@ -601,13 +649,7 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
601
649
  const ref = packageRef(id, entry, catalog, remote);
602
650
  const details = { capability: name, id, version: entry.version, commit: entry.commit, path: entry.path };
603
651
  const { capabilities } = await readPackageManifests(remote, ref, entry.commit, entry.path, details);
604
- // The lock must describe the tree it names: its capability list is what the package declares there.
605
- const listed = capabilities.map((c) => c.name).sort(), locked = [...entry.capabilities].sort(); // validateLock guarantees the array
606
- if (listed.length !== locked.length || listed.some((c, i) => c !== locked[i])) {
607
- throw fail("E_PACKAGE_INTEGRITY", `${name}: the lock says package ${id} v${entry.version} provides [${locked.join(", ")}], but ${entry.path}/oats-package.json at ${short(entry.commit)} declares [${listed.join(", ")}]`, { ...details, why: "capabilities", listed, locked });
608
- }
609
- const cap = capabilities.find((c) => c.name === name);
610
- if (!cap) throw fail("E_PACKAGE_INTEGRITY", `${name}: the lock says package ${id} v${entry.version} provides it, but ${entry.path}/oats-package.json at ${short(entry.commit)} does not`, { ...details, listed: capabilities.map((c) => c.name) });
652
+ const cap = lockedCapability(name, id, entry, capabilities, details);
611
653
  const layer = layerOf(cap.manifest);
612
654
  if (layer && emptied.has(layer) && via !== "soul") { turnedOff.push({ name, reason: "slot-none", slot: layer, overrides: fromOfVia(via) }); continue; }
613
655
  module = {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "oats.aweb": {
10
10
  "url": "https://github.com/awebai/oats-aweb.git",
11
- "ref": "v1.17.3",
11
+ "ref": "v1.17.5",
12
12
  "path": "oats-package"
13
13
  },
14
14
  "oats.jira": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "oats.engineering": {
30
30
  "url": "https://github.com/awebai/oats-engineering.git",
31
- "ref": "v1.3.0",
31
+ "ref": "v1.4.0",
32
32
  "path": "oats-package"
33
33
  },
34
34
  "oats.framework": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
5
5
  "keywords": [
6
6
  "agents",