@awebai/oats 0.24.1 → 0.24.2
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 +163 -12
- 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/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/souls-and-instances.md +25 -0
- package/docs/workspace-adoption.md +94 -65
- package/injects/oats-portable.md +4 -0
- package/lib/core.mjs +46 -13
- 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,
|
|
@@ -53,11 +53,13 @@ import { readPortablePreparationRequest } from "../lib/portable-onboarding-reque
|
|
|
53
53
|
import { portableScope } from "../lib/portable-state.mjs";
|
|
54
54
|
import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../lib/captured-operation-process.mjs";
|
|
55
55
|
import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
|
|
56
|
+
import { loadSetupExpertEdition, SETUP_EXPERT, SETUP_CAPABILITIES } from "../lib/setup-expert-source.mjs";
|
|
57
|
+
import { parsePortableSource } from "../lib/source-spec.mjs";
|
|
56
58
|
|
|
57
59
|
const args = process.argv.slice(2);
|
|
58
60
|
let cmd = args[0];
|
|
59
61
|
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"]);
|
|
62
|
+
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
63
|
const flag = (name) => {
|
|
62
64
|
const i = args.indexOf(`--${name}`);
|
|
63
65
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -385,6 +387,9 @@ function capturedCommand(selector) {
|
|
|
385
387
|
const selected = helperSelection?.executionBinding ?? target;
|
|
386
388
|
const loaded = loadCapturedDispatch({ deployment: selected.deployment, resolution: selected.resolution, action: { kind: args.includes("--composition") ? "compose" : "inspect" } });
|
|
387
389
|
const result = { resolution: loaded.resolution, capture: loaded.record.capture, nativeSession: capturedNativeSessionAvailability(),
|
|
390
|
+
launchSelection: loaded.record.dispatch.launch === null ? null : {
|
|
391
|
+
runtime: loaded.record.dispatch.launch.runtime, model: loaded.record.dispatch.launch.model,
|
|
392
|
+
},
|
|
388
393
|
...(helperSelection ? { helperSelection } : {}),
|
|
389
394
|
capabilities: [...loaded.capabilities.values()].map(({ id, manifest }) => ({ id, version: manifest.version,
|
|
390
395
|
approval: loaded.approvals.find((entry) => entry.artifact.capability === id).status })),
|
|
@@ -505,6 +510,10 @@ function resolveForDoctor(ctx, soulName, { json } = {}) {
|
|
|
505
510
|
die(`${e.message}`);
|
|
506
511
|
}
|
|
507
512
|
}
|
|
513
|
+
function operationalKnowledgeNote(composition, soulName) {
|
|
514
|
+
return composition && !composition.oatsCoreDeclared
|
|
515
|
+
? `soul ${soulName} has no oats.core capability; kernel-shipped operational skills are deprecated` : null;
|
|
516
|
+
}
|
|
508
517
|
function doctorComposition(ctx, soulName) {
|
|
509
518
|
if (!soulName) return undefined;
|
|
510
519
|
const root = findRoot(ctx);
|
|
@@ -1259,7 +1268,8 @@ function doctorJson(dir) {
|
|
|
1259
1268
|
hooks: Object.keys(r.layers[l].hooks || {}), missingRequires: r.layers[l].missingRequires,
|
|
1260
1269
|
provenance: r.provenance[l],
|
|
1261
1270
|
} : { provenance: r.provenance[l] || null }])),
|
|
1262
|
-
kernelInjection: r.kernelInjection,
|
|
1271
|
+
kernelInjection: composition?.resolved.kernelInjection ?? r.kernelInjection,
|
|
1272
|
+
information: operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : [],
|
|
1263
1273
|
injects: r.injects,
|
|
1264
1274
|
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
1275
|
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 +1298,7 @@ function doctor(dir) {
|
|
|
1288
1298
|
const soulName = flag("soul");
|
|
1289
1299
|
const chain = configChain(ctx);
|
|
1290
1300
|
const r = resolveForDoctor(ctx, soulName);
|
|
1301
|
+
const composition = doctorComposition(ctx, soulName);
|
|
1291
1302
|
console.log(`oats doctor — resolved from ${shortPath(ctx)}\n`);
|
|
1292
1303
|
|
|
1293
1304
|
// Kernel/bridge version skew (published in lockstep from one tag).
|
|
@@ -1332,7 +1343,8 @@ function doctor(dir) {
|
|
|
1332
1343
|
}
|
|
1333
1344
|
|
|
1334
1345
|
console.log("\nKernel injection:");
|
|
1335
|
-
|
|
1346
|
+
const kernelInjection = composition?.resolved.kernelInjection ?? r.kernelInjection;
|
|
1347
|
+
console.log(` oats: ${kernelInjection?.inject ? shortPath(kernelInjection.inject) : "none"} [${kernelInjection?.provenance || "default"}]`);
|
|
1336
1348
|
|
|
1337
1349
|
console.log("\nUnconditional injections (outermost→innermost):");
|
|
1338
1350
|
if (r.injects.length === 0) console.log(" (none)");
|
|
@@ -1440,7 +1452,8 @@ function doctor(dir) {
|
|
|
1440
1452
|
}
|
|
1441
1453
|
|
|
1442
1454
|
if (soulName) {
|
|
1443
|
-
const
|
|
1455
|
+
const information = operationalKnowledgeNote(composition, soulName);
|
|
1456
|
+
if (information) console.log(`\nINFO: ${information}`);
|
|
1444
1457
|
console.log(`\nFinal composed AGENTS.md for ${soulName}:\n\n${composition.text}`);
|
|
1445
1458
|
} else console.log("\nPass --soul <name> to inspect final composed AGENTS.md.");
|
|
1446
1459
|
}
|
|
@@ -3928,16 +3941,17 @@ function spawnCmd() {
|
|
|
3928
3941
|
if (!agent && !instrFile && !defFile) {
|
|
3929
3942
|
const def = listAgentDefs(process.cwd()).find((d) => d.name === name);
|
|
3930
3943
|
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") });
|
|
3944
|
+
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
3945
|
} else if (!agent || agent.kind === "local") {
|
|
3933
3946
|
agent = upsertLocalAgent(root, {
|
|
3934
|
-
name, file: defFile, instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
|
|
3947
|
+
name, file: defFile, instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined, oatsCore: !args.includes("--no-oats-core"),
|
|
3935
3948
|
repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo: yoloFlag(),
|
|
3936
3949
|
});
|
|
3937
3950
|
} else {
|
|
3938
3951
|
bail("E_BAD_ARGS", `"${name}" is a persistent agent — spawn it without --instructions-file/--def-file`);
|
|
3939
3952
|
}
|
|
3940
3953
|
}
|
|
3954
|
+
for (const information of agent.notes || []) note(`[${information.code}] ${information.message}`);
|
|
3941
3955
|
// Lineage is explicit: --relation child|sibling|parent|unrelated anchors the new
|
|
3942
3956
|
// instance to --relative-to <instance>. --parent X is sugar for
|
|
3943
3957
|
// --relative-to X --relation child (agents spawning sub-agents pass their own
|
|
@@ -4221,10 +4235,134 @@ async function paneCmd() {
|
|
|
4221
4235
|
die("`oats pane` has been retired — the OATS Desktop app (packages/desktop) is the control panel now.");
|
|
4222
4236
|
}
|
|
4223
4237
|
|
|
4238
|
+
function onboardCmd() {
|
|
4239
|
+
const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
|
|
4240
|
+
const values = new Map();
|
|
4241
|
+
for (let i = 1; i < args.length; i++) {
|
|
4242
|
+
const arg = args[i];
|
|
4243
|
+
if (["--json", "--force-existing"].includes(arg)) { values.set(arg.slice(2), true); continue; }
|
|
4244
|
+
if (!["--dir", "--workspace"].includes(arg) || values.has(arg.slice(2)) || !args[i + 1] || args[i + 1].startsWith("--")) {
|
|
4245
|
+
fail("E_BAD_ARGS", "usage: oats onboard [--dir <deployment>] [--workspace <git:source[@revision]>] [--force-existing] [--json]");
|
|
4246
|
+
}
|
|
4247
|
+
values.set(arg.slice(2), args[++i]);
|
|
4248
|
+
}
|
|
4249
|
+
dropAmbientRoot();
|
|
4250
|
+
let deployment, root, acquired, created, configFile, configBefore, configWritten;
|
|
4251
|
+
try {
|
|
4252
|
+
// Like create: an existing enclosing roster wins, otherwise bootstrap at
|
|
4253
|
+
// the enclosing Git root or explicit directory. Canonicalize existing parents.
|
|
4254
|
+
const requested = resolve(values.get("dir") || process.cwd()), missing = [];
|
|
4255
|
+
let parent = requested;
|
|
4256
|
+
while (!existsSync(parent)) { missing.unshift(basename(parent)); parent = dirname(parent); }
|
|
4257
|
+
const start = join(realpathSync(parent), ...missing);
|
|
4258
|
+
root = findRoot(start) || join(defaultRepo(start) || start, "agents");
|
|
4259
|
+
deployment = dirname(root);
|
|
4260
|
+
assertNoSymlinkedParents(deployment, root, "onboard agents root");
|
|
4261
|
+
assertNoSymlinkedParents(deployment, join(deployment, "local-agents", SETUP_EXPERT), "local setup soul");
|
|
4262
|
+
const assertSetupAbsent = () => {
|
|
4263
|
+
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)]) {
|
|
4264
|
+
let present = false;
|
|
4265
|
+
try { lstatSync(candidate); present = true; } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
4266
|
+
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" });
|
|
4267
|
+
}
|
|
4268
|
+
};
|
|
4269
|
+
assertSetupAbsent();
|
|
4270
|
+
const agents = listAgents(root);
|
|
4271
|
+
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" });
|
|
4272
|
+
if (findAgent(root, SETUP_EXPERT)) throw Object.assign(new Error("oats-setup-expert already exists; it will not be overwritten"), { code: "E_AGENT_EXISTS" });
|
|
4273
|
+
const edition = loadSetupExpertEdition(values.get("workspace"));
|
|
4274
|
+
for (const key of ["description", "runtime", "model"]) if (edition.declaration[key] !== undefined) assertSafeConfigValue(edition.declaration[key], `setup edition ${key}`);
|
|
4275
|
+
const catalog = officialPackageCatalog(), entry = catalog["oats.framework"];
|
|
4276
|
+
if (!Object.hasOwn(catalog, "oats.framework") || !entry?.url || !entry.ref
|
|
4277
|
+
|| SETUP_CAPABILITIES.some(id => { const m = officialCapabilityPackage(id); return !m.available || m.package !== "oats.framework" || m.migratedCapability !== id; })) {
|
|
4278
|
+
throw Object.assign(new Error("official oats.framework with core/setup aliases and a published revision is required"), { code: "needs-configuration" });
|
|
4279
|
+
}
|
|
4280
|
+
const catalogSource = parsePortableSource(`git:${entry.url}@${entry.ref}#${entry.path ?? DEFAULT_PACKAGE_PATH}`);
|
|
4281
|
+
const file = join(deployment, "oats-config.yaml");
|
|
4282
|
+
if (existsSync(file) && !lstatSync(file).isFile()) throw Object.assign(new Error("onboard will not replace a non-regular deployment configuration"), { code: "E_CONFIG_BROKEN" });
|
|
4283
|
+
const before = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
4284
|
+
configFile = file; configBefore = before;
|
|
4285
|
+
const caps = readCapabilitiesModel(file), previous = resolveOatsConfig(deployment, SETUP_EXPERT);
|
|
4286
|
+
// Exclusions are for the NEW soul only. Never turn off an existing root's
|
|
4287
|
+
// global provider/layer just to make bootstrap work under --force-existing.
|
|
4288
|
+
for (const cap of previous.capabilities) {
|
|
4289
|
+
if (SETUP_CAPABILITIES.includes(cap.id)) continue;
|
|
4290
|
+
let target;
|
|
4291
|
+
if (cap.layer) {
|
|
4292
|
+
const existing = caps.layers[cap.layer];
|
|
4293
|
+
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" });
|
|
4294
|
+
target = caps.layers[cap.layer] ||= { capability: cap.id };
|
|
4295
|
+
} else target = caps.additive[cap.id] ||= {};
|
|
4296
|
+
target.souls = { ...target.souls, [SETUP_EXPERT]: false };
|
|
4297
|
+
}
|
|
4298
|
+
if (before === null && !agents.length) for (const layer of LAYERS) caps.layers[layer] ??= "none";
|
|
4299
|
+
for (const id of SETUP_CAPABILITIES) {
|
|
4300
|
+
const target = caps.additive[id] ||= {};
|
|
4301
|
+
if (target.from && target.from !== "installed") throw Object.assign(new Error(`${id} already selects another provenance; choose a fresh deployment`), { code: "needs-configuration" });
|
|
4302
|
+
target.from = "installed"; target.souls = { ...target.souls, [SETUP_EXPERT]: true };
|
|
4303
|
+
}
|
|
4304
|
+
const text = replaceCapabilitiesBlock(before ?? `name: ${scaffoldConfigName(deployment)}\n`, caps);
|
|
4305
|
+
mkdirSync(deployment, { recursive: true });
|
|
4306
|
+
acquired = acquirePackage(deployment, "oats.framework", { expectPackage: "oats.framework",
|
|
4307
|
+
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; },
|
|
4308
|
+
assertCommittable(plan) {
|
|
4309
|
+
const pkg = plan.packages.find(p => p.package === "oats.framework");
|
|
4310
|
+
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" });
|
|
4311
|
+
for (const id of SETUP_CAPABILITIES) {
|
|
4312
|
+
const cap = plan.capabilities.find(c => c.capability === id);
|
|
4313
|
+
if (!cap || cap.package !== "oats.framework" || cap.layer || Object.values(cap.executableSurface || {}).some(value => Array.isArray(value) && value.length)) {
|
|
4314
|
+
throw Object.assign(new Error(`setup bootstrap needs resources-only ${id}; executable surfaces require a separate explicit approval path`), { code: "approval-required" });
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
} });
|
|
4318
|
+
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" });
|
|
4319
|
+
writeFileAtomic(file, text); configWritten = text;
|
|
4320
|
+
const selected = resolveOatsConfig(deployment, SETUP_EXPERT);
|
|
4321
|
+
if (selected.capabilities.length !== 2 || SETUP_CAPABILITIES.some(id => !selected.capabilities.some(c => c.id === id))) {
|
|
4322
|
+
throw Object.assign(new Error("setup expert's effective configuration contains other capabilities; no soul or hook was created"), { code: "needs-configuration" });
|
|
4323
|
+
}
|
|
4324
|
+
// Required capabilities were checked resources-only before acquisition: no
|
|
4325
|
+
// unrelated or executable soul-scaffold hooks can run during local creation.
|
|
4326
|
+
assertNoSymlinkedParents(deployment, root, "onboard agents root");
|
|
4327
|
+
assertNoSymlinkedParents(deployment, join(deployment, "local-agents", SETUP_EXPERT), "local setup soul");
|
|
4328
|
+
assertSetupAbsent();
|
|
4329
|
+
mkdirSync(root, { recursive: true });
|
|
4330
|
+
created = coreCreateAgent(root, { name: SETUP_EXPERT, local: true, oatsCore: false, repo: deployment, work: "directory",
|
|
4331
|
+
runtime: edition.declaration.runtime, model: edition.declaration.model, yolo: false,
|
|
4332
|
+
description: edition.declaration.description, instructions: edition.instructions });
|
|
4333
|
+
const pkg = acquired.installed.find(p => p.package === "oats.framework");
|
|
4334
|
+
const source = parsePortableSource(`git:${catalogSource.url}@${pkg.commit}#${pkg.path}`);
|
|
4335
|
+
const requires = { capabilities: Object.fromEntries(SETUP_CAPABILITIES.map(id => [id, { source: `${source.source}#${source.path}` }])) };
|
|
4336
|
+
const soulFile = join(created.soul, "soul.yaml");
|
|
4337
|
+
writeFileAtomic(soulFile, readFileSync(soulFile, "utf8") + `requires: ${JSON.stringify(requires)}\ndefaults: ${JSON.stringify(edition.declaration.defaults)}\n`);
|
|
4338
|
+
const agent = findAgent(root, SETUP_EXPERT), composition = composeInstanceAgentsMd(created.soul, deployment, SETUP_EXPERT, "directory", "local");
|
|
4339
|
+
planInstanceResources({ resolved: composition.resolved, soulDir: created.soul, agent, contextDir: deployment, composition });
|
|
4340
|
+
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."];
|
|
4341
|
+
const result = { mode: "classic", captured: false, deployment, agentsRoot: root, ...created, source: edition.source,
|
|
4342
|
+
package: { id: pkg.package, version: pkg.version, commit: pkg.commit, path: pkg.path }, lockFile: acquired.lockFile,
|
|
4343
|
+
capabilities: [...SETUP_CAPABILITIES], launched: false, next: { argv, command: argv.map(shellQuote).join(" ") } };
|
|
4344
|
+
if (JSON_MODE) jsonOk(result);
|
|
4345
|
+
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}`); }
|
|
4346
|
+
} catch (error) {
|
|
4347
|
+
// Roll back only configuration bytes still exactly owned by this attempt.
|
|
4348
|
+
// Acquired artifacts/locks and any incomplete new soul remain visible evidence.
|
|
4349
|
+
let configRestored = false;
|
|
4350
|
+
if (configWritten !== undefined) {
|
|
4351
|
+
try {
|
|
4352
|
+
if (lstatSync(configFile).isFile() && readFileSync(configFile, "utf8") === configWritten) {
|
|
4353
|
+
if (configBefore === null) rmSync(configFile); else writeFileAtomic(configFile, configBefore);
|
|
4354
|
+
configRestored = true;
|
|
4355
|
+
}
|
|
4356
|
+
} catch { /* never erase another writer's change or hide a failed rollback */ }
|
|
4357
|
+
}
|
|
4358
|
+
fail(error.code || "E_ONBOARD_FAILED", error.message, { deployment, agentsRoot: root, packageAcquired: !!acquired, soul: created?.soul, configRestored, launched: false });
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4224
4362
|
function createCmd() {
|
|
4225
4363
|
const yolo = yoloFlag();
|
|
4226
4364
|
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>]");
|
|
4365
|
+
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
4366
|
const local = args.includes("--local");
|
|
4229
4367
|
const startDir = dirFlag();
|
|
4230
4368
|
// `create` BOOTSTRAPS a deployment: with no agents/ or local-agents/ yet,
|
|
@@ -4243,11 +4381,12 @@ function createCmd() {
|
|
|
4243
4381
|
}
|
|
4244
4382
|
const instrFile = flag("instructions-file");
|
|
4245
4383
|
const r = coreCreateAgent(root, {
|
|
4246
|
-
name, local, description: flag("description"), type: flag("type"), repo: flag("repo") || (flag("work") === "directory" ? undefined : defaultRepo(process.cwd())),
|
|
4384
|
+
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
4385
|
work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
|
|
4248
4386
|
instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
|
|
4249
4387
|
});
|
|
4250
4388
|
if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
|
|
4389
|
+
for (const information of r.notes || []) console.error(`[${information.code}] ${information.message}`);
|
|
4251
4390
|
if (bootstrapped) console.log(`Created deployment root ${shortPath(root)} (this scope had no agents/ yet)`);
|
|
4252
4391
|
console.log(`Created ${r.kind === "local" ? "LOCAL agent (uncommitted — soul lives in local-agents/, gitignored)" : "agent"} "${r.agent}" — soul at ${shortPath(r.soul)}`);
|
|
4253
4392
|
console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then: oats spawn ${r.agent} --task "..."`);
|
|
@@ -4900,6 +5039,14 @@ if (cmd === "prepare" || captured?.args[0] === "prepare") {
|
|
|
4900
5039
|
if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
|
|
4901
5040
|
prepareCmd(); process.exit(0);
|
|
4902
5041
|
}
|
|
5042
|
+
if (cmd === "onboard" || captured?.args[0] === "onboard") {
|
|
5043
|
+
if (captured) {
|
|
5044
|
+
if (JSON_MODE) jsonFail("E_BAD_ARGS", "onboard is explicit classic bootstrap and cannot use captured selectors");
|
|
5045
|
+
die("onboard is explicit classic bootstrap and cannot use captured selectors");
|
|
5046
|
+
}
|
|
5047
|
+
if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
|
|
5048
|
+
onboardCmd(); process.exit(0);
|
|
5049
|
+
}
|
|
4903
5050
|
// Other commands retain their existing explicit/inherited selection rules.
|
|
4904
5051
|
try { captured ??= capturedSelector(args); }
|
|
4905
5052
|
catch (error) {
|
|
@@ -5028,7 +5175,10 @@ Usage:
|
|
|
5028
5175
|
--instance <name> | --home <abs> attachments over its saved route (bytes stream on
|
|
5029
5176
|
--file <path> [--json] ssh stdin; sha256 verified); the server must
|
|
5030
5177
|
advertise session-upload (oats 0.22.13 or later)
|
|
5031
|
-
oats
|
|
5178
|
+
oats onboard [--dir <deployment>] bootstrap a LOCAL setup expert from official
|
|
5179
|
+
[--workspace <git:source[@revision]>] capabilities; classic path, not captured prepare;
|
|
5180
|
+
[--force-existing] [--json] prints the next spawn command, never launches
|
|
5181
|
+
oats create <name> [--local] [--no-oats-core] create an agent soul; --local = full
|
|
5032
5182
|
[--description <d>] [--repo <r>] soul under local-agents/ (uncommitted,
|
|
5033
5183
|
[--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
|
|
5034
5184
|
[--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]
|
|
@@ -5058,6 +5208,7 @@ Usage:
|
|
|
5058
5208
|
oats spawn <agent> [--task <text>] spawn an instance (tmux/Herdr; --no-launch
|
|
5059
5209
|
[--purpose <slug>] [--repo <r>] = scaffold only); --instructions-file/
|
|
5060
5210
|
[--parent <instance>] --def-file creates a local agent;
|
|
5211
|
+
[--no-oats-core] omit the default only on NEW local souls;
|
|
5061
5212
|
[--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
|
|
5062
5213
|
[--relative-to <instance>] new instance to an existing one; --parent X
|
|
5063
5214
|
[--relative-root <agents-root>] disambiguates same-named team anchors
|
|
@@ -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();
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { TextDecoder } from 'node:util';
|
|
2
|
+
import { assessCapturedSessionReadiness } from './session-readiness.mjs';
|
|
3
|
+
import {
|
|
4
|
+
MESSAGING_CONTRACT,
|
|
5
|
+
MESSAGING_CONTRACT_VERSION,
|
|
6
|
+
bindMessagingDomain,
|
|
7
|
+
normalizeMessagingDeclarations,
|
|
8
|
+
validateAwebBinding,
|
|
9
|
+
validateAwebInvocationContext,
|
|
10
|
+
} from './portable-binding.mjs';
|
|
11
|
+
|
|
12
|
+
export const BINDING_WIRE_LIMITS=Object.freeze({bytes:1024*1024,depth:32,entries:16384});
|
|
13
|
+
const CAPABILITY='oats.aweb',SLOT='messaging';
|
|
14
|
+
const phases=new Set(['normalize','bind','check']);
|
|
15
|
+
const declarationKinds=new Set(['soul','workspace','adoption','operator']);
|
|
16
|
+
const errorCodes=new Set(['needs-configuration','requirement-conflict','invalid-binding','authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified']);
|
|
17
|
+
const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
|
|
18
|
+
const wireError=code=>{throw Object.assign(new Error(code),{wireCode:code});};
|
|
19
|
+
const canonical=value=>value===null || typeof value!=='object'?JSON.stringify(value):Array.isArray(value)?`[${value.map(canonical).join(',')}]`:`{${Object.keys(value).sort().map(key=>`${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
20
|
+
const same=(a,b)=>canonical(a)===canonical(b);
|
|
21
|
+
function keys(value,allowed,required) {
|
|
22
|
+
if(!obj(value)) wireError('invalid-binding');
|
|
23
|
+
for(const key of Object.keys(value)) if(!allowed.includes(key)) wireError('invalid-binding');
|
|
24
|
+
for(const key of required) if(!Object.hasOwn(value,key)) wireError('invalid-binding');
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function pointer(value) {return typeof value==='string' && /^(?:\/(?:[^~]|~[01])*)+$/.test(value);}
|
|
28
|
+
function bindingKey(value) {return pointer(value) && value.startsWith('/bindings/messaging/') && value.length>'/bindings/messaging/'.length;}
|
|
29
|
+
|
|
30
|
+
class StrictJsonParser {
|
|
31
|
+
constructor(text,{depth,entries}) {this.text=text;this.maxDepth=depth;this.maxEntries=entries;this.at=0;this.entries=0;}
|
|
32
|
+
whitespace() {while(/[\u0009\u000a\u000d\u0020]/.test(this.text[this.at] || '')) this.at++;}
|
|
33
|
+
count() {if(++this.entries>this.maxEntries) wireError('invalid-binding');}
|
|
34
|
+
string() {
|
|
35
|
+
if(this.text[this.at]!=='"') wireError('invalid-binding');
|
|
36
|
+
const start=this.at++;
|
|
37
|
+
while(this.at<this.text.length) {
|
|
38
|
+
const code=this.text.charCodeAt(this.at++);
|
|
39
|
+
if(code===0x22) {try{return JSON.parse(this.text.slice(start,this.at));}catch{wireError('invalid-binding');}}
|
|
40
|
+
if(code<0x20) wireError('invalid-binding');
|
|
41
|
+
if(code===0x5c) {
|
|
42
|
+
const escaped=this.text[this.at++];
|
|
43
|
+
if(escaped==='u') {if(!/^[0-9a-fA-F]{4}$/.test(this.text.slice(this.at,this.at+4))) wireError('invalid-binding');this.at+=4;}
|
|
44
|
+
else if(!'"\\/bfnrt'.includes(escaped || '')) wireError('invalid-binding');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
wireError('invalid-binding');
|
|
48
|
+
}
|
|
49
|
+
value(depth=1) {
|
|
50
|
+
this.whitespace();const char=this.text[this.at];
|
|
51
|
+
if(char==='"') return this.string();
|
|
52
|
+
if(char==='{') {
|
|
53
|
+
if(depth>this.maxDepth) wireError('invalid-binding');this.at++;this.whitespace();
|
|
54
|
+
const result=Object.create(null),seen=new Set();if(this.text[this.at]==='}') {this.at++;return result;}
|
|
55
|
+
while(true) {
|
|
56
|
+
this.whitespace();const key=this.string();if(seen.has(key)) wireError('invalid-binding');seen.add(key);this.count();
|
|
57
|
+
this.whitespace();if(this.text[this.at++]!==':') wireError('invalid-binding');result[key]=this.value(depth+1);this.whitespace();
|
|
58
|
+
const next=this.text[this.at++];if(next==='}') return result;if(next!==',') wireError('invalid-binding');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if(char==='[') {
|
|
62
|
+
if(depth>this.maxDepth) wireError('invalid-binding');this.at++;this.whitespace();
|
|
63
|
+
const result=[];if(this.text[this.at]===']') {this.at++;return result;}
|
|
64
|
+
while(true) {this.count();result.push(this.value(depth+1));this.whitespace();const next=this.text[this.at++];if(next===']') return result;if(next!==',') wireError('invalid-binding');}
|
|
65
|
+
}
|
|
66
|
+
for(const [token,value] of [['true',true],['false',false],['null',null]]) if(this.text.startsWith(token,this.at)) {this.at+=token.length;return value;}
|
|
67
|
+
const match=/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.at));
|
|
68
|
+
if(!match) wireError('invalid-binding');this.at+=match[0].length;const number=Number(match[0]);if(!Number.isFinite(number)) wireError('invalid-binding');return number;
|
|
69
|
+
}
|
|
70
|
+
parse() {this.whitespace();const value=this.value();this.whitespace();if(this.at!==this.text.length) wireError('invalid-binding');return value;}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseBindingJson(bytes,limits=BINDING_WIRE_LIMITS) {
|
|
74
|
+
if(!Buffer.isBuffer(bytes)) bytes=Buffer.from(bytes);
|
|
75
|
+
if(bytes.length>limits.bytes) wireError('invalid-binding');
|
|
76
|
+
let text;try{text=new TextDecoder('utf-8',{fatal:true}).decode(bytes);}catch{wireError('invalid-binding');}
|
|
77
|
+
return new StrictJsonParser(text,limits).parse();
|
|
78
|
+
}
|
|
79
|
+
function settings(value) {
|
|
80
|
+
if(!obj(value)) wireError('invalid-binding');
|
|
81
|
+
if(Object.hasOwn(value,'identity')) wireError('provider-not-qualified');
|
|
82
|
+
keys(value,['delivery'],[]);
|
|
83
|
+
if(value.delivery!==undefined && !['channel','session'].includes(value.delivery)) wireError('needs-configuration');
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function request(value,phase) {
|
|
87
|
+
keys(value,['schemaVersion','phase','slot','capability','settings','input'],['schemaVersion','phase','slot','capability','settings','input']);
|
|
88
|
+
if(value.schemaVersion!==1 || value.phase!==phase || value.slot!==SLOT || value.capability!==CAPABILITY) wireError('invalid-binding');
|
|
89
|
+
settings(value.settings);return value;
|
|
90
|
+
}
|
|
91
|
+
function declaration(value) {
|
|
92
|
+
keys(value,['kind','value','origin','origins'],['kind','value','origin','origins']);
|
|
93
|
+
if(!declarationKinds.has(value.kind) || !obj(value.value) || !obj(value.origin) || !obj(value.origins)) wireError('invalid-binding');
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function context(value,{request=false}={}) {
|
|
97
|
+
if(value?.kind==='workspace') {
|
|
98
|
+
keys(value,request?['kind','identity','observation']:['kind','identity'],request?['kind','identity','observation']:['kind','identity']);
|
|
99
|
+
if(!obj(value.identity) || (request && !obj(value.observation))) wireError('invalid-binding');
|
|
100
|
+
return {kind:'workspace',identity:value.identity};
|
|
101
|
+
}
|
|
102
|
+
if(value?.kind==='standalone') {
|
|
103
|
+
keys(value,['kind','key'],['kind','key']);
|
|
104
|
+
if(value.key!==null && (typeof value.key!=='string' || !value.key.trim())) wireError('invalid-binding');
|
|
105
|
+
return {kind:'standalone',key:value.key};
|
|
106
|
+
}
|
|
107
|
+
wireError('invalid-binding');
|
|
108
|
+
}
|
|
109
|
+
function normalizePhase(req) {
|
|
110
|
+
keys(req.input,['declarations','context'],['declarations','context']);
|
|
111
|
+
if(!Array.isArray(req.input.declarations)) wireError('invalid-binding');
|
|
112
|
+
const declarations=req.input.declarations.map(declaration);context(req.input.context,{request:true});
|
|
113
|
+
return normalizeMessagingDeclarations({declarations,context:req.input.context});
|
|
114
|
+
}
|
|
115
|
+
function choiceMap(value,model) {
|
|
116
|
+
if(!obj(value)) wireError('invalid-binding');
|
|
117
|
+
const allowed=new Set(['/bindings/messaging/responsibleHuman','/bindings/messaging/privateTeam','/bindings/messaging/wider',...Object.values(model.teams)]);
|
|
118
|
+
for(const [key,choice] of Object.entries(value)) {
|
|
119
|
+
if(!bindingKey(key) || !allowed.has(key)) wireError('invalid-binding');
|
|
120
|
+
keys(choice,['value','selectedBy','constraints','considered'],['value','selectedBy','constraints','considered']);
|
|
121
|
+
if(!Array.isArray(choice.constraints) || !Array.isArray(choice.considered) || (choice.selectedBy!==null && !obj(choice.selectedBy))) wireError('invalid-binding');
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
function model(value) {
|
|
126
|
+
keys(value,['contract','version','context','contextOrigin','requested','requestedOrigins','aliases','aliasOrigins','teams','privatePolicy','privatePolicyOrigin'],['contract','version','context','contextOrigin','requested','requestedOrigins','aliases','aliasOrigins','teams','privatePolicy','privatePolicyOrigin']);
|
|
127
|
+
if(value.contract!==MESSAGING_CONTRACT || value.version!==MESSAGING_CONTRACT_VERSION || !Array.isArray(value.requested) || !obj(value.requestedOrigins) || !obj(value.aliases) || !obj(value.aliasOrigins) || !obj(value.teams) || typeof value.privatePolicy!=='boolean') wireError('invalid-binding');
|
|
128
|
+
context(value.context);if(value.contextOrigin!==null && !obj(value.contextOrigin)) wireError('invalid-binding');if(value.privatePolicyOrigin!==null && !obj(value.privatePolicyOrigin)) wireError('invalid-binding');
|
|
129
|
+
for(const key of Object.values(value.teams)) if(!bindingKey(key)) wireError('invalid-binding');
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
function bindPhase(req) {
|
|
133
|
+
keys(req.input,['model','choices','context'],['model','choices','context']);
|
|
134
|
+
const normalized=model(req.input.model),selectedContext=context(req.input.context,{request:true});
|
|
135
|
+
if(!same(selectedContext,normalized.context) || (selectedContext.kind==='workspace' && !same(req.input.context.observation,normalized.contextOrigin))) wireError('invalid-binding');
|
|
136
|
+
return bindMessagingDomain({model:normalized,choices:choiceMap(req.input.choices,normalized)});
|
|
137
|
+
}
|
|
138
|
+
function binding(value) {
|
|
139
|
+
keys(value,['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance'],['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance']);
|
|
140
|
+
if(value.schemaVersion!==1 || value.capability!==CAPABILITY || value.payloadContract!==MESSAGING_CONTRACT || value.payloadVersion!==MESSAGING_CONTRACT_VERSION || !obj(value.payload) || !obj(value.credentialRefs) || Object.keys(value.credentialRefs).length || !Array.isArray(value.provenance)) wireError('invalid-binding');
|
|
141
|
+
for(const origin of value.provenance) if(!obj(origin)) wireError('invalid-binding');
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
function checkResult(message) {return {status:'needs-configuration',problems:[{code:'needs-configuration',message}]};}
|
|
145
|
+
function checkPhase(req) {
|
|
146
|
+
keys(req.input,['binding','context','action','invocation'],['binding','context','action']);
|
|
147
|
+
if(!obj(req.input.action) || typeof req.input.action.kind!=='string') wireError('invalid-binding');
|
|
148
|
+
const current=validateAwebBinding(binding(req.input.binding)),selectedContext=context(req.input.context,{request:true});
|
|
149
|
+
if(!same(selectedContext,current.payload.context)) wireError('invalid-binding');
|
|
150
|
+
const invocation=Object.hasOwn(req.input,'invocation')?validateAwebInvocationContext(req.input.invocation,current,{context:req.input.context,action:req.input.action}):null;
|
|
151
|
+
if(['command','hook','operation'].includes(req.input.action.kind) && (!invocation || invocation.instance===null || invocation.intent===null)) return checkResult('an admitted captured instance intent is required for execution');
|
|
152
|
+
if(current.payload.privateTeam===null) return checkResult('an explicit private-team binding is required');
|
|
153
|
+
// Read-only public kernel observations, not an account/grant attestation.
|
|
154
|
+
// Native setup is performed only by the separately admitted execution path.
|
|
155
|
+
return assessCapturedSessionReadiness({binding:current,invocation,settings:req.settings});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function handleBindingRequest(phase,value) {
|
|
159
|
+
if(!phases.has(phase)) wireError('invalid-binding');
|
|
160
|
+
const req=request(value,phase);
|
|
161
|
+
if(phase==='normalize') return normalizePhase(req);
|
|
162
|
+
if(phase==='bind') return bindPhase(req);
|
|
163
|
+
return checkPhase(req);
|
|
164
|
+
}
|
|
165
|
+
function response(phase,body) {return {schemaVersion:1,phase,slot:SLOT,capability:CAPABILITY,...body};}
|
|
166
|
+
function errorCode(error) {if(errorCodes.has(error?.wireCode)) return error.wireCode;if(errorCodes.has(error?.code)) return error.code;return 'invalid-binding';}
|
|
167
|
+
// Only these literal domain diagnostics may cross the wire. Never reflect a
|
|
168
|
+
// caught exception's dynamic alias/key/path, native stderr or credential text.
|
|
169
|
+
const safeReasons=new Map([
|
|
170
|
+
['needs-configuration',[
|
|
171
|
+
'messaging-enabled standalone preparation needs an explicit context key',
|
|
172
|
+
'messaging binding needs one soul declaration',
|
|
173
|
+
'messaging workspace must declare private: per-human',
|
|
174
|
+
'an explicit responsible-human binding is required',
|
|
175
|
+
'an explicit wider-membership consent list is required',
|
|
176
|
+
'a selected wider-team binding is required',
|
|
177
|
+
'a selected wider alias needs an explicit workspace team mapping',
|
|
178
|
+
]],
|
|
179
|
+
['requirement-conflict',['multiple soul messaging declarations','adoption team aliases have conflicting mappings']],
|
|
180
|
+
]);
|
|
181
|
+
const fallbackReasons=Object.freeze({
|
|
182
|
+
'needs-configuration':'messaging settings and explicit binding selections are required',
|
|
183
|
+
'requirement-conflict':'messaging declarations contain incompatible requirements',
|
|
184
|
+
'invalid-binding':'messaging input must match the supported binding contract',
|
|
185
|
+
'authorization-required':'explicit native messaging authorization is required',
|
|
186
|
+
'host-requirement-missing':'a required native messaging host resource is unavailable',
|
|
187
|
+
'provider-unavailable':'the selected messaging provider is unavailable',
|
|
188
|
+
'provider-not-qualified':'the requested messaging configuration is not qualified',
|
|
189
|
+
});
|
|
190
|
+
function errorProblem(error) {
|
|
191
|
+
const code=errorCode(error),message=safeReasons.get(code)?.find(literal=>literal===error?.message)??fallbackReasons[code];
|
|
192
|
+
return {code,message};
|
|
193
|
+
}
|
|
194
|
+
function enforceOutputLimits(value,depth=1,state={entries:0}) {
|
|
195
|
+
if(depth>BINDING_WIRE_LIMITS.depth) wireError('provider-not-qualified');
|
|
196
|
+
if(value===null || typeof value!=='object') return;
|
|
197
|
+
for(const child of Array.isArray(value)?value:Object.values(value)) {
|
|
198
|
+
if(++state.entries>BINDING_WIRE_LIMITS.entries) wireError('provider-not-qualified');
|
|
199
|
+
enforceOutputLimits(child,depth+1,state);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export async function runBindingWire(phase,input=process.stdin,output=process.stdout) {
|
|
203
|
+
let answer;
|
|
204
|
+
try {
|
|
205
|
+
const chunks=[];let length=0;
|
|
206
|
+
for await(const chunk of input) {const bytes=Buffer.from(chunk);length+=bytes.length;if(length>BINDING_WIRE_LIMITS.bytes) wireError('invalid-binding');chunks.push(bytes);}
|
|
207
|
+
const result=handleBindingRequest(phase,parseBindingJson(Buffer.concat(chunks,length)));
|
|
208
|
+
answer=response(phase,{ok:true,result});
|
|
209
|
+
} catch(error) {answer=response(phases.has(phase)?phase:'check',{ok:false,error:errorProblem(error)});}
|
|
210
|
+
let bytes;
|
|
211
|
+
try {enforceOutputLimits(answer);bytes=Buffer.from(JSON.stringify(answer)+'\n');if(bytes.length>BINDING_WIRE_LIMITS.bytes) wireError('provider-not-qualified');}
|
|
212
|
+
catch {bytes=Buffer.from(JSON.stringify(response(phases.has(phase)?phase:'check',{ok:false,error:{code:'provider-not-qualified',message:'messaging response exceeds the supported wire limits'}}))+'\n');}
|
|
213
|
+
output.write(bytes);return answer.ok;
|
|
214
|
+
}
|