@awebai/oats 0.24.1 → 0.24.3
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/bin/oats.mjs +227 -19
- package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +11 -0
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +26 -0
- package/capabilities/oats-aweb/lib/binding-wire.mjs +214 -0
- package/capabilities/oats-aweb/lib/captured-execution.mjs +91 -0
- package/capabilities/oats-aweb/lib/captured-native.mjs +91 -0
- package/capabilities/oats-aweb/lib/invocation-shape.mjs +135 -0
- package/capabilities/oats-aweb/lib/portable-binding.mjs +146 -0
- package/capabilities/oats-aweb/lib/session-readiness.mjs +56 -0
- package/capabilities/oats-aweb/oats.json +12 -3
- package/docs/design/2026-09-20-redesign-program-board.md +28 -15
- package/docs/design/2026-09-20-workspace-onboarding-public.md +73 -7
- package/docs/first-team.md +42 -0
- package/docs/layers.md +1 -1
- package/docs/official-marketplace.md +14 -9
- package/docs/packages.md +11 -7
- package/docs/release-notes/v0.24.2.md +21 -0
- package/docs/release-notes/v0.24.3.md +13 -0
- package/docs/souls-and-instances.md +25 -0
- package/docs/workspace-adoption.md +94 -65
- package/injects/oats-portable.md +4 -0
- package/lib/core.mjs +65 -19
- package/lib/portable-onboarding.mjs +3 -3
- package/lib/prepared-bindings.mjs +21 -8
- package/lib/provider-binding-broker.mjs +6 -1
- package/lib/setup-expert-source.mjs +76 -0
- package/package-catalog.json +7 -4
- package/package.json +3 -1
- package/souls/oats-setup-expert/AGENTS.md +60 -0
- package/souls/oats-setup-expert/soul.yaml +14 -0
package/bin/oats.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* The kernel resolves per-key closest-wins from wherever agents actually run,
|
|
14
14
|
* so binding at a level scopes the capability to everything under it.
|
|
15
15
|
*/
|
|
16
|
-
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
18
18
|
import { homedir, tmpdir } from "node:os";
|
|
19
19
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
@@ -26,10 +26,10 @@ import {
|
|
|
26
26
|
capabilityManifests, capabilityManifest, capabilityMissingRequires, capabilityIntegrity, capabilityTrust, capabilityExecutablePath, activateCapturedScaffold, loadCapturedDispatch, inspectPortableOnboarding, prepareCapturedComposition, resolveCapturedHelper, capturedNativeSessionAvailability, scaffoldCapturedInstance, startCapturedInstanceSession, withCapturedBindingFile, withCapturedInvocationContextFile,
|
|
27
27
|
readCapabilityLocks, writeCapabilityLock, admitCapturedAction, beginCapturedIntent, settleCapturedIntent,
|
|
28
28
|
parsePackageSource, inspectGitSourceRoot, acquirePackage, restorePackages, listInstalledPackages, readPackageLocks, readLockedConfigTemplates,
|
|
29
|
-
officialCapabilityPackage, officialPackageCatalog,
|
|
29
|
+
officialCapabilityPackage, officialPackageCatalog, DEFAULT_PACKAGE_PATH,
|
|
30
30
|
approveCapability, approveAvailableCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
|
|
31
31
|
packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir, installedCapabilitiesDir, ownedCapabilitiesDir, loadPackageManifestAt,
|
|
32
|
-
resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
|
|
32
|
+
resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, planInstanceResources, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
|
|
33
33
|
findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, listCapabilityAgents, workspaceOf,
|
|
34
34
|
ensureRoot, findRoot, findAgent, listAgents, listInstances, listAgentDefs, createAgent as coreCreateAgent,
|
|
35
35
|
spawnInstance, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS, validateLaunchConfig, resolveLaunchSelection, resolveLaunchExecutable, checkLaunchExecutable, missingLaunchEnvRefs, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, LAUNCH_RECIPE_VERSION, parseLaunchCommand, resolveYolo, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
@@ -49,15 +49,21 @@ import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_
|
|
|
49
49
|
import { capturedSelector } from "../lib/captured-selector.mjs";
|
|
50
50
|
import { inspectCapturedPiOutcome } from "../lib/captured-pi-host.mjs";
|
|
51
51
|
import { readCapturedResolution } from "../lib/captured-resolutions.mjs";
|
|
52
|
+
import { readLock3 } from "../lib/portable-lock.mjs";
|
|
53
|
+
import { oatsError } from "../lib/errors.mjs";
|
|
54
|
+
import { readPortableBytes } from "../lib/portable-files.mjs";
|
|
55
|
+
import { canonicalJson, parseStrictJson } from "../lib/portable-values.mjs";
|
|
52
56
|
import { readPortablePreparationRequest } from "../lib/portable-onboarding-request.mjs";
|
|
53
57
|
import { portableScope } from "../lib/portable-state.mjs";
|
|
54
58
|
import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../lib/captured-operation-process.mjs";
|
|
55
59
|
import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
|
|
60
|
+
import { loadSetupExpertEdition, SETUP_EXPERT, SETUP_CAPABILITIES } from "../lib/setup-expert-source.mjs";
|
|
61
|
+
import { parsePortableSource } from "../lib/source-spec.mjs";
|
|
56
62
|
|
|
57
63
|
const args = process.argv.slice(2);
|
|
58
64
|
let cmd = args[0];
|
|
59
65
|
const HELP_WORDS = new Set(["help", "--help", "-h"]);
|
|
60
|
-
const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
66
|
+
const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "onboard", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
61
67
|
const flag = (name) => {
|
|
62
68
|
const i = args.indexOf(`--${name}`);
|
|
63
69
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -97,15 +103,40 @@ const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok:
|
|
|
97
103
|
|
|
98
104
|
function inspectOnboardingCmd() {
|
|
99
105
|
const fail = (code, message) => JSON_MODE ? jsonFail(code, message) : die(message);
|
|
100
|
-
|
|
106
|
+
const values = new Map();
|
|
101
107
|
for (let index = 1; index < args.length; index++) {
|
|
102
108
|
if (args[index] === "--json") continue;
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
const key = args[index];
|
|
110
|
+
if (!["--request", "--emit-prepare-request"].includes(key) || values.has(key) || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "source inspection accepts one --request <absolute-json>, optional --emit-prepare-request <new-absolute-json>, and --json");
|
|
111
|
+
values.set(key, args[++index]);
|
|
105
112
|
}
|
|
106
113
|
try {
|
|
107
|
-
const
|
|
108
|
-
|
|
114
|
+
const output = values.get("--emit-prepare-request");
|
|
115
|
+
let parent;
|
|
116
|
+
const checkOutput = () => {
|
|
117
|
+
if (!isAbsolute(output) || resolve(output) !== output || output.includes("\0")) throw oatsError("E_BAD_ARGS", "prepare-request output needs a normalized absolute path");
|
|
118
|
+
let stat;
|
|
119
|
+
try {
|
|
120
|
+
stat = lstatSync(dirname(output));
|
|
121
|
+
if (!stat.isDirectory() || realpathSync(dirname(output)) !== dirname(output)) throw new Error();
|
|
122
|
+
} catch { throw oatsError("E_BAD_ARGS", "prepare-request output parent must be an existing real directory"); }
|
|
123
|
+
if (parent && (parent.dev !== stat.dev || parent.ino !== stat.ino)) throw oatsError("selection-changed", "prepare-request output parent changed during inspection");
|
|
124
|
+
try { lstatSync(output); }
|
|
125
|
+
catch (error) { if (error.code === "ENOENT") return stat; throw oatsError("E_BAD_ARGS", "prepare-request output could not be checked"); }
|
|
126
|
+
throw oatsError("E_BAD_ARGS", "prepare-request output already exists; choose a new file (nothing overwritten)");
|
|
127
|
+
};
|
|
128
|
+
if (output !== undefined) parent = checkOutput();
|
|
129
|
+
const input = readPortablePreparationRequest({ file: values.get("--request") });
|
|
130
|
+
const { prepareRequest, ...view } = inspectPortableOnboarding(input, { includePrepareRequest: output !== undefined });
|
|
131
|
+
let result = view;
|
|
132
|
+
if (output !== undefined) {
|
|
133
|
+
checkOutput();
|
|
134
|
+
try { writeFileSync(output, canonicalJson(prepareRequest) + "\n", { flag: "wx", mode: 0o600 }); }
|
|
135
|
+
catch { throw oatsError("E_BAD_ARGS", "prepare-request output could not be created exclusively; no existing file was overwritten"); }
|
|
136
|
+
const deployment = view.deployment.deployment.path;
|
|
137
|
+
result = { ...view, prepareRequestFile: output, effects: { ...view.effects, requestFileWrite: true,
|
|
138
|
+
deploymentWrites: output === deployment || output.startsWith(deployment + sep) } };
|
|
139
|
+
}
|
|
109
140
|
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
110
141
|
} catch (error) { fail(error.code || "E_INSPECT_FAILED", error.message); }
|
|
111
142
|
}
|
|
@@ -385,6 +416,9 @@ function capturedCommand(selector) {
|
|
|
385
416
|
const selected = helperSelection?.executionBinding ?? target;
|
|
386
417
|
const loaded = loadCapturedDispatch({ deployment: selected.deployment, resolution: selected.resolution, action: { kind: args.includes("--composition") ? "compose" : "inspect" } });
|
|
387
418
|
const result = { resolution: loaded.resolution, capture: loaded.record.capture, nativeSession: capturedNativeSessionAvailability(),
|
|
419
|
+
launchSelection: loaded.record.dispatch.launch === null ? null : {
|
|
420
|
+
runtime: loaded.record.dispatch.launch.runtime, model: loaded.record.dispatch.launch.model,
|
|
421
|
+
},
|
|
388
422
|
...(helperSelection ? { helperSelection } : {}),
|
|
389
423
|
capabilities: [...loaded.capabilities.values()].map(({ id, manifest }) => ({ id, version: manifest.version,
|
|
390
424
|
approval: loaded.approvals.find((entry) => entry.artifact.capability === id).status })),
|
|
@@ -505,6 +539,10 @@ function resolveForDoctor(ctx, soulName, { json } = {}) {
|
|
|
505
539
|
die(`${e.message}`);
|
|
506
540
|
}
|
|
507
541
|
}
|
|
542
|
+
function operationalKnowledgeNote(composition, soulName) {
|
|
543
|
+
return composition && !composition.oatsCoreDeclared
|
|
544
|
+
? `soul ${soulName} has no oats.core capability; kernel-shipped operational skills are deprecated` : null;
|
|
545
|
+
}
|
|
508
546
|
function doctorComposition(ctx, soulName) {
|
|
509
547
|
if (!soulName) return undefined;
|
|
510
548
|
const root = findRoot(ctx);
|
|
@@ -1259,7 +1297,8 @@ function doctorJson(dir) {
|
|
|
1259
1297
|
hooks: Object.keys(r.layers[l].hooks || {}), missingRequires: r.layers[l].missingRequires,
|
|
1260
1298
|
provenance: r.provenance[l],
|
|
1261
1299
|
} : { provenance: r.provenance[l] || null }])),
|
|
1262
|
-
kernelInjection: r.kernelInjection,
|
|
1300
|
+
kernelInjection: composition?.resolved.kernelInjection ?? r.kernelInjection,
|
|
1301
|
+
information: operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : [],
|
|
1263
1302
|
injects: r.injects,
|
|
1264
1303
|
capabilities: r.capabilities.map((c) => ({ id: c.id, layer: c.layer, command: c.command, origin: c.origin, provenance: c.provenance, settings: c.settings, skills: c.skills, inject: c.inject, hooks: Object.keys(c.hooks || {}), trust: c.trust })),
|
|
1265
1304
|
acquired: Object.fromEntries(Object.entries(mans).map(([n, m]) => [n, { layer: m.layer, command: m.command, version: m.version, dir: m._dir, origin: m._origin, description: m.description }])),
|
|
@@ -1288,6 +1327,7 @@ function doctor(dir) {
|
|
|
1288
1327
|
const soulName = flag("soul");
|
|
1289
1328
|
const chain = configChain(ctx);
|
|
1290
1329
|
const r = resolveForDoctor(ctx, soulName);
|
|
1330
|
+
const composition = doctorComposition(ctx, soulName);
|
|
1291
1331
|
console.log(`oats doctor — resolved from ${shortPath(ctx)}\n`);
|
|
1292
1332
|
|
|
1293
1333
|
// Kernel/bridge version skew (published in lockstep from one tag).
|
|
@@ -1332,7 +1372,8 @@ function doctor(dir) {
|
|
|
1332
1372
|
}
|
|
1333
1373
|
|
|
1334
1374
|
console.log("\nKernel injection:");
|
|
1335
|
-
|
|
1375
|
+
const kernelInjection = composition?.resolved.kernelInjection ?? r.kernelInjection;
|
|
1376
|
+
console.log(` oats: ${kernelInjection?.inject ? shortPath(kernelInjection.inject) : "none"} [${kernelInjection?.provenance || "default"}]`);
|
|
1336
1377
|
|
|
1337
1378
|
console.log("\nUnconditional injections (outermost→innermost):");
|
|
1338
1379
|
if (r.injects.length === 0) console.log(" (none)");
|
|
@@ -1440,7 +1481,8 @@ function doctor(dir) {
|
|
|
1440
1481
|
}
|
|
1441
1482
|
|
|
1442
1483
|
if (soulName) {
|
|
1443
|
-
const
|
|
1484
|
+
const information = operationalKnowledgeNote(composition, soulName);
|
|
1485
|
+
if (information) console.log(`\nINFO: ${information}`);
|
|
1444
1486
|
console.log(`\nFinal composed AGENTS.md for ${soulName}:\n\n${composition.text}`);
|
|
1445
1487
|
} else console.log("\nPass --soul <name> to inspect final composed AGENTS.md.");
|
|
1446
1488
|
}
|
|
@@ -2433,6 +2475,28 @@ function trust() {
|
|
|
2433
2475
|
if (!id || id.startsWith("--")) { cmdFail("E_USAGE", "usage: oats trust <capability> [--dir <dir>] | oats trust <package> --all-capabilities [--dir <dir>]"); return; }
|
|
2434
2476
|
const dir = dirFlag();
|
|
2435
2477
|
const all = args.includes("--all-capabilities");
|
|
2478
|
+
// A v3 deployment needs an EXPLICIT immutable artifact-set selection. Never
|
|
2479
|
+
// guess one or reinterpret its lock through the classic approval engine.
|
|
2480
|
+
try {
|
|
2481
|
+
for (const scope of lockLevelsUp(dir).reverse()) {
|
|
2482
|
+
// Discriminate with the shared bounded decoder only. Classic readers
|
|
2483
|
+
// retain all v1/v2 validation, including implicit (versionless) v1 locks.
|
|
2484
|
+
let version;
|
|
2485
|
+
try { version = parseStrictJson(readPortableBytes(join(scope, OATS_LOCK_FILE)))?.lockfileVersion; }
|
|
2486
|
+
catch { continue; } // Let the existing classic reader diagnose its input.
|
|
2487
|
+
if (version !== 3) continue;
|
|
2488
|
+
const portable = readLock3(scope).lock;
|
|
2489
|
+
if (!portable) throw oatsError("selection-changed", "selection lock disappeared; repeat explicit trust selection");
|
|
2490
|
+
const sets = [...new Set(Object.values(portable.selections).map(row => row.available).filter(key => key && Object.hasOwn(portable.artifactSets[key].capabilities, id)))].sort();
|
|
2491
|
+
const commands = sets.map(artifactSet => ({ artifactSet,
|
|
2492
|
+
command: `oats trust ${shellQuote(id)} --deployment ${shellQuote(scope)} --artifact-set ${shellQuote(artifactSet)}` }));
|
|
2493
|
+
const message = commands.length && !all
|
|
2494
|
+
? `lock v3 requires exact artifact-set approval; run ${commands.map(item => item.command).join(" or ")}`
|
|
2495
|
+
: `lock v3 approvals are per capability; use prepare's selections[].artifactSet for each capability in approvalRequired[]: oats trust <capability> --deployment ${shellQuote(scope)} --artifact-set <sha256-artifact-set>`;
|
|
2496
|
+
if (JSON_MODE) jsonFail("needs-configuration", message, { deployment: scope, commands });
|
|
2497
|
+
die(message);
|
|
2498
|
+
}
|
|
2499
|
+
} catch (error) { cmdFail(error.code || "invalid-lock", error.message); return; }
|
|
2436
2500
|
// Package-backed approval path (per-capability, or explicit bulk on a package id).
|
|
2437
2501
|
let pkgs, locks;
|
|
2438
2502
|
try { pkgs = listInstalledPackages(dir); locks = readPackageLocks(dir); } catch (e) { cmdFail(e.code || "invalid-lock", e.message || e); return; }
|
|
@@ -3928,16 +3992,17 @@ function spawnCmd() {
|
|
|
3928
3992
|
if (!agent && !instrFile && !defFile) {
|
|
3929
3993
|
const def = listAgentDefs(process.cwd()).find((d) => d.name === name);
|
|
3930
3994
|
if (!def) bail("E_UNKNOWN_AGENT", `unknown agent "${name}" (known: ${listAgents(root).map((a) => a.name).join(", ") || "none"}; importable defs: ${listAgentDefs(process.cwd()).map((d) => d.name).join(", ") || "none"}) — pass --instructions-file or --def-file to create a local agent`);
|
|
3931
|
-
agent = upsertLocalAgent(root, { name: def.name, file: def.path, repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model") });
|
|
3995
|
+
agent = upsertLocalAgent(root, { name: def.name, file: def.path, repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), oatsCore: !args.includes("--no-oats-core") });
|
|
3932
3996
|
} else if (!agent || agent.kind === "local") {
|
|
3933
3997
|
agent = upsertLocalAgent(root, {
|
|
3934
|
-
name, file: defFile, instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
|
|
3998
|
+
name, file: defFile, instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined, oatsCore: !args.includes("--no-oats-core"),
|
|
3935
3999
|
repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo: yoloFlag(),
|
|
3936
4000
|
});
|
|
3937
4001
|
} else {
|
|
3938
4002
|
bail("E_BAD_ARGS", `"${name}" is a persistent agent — spawn it without --instructions-file/--def-file`);
|
|
3939
4003
|
}
|
|
3940
4004
|
}
|
|
4005
|
+
for (const information of agent.notes || []) note(`[${information.code}] ${information.message}`);
|
|
3941
4006
|
// Lineage is explicit: --relation child|sibling|parent|unrelated anchors the new
|
|
3942
4007
|
// instance to --relative-to <instance>. --parent X is sugar for
|
|
3943
4008
|
// --relative-to X --relation child (agents spawning sub-agents pass their own
|
|
@@ -4221,10 +4286,134 @@ async function paneCmd() {
|
|
|
4221
4286
|
die("`oats pane` has been retired — the OATS Desktop app (packages/desktop) is the control panel now.");
|
|
4222
4287
|
}
|
|
4223
4288
|
|
|
4289
|
+
function onboardCmd() {
|
|
4290
|
+
const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
|
|
4291
|
+
const values = new Map();
|
|
4292
|
+
for (let i = 1; i < args.length; i++) {
|
|
4293
|
+
const arg = args[i];
|
|
4294
|
+
if (["--json", "--force-existing"].includes(arg)) { values.set(arg.slice(2), true); continue; }
|
|
4295
|
+
if (!["--dir", "--workspace"].includes(arg) || values.has(arg.slice(2)) || !args[i + 1] || args[i + 1].startsWith("--")) {
|
|
4296
|
+
fail("E_BAD_ARGS", "usage: oats onboard [--dir <deployment>] [--workspace <git:source[@revision]>] [--force-existing] [--json]");
|
|
4297
|
+
}
|
|
4298
|
+
values.set(arg.slice(2), args[++i]);
|
|
4299
|
+
}
|
|
4300
|
+
dropAmbientRoot();
|
|
4301
|
+
let deployment, root, acquired, created, configFile, configBefore, configWritten;
|
|
4302
|
+
try {
|
|
4303
|
+
// Like create: an existing enclosing roster wins, otherwise bootstrap at
|
|
4304
|
+
// the enclosing Git root or explicit directory. Canonicalize existing parents.
|
|
4305
|
+
const requested = resolve(values.get("dir") || process.cwd()), missing = [];
|
|
4306
|
+
let parent = requested;
|
|
4307
|
+
while (!existsSync(parent)) { missing.unshift(basename(parent)); parent = dirname(parent); }
|
|
4308
|
+
const start = join(realpathSync(parent), ...missing);
|
|
4309
|
+
root = findRoot(start) || join(defaultRepo(start) || start, "agents");
|
|
4310
|
+
deployment = dirname(root);
|
|
4311
|
+
assertNoSymlinkedParents(deployment, root, "onboard agents root");
|
|
4312
|
+
assertNoSymlinkedParents(deployment, join(deployment, "local-agents", SETUP_EXPERT), "local setup soul");
|
|
4313
|
+
const assertSetupAbsent = () => {
|
|
4314
|
+
for (const candidate of [join(deployment, "local-agents", SETUP_EXPERT), join(root, SETUP_EXPERT), join(root, "local-agents", SETUP_EXPERT), join(root, "tmp-agents", SETUP_EXPERT)]) {
|
|
4315
|
+
let present = false;
|
|
4316
|
+
try { lstatSync(candidate); present = true; } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
4317
|
+
if (present) throw Object.assign(new Error(`existing or incomplete setup expert is preserved at ${candidate}; it will not be overwritten`), { code: "E_AGENT_EXISTS" });
|
|
4318
|
+
}
|
|
4319
|
+
};
|
|
4320
|
+
assertSetupAbsent();
|
|
4321
|
+
const agents = listAgents(root);
|
|
4322
|
+
if (agents.length && !values.get("force-existing")) throw Object.assign(new Error("deployment already has agents; use --force-existing to add the setup expert without replacing them"), { code: "E_DEPLOYMENT_NOT_EMPTY" });
|
|
4323
|
+
if (findAgent(root, SETUP_EXPERT)) throw Object.assign(new Error("oats-setup-expert already exists; it will not be overwritten"), { code: "E_AGENT_EXISTS" });
|
|
4324
|
+
const edition = loadSetupExpertEdition(values.get("workspace"));
|
|
4325
|
+
for (const key of ["description", "runtime", "model"]) if (edition.declaration[key] !== undefined) assertSafeConfigValue(edition.declaration[key], `setup edition ${key}`);
|
|
4326
|
+
const catalog = officialPackageCatalog(), entry = catalog["oats.framework"];
|
|
4327
|
+
if (!Object.hasOwn(catalog, "oats.framework") || !entry?.url || !entry.ref
|
|
4328
|
+
|| SETUP_CAPABILITIES.some(id => { const m = officialCapabilityPackage(id); return !m.available || m.package !== "oats.framework" || m.migratedCapability !== id; })) {
|
|
4329
|
+
throw Object.assign(new Error("official oats.framework with core/setup aliases and a published revision is required"), { code: "needs-configuration" });
|
|
4330
|
+
}
|
|
4331
|
+
const catalogSource = parsePortableSource(`git:${entry.url}@${entry.ref}#${entry.path ?? DEFAULT_PACKAGE_PATH}`);
|
|
4332
|
+
const file = join(deployment, "oats-config.yaml");
|
|
4333
|
+
if (existsSync(file) && !lstatSync(file).isFile()) throw Object.assign(new Error("onboard will not replace a non-regular deployment configuration"), { code: "E_CONFIG_BROKEN" });
|
|
4334
|
+
const before = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
4335
|
+
configFile = file; configBefore = before;
|
|
4336
|
+
const caps = readCapabilitiesModel(file), previous = resolveOatsConfig(deployment, SETUP_EXPERT);
|
|
4337
|
+
// Exclusions are for the NEW soul only. Never turn off an existing root's
|
|
4338
|
+
// global provider/layer just to make bootstrap work under --force-existing.
|
|
4339
|
+
for (const cap of previous.capabilities) {
|
|
4340
|
+
if (SETUP_CAPABILITIES.includes(cap.id)) continue;
|
|
4341
|
+
let target;
|
|
4342
|
+
if (cap.layer) {
|
|
4343
|
+
const existing = caps.layers[cap.layer];
|
|
4344
|
+
if (existing && (existing === "none" || existing.capability !== cap.id)) throw Object.assign(new Error(`cannot safely exclude ${cap.id} for the setup soul at this level; choose a fresh deployment`), { code: "needs-configuration" });
|
|
4345
|
+
target = caps.layers[cap.layer] ||= { capability: cap.id };
|
|
4346
|
+
} else target = caps.additive[cap.id] ||= {};
|
|
4347
|
+
target.souls = { ...target.souls, [SETUP_EXPERT]: false };
|
|
4348
|
+
}
|
|
4349
|
+
if (before === null && !agents.length) for (const layer of LAYERS) caps.layers[layer] ??= "none";
|
|
4350
|
+
for (const id of SETUP_CAPABILITIES) {
|
|
4351
|
+
const target = caps.additive[id] ||= {};
|
|
4352
|
+
if (target.from && target.from !== "installed") throw Object.assign(new Error(`${id} already selects another provenance; choose a fresh deployment`), { code: "needs-configuration" });
|
|
4353
|
+
target.from = "installed"; target.souls = { ...target.souls, [SETUP_EXPERT]: true };
|
|
4354
|
+
}
|
|
4355
|
+
const text = replaceCapabilitiesBlock(before ?? `name: ${scaffoldConfigName(deployment)}\n`, caps);
|
|
4356
|
+
mkdirSync(deployment, { recursive: true });
|
|
4357
|
+
acquired = acquirePackage(deployment, "oats.framework", { expectPackage: "oats.framework",
|
|
4358
|
+
catalog(id, selector) { const selected = Object.hasOwn(catalog, id) ? catalog[id] : null; return selected?.url ? { url: selected.url, ref: selector || selected.ref, path: selected.path } : undefined; },
|
|
4359
|
+
assertCommittable(plan) {
|
|
4360
|
+
const pkg = plan.packages.find(p => p.package === "oats.framework");
|
|
4361
|
+
if (edition.packageIntegrity && pkg?.integrity !== edition.packageIntegrity) throw Object.assign(new Error("selected edition's same-repository package differs from the official acquisition; align the reviewed source and catalog explicitly"), { code: "integrity-drift" });
|
|
4362
|
+
for (const id of SETUP_CAPABILITIES) {
|
|
4363
|
+
const cap = plan.capabilities.find(c => c.capability === id);
|
|
4364
|
+
if (!cap || cap.package !== "oats.framework" || cap.layer || Object.values(cap.executableSurface || {}).some(value => Array.isArray(value) && value.length)) {
|
|
4365
|
+
throw Object.assign(new Error(`setup bootstrap needs resources-only ${id}; executable surfaces require a separate explicit approval path`), { code: "approval-required" });
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
} });
|
|
4369
|
+
if ((existsSync(file) ? readFileSync(file, "utf8") : null) !== before) throw Object.assign(new Error("deployment configuration changed during acquisition; nothing was activated"), { code: "E_CONFIG_CHANGED" });
|
|
4370
|
+
writeFileAtomic(file, text); configWritten = text;
|
|
4371
|
+
const selected = resolveOatsConfig(deployment, SETUP_EXPERT);
|
|
4372
|
+
if (selected.capabilities.length !== 2 || SETUP_CAPABILITIES.some(id => !selected.capabilities.some(c => c.id === id))) {
|
|
4373
|
+
throw Object.assign(new Error("setup expert's effective configuration contains other capabilities; no soul or hook was created"), { code: "needs-configuration" });
|
|
4374
|
+
}
|
|
4375
|
+
// Required capabilities were checked resources-only before acquisition: no
|
|
4376
|
+
// unrelated or executable soul-scaffold hooks can run during local creation.
|
|
4377
|
+
assertNoSymlinkedParents(deployment, root, "onboard agents root");
|
|
4378
|
+
assertNoSymlinkedParents(deployment, join(deployment, "local-agents", SETUP_EXPERT), "local setup soul");
|
|
4379
|
+
assertSetupAbsent();
|
|
4380
|
+
mkdirSync(root, { recursive: true });
|
|
4381
|
+
created = coreCreateAgent(root, { name: SETUP_EXPERT, local: true, oatsCore: false, repo: deployment, work: "directory",
|
|
4382
|
+
runtime: edition.declaration.runtime, model: edition.declaration.model, yolo: false,
|
|
4383
|
+
description: edition.declaration.description, instructions: edition.instructions });
|
|
4384
|
+
const pkg = acquired.installed.find(p => p.package === "oats.framework");
|
|
4385
|
+
const source = parsePortableSource(`git:${catalogSource.url}@${pkg.commit}#${pkg.path}`);
|
|
4386
|
+
const requires = { capabilities: Object.fromEntries(SETUP_CAPABILITIES.map(id => [id, { source: `${source.source}#${source.path}` }])) };
|
|
4387
|
+
const soulFile = join(created.soul, "soul.yaml");
|
|
4388
|
+
writeFileAtomic(soulFile, readFileSync(soulFile, "utf8") + `requires: ${JSON.stringify(requires)}\ndefaults: ${JSON.stringify(edition.declaration.defaults)}\n`);
|
|
4389
|
+
const agent = findAgent(root, SETUP_EXPERT), composition = composeInstanceAgentsMd(created.soul, deployment, SETUP_EXPERT, "directory", "local");
|
|
4390
|
+
planInstanceResources({ resolved: composition.resolved, soulDir: created.soul, agent, contextDir: deployment, composition });
|
|
4391
|
+
const argv = [process.execPath, CLI_BIN, "spawn", SETUP_EXPERT, "--dir", deployment, "--no-yolo", "--task", "Help me configure this deployment and adopt my workspace with explicit approvals."];
|
|
4392
|
+
const result = { mode: "classic", captured: false, deployment, agentsRoot: root, ...created, source: edition.source,
|
|
4393
|
+
package: { id: pkg.package, version: pkg.version, commit: pkg.commit, path: pkg.path }, lockFile: acquired.lockFile,
|
|
4394
|
+
capabilities: [...SETUP_CAPABILITIES], launched: false, next: { argv, command: argv.map(shellQuote).join(" ") } };
|
|
4395
|
+
if (JSON_MODE) jsonOk(result);
|
|
4396
|
+
else { console.log(`Created local ${SETUP_EXPERT} in ${deployment} (classic bootstrap, not captured preparation).`); console.log(`No model was launched. Next:\n${result.next.command}`); }
|
|
4397
|
+
} catch (error) {
|
|
4398
|
+
// Roll back only configuration bytes still exactly owned by this attempt.
|
|
4399
|
+
// Acquired artifacts/locks and any incomplete new soul remain visible evidence.
|
|
4400
|
+
let configRestored = false;
|
|
4401
|
+
if (configWritten !== undefined) {
|
|
4402
|
+
try {
|
|
4403
|
+
if (lstatSync(configFile).isFile() && readFileSync(configFile, "utf8") === configWritten) {
|
|
4404
|
+
if (configBefore === null) rmSync(configFile); else writeFileAtomic(configFile, configBefore);
|
|
4405
|
+
configRestored = true;
|
|
4406
|
+
}
|
|
4407
|
+
} catch { /* never erase another writer's change or hide a failed rollback */ }
|
|
4408
|
+
}
|
|
4409
|
+
fail(error.code || "E_ONBOARD_FAILED", error.message, { deployment, agentsRoot: root, packageAcquired: !!acquired, soul: created?.soul, configRestored, launched: false });
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4224
4413
|
function createCmd() {
|
|
4225
4414
|
const yolo = yoloFlag();
|
|
4226
4415
|
const name = args[1];
|
|
4227
|
-
if (!name || name.startsWith("--")) die("usage: oats create <name> [--local] [--description <d>] [--type <agent-type>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--runtime pi|claude|codex] [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]");
|
|
4416
|
+
if (!name || name.startsWith("--")) die("usage: oats create <name> [--local] [--no-oats-core] [--description <d>] [--type <agent-type>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--runtime pi|claude|codex] [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]");
|
|
4228
4417
|
const local = args.includes("--local");
|
|
4229
4418
|
const startDir = dirFlag();
|
|
4230
4419
|
// `create` BOOTSTRAPS a deployment: with no agents/ or local-agents/ yet,
|
|
@@ -4243,11 +4432,12 @@ function createCmd() {
|
|
|
4243
4432
|
}
|
|
4244
4433
|
const instrFile = flag("instructions-file");
|
|
4245
4434
|
const r = coreCreateAgent(root, {
|
|
4246
|
-
name, local, description: flag("description"), type: flag("type"), repo: flag("repo") || (flag("work") === "directory" ? undefined : defaultRepo(process.cwd())),
|
|
4435
|
+
name, local, oatsCore: !args.includes("--no-oats-core"), description: flag("description"), type: flag("type"), repo: flag("repo") || (flag("work") === "directory" ? undefined : defaultRepo(process.cwd())),
|
|
4247
4436
|
work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
|
|
4248
4437
|
instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
|
|
4249
4438
|
});
|
|
4250
4439
|
if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
|
|
4440
|
+
for (const information of r.notes || []) console.error(`[${information.code}] ${information.message}`);
|
|
4251
4441
|
if (bootstrapped) console.log(`Created deployment root ${shortPath(root)} (this scope had no agents/ yet)`);
|
|
4252
4442
|
console.log(`Created ${r.kind === "local" ? "LOCAL agent (uncommitted — soul lives in local-agents/, gitignored)" : "agent"} "${r.agent}" — soul at ${shortPath(r.soul)}`);
|
|
4253
4443
|
console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then: oats spawn ${r.agent} --task "..."`);
|
|
@@ -4900,6 +5090,14 @@ if (cmd === "prepare" || captured?.args[0] === "prepare") {
|
|
|
4900
5090
|
if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
|
|
4901
5091
|
prepareCmd(); process.exit(0);
|
|
4902
5092
|
}
|
|
5093
|
+
if (cmd === "onboard" || captured?.args[0] === "onboard") {
|
|
5094
|
+
if (captured) {
|
|
5095
|
+
if (JSON_MODE) jsonFail("E_BAD_ARGS", "onboard is explicit classic bootstrap and cannot use captured selectors");
|
|
5096
|
+
die("onboard is explicit classic bootstrap and cannot use captured selectors");
|
|
5097
|
+
}
|
|
5098
|
+
if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
|
|
5099
|
+
onboardCmd(); process.exit(0);
|
|
5100
|
+
}
|
|
4903
5101
|
// Other commands retain their existing explicit/inherited selection rules.
|
|
4904
5102
|
try { captured ??= capturedSelector(args); }
|
|
4905
5103
|
catch (error) {
|
|
@@ -5028,7 +5226,10 @@ Usage:
|
|
|
5028
5226
|
--instance <name> | --home <abs> attachments over its saved route (bytes stream on
|
|
5029
5227
|
--file <path> [--json] ssh stdin; sha256 verified); the server must
|
|
5030
5228
|
advertise session-upload (oats 0.22.13 or later)
|
|
5031
|
-
oats
|
|
5229
|
+
oats onboard [--dir <deployment>] bootstrap a LOCAL setup expert from official
|
|
5230
|
+
[--workspace <git:source[@revision]>] capabilities; classic path, not captured prepare;
|
|
5231
|
+
[--force-existing] [--json] prints the next spawn command, never launches
|
|
5232
|
+
oats create <name> [--local] [--no-oats-core] create an agent soul; --local = full
|
|
5032
5233
|
[--description <d>] [--repo <r>] soul under local-agents/ (uncommitted,
|
|
5033
5234
|
[--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
|
|
5034
5235
|
[--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]
|
|
@@ -5058,6 +5259,7 @@ Usage:
|
|
|
5058
5259
|
oats spawn <agent> [--task <text>] spawn an instance (tmux/Herdr; --no-launch
|
|
5059
5260
|
[--purpose <slug>] [--repo <r>] = scaffold only); --instructions-file/
|
|
5060
5261
|
[--parent <instance>] --def-file creates a local agent;
|
|
5262
|
+
[--no-oats-core] omit the default only on NEW local souls;
|
|
5061
5263
|
[--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
|
|
5062
5264
|
[--relative-to <instance>] new instance to an existing one; --parent X
|
|
5063
5265
|
[--relative-root <agents-root>] disambiguates same-named team anchors
|
|
@@ -5210,8 +5412,10 @@ The turn record (core — every conversation captured, searchable, replicated):
|
|
|
5210
5412
|
packages/experimental/README.md
|
|
5211
5413
|
|
|
5212
5414
|
oats inspect --request <absolute-json-file> [--json]
|
|
5213
|
-
|
|
5214
|
-
|
|
5415
|
+
[--emit-prepare-request <new-absolute-json-file>]
|
|
5416
|
+
fresh source/workspace/member metadata; optional private
|
|
5417
|
+
request export uses the existing fresh-request builder;
|
|
5418
|
+
no provider execution or preparation authority
|
|
5215
5419
|
oats prepare --request <absolute-json-file> [--json]
|
|
5216
5420
|
complete public preparation input; no mixed flags,
|
|
5217
5421
|
inherited binding, implicit setup or launch authority
|
|
@@ -5223,6 +5427,10 @@ The turn record (core — every conversation captured, searchable, replicated):
|
|
|
5223
5427
|
same preparation through workspace imports
|
|
5224
5428
|
oats inspect --deployment <abs> --resolution <id> [--composition] [--json]
|
|
5225
5429
|
[--helper <exact-map-key>] inspect retained source/helper inputs, not today's configuration
|
|
5430
|
+
oats trust <capability> --deployment <abs> --artifact-set <sha256-…> [--json]
|
|
5431
|
+
approve one exact prepared artifact; use each
|
|
5432
|
+
selections[].artifactSet for capability ids
|
|
5433
|
+
in prepare's approvalRequired[] at that selection
|
|
5226
5434
|
oats trust <capability> --deployment <abs> --resolution <id> [--json]
|
|
5227
5435
|
explicitly approve that exact captured artifact
|
|
5228
5436
|
oats <namespace> <command> --deployment <abs> --resolution <id> -- [args…]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runBindingWire } from '../lib/binding-wire.mjs';
|
|
3
|
+
|
|
4
|
+
const args=process.argv.slice(2);
|
|
5
|
+
if(args.includes('--help') || args.includes('-h')) {
|
|
6
|
+
process.stdout.write('oats aweb provider binding phase (manifest-owned JSON stdin/stdout)\n');
|
|
7
|
+
} else {
|
|
8
|
+
const phase=args[0];
|
|
9
|
+
if(args.length!==1) await runBindingWire(phase,[],process.stdout);
|
|
10
|
+
else await runBindingWire(phase);
|
|
11
|
+
}
|
|
@@ -40,6 +40,10 @@ import { execFileSync } from "node:child_process";
|
|
|
40
40
|
import { chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
41
41
|
import { hostname } from "node:os";
|
|
42
42
|
import { join, dirname, resolve, delimiter } from "node:path";
|
|
43
|
+
import { loadCapturedAwebExecution, requireCapturedAwebAction } from "../lib/captured-execution.mjs";
|
|
44
|
+
import { assessCapturedSessionReadiness, querySelectedKernel } from "../lib/session-readiness.mjs";
|
|
45
|
+
import { runCapturedNative } from "../lib/captured-native.mjs";
|
|
46
|
+
import { parseBindingJson } from "../lib/binding-wire.mjs";
|
|
43
47
|
|
|
44
48
|
/** Run a command as ARGV — never a shell string. Team ids, aliases, instance
|
|
45
49
|
* names and invite tokens all flow through here; quoting them correctly is a
|
|
@@ -110,6 +114,28 @@ const warn = (m) => out({ warning: `oats-aweb: ${String(m).slice(0, 300)}` });
|
|
|
110
114
|
* state already exists (e.g. a joined identity) so retire can undo it. */
|
|
111
115
|
const fatal = (m, meta) => out({ ...(meta ? { meta } : {}), warning: `oats-aweb: ${String(m).slice(0, 300)}` }, 1);
|
|
112
116
|
|
|
117
|
+
// Any selected snapshot enters the captured consumer BEFORE legacy settings,
|
|
118
|
+
// root discovery or identity handling. Invalid-present never falls back.
|
|
119
|
+
try {
|
|
120
|
+
const loaded = loadCapturedAwebExecution();
|
|
121
|
+
if (loaded.kind === "captured") {
|
|
122
|
+
const event = process.argv[2] || process.env.OATS_EVENT;
|
|
123
|
+
if ((process.env.OATS_EVENT && process.env.OATS_EVENT !== event) || process.argv.slice(3).some(arg => arg !== "--json")) throw new Error("captured entrypoint arguments differ from the selected action");
|
|
124
|
+
const manifest = JSON.parse(readFileSync(new URL("../oats.json", import.meta.url), "utf8"));
|
|
125
|
+
const selected = requireCapturedAwebAction(loaded, event, manifest);
|
|
126
|
+
const settings = parseBindingJson(Buffer.from(process.env.OATS_SETTINGS || "{}"));
|
|
127
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k => k !== "delivery")) throw new Error("captured settings support delivery only; no identity copying or ambient fallback");
|
|
128
|
+
const checked = assessCapturedSessionReadiness({ binding: selected.binding, invocation: selected.context, settings }, {
|
|
129
|
+
query(args, options) { selected.assertCurrent(); const result = querySelectedKernel(args, options); selected.assertCurrent(); return result; },
|
|
130
|
+
});
|
|
131
|
+
if (checked.status !== "ready") out({ ...checked, warning: `oats-aweb: ${checked.problems[0].message}` }, 1);
|
|
132
|
+
const result = runCapturedNative({ selected, event, settings, run });
|
|
133
|
+
out(result.output, result.exitCode);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
out({ status: "needs-configuration", problems: [{ code: "invalid-binding", message: "invalid or changed captured aweb execution input" }], warning: "oats-aweb: invalid or changed captured aweb execution input; no legacy fallback was used" }, 1);
|
|
137
|
+
}
|
|
138
|
+
|
|
113
139
|
const event = process.env.OATS_EVENT || process.argv[2];
|
|
114
140
|
const instance = process.env.OATS_INSTANCE;
|
|
115
141
|
const home = process.env.OATS_HOME || process.cwd();
|