@awebai/oats 0.24.0 → 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.
Files changed (43) hide show
  1. package/README.md +224 -394
  2. package/bin/oats.mjs +192 -13
  3. package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +11 -0
  4. package/capabilities/oats-aweb/bin/oats-aweb.mjs +26 -0
  5. package/capabilities/oats-aweb/lib/binding-wire.mjs +214 -0
  6. package/capabilities/oats-aweb/lib/captured-execution.mjs +91 -0
  7. package/capabilities/oats-aweb/lib/captured-native.mjs +91 -0
  8. package/capabilities/oats-aweb/lib/invocation-shape.mjs +135 -0
  9. package/capabilities/oats-aweb/lib/portable-binding.mjs +146 -0
  10. package/capabilities/oats-aweb/lib/session-readiness.mjs +56 -0
  11. package/capabilities/oats-aweb/oats.json +12 -3
  12. package/capabilities/oats-okf/lib/captured-worker.mjs +12 -4
  13. package/capabilities/oats-okf/oats.json +1 -1
  14. package/docs/capabilities.md +4 -0
  15. package/docs/design/2026-09-20-redesign-program-board.md +83 -0
  16. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +287 -0
  17. package/docs/design/2026-09-20-workspace-onboarding-public.md +124 -0
  18. package/docs/design/README.md +42 -0
  19. package/docs/first-team.md +43 -1
  20. package/docs/knowledge-theory.md +353 -111
  21. package/docs/knowledge.md +10 -1
  22. package/docs/layers.md +89 -354
  23. package/docs/official-marketplace.md +84 -0
  24. package/docs/packages.md +15 -7
  25. package/docs/release-notes/v0.24.1.md +17 -0
  26. package/docs/release-notes/v0.24.2.md +21 -0
  27. package/docs/souls-and-instances.md +45 -7
  28. package/docs/workspace-adoption.md +314 -0
  29. package/docs/workspaces.md +154 -0
  30. package/injects/oats-portable.md +8 -5
  31. package/lib/core.mjs +89 -17
  32. package/lib/portable-onboarding.mjs +19 -0
  33. package/lib/prepared-resources.mjs +1 -1
  34. package/lib/provider-binding-broker.mjs +6 -1
  35. package/lib/setup-expert-source.mjs +76 -0
  36. package/package-catalog.json +9 -5
  37. package/package.json +3 -1
  38. package/skills/oats-config/SKILL.md +4 -5
  39. package/skills/oats-portable/SKILL.md +1 -2
  40. package/skills/oats-portable-artifacts/SKILL.md +2 -2
  41. package/souls/oats-setup-expert/AGENTS.md +60 -0
  42. package/souls/oats-setup-expert/soul.yaml +14 -0
  43. package/skills/oats-portable-setup/SKILL.md +0 -69
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";
@@ -23,13 +23,13 @@ import { enableTmuxMouse, tmuxConfigPath, tmuxMouseEnabled } from "../lib/tmux-c
23
23
  import {
24
24
  LAYERS, WORK_MODES, LEGACY_HOME_CAPABILITIES_DIR, OATS_LOCK_FILE, OATS_VERSION, OAS_SCOPE_REMEDY, RETIRED_CAPABILITIES, detectOasScopes, retiredCapabilityReason, configChain, configCapabilityEntries, manifestOperations,
25
25
  acquireCapability, restoreCapabilities, marketplaceCapabilities,
26
- capabilityManifests, capabilityManifest, capabilityMissingRequires, capabilityIntegrity, capabilityTrust, capabilityExecutablePath, activateCapturedScaffold, loadCapturedDispatch, prepareCapturedComposition, resolveCapturedHelper, capturedNativeSessionAvailability, scaffoldCapturedInstance, startCapturedInstanceSession, withCapturedBindingFile, withCapturedInvocationContextFile,
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;
@@ -95,6 +97,21 @@ const CLI_BIN = realpathSync(fileURLToPath(import.meta.url));
95
97
  const jsonFail = (code, message, details) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) } })); process.exit(1); };
96
98
  const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result })); };
97
99
 
100
+ function inspectOnboardingCmd() {
101
+ const fail = (code, message) => JSON_MODE ? jsonFail(code, message) : die(message);
102
+ let file;
103
+ for (let index = 1; index < args.length; index++) {
104
+ if (args[index] === "--json") continue;
105
+ if (args[index] !== "--request" || file !== undefined || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "source inspection accepts one --request <absolute-json> and --json only");
106
+ file = args[++index];
107
+ }
108
+ try {
109
+ const input = readPortablePreparationRequest({ file });
110
+ const result = inspectPortableOnboarding(input);
111
+ if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
112
+ } catch (error) { fail(error.code || "E_INSPECT_FAILED", error.message); }
113
+ }
114
+
98
115
  function prepareCmd() {
99
116
  const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
100
117
  const values = new Map(), allowed = new Set(["request", "dir", "source", "revision", "export", "alias", "workspace", "workspace-revision", "work"]);
@@ -370,6 +387,9 @@ function capturedCommand(selector) {
370
387
  const selected = helperSelection?.executionBinding ?? target;
371
388
  const loaded = loadCapturedDispatch({ deployment: selected.deployment, resolution: selected.resolution, action: { kind: args.includes("--composition") ? "compose" : "inspect" } });
372
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
+ },
373
393
  ...(helperSelection ? { helperSelection } : {}),
374
394
  capabilities: [...loaded.capabilities.values()].map(({ id, manifest }) => ({ id, version: manifest.version,
375
395
  approval: loaded.approvals.find((entry) => entry.artifact.capability === id).status })),
@@ -490,6 +510,10 @@ function resolveForDoctor(ctx, soulName, { json } = {}) {
490
510
  die(`${e.message}`);
491
511
  }
492
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
+ }
493
517
  function doctorComposition(ctx, soulName) {
494
518
  if (!soulName) return undefined;
495
519
  const root = findRoot(ctx);
@@ -1244,7 +1268,8 @@ function doctorJson(dir) {
1244
1268
  hooks: Object.keys(r.layers[l].hooks || {}), missingRequires: r.layers[l].missingRequires,
1245
1269
  provenance: r.provenance[l],
1246
1270
  } : { provenance: r.provenance[l] || null }])),
1247
- kernelInjection: r.kernelInjection,
1271
+ kernelInjection: composition?.resolved.kernelInjection ?? r.kernelInjection,
1272
+ information: operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : [],
1248
1273
  injects: r.injects,
1249
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 })),
1250
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 }])),
@@ -1273,6 +1298,7 @@ function doctor(dir) {
1273
1298
  const soulName = flag("soul");
1274
1299
  const chain = configChain(ctx);
1275
1300
  const r = resolveForDoctor(ctx, soulName);
1301
+ const composition = doctorComposition(ctx, soulName);
1276
1302
  console.log(`oats doctor — resolved from ${shortPath(ctx)}\n`);
1277
1303
 
1278
1304
  // Kernel/bridge version skew (published in lockstep from one tag).
@@ -1317,7 +1343,8 @@ function doctor(dir) {
1317
1343
  }
1318
1344
 
1319
1345
  console.log("\nKernel injection:");
1320
- console.log(` oats: ${r.kernelInjection?.inject ? shortPath(r.kernelInjection.inject) : "none"} [${r.kernelInjection?.provenance || "default"}]`);
1346
+ const kernelInjection = composition?.resolved.kernelInjection ?? r.kernelInjection;
1347
+ console.log(` oats: ${kernelInjection?.inject ? shortPath(kernelInjection.inject) : "none"} [${kernelInjection?.provenance || "default"}]`);
1321
1348
 
1322
1349
  console.log("\nUnconditional injections (outermost→innermost):");
1323
1350
  if (r.injects.length === 0) console.log(" (none)");
@@ -1425,7 +1452,8 @@ function doctor(dir) {
1425
1452
  }
1426
1453
 
1427
1454
  if (soulName) {
1428
- const composition = doctorComposition(ctx, soulName);
1455
+ const information = operationalKnowledgeNote(composition, soulName);
1456
+ if (information) console.log(`\nINFO: ${information}`);
1429
1457
  console.log(`\nFinal composed AGENTS.md for ${soulName}:\n\n${composition.text}`);
1430
1458
  } else console.log("\nPass --soul <name> to inspect final composed AGENTS.md.");
1431
1459
  }
@@ -3913,16 +3941,17 @@ function spawnCmd() {
3913
3941
  if (!agent && !instrFile && !defFile) {
3914
3942
  const def = listAgentDefs(process.cwd()).find((d) => d.name === name);
3915
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`);
3916
- 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") });
3917
3945
  } else if (!agent || agent.kind === "local") {
3918
3946
  agent = upsertLocalAgent(root, {
3919
- 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"),
3920
3948
  repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo: yoloFlag(),
3921
3949
  });
3922
3950
  } else {
3923
3951
  bail("E_BAD_ARGS", `"${name}" is a persistent agent — spawn it without --instructions-file/--def-file`);
3924
3952
  }
3925
3953
  }
3954
+ for (const information of agent.notes || []) note(`[${information.code}] ${information.message}`);
3926
3955
  // Lineage is explicit: --relation child|sibling|parent|unrelated anchors the new
3927
3956
  // instance to --relative-to <instance>. --parent X is sugar for
3928
3957
  // --relative-to X --relation child (agents spawning sub-agents pass their own
@@ -4206,10 +4235,134 @@ async function paneCmd() {
4206
4235
  die("`oats pane` has been retired — the OATS Desktop app (packages/desktop) is the control panel now.");
4207
4236
  }
4208
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
+
4209
4362
  function createCmd() {
4210
4363
  const yolo = yoloFlag();
4211
4364
  const name = args[1];
4212
- 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>]");
4213
4366
  const local = args.includes("--local");
4214
4367
  const startDir = dirFlag();
4215
4368
  // `create` BOOTSTRAPS a deployment: with no agents/ or local-agents/ yet,
@@ -4228,11 +4381,12 @@ function createCmd() {
4228
4381
  }
4229
4382
  const instrFile = flag("instructions-file");
4230
4383
  const r = coreCreateAgent(root, {
4231
- 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())),
4232
4385
  work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
4233
4386
  instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
4234
4387
  });
4235
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}`);
4236
4390
  if (bootstrapped) console.log(`Created deployment root ${shortPath(root)} (this scope had no agents/ yet)`);
4237
4391
  console.log(`Created ${r.kind === "local" ? "LOCAL agent (uncommitted — soul lives in local-agents/, gitignored)" : "agent"} "${r.agent}" — soul at ${shortPath(r.soul)}`);
4238
4392
  console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then: oats spawn ${r.agent} --task "..."`);
@@ -4867,6 +5021,16 @@ catch (error) {
4867
5021
  if (JSON_MODE) jsonFail(error.code || "E_BAD_ARGS", error.message);
4868
5022
  die(error.message);
4869
5023
  }
5024
+ const sourceInspection = (cmd === "inspect" || captured?.args[0] === "inspect")
5025
+ && args.some(arg => arg === "--request" || arg.startsWith("--request="));
5026
+ if (sourceInspection) {
5027
+ if (captured) {
5028
+ if (JSON_MODE) jsonFail("E_BAD_ARGS", "source inspection is explicit new work and cannot use captured selectors");
5029
+ die("source inspection is explicit new work and cannot use captured selectors");
5030
+ }
5031
+ if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
5032
+ inspectOnboardingCmd(); process.exit(0);
5033
+ }
4870
5034
  if (cmd === "prepare" || captured?.args[0] === "prepare") {
4871
5035
  if (captured) {
4872
5036
  if (JSON_MODE) jsonFail("E_BAD_ARGS", "prepare is explicit new work and cannot use captured selectors");
@@ -4875,6 +5039,14 @@ if (cmd === "prepare" || captured?.args[0] === "prepare") {
4875
5039
  if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
4876
5040
  prepareCmd(); process.exit(0);
4877
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
+ }
4878
5050
  // Other commands retain their existing explicit/inherited selection rules.
4879
5051
  try { captured ??= capturedSelector(args); }
4880
5052
  catch (error) {
@@ -5003,7 +5175,10 @@ Usage:
5003
5175
  --instance <name> | --home <abs> attachments over its saved route (bytes stream on
5004
5176
  --file <path> [--json] ssh stdin; sha256 verified); the server must
5005
5177
  advertise session-upload (oats 0.22.13 or later)
5006
- oats create <name> [--local] create an agent soul; --local = full
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
5007
5182
  [--description <d>] [--repo <r>] soul under local-agents/ (uncommitted,
5008
5183
  [--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
5009
5184
  [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]
@@ -5033,6 +5208,7 @@ Usage:
5033
5208
  oats spawn <agent> [--task <text>] spawn an instance (tmux/Herdr; --no-launch
5034
5209
  [--purpose <slug>] [--repo <r>] = scaffold only); --instructions-file/
5035
5210
  [--parent <instance>] --def-file creates a local agent;
5211
+ [--no-oats-core] omit the default only on NEW local souls;
5036
5212
  [--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
5037
5213
  [--relative-to <instance>] new instance to an existing one; --parent X
5038
5214
  [--relative-root <agents-root>] disambiguates same-named team anchors
@@ -5184,6 +5360,9 @@ The turn record (core — every conversation captured, searchable, replicated):
5184
5360
  design, repo checkout only; see
5185
5361
  packages/experimental/README.md
5186
5362
 
5363
+ oats inspect --request <absolute-json-file> [--json]
5364
+ read-only fresh source/workspace/member metadata;
5365
+ no current config, provider execution or preparation authority
5187
5366
  oats prepare --request <absolute-json-file> [--json]
5188
5367
  complete public preparation input; no mixed flags,
5189
5368
  inherited binding, implicit setup or launch authority
@@ -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
+ }