@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/README.md +1 -1
- package/bin/oats.mjs +128 -20
- package/docs/capabilities.md +22 -0
- package/docs/configuration.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +3 -2
- package/docs/desktop-cli-api.md +231 -7
- package/docs/first-team.md +20 -2
- package/docs/implementation.md +78 -2
- package/docs/integrations.md +1 -1
- package/docs/official-catalog.md +2 -2
- package/docs/packages.md +2 -2
- package/docs/release-notes/v0.33.0.md +174 -0
- package/docs/release-notes/v0.34.0.md +63 -0
- package/docs/souls-and-instances.md +64 -0
- package/docs/workspaces.md +1 -1
- package/lib/capability-show.mjs +208 -0
- package/lib/core.mjs +89 -17
- package/lib/harness-trust.mjs +139 -0
- package/lib/instance-inspect.mjs +16 -2
- package/lib/packages.mjs +1 -1
- package/lib/process-group.mjs +54 -0
- package/lib/remote.mjs +721 -112
- package/lib/resolve.mjs +56 -14
- package/package-catalog.json +2 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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 = {
|
package/package-catalog.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"oats.aweb": {
|
|
10
10
|
"url": "https://github.com/awebai/oats-aweb.git",
|
|
11
|
-
"ref": "v1.17.
|
|
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.
|
|
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.
|
|
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",
|