@awebai/oats 0.25.8 → 0.25.9
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/capabilities/oats-aweb/bin/oats-aweb.mjs +97 -21
- package/capabilities/oats-aweb/lib/binding-wire.mjs +27 -1
- package/capabilities/oats-aweb/oats.json +7 -1
- package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
- package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
- package/capabilities/oats-okf/lib/migration.mjs +2 -2
- package/capabilities/oats-okf/lib/sources.mjs +5 -4
- package/capabilities/oats-okf/lib/stores.mjs +40 -9
- package/capabilities/oats-okf/lib/worker.mjs +3 -3
- package/capabilities/oats-okf/oats.json +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +20 -9
- package/docs/design/2026-09-24-phase-d-plan.md +20 -0
- package/docs/desktop-cli-api.md +9 -3
- package/docs/integrations.md +31 -6
- package/docs/rebuild-to-v2.md +4 -4
- package/docs/release-notes/v0.25.9.md +23 -0
- package/package-catalog.json +2 -2
- package/package.json +1 -1
|
@@ -137,7 +137,7 @@ try {
|
|
|
137
137
|
const manifest = JSON.parse(readFileSync(new URL("../oats.json", import.meta.url), "utf8"));
|
|
138
138
|
const selected = requireCapturedAwebAction(loaded, event, manifest);
|
|
139
139
|
const settings = parseBindingJson(Buffer.from(process.env.OATS_SETTINGS || "{}"));
|
|
140
|
-
if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k =>
|
|
140
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k => !["delivery", "team", "root", "roots"].includes(k))) throw new Error("captured settings support delivery/team/root readiness only; no identity copying or ambient fallback");
|
|
141
141
|
const checked = assessCapturedSessionReadiness({ binding: selected.binding, invocation: selected.context, settings }, {
|
|
142
142
|
query(args, options) { selected.assertCurrent(); const result = querySelectedKernel(args, options); selected.assertCurrent(); return result; },
|
|
143
143
|
});
|
|
@@ -194,7 +194,7 @@ function gitRootOf(startDir) {
|
|
|
194
194
|
d = parent;
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
function
|
|
197
|
+
function classicAwebRoot() {
|
|
198
198
|
const candidates = [];
|
|
199
199
|
const push = (p) => { if (p && !candidates.includes(resolve(p))) candidates.push(resolve(p)); };
|
|
200
200
|
push(process.env.OATS_TEAM_SCOPE);
|
|
@@ -206,6 +206,36 @@ function awebRoot() {
|
|
|
206
206
|
for (const c of candidates) if (existsSync(join(c, ".aw"))) return c;
|
|
207
207
|
return undefined;
|
|
208
208
|
}
|
|
209
|
+
const hasWorkspaceV2Facts = () => !!(process.env.OATS_WORKSPACE_KEY || process.env.OATS_WORKSPACE_NAME || process.env.OATS_TEAM_LABEL);
|
|
210
|
+
const isClassicDeployment = () => !!process.env.OATS_TEAM_SCOPE && !hasWorkspaceV2Facts();
|
|
211
|
+
const teamConfigRemedy = () => `set messaging.byTeam.<label>.team in the workspace file or settings.oats.aweb.team${isClassicDeployment() ? " (classic: set team.id in oats-config.yaml)" : ""}`;
|
|
212
|
+
function declaredRootCandidate(team = payloadTeam().team) {
|
|
213
|
+
const roots = settings.roots && typeof settings.roots === "object" && !Array.isArray(settings.roots) ? settings.roots : {};
|
|
214
|
+
if (team && typeof roots[team] === "string" && roots[team].trim()) return { root: roots[team].trim(), key: `settings.oats.aweb.roots[${JSON.stringify(team)}]`, declared: true };
|
|
215
|
+
if (typeof settings.root === "string" && settings.root.trim()) return { root: settings.root.trim(), key: "settings.oats.aweb.root", declared: true };
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
function rootSettingCandidate(team = payloadTeam().team) {
|
|
219
|
+
const declared = declaredRootCandidate(team);
|
|
220
|
+
if (declared) return declared;
|
|
221
|
+
const fallback = process.env.OATS_WORKSPACE || (!isClassicDeployment() ? process.env.OATS_TEAM_SCOPE : undefined) || process.cwd();
|
|
222
|
+
return fallback ? { root: fallback, key: "settings.oats.aweb.root", declared: false } : undefined;
|
|
223
|
+
}
|
|
224
|
+
function awebRootProblem(candidate) {
|
|
225
|
+
if (!candidate?.root) return `no messaging root at ${process.cwd()}: run oats aweb setup there or set settings.oats.aweb.root`;
|
|
226
|
+
if (!isAbsolute(candidate.root)) return `${candidate.key} must be an absolute directory whose .aw is the aweb minting root`;
|
|
227
|
+
return `no messaging root at ${resolve(candidate.root)}: run oats aweb setup there or set ${candidate.key}`;
|
|
228
|
+
}
|
|
229
|
+
function resolveAwebRoot() {
|
|
230
|
+
const declared = declaredRootCandidate();
|
|
231
|
+
if (declared && isAbsolute(declared.root) && existsSync(join(resolve(declared.root), ".aw"))) return resolve(declared.root);
|
|
232
|
+
if (declared?.declared) return undefined;
|
|
233
|
+
if (isClassicDeployment()) return classicAwebRoot();
|
|
234
|
+
const candidate = rootSettingCandidate();
|
|
235
|
+
if (candidate && isAbsolute(candidate.root) && existsSync(join(resolve(candidate.root), ".aw"))) return resolve(candidate.root);
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
function awebRoot() { return resolveAwebRoot(); }
|
|
209
239
|
|
|
210
240
|
/** Team memberships from `aw team list --json`. The current CLI returns
|
|
211
241
|
* `memberships`; older output used `teams`. Spawn resolution and `oats aweb
|
|
@@ -411,9 +441,9 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
411
441
|
const service = process.env.OATS_AWEB_URL || yamlScalar(srcWorkspace, "aweb_url");
|
|
412
442
|
if (!service) fatal(`cannot determine the aweb service for ${source} (no aweb_url in its workspace.yaml)`);
|
|
413
443
|
const role = yamlScalar(srcWorkspace, "role_name");
|
|
414
|
-
let team =
|
|
444
|
+
let team = payloadTeam().team;
|
|
415
445
|
if (!team && existsSync(join(source, "teams.yaml"))) team = yamlScalar(readFileSync(join(source, "teams.yaml"), "utf8"), "active_team") || yamlScalar(readFileSync(join(source, "teams.yaml"), "utf8"), "active");
|
|
416
|
-
if (!team || !team.includes(":")) fatal(`cannot determine the team for the retained identity (
|
|
446
|
+
if (!team || !team.includes(":")) fatal(`cannot determine the team for the retained identity (${teamConfigRemedy()}, or keep an active team in ${join(source, "teams.yaml")})`);
|
|
417
447
|
const dest = join(home, ".aw");
|
|
418
448
|
const legacyHome = dirname(source);
|
|
419
449
|
// The lock is taken FIRST: a concurrent second spawn must see it before any
|
|
@@ -490,7 +520,7 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
490
520
|
const launch = (process.env.OATS_RUNTIME || "") === "claude" && deliveryMode === "channel"
|
|
491
521
|
? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" }
|
|
492
522
|
: undefined;
|
|
493
|
-
const env = deliveryMode === "session" ? { AWEB_DELIVERY: "session" } :
|
|
523
|
+
const env = { ...(deliveryMode === "session" ? { AWEB_DELIVERY: "session" } : {}), AWEB_IDENTITY_HOME: dest };
|
|
494
524
|
const deliveryBrief = deliveryMode === "session"
|
|
495
525
|
? ` Notification delivery: external (AWEB_DELIVERY=session); until the host wake broker registers this instance NOTHING wakes you: check \`aw mail inbox\` and \`aw chat pending\` at every task boundary.`
|
|
496
526
|
: "";
|
|
@@ -500,7 +530,7 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
500
530
|
if (hostNote) warnings.push(`oats-aweb: seated${hostNote}`);
|
|
501
531
|
out({
|
|
502
532
|
meta: { team, alias, retained: true, source, lock: lockPath, delivery: deliveryMode, identity: identityMeta({ mode: "global", alias, team, address: shownAddress || expectedAddress || null }), ...(takenOver ? { tookOverFrom: takenOver } : {}) },
|
|
503
|
-
|
|
533
|
+
env,
|
|
504
534
|
brief: `Comms: you are the retained seat of the existing aweb identity "${alias}" on team ${team} (same did and address as the seat you replace; its contacts, routes and conversations are yours).${deliveryBrief} Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill).`,
|
|
505
535
|
...(launch ? { launch } : {}),
|
|
506
536
|
...(warnings.length ? { warning: warnings.join(" | ") } : {}),
|
|
@@ -517,7 +547,11 @@ if (event === "spawn") {
|
|
|
517
547
|
if (identityMode === "local" && settings.identity && typeof settings.identity === "object" && settings.identity.source) retainedSeatSpawn(String(settings.identity.source), settings.identity.takeOver === true);
|
|
518
548
|
let minted; // external identity, once `aw team join` succeeds
|
|
519
549
|
const root = awebRoot();
|
|
520
|
-
if (!root)
|
|
550
|
+
if (!root) {
|
|
551
|
+
const candidate = rootSettingCandidate();
|
|
552
|
+
const classicHint = isClassicDeployment() ? " (classic fallback also checked the bounded team-scope candidates)" : "";
|
|
553
|
+
fatal(`${awebRootProblem(candidate)}, so no identity could be minted and this instance would have no messaging${classicHint}`);
|
|
554
|
+
}
|
|
521
555
|
try {
|
|
522
556
|
// Team correctness: the config's `team:` block wins (id, then name), else the
|
|
523
557
|
// root's active team. ALWAYS pass --team-id explicitly — never inherit whatever
|
|
@@ -527,15 +561,16 @@ if (event === "spawn") {
|
|
|
527
561
|
const resolvedTeam = payloadTeam();
|
|
528
562
|
let team = resolvedTeam.team;
|
|
529
563
|
const teamPayloadMismatch = resolvedTeam.payload && resolvedTeam.env && resolvedTeam.payload !== resolvedTeam.env;
|
|
564
|
+
if (!team && process.env.OATS_TEAM_LABEL) fatal(`cannot determine target team for workspace team label ${JSON.stringify(process.env.OATS_TEAM_LABEL)}, so no identity could be minted — ${teamConfigRemedy()}`);
|
|
530
565
|
if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
531
|
-
if (!team) fatal(
|
|
566
|
+
if (!team) fatal(`cannot determine target team, so no identity could be minted — ${teamConfigRemedy()}, or activate a team at the aweb root`);
|
|
532
567
|
// A bare team name (no namespace) resolves against the root's memberships.
|
|
533
568
|
if (!team.includes(":")) {
|
|
534
569
|
const teams = JSON.parse(run(["aw", "team", "list", "--json"], root));
|
|
535
570
|
const match = teamIdsOf(teams).filter((tid) => String(tid).startsWith(`${team}:`));
|
|
536
571
|
if (match.length === 1) team = match[0];
|
|
537
|
-
else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted —
|
|
538
|
-
else fatal(`no membership matching team "${team}" at ${root}, so no identity could be minted — join or create it first (aweb-team-membership skill), or
|
|
572
|
+
else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted — ${teamConfigRemedy()}`);
|
|
573
|
+
else fatal(`no membership matching team "${team}" at ${root}, so no identity could be minted — join or create it first (aweb-team-membership skill), or ${teamConfigRemedy()}`);
|
|
539
574
|
}
|
|
540
575
|
// Both of these carry the invite token — one mints it, the other spends it —
|
|
541
576
|
// so neither their output nor their diagnostics may reach a log.
|
|
@@ -597,7 +632,7 @@ if (event === "spawn") {
|
|
|
597
632
|
const launch = (process.env.OATS_RUNTIME || "") === "claude" && deliveryMode === "channel"
|
|
598
633
|
? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" }
|
|
599
634
|
: undefined;
|
|
600
|
-
const env = deliveryMode === "session" ? { AWEB_DELIVERY: "session" } :
|
|
635
|
+
const env = { ...(deliveryMode === "session" ? { AWEB_DELIVERY: "session" } : {}), AWEB_IDENTITY_HOME: join(home, ".aw") };
|
|
601
636
|
const channelWarning = undefined;
|
|
602
637
|
if (deliveryMode === "session") wakeRegister(home, join(home, ".aw"));
|
|
603
638
|
const deliveryBrief = deliveryMode === "session"
|
|
@@ -605,7 +640,7 @@ if (event === "spawn") {
|
|
|
605
640
|
: "";
|
|
606
641
|
out({
|
|
607
642
|
meta: { team: joined.team_id, alias, delivery: deliveryMode, identity: identityMeta({ mode: "local", alias, team: joined.team_id }) },
|
|
608
|
-
|
|
643
|
+
env,
|
|
609
644
|
brief: `Comms: you have an aweb identity — alias "${alias}" on team ${joined.team_id}.${mismatch}${deliveryBrief} Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill); coordination stays in your deployment's task layer.`,
|
|
610
645
|
...(launch ? { launch } : {}),
|
|
611
646
|
...(joined.team_id !== team ? { warning: `oats-aweb: team mismatch — joined ${joined.team_id}, expected ${team}` } : teamPayloadMismatch ? { warning: `oats-aweb: settings.oats.aweb.team ${resolvedTeam.payload} differs from OATS team ${resolvedTeam.env}; using payload team` } : channelWarning ? { warning: channelWarning } : {}),
|
|
@@ -673,9 +708,9 @@ if (event === "spawn") {
|
|
|
673
708
|
// wherever they run (plus human members). Local liveness comes from
|
|
674
709
|
// `oats status --team`; this is the network view.
|
|
675
710
|
const root = awebRoot();
|
|
676
|
-
if (!root) { console.error(
|
|
711
|
+
if (!root) { console.error(`oats aweb roster: ${awebRootProblem(rootSettingCandidate())}`); process.exit(1); }
|
|
677
712
|
const team = process.env.OATS_TEAM_ID || process.env.OATS_TEAM_NAME || JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
678
|
-
if (!team) { console.error(
|
|
713
|
+
if (!team) { console.error(`oats aweb roster: cannot determine team (${teamConfigRemedy()}, or activate a team at the aweb root)`); process.exit(1); }
|
|
679
714
|
const teamFlag = team.includes(":") ? ["--team-id", team] : ["--team", team];
|
|
680
715
|
const r = JSON.parse(run(["aw", "id", "team", "members", ...teamFlag, "--json"], root, 60000));
|
|
681
716
|
if (process.argv.includes("--json")) { console.log(JSON.stringify(r, null, 2)); process.exit(0); }
|
|
@@ -687,18 +722,59 @@ if (event === "spawn") {
|
|
|
687
722
|
process.exit(0);
|
|
688
723
|
} else if (event === "setup") {
|
|
689
724
|
// Guided onboarding — idempotent, prints what it finds and the one next step.
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
725
|
+
if (isClassicDeployment() && settings.root === undefined && settings.roots === undefined && settings.team === undefined) {
|
|
726
|
+
const scope = process.env.OATS_TEAM_SCOPE || process.cwd();
|
|
727
|
+
const teamName = process.env.OATS_TEAM_NAME;
|
|
728
|
+
const teamId = process.env.OATS_TEAM_ID;
|
|
729
|
+
console.log(`aweb onboarding — team scope: ${scope}${teamName ? `, config team: ${teamName}${teamId ? ` (${teamId})` : ""}` : ""}\n`);
|
|
730
|
+
if (!teamName) {
|
|
731
|
+
console.log("1. Declare your team in the deployment scope's oats-config.yaml first:");
|
|
732
|
+
console.log(" team:\n name: <your-team>\n then re-run `oats aweb setup` from there.");
|
|
733
|
+
process.exit(0);
|
|
734
|
+
}
|
|
735
|
+
if (!existsSync(join(scope, ".aw"))) {
|
|
736
|
+
console.log(`No aweb workspace at the team scope yet. Initialize it (interactive — creates or connects an aweb account):`);
|
|
737
|
+
console.log(` cd ${scope} && aw init`);
|
|
738
|
+
console.log(" First time on aweb? `aw init` walks you through creating a hosted aweb.ai account.");
|
|
739
|
+
console.log(" Own your domain? Use `aw init --byod` (see the aweb-team-membership skill).");
|
|
740
|
+
process.exit(0);
|
|
741
|
+
}
|
|
742
|
+
let teams = { memberships: [] };
|
|
743
|
+
try { teams = JSON.parse(run(["aw", "team", "list", "--json"], scope)); } catch { /* fall through */ }
|
|
744
|
+
const want = teamId || teamName;
|
|
745
|
+
const match = teamIdsOf(teams).find((tid) => String(tid) === want || String(tid).startsWith(`${want}:`));
|
|
746
|
+
if (match) {
|
|
747
|
+
console.log(`✓ aweb workspace initialized and member of ${match}.`);
|
|
748
|
+
if (teams.active_team && teams.active_team !== match) console.log(` Note: active team is ${teams.active_team}; instances join ${match} explicitly, but consider \`aw team switch ${match}\`.`);
|
|
749
|
+
console.log(" Done — spawned instances will join this team automatically (alias = instance name).");
|
|
750
|
+
console.log(" Roster: `oats aweb roster` · local: `oats status --team`");
|
|
751
|
+
} else {
|
|
752
|
+
console.log(`Workspace initialized, but no membership matching "${want}".`);
|
|
753
|
+
console.log(` Create the team: cd ${scope} && aw team create ${teamName}`);
|
|
754
|
+
console.log(" Or join an existing one: get an invite token from a member, then `aw team join <token>`");
|
|
755
|
+
console.log(" (details: aweb-team-membership skill)");
|
|
756
|
+
}
|
|
757
|
+
process.exit(0);
|
|
758
|
+
}
|
|
759
|
+
const resolvedTeam = payloadTeam();
|
|
760
|
+
const teamName = resolvedTeam.team;
|
|
761
|
+
const teamId = typeof settings.team === "string" && settings.team.trim() ? settings.team.trim() : process.env.OATS_TEAM_ID;
|
|
762
|
+
const candidate = rootSettingCandidate(teamName);
|
|
763
|
+
const scope = candidate?.root ? resolve(candidate.root) : process.cwd();
|
|
764
|
+
console.log(`aweb onboarding — messaging root: ${scope}${teamName ? `, team: ${teamName}` : ""}\n`);
|
|
694
765
|
if (!teamName) {
|
|
695
|
-
console.log(
|
|
696
|
-
console.log("
|
|
766
|
+
console.log(`1. Choose the aweb team for this deployment: ${teamConfigRemedy()}.`);
|
|
767
|
+
console.log(" The workspace file's `messaging:` / `messaging.byTeam.<label>.team` payload is portable; host-specific overrides belong in `settings.oats.aweb.team`.");
|
|
768
|
+
process.exit(0);
|
|
769
|
+
}
|
|
770
|
+
if (!isAbsolute(scope)) {
|
|
771
|
+
console.log(`${candidate?.key || "settings.oats.aweb.root"} must be an absolute directory whose .aw is the aweb minting root.`);
|
|
697
772
|
process.exit(0);
|
|
698
773
|
}
|
|
699
774
|
if (!existsSync(join(scope, ".aw"))) {
|
|
700
|
-
console.log(`No aweb workspace at the
|
|
775
|
+
console.log(`No aweb workspace at the messaging root yet (${candidate?.key || "settings.oats.aweb.root"}). Initialize it (interactive — creates or connects an aweb account):`);
|
|
701
776
|
console.log(` cd ${scope} && aw init`);
|
|
777
|
+
console.log(" Or set settings.oats.aweb.root to an absolute directory whose .aw is the aweb minting root, then re-run `oats aweb setup`.");
|
|
702
778
|
console.log(" First time on aweb? `aw init` walks you through creating a hosted aweb.ai account.");
|
|
703
779
|
console.log(" Own your domain? Use `aw init --byod` (see the aweb-team-membership skill).");
|
|
704
780
|
process.exit(0);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
1
3
|
import { TextDecoder } from 'node:util';
|
|
2
4
|
import { assessCapturedSessionReadiness } from './session-readiness.mjs';
|
|
3
5
|
import {
|
|
@@ -79,8 +81,12 @@ export function parseBindingJson(bytes,limits=BINDING_WIRE_LIMITS) {
|
|
|
79
81
|
function settings(value) {
|
|
80
82
|
if(!obj(value)) wireError('invalid-binding');
|
|
81
83
|
if(Object.hasOwn(value,'identity')) wireError('provider-not-qualified');
|
|
82
|
-
keys(value,['delivery'],[]);
|
|
84
|
+
keys(value,['delivery','team','root','roots'],[]);
|
|
83
85
|
if(value.delivery!==undefined && !['channel','session'].includes(value.delivery)) wireError('needs-configuration');
|
|
86
|
+
if(value.team!==undefined && (typeof value.team!=='string' || !value.team.trim())) wireError('needs-configuration');
|
|
87
|
+
if(value.root!==undefined && (typeof value.root!=='string' || !value.root.trim())) wireError('needs-configuration');
|
|
88
|
+
if(value.roots!==undefined && !obj(value.roots)) wireError('needs-configuration');
|
|
89
|
+
if(obj(value.roots)) for(const [team,root] of Object.entries(value.roots)) if(!team || typeof root!=='string' || !root.trim()) wireError('needs-configuration');
|
|
84
90
|
return value;
|
|
85
91
|
}
|
|
86
92
|
function request(value,phase) {
|
|
@@ -142,6 +148,23 @@ function binding(value) {
|
|
|
142
148
|
return value;
|
|
143
149
|
}
|
|
144
150
|
function checkResult(message) {return {status:'needs-configuration',problems:[{code:'needs-configuration',message}]};}
|
|
151
|
+
function checkProblems(problems) {return problems.length?{status:'needs-configuration',problems}:null;}
|
|
152
|
+
function teamFromSettings(settings) {return typeof settings.team==='string' && settings.team.trim()?settings.team.trim():(process.env.OATS_TEAM_ID || process.env.OATS_TEAM_NAME || undefined);}
|
|
153
|
+
function classicEnv() {return !!process.env.OATS_TEAM_SCOPE && !(process.env.OATS_WORKSPACE_KEY || process.env.OATS_WORKSPACE_NAME || process.env.OATS_TEAM_LABEL);}
|
|
154
|
+
function rootCandidate(settings,team) {
|
|
155
|
+
const roots=obj(settings.roots)?settings.roots:{};
|
|
156
|
+
if(team && typeof roots[team]==='string' && roots[team].trim()) return {root:roots[team].trim(),key:`settings.oats.aweb.roots[${JSON.stringify(team)}]`,declared:true};
|
|
157
|
+
if(typeof settings.root==='string' && settings.root.trim()) return {root:settings.root.trim(),key:'settings.oats.aweb.root',declared:true};
|
|
158
|
+
const candidates=classicEnv()?[process.env.OATS_TEAM_SCOPE,process.env.OATS_WORKSPACE].filter(Boolean):[process.env.OATS_WORKSPACE || process.env.OATS_TEAM_SCOPE || process.cwd()];
|
|
159
|
+
for(const root of candidates) if(isAbsolute(root) && existsSync(join(resolve(root),'.aw'))) return {root,key:'settings.oats.aweb.root',declared:false};
|
|
160
|
+
return {root:candidates[0] || process.cwd(),key:'settings.oats.aweb.root',declared:false};
|
|
161
|
+
}
|
|
162
|
+
function readinessFromSettings(settings) {
|
|
163
|
+
const team=teamFromSettings(settings),candidate=rootCandidate(settings,team),problems=[];
|
|
164
|
+
if(!candidate.root || !isAbsolute(candidate.root) || !existsSync(join(resolve(candidate.root),'.aw'))) problems.push({code:'needs-configuration',message:`no messaging root at ${candidate.root?resolve(candidate.root):process.cwd()}: run oats aweb setup there or set ${candidate.key}`});
|
|
165
|
+
if(!team) problems.push({code:'needs-configuration',message:'no team: set messaging.byTeam.<label>.team in the workspace file or settings.oats.aweb.team'});
|
|
166
|
+
return checkProblems(problems) || {status:'ready',problems:[]};
|
|
167
|
+
}
|
|
145
168
|
function checkPhase(req) {
|
|
146
169
|
keys(req.input,['binding','context','action','invocation'],['binding','context','action']);
|
|
147
170
|
if(!obj(req.input.action) || typeof req.input.action.kind!=='string') wireError('invalid-binding');
|
|
@@ -150,6 +173,9 @@ function checkPhase(req) {
|
|
|
150
173
|
const invocation=Object.hasOwn(req.input,'invocation')?validateAwebInvocationContext(req.input.invocation,current,{context:req.input.context,action:req.input.action}):null;
|
|
151
174
|
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
175
|
if(current.payload.privateTeam===null) return checkResult('an explicit private-team binding is required');
|
|
176
|
+
const hostReady=readinessFromSettings(req.settings);
|
|
177
|
+
if(hostReady.status!=='ready') return hostReady;
|
|
178
|
+
if(!invocation) return hostReady;
|
|
153
179
|
// Read-only public kernel observations, not an account/grant attestation.
|
|
154
180
|
// Native setup is performed only by the separately admitted execution path.
|
|
155
181
|
return assessCapturedSessionReadiness({binding:current,invocation,settings:req.settings});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"capability": "oats.aweb",
|
|
3
3
|
"command": "aweb",
|
|
4
|
-
"version": "1.12.
|
|
4
|
+
"version": "1.12.1",
|
|
5
5
|
"compatibility": {
|
|
6
6
|
"oats": ">=0.24.4"
|
|
7
7
|
},
|
|
@@ -139,6 +139,12 @@
|
|
|
139
139
|
"team": {
|
|
140
140
|
"description": "Target aweb team id for identity lifecycle. In workspace v2 spawns this payload value wins over OATS_TEAM_ID/OATS_TEAM_NAME; if both are set and differ the hook warns and uses this setting."
|
|
141
141
|
},
|
|
142
|
+
"root": {
|
|
143
|
+
"description": "Host-owned absolute directory whose .aw is the aweb minting root. Put this only in oats-local.yaml settings.oats.aweb.root; use oats aweb setup to initialize it."
|
|
144
|
+
},
|
|
145
|
+
"roots": {
|
|
146
|
+
"description": "Host-owned map of aweb team id to absolute directory whose .aw is that team's minting root, for deployments that mint into several teams. Put this only in oats-local.yaml settings.oats.aweb.roots."
|
|
147
|
+
},
|
|
142
148
|
"identity": {
|
|
143
149
|
"default": {
|
|
144
150
|
"mode": "local"
|
|
@@ -175,7 +175,7 @@ else {
|
|
|
175
175
|
} else if(event==='unlock') result=unlock(resolve(flags.lock),flags.token);
|
|
176
176
|
else fail('E_USAGE',`unknown command ${event}; see --help`);
|
|
177
177
|
answer=hook?result:{schemaVersion:1,ok:true,result};
|
|
178
|
-
} catch(e) {exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:e.message}:{})},warning:`oats-okf: ${e.message}`}:{schemaVersion:1,ok:false,error:{code
|
|
178
|
+
} catch(e) {const code=e.code || 'E_OKF';exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:e.message}:{})},warning:`oats-okf ${code}: ${e.message}`}:{schemaVersion:1,ok:false,error:{code,message:e.message}};}
|
|
179
179
|
// Let Node drain the pipe; no process.exit after a possibly large view.
|
|
180
180
|
process.stdout.write(JSON.stringify(answer)+'\n');process.exitCode=exit;
|
|
181
181
|
}
|
|
@@ -291,7 +291,7 @@ function checkPhase(req) {
|
|
|
291
291
|
if(gitBases.length) scratch=fs.mkdtempSync(join(fs.realpathSync(tmpdir()),'oats-okf-binding-check-'));
|
|
292
292
|
for(const [alias,base] of Object.entries(bindings.bases)) {
|
|
293
293
|
stage=base.kind==='directory'?'validate':'stage';
|
|
294
|
-
accepted[alias]=(base.kind==='directory'?validateBase(base.path,base):stageBase(base,join(scratch,alias))).meta;
|
|
294
|
+
accepted[alias]=(base.kind==='directory'?validateBase(base.path,base):stageBase(base,join(scratch,alias),{alias})).meta;
|
|
295
295
|
}
|
|
296
296
|
stage='runtime';
|
|
297
297
|
checkKnowledgeRuntime({rendered:runtime,accepted});
|
|
@@ -33,7 +33,7 @@ export function migrate(bindings,{legacy,alias,node,output}) {
|
|
|
33
33
|
// Stage the accepted base before any record exists: a base that cannot be
|
|
34
34
|
// read leaves nothing behind. From the record on, a failure is recorded in
|
|
35
35
|
// it rather than left as an unexplained directory.
|
|
36
|
-
const stage=stageBase(base,output);
|
|
36
|
+
const stage=stageBase(base,output,{alias});
|
|
37
37
|
fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});
|
|
38
38
|
const id=randomUUID();const dir=join(bindings.stateDir,'migrations',id);fs.mkdirSync(dir,{recursive:true,mode:0o700});
|
|
39
39
|
save(join(dir,'legacy.json'),original); // byte-preserving backup BEFORE any delivery
|
|
@@ -106,7 +106,7 @@ export function cutoverMigration(file,soul) {
|
|
|
106
106
|
try {
|
|
107
107
|
for(const alias of new Set([...decl.owns,...decl.reads].map(r=>splitRef(r)[0]))) {
|
|
108
108
|
if(!Object.hasOwn(bindings.bases,alias)) fail('E_CONFIG',`unresolved base: ${alias}`);
|
|
109
|
-
const staged=stageBase(bindings.bases[alias],join(scratch,alias));
|
|
109
|
+
const staged=stageBase(bindings.bases[alias],join(scratch,alias),{alias});
|
|
110
110
|
accepted[alias]=staged.meta;
|
|
111
111
|
if(alias===m.alias) {
|
|
112
112
|
if(hash(staged.meta.nodes[m.node] || null)!==hash(frozen)) fail('E_OWNER','accepted migration ownership/path differs from delivered node');
|
|
@@ -101,7 +101,7 @@ export function views(bindings, decl, target) {
|
|
|
101
101
|
for(const [alias,base] of Object.entries(bindings.bases)) {
|
|
102
102
|
const scratch=fs.mkdtempSync(join(bindings.stateDir,'read-'));
|
|
103
103
|
try {
|
|
104
|
-
const staged=stageBase(base,join(scratch,'base'));
|
|
104
|
+
const staged=stageBase(base,join(scratch,'base'),{alias});
|
|
105
105
|
all[alias]=staged.meta;
|
|
106
106
|
const path=`bases/${alias}`;
|
|
107
107
|
materialize(join(pending,path),staged.files);
|
|
@@ -192,7 +192,8 @@ export function register(home) {
|
|
|
192
192
|
if(fs.existsSync(join(home,'knowledge'))) fail('E_VIEW','unregistered knowledge view exists; preserve it and inspect before registering');
|
|
193
193
|
if(['.okf-harvest-record.json','.okf-harvest-record.next.json'].some(p=>fs.existsSync(join(home,p))) && !fs.existsSync(join(home,'.okf-v1-migration.json'))) fail('E_MIGRATION','legacy source watermarks require explicit oats okf migrate --source-home PATH before v2 registration; no cursor is silently trusted');
|
|
194
194
|
const meta=fs.existsSync(join(home,'instance.json'))?readJSON(join(home,'instance.json')):{};
|
|
195
|
-
|
|
195
|
+
if(!process.env.OATS_SOUL) fail('E_OATS_SOUL_MISSING','OATS_SOUL is not set; oats.okf hooks and commands run only under the OATS kernel');
|
|
196
|
+
const soul=fs.realpathSync(process.env.OATS_SOUL);
|
|
196
197
|
const work=fs.existsSync(join(home,'work'))?fs.realpathSync(join(home,'work')):join(home,'work');
|
|
197
198
|
const decl=declaration(soul);
|
|
198
199
|
const soulId=process.env.OATS_SOUL_ID || null;
|
|
@@ -204,7 +205,6 @@ export function register(home) {
|
|
|
204
205
|
if(!agent || !instance) fail('E_SOURCE','source instance/agent required');
|
|
205
206
|
fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});
|
|
206
207
|
const ownersFile=join(bindings.stateDir,'owners.json');
|
|
207
|
-
pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
|
|
208
208
|
const id=randomUUID(); const dir=join(bindings.stateDir,'sources',id);
|
|
209
209
|
// Copy only the role document, never instance.json wholesale, launch recipes,
|
|
210
210
|
// environment, credentials, source worktree, or third-party message stores.
|
|
@@ -217,6 +217,7 @@ export function register(home) {
|
|
|
217
217
|
fs.mkdirSync(dir,{recursive:true,mode:0o700});
|
|
218
218
|
try {
|
|
219
219
|
source.acceptedView=views(bindings,decl,pending);
|
|
220
|
+
pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
|
|
220
221
|
source.acceptedNodes=Object.fromEntries(Object.entries(source.acceptedView).map(([alias,r])=>[alias,r.nodes]));
|
|
221
222
|
save(file,source);
|
|
222
223
|
save(join(dir,'status.json'),{version:1,captured:{notes:[],threads:{},inputs:[]},processed:[],delivered:{},accepted:{},retired:false,auto:true,activeRun:null});
|
|
@@ -245,7 +246,7 @@ export function pinOwner(ownersFile,owner,{id,soulName,path}) {
|
|
|
245
246
|
if(!obj(owners)) fail('E_OWNER','invalid owner registry');
|
|
246
247
|
const prior=Object.hasOwn(owners,owner)?owners[owner]:undefined;
|
|
247
248
|
const samePath=typeof prior==='string' && new RegExp(`/agents/${soulName.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')}/(soul|souls/[^/]+)$`).test(prior);
|
|
248
|
-
if(prior!==undefined && prior!==value && !(id && samePath)) fail('E_OWNER'
|
|
249
|
+
if(prior!==undefined && prior!==value && !(id && samePath)) fail('E_OWNER',`stable owner ID already identifies a different soul in this state namespace: existing soul ${prior}; new soul ${value}. Remedies: retire the existing registration first, or use a fresh state directory.`);
|
|
249
250
|
if(prior!==value) {owners[owner]=value;save(ownersFile,owners);}
|
|
250
251
|
return value;
|
|
251
252
|
});
|
|
@@ -9,6 +9,31 @@ const validator = fileURLToPath(new URL('../skills/okf/scripts/okf-validate.mjs'
|
|
|
9
9
|
// transport subprocesses. Override even an explicitly supplied command env.
|
|
10
10
|
const gitEnv = (env = cleanEnv()) => ({...env,GIT_NO_REPLACE_OBJECTS:'1'});
|
|
11
11
|
export const git = (cwd,args,opts={}) => exec('git',['--no-replace-objects','-c','core.hooksPath=/dev/null','-c','protocol.ext.allow=never','-C',cwd,...args],{cwd,...opts,env:gitEnv(opts.env)});
|
|
12
|
+
function baseError(code,message,base,alias,step,reason) {throw Object.assign(new Error(message),{code,base:alias,repository:base.repository,step,reason});}
|
|
13
|
+
function baseRemedy(alias) {return `fix the binding for base alias "${alias}" in the bindings file, or remove the base from the bindings`;}
|
|
14
|
+
function classifyGitFailure(error) {
|
|
15
|
+
const text=String(error?.message || '');
|
|
16
|
+
if(error?.code==='ETIMEDOUT' || /ETIMEDOUT|timed out|timeout/i.test(text)) return 'timeout';
|
|
17
|
+
if(/authentication|permission denied|publickey|access denied|could not read Username|authorization|not authorized/i.test(text)) return 'auth';
|
|
18
|
+
if(/not found|repository .*not exist|does not exist|no such (file|repository)|couldn't find remote ref|Repository not found/i.test(text)) return 'not-found';
|
|
19
|
+
if(/could not resolve host|failed to connect|connection refused|network is unreachable|no route to host|proxy/i.test(text)) return 'network';
|
|
20
|
+
return 'unknown';
|
|
21
|
+
}
|
|
22
|
+
function unavailable(base,alias,step,error) {
|
|
23
|
+
const reason=classifyGitFailure(error),detail=reason==='unknown'?`; original Git failure: ${String(error?.message || 'unknown failure')}`:'';
|
|
24
|
+
const message=`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but ${step} failed (reason: ${reason}${detail}); ${baseRemedy(alias)}`;
|
|
25
|
+
baseError('E_BASE_UNAVAILABLE',message,base,alias,step,reason);
|
|
26
|
+
}
|
|
27
|
+
function requireNotShallow(base,alias,cwd) {
|
|
28
|
+
const shallow=git(cwd,['rev-parse','--is-shallow-repository']);
|
|
29
|
+
if(shallow==='true') baseError('E_BASE_SHALLOW',`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but the repository is shallow; oats.okf requires full accepted history before staging; ${baseRemedy(alias)}`,base,alias,'clone','shallow');
|
|
30
|
+
}
|
|
31
|
+
function preflightLocalRepository(base,alias) {
|
|
32
|
+
if(!base.repository.startsWith('/')) return;
|
|
33
|
+
if(!fs.existsSync(base.repository)) unavailable(base,alias,'clone',Object.assign(new Error('repository not found'),{code:'ENOENT'}));
|
|
34
|
+
try { requireNotShallow(base,alias,base.repository); }
|
|
35
|
+
catch(e) { if(e.code==='E_BASE_SHALLOW') throw e; unavailable(base,alias,'clone',e); }
|
|
36
|
+
}
|
|
12
37
|
export const baseLock = b => `${b.path}.okf-lock`;
|
|
13
38
|
export const journalPath = b => `${b.path}.okf-publication.json`;
|
|
14
39
|
export function validateBase(root, base) {
|
|
@@ -53,9 +78,10 @@ function materializeGitObjects(base,dest,head) {
|
|
|
53
78
|
writeBlob(dest,entry.oid,target,entry.mode==='100755'?0o755:0o644);
|
|
54
79
|
}
|
|
55
80
|
}
|
|
56
|
-
function clone(base, dest, selectedHead) {
|
|
81
|
+
function clone(base, dest, selectedHead, { alias = base.id } = {}) {
|
|
57
82
|
safePath(dest);
|
|
58
83
|
if(fs.existsSync(dest)) fail('E_PATH',`staging destination exists: ${dest}`);
|
|
84
|
+
preflightLocalRepository(base,alias);
|
|
59
85
|
fs.mkdirSync(dirname(dest),{recursive:true});
|
|
60
86
|
// Fetch only what the store reads: the accepted branch, trees now and blobs
|
|
61
87
|
// on demand. Only a remote that does not offer object filtering gets a plain
|
|
@@ -64,25 +90,30 @@ function clone(base, dest, selectedHead) {
|
|
|
64
90
|
const cloneArgs=['clone','--no-hardlinks','--no-checkout','--single-branch','--branch',base.acceptedBranch];
|
|
65
91
|
try { git(dirname(dest),[...cloneArgs,'--filter=blob:none','--',base.repository,dest],{timeout:gitTimeoutMs()}); }
|
|
66
92
|
catch(e) {
|
|
67
|
-
if(e.code
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
if(e.code==='E_COMMAND' && /filter/i.test(e.message)) {
|
|
94
|
+
fs.rmSync(dest,{recursive:true,force:true});
|
|
95
|
+
try { git(dirname(dest),[...cloneArgs,'--',base.repository,dest],{timeout:gitTimeoutMs()}); }
|
|
96
|
+
catch(error) { unavailable(base,alias,'clone',error); }
|
|
97
|
+
} else unavailable(base,alias,'clone',e);
|
|
70
98
|
}
|
|
71
|
-
|
|
99
|
+
try { requireNotShallow(base,alias,dest); }
|
|
100
|
+
catch(e) { if(e.code==='E_BASE_SHALLOW') throw e; unavailable(base,alias,'clone',e); }
|
|
101
|
+
try { git(dest,['fetch','origin',`refs/heads/${base.acceptedBranch}`],{timeout:gitTimeoutMs()}); }
|
|
102
|
+
catch(e) { unavailable(base,alias,'fetch',e); }
|
|
72
103
|
const head=selectedHead ?? git(dest,['rev-parse','FETCH_HEAD']);
|
|
73
|
-
verifyRemote(base,dest);
|
|
74
|
-
|
|
104
|
+
try { requireNotShallow(base,alias,dest);verifyRemote(base,dest);materializeGitObjects(base,dest,head); }
|
|
105
|
+
catch(e) { if(['E_BASE_SHALLOW','E_OWNER','E_CONFIRM','E_PATH','E_BASE','E_VALIDATION'].includes(e.code)) throw e; unavailable(base,alias,'checkout',e); }
|
|
75
106
|
// Reject a linked bundle even if Git happily checked the link out.
|
|
76
107
|
safePath(join(dest,base.root));
|
|
77
108
|
return head;
|
|
78
109
|
}
|
|
79
|
-
export function stageBase(base, dest) {
|
|
110
|
+
export function stageBase(base, dest, { alias = base.id } = {}) {
|
|
80
111
|
safePath(dest);
|
|
81
112
|
if(fs.existsSync(dest)) fail('E_PATH',`staging destination exists: ${dest}`);
|
|
82
113
|
const acceptedPath=base.kind==='directory'?base.path:(base.repository.startsWith('/')?resolve(base.repository,base.root):null);
|
|
83
114
|
if(acceptedPath && overlaps(acceptedPath,dest)) fail('E_PATH','stage overlaps accepted base');
|
|
84
115
|
if(base.kind==='git') {
|
|
85
|
-
const head=clone(base,dest); const root=join(dest,base.root);
|
|
116
|
+
const head=clone(base,dest,undefined,{alias}); const root=join(dest,base.root);
|
|
86
117
|
const validated=validateBase(root,base);
|
|
87
118
|
verifyPublicationTree(base,dest,head,head,validated.files);
|
|
88
119
|
return { ...validated, head, root, checkout:dest };
|
|
@@ -142,7 +142,7 @@ function prepareWorker(source,run) {
|
|
|
142
142
|
for(const [alias,base] of Object.entries(source.bindings.bases)) {
|
|
143
143
|
if(run.settled?.includes(alias)) continue;
|
|
144
144
|
const dest=join(work,'bases',alias);
|
|
145
|
-
const staged=stageBase(base,dest);
|
|
145
|
+
const staged=stageBase(base,dest,{alias});
|
|
146
146
|
const owned=source.decl.owns.map(splitRef).filter(([a])=>a===alias).map(([,n])=>n);
|
|
147
147
|
for(const n of owned) if(staged.meta.nodes[n]?.owner!==source.owner || JSON.stringify(staged.meta.nodes[n])!==JSON.stringify(source.acceptedNodes[alias][n])) fail('E_OWNER','accepted ownership/path changed from frozen destination; explicit migration required');
|
|
148
148
|
run.stages[alias]={root:staged.root,checkout:staged.checkout,head:staged.head,baseline:staged.files,digest:staged.digest,owned};persist(source,run);
|
|
@@ -235,7 +235,7 @@ export function complete(source,id,judgmentFile,opts={}) {
|
|
|
235
235
|
if(base.kind==='git') verifyGitScope(base,s.checkout,s.head);
|
|
236
236
|
const checkDir=fs.mkdtempSync(join(source.bindings.stateDir,'baseline-'));
|
|
237
237
|
try {
|
|
238
|
-
const current=stageBase(base,join(checkDir,'base'));
|
|
238
|
+
const current=stageBase(base,join(checkDir,'base'),{alias});
|
|
239
239
|
if(current.digest!==s.digest || (base.kind==='git' && current.head!==s.head)) fail('E_BASELINE','accepted base changed; rejudge on fresh baseline');
|
|
240
240
|
} finally {fs.rmSync(checkDir,{recursive:true,force:true});}
|
|
241
241
|
const claims=judgment.outcomes.flatMap(o=>o.concepts).filter(c=>c.base===alias).map(c=>c.path);
|
|
@@ -310,7 +310,7 @@ export function retry(source,{run:id,rejudge=false,launch=false,adoptHome}={}) {
|
|
|
310
310
|
const stages={...run.stages},outstanding=Object.keys(stages).filter(a=>!settled.includes(a));
|
|
311
311
|
for(const alias of outstanding) {
|
|
312
312
|
const base=source.bindings.bases[alias];
|
|
313
|
-
const staged=stageBase(base,join(work,'rejudgments',attempt,'bases',alias));
|
|
313
|
+
const staged=stageBase(base,join(work,'rejudgments',attempt,'bases',alias),{alias});
|
|
314
314
|
const owned=run.stages[alias].owned;
|
|
315
315
|
for(const n of owned) if(staged.meta.nodes[n]?.owner!==source.owner || JSON.stringify(staged.meta.nodes[n])!==JSON.stringify(source.acceptedNodes[alias][n])) fail('E_OWNER','accepted ownership/path changed from frozen destination; explicit migration required');
|
|
316
316
|
stages[alias]={root:staged.root,checkout:staged.checkout,head:staged.head,baseline:staged.files,digest:staged.digest,owned};
|
|
@@ -87,15 +87,26 @@ the purpose field becomes a name field that shows `<soul>-<purpose>` live and
|
|
|
87
87
|
then the kernel's final name from the preview. A no-prefix toggle maps to
|
|
88
88
|
`spawn --name <slug>`, gated on the `spawn-name` feature; `--purpose` stays the
|
|
89
89
|
default. **Runtime and model** are always visible, with their resolved value and
|
|
90
|
-
its source. **
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
its source. **Relationship** sits in the main form, shown by default: None /
|
|
91
|
+
Child of / Sibling of / Parent of. There is **no** modules/capabilities list and
|
|
92
|
+
no attach-knowledge / child-spawn / open-PR toggles, because those behaviours
|
|
93
|
+
come from capabilities. There is **no** preview button either. The preview runs
|
|
94
|
+
in the background to fill the real defaults, and apply still binds with
|
|
95
|
+
`--expect-decision` (`E_DECISION_STALE` re-previews). A collapsed section named
|
|
96
|
+
**Developer settings** holds:
|
|
97
|
+
- **work**: base | branch and the worktree path. A `checkout` soul is offered
|
|
98
|
+
"Use a worktree instead?" (`--work worktree`); the work mode itself comes
|
|
99
|
+
from the soul's `work:`.
|
|
100
|
+
- harness permissions
|
|
101
|
+
- launch config
|
|
102
|
+
- session backend
|
|
103
|
+
- Run on (the execution server)
|
|
104
|
+
- wake-up
|
|
105
|
+
- messaging identity (`--provider <cap> identity.mode=…`, decision 27)
|
|
106
|
+
|
|
107
|
+
(Second human redirect, same day: relationship moved into the main form, work
|
|
108
|
+
moved into the collapsed section, and the section was renamed from "Advanced".)
|
|
109
|
+
Every modal gets a darker backdrop.
|
|
99
110
|
|
|
100
111
|
**F4 — Instance card and roster on v2 facts.** The served-identity line (`acts
|
|
101
112
|
as <address> via grant, expires <t>` / `alias <a> on <team>`); module rows with
|
|
@@ -42,6 +42,26 @@ pushes. Agreed by both on 2026-09-24:
|
|
|
42
42
|
- `main` on these repositories carries no branch or tag protection: the parity
|
|
43
43
|
gate and the cross-review are the only things between a merge and `main`.
|
|
44
44
|
|
|
45
|
+
## Human decision (2026-09-24): no package approval
|
|
46
|
+
|
|
47
|
+
**Package approval is removed from the kernel and from the Desktop.** People
|
|
48
|
+
install a package only when they trust it. Declaring it in the workspace's
|
|
49
|
+
`packages:` IS the trust decision, so there's no second, per-version approval
|
|
50
|
+
step. This supersedes the "executables approved once per version" rule in
|
|
51
|
+
`docs/workspaces.md` (§ Packages, lock, approval, catalog) and everything built
|
|
52
|
+
on it:
|
|
53
|
+
- `oats sync` exit 2 for pending approvals and `approvalNeeded`
|
|
54
|
+
- `--approve <id>@<version>` and the interactive prompt
|
|
55
|
+
- the lock's `approved` record
|
|
56
|
+
- `E_PACKAGE_UNAPPROVED` at spawn and dispatch
|
|
57
|
+
- the Desktop F2 approval flow (`E_APPROVAL_STALE`)
|
|
58
|
+
- the planned pinned `--approve …=<digest>`
|
|
59
|
+
|
|
60
|
+
What stays: the lock still pins each package to the exact commit and integrity,
|
|
61
|
+
and restore still refuses drift (`E_PACKAGE_INTEGRITY`). Reproducibility is not
|
|
62
|
+
approval. It's a breaking contract change, so it ships in a minor release, and
|
|
63
|
+
the Desktop requires that kernel.
|
|
64
|
+
|
|
45
65
|
## Slices, in order
|
|
46
66
|
|
|
47
67
|
### D1 — Knowledge centralisation (IN PROGRESS)
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -960,9 +960,15 @@ between preview and apply is `E_DECISION_STALE`):
|
|
|
960
960
|
(unchanged since the newest previous instance), or
|
|
961
961
|
`{ instance, was }` (`was` = the previous commit, or `null` when the previous
|
|
962
962
|
instance had no such module).
|
|
963
|
-
- `capabilities[]` / `skills[]`
|
|
964
|
-
|
|
965
|
-
|
|
963
|
+
- `capabilities[]` / `skills[]` on a **workspace** spawn are **objects**, not
|
|
964
|
+
Preview-1's strings: `capabilities[]` is `{ name, origin }` (`origin` =
|
|
965
|
+
`package:<id>@<version>` or `member:<repoKey>@<commit>`), and `skills[]` is
|
|
966
|
+
`{ name, source }`. Neither is a binding surface, because the authoritative
|
|
967
|
+
module set is `modules[]` and what apply binds is `decision.effective` /
|
|
968
|
+
`decision.resolution`. Consumers should read those fields, not project
|
|
969
|
+
`capabilities[]` / `skills[]`. (Corrected 2026-09-24: this said "keep their
|
|
970
|
+
Preview-1 meaning", which read as strings; found by the Desktop engineer in
|
|
971
|
+
F3.)
|
|
966
972
|
- `workspace` is the workspace host's canonical key, `team` the soul's label
|
|
967
973
|
(or `null`), `resolution` the 24-hex revision `decision.resolution` binds.
|
|
968
974
|
- `--provider <cap> <key>=<value>` (repeatable; `a.b=c` nests) is accepted by
|
package/docs/integrations.md
CHANGED
|
@@ -218,27 +218,52 @@ warning naming the fresh-purpose remedy. On an older `aw` the pre-1.36.1
|
|
|
218
218
|
report stands (`aliasReusable: false`, warning naming aweb-abim), because
|
|
219
219
|
that CLI cannot revoke the certificate.
|
|
220
220
|
|
|
221
|
-
## oats.aweb settings (1.12.
|
|
221
|
+
## oats.aweb settings (1.12.1)
|
|
222
222
|
|
|
223
223
|
Set in `oats-local.yaml` under `settings.oats.aweb.<key>` (host-owned), in the
|
|
224
224
|
soul's `messaging:` payload (true of every instance), or per spawn with
|
|
225
225
|
`oats spawn … --provider oats.aweb <key>=<value>`. The effective payload is
|
|
226
226
|
merged in order: workspace messaging, `byTeam[team]`, soul messaging,
|
|
227
227
|
`oats-local.yaml` `settings.oats.aweb`, then per-spawn `--provider` values.
|
|
228
|
-
`residents`
|
|
229
|
-
never in a committed workspace or soul file
|
|
230
|
-
but do not yet enforce provenance in the
|
|
228
|
+
`root`, `roots`, and `residents` are host-file-only: put absolute root/custody
|
|
229
|
+
paths only in `oats-local.yaml`, never in a committed workspace or soul file
|
|
230
|
+
(current kernels document this rule but do not yet enforce provenance in the
|
|
231
|
+
hook payload; the manifest schema does not yet carry a host-only marker).
|
|
231
232
|
|
|
232
233
|
- `team: <team id>`. The payload team wins over `OATS_TEAM_ID`/
|
|
233
234
|
`OATS_TEAM_NAME`; if both are set and differ, the hook warns and uses the
|
|
234
235
|
payload. Workspace v2 spawns can have an empty `OATS_TEAM_ID`, so set this in
|
|
235
|
-
the
|
|
236
|
+
the workspace file's `messaging:` / `messaging.byTeam.<label>.team`, or in
|
|
237
|
+
`settings.oats.aweb.team` for a host override.
|
|
238
|
+
- `root: /absolute/dir`. Host-owned absolute directory whose `.aw` is the aweb
|
|
239
|
+
minting root. A declared root without `.aw` is fatal; run `oats aweb setup`
|
|
240
|
+
there or set `settings.oats.aweb.root` to the initialized root.
|
|
241
|
+
- `roots: { <team id>: /absolute/dir }`. Host-owned map for deployments that
|
|
242
|
+
mint into several aweb teams. When a team is known, `roots[team]` wins over
|
|
243
|
+
`root`.
|
|
244
|
+
- Minting root resolution for spawn and setup is: `roots[team]` when the team is
|
|
245
|
+
known and present, else `root`. With no declared root, workspace v2 uses
|
|
246
|
+
`<OATS_WORKSPACE>` (the deployment directory whose `.aw` is used) and never
|
|
247
|
+
searches above it; classic deployments with `OATS_TEAM_SCOPE` keep the
|
|
248
|
+
historical bounded candidate search order exactly (team scope, home, home git
|
|
249
|
+
root, context, context git root, then workspace).
|
|
250
|
+
- `binding-check` answers `needs-configuration` before spawn with one problem
|
|
251
|
+
per missing item: `no messaging root at <dir>: run oats aweb setup there or
|
|
252
|
+
set settings.oats.aweb.root`; `no team: set messaging.byTeam.<label>.team in
|
|
253
|
+
the workspace file or settings.oats.aweb.team`. With both present it answers
|
|
254
|
+
`ready` (subject to captured-session checks when an invocation is supplied).
|
|
255
|
+
In classic deployments this readiness check approximates the full bounded
|
|
256
|
+
spawn search by checking `OATS_TEAM_SCOPE` before `OATS_WORKSPACE`; the spawn
|
|
257
|
+
hook itself still keeps the exact 1.12.0 bounded candidate order.
|
|
236
258
|
- `identity.mode: local | global` (default `local`). Any other value is fatal.
|
|
237
259
|
Local mode is the historical behavior: a spawned team identity is minted for
|
|
238
260
|
the instance, or `identity.source` uses the existing retained-seat flow below.
|
|
239
261
|
Its spawn meta includes `identity: { mode: "local", alias, team, address:
|
|
240
262
|
null, resident: null }` beside the existing top-level `alias`, `team`, and
|
|
241
|
-
`delivery` keys.
|
|
263
|
+
`delivery` keys. Local-mode spawn output contributes
|
|
264
|
+
`env.AWEB_IDENTITY_HOME=<home>/.aw` (and retained-seat local mode contributes
|
|
265
|
+
the same path) so `aw mail`, `aw chat`, `aw whoami`, `aw wake`, and
|
|
266
|
+
`aw workspace status` work from the instance's `work/` or any other cwd.
|
|
242
267
|
- `identity.mode: global` makes the instance act as a resident global identity
|
|
243
268
|
through an aweb session grant; it never mints a new global identity and never
|
|
244
269
|
copies root keys into the instance home. `identity.resident` is required and
|
package/docs/rebuild-to-v2.md
CHANGED
|
@@ -77,8 +77,8 @@ members:
|
|
|
77
77
|
- git:github.com/acme/platform
|
|
78
78
|
packages:
|
|
79
79
|
oats.framework: v1.1.3
|
|
80
|
-
oats.okf: v2.1.
|
|
81
|
-
oats.aweb: v1.12.
|
|
80
|
+
oats.okf: v2.1.5
|
|
81
|
+
oats.aweb: v1.12.1
|
|
82
82
|
teams:
|
|
83
83
|
global: { description: Org-wide }
|
|
84
84
|
engineering: { description: Platform }
|
|
@@ -328,7 +328,7 @@ Member capabilities need no approval: membership is the trust.
|
|
|
328
328
|
**Non-interactive approval (CI, scripted rebuilds):**
|
|
329
329
|
|
|
330
330
|
```bash
|
|
331
|
-
oats sync --approve oats.okf@v2.1.
|
|
331
|
+
oats sync --approve oats.okf@v2.1.5 --approve oats.aweb@v1.12.1
|
|
332
332
|
```
|
|
333
333
|
|
|
334
334
|
`--approve <id>@<version>` is repeatable and approves **exactly** the entry the
|
|
@@ -340,7 +340,7 @@ stays unapproved (exit `2`, as above).
|
|
|
340
340
|
|
|
341
341
|
`<version>` is the value `sync --json` reports as `approvalNeeded[].version`,
|
|
342
342
|
which is what the lock records as the package's `version`. For a **catalog**
|
|
343
|
-
package that is the published version (`oats.okf@2.1.
|
|
343
|
+
package that is the published version (`oats.okf@2.1.5`). For a **git** source
|
|
344
344
|
pinned by commit (`git:github.com/awebai/oats-okf@<oid>`) it is the **full
|
|
345
345
|
commit OID**, not the `git:` reference and not a tag name — copy it from the
|
|
346
346
|
`approvalNeeded` line rather than from your workspace file.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# OATS 0.25.9
|
|
2
|
+
|
|
3
|
+
A pins-only patch: the kernel is unchanged since 0.25.8.
|
|
4
|
+
|
|
5
|
+
## Updated
|
|
6
|
+
|
|
7
|
+
- **`oats.aweb` 1.12.1** (catalog pin + bundled copy; #145). The messaging
|
|
8
|
+
root is explicit: `settings.oats.aweb.root` / `roots[team]` in
|
|
9
|
+
`oats-local.yaml`. On a workspace-model deployment it defaults to the
|
|
10
|
+
deployment directory and is never searched for upward. Error texts name the
|
|
11
|
+
workspace-model remedies. In local mode an instance gets `AWEB_IDENTITY_HOME`,
|
|
12
|
+
so `aw` works from any directory. `binding-check` answers
|
|
13
|
+
`needs-configuration` before spawn, with one problem per missing item (no
|
|
14
|
+
messaging root; no team), and an unmapped workspace team label fails the spawn
|
|
15
|
+
closed instead of inheriting the root's active team.
|
|
16
|
+
- **`oats.okf` 2.1.5** (catalog pin + bundled copy; #148).
|
|
17
|
+
- Every configured base must be usable at spawn. A shallow base is refused
|
|
18
|
+
with `E_BASE_SHALLOW`; clone, fetch and checkout failures are
|
|
19
|
+
`E_BASE_UNAVAILABLE` naming the alias, repository, step and reason, never a
|
|
20
|
+
raw `ETIMEDOUT`.
|
|
21
|
+
- A missing `OATS_SOUL` is `E_OATS_SOUL_MISSING`; there's no fallback to the
|
|
22
|
+
home's soul link.
|
|
23
|
+
- Refusing an owner rename names both souls and both remedies.
|
package/package-catalog.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"packages": {
|
|
4
4
|
"oats.okf": {
|
|
5
5
|
"url": "https://github.com/awebai/oats-okf.git",
|
|
6
|
-
"ref": "v2.1.
|
|
6
|
+
"ref": "v2.1.5",
|
|
7
7
|
"path": "oats-package"
|
|
8
8
|
},
|
|
9
9
|
"oats.aweb": {
|
|
10
10
|
"url": "https://github.com/awebai/oats-aweb.git",
|
|
11
|
-
"ref": "v1.12.
|
|
11
|
+
"ref": "v1.12.1",
|
|
12
12
|
"path": "oats-package"
|
|
13
13
|
},
|
|
14
14
|
"oats.jira": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.9",
|
|
4
4
|
"description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|