@awebai/oats 0.29.1 → 0.29.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +210 -58
- package/capabilities/oats-aweb/injects/aweb.md +39 -74
- package/capabilities/oats-aweb/lib/binding-wire.mjs +14 -5
- package/capabilities/oats-aweb/lib/wake-receive.mjs +56 -0
- package/capabilities/oats-aweb/oats.json +7 -6
- package/capabilities/oats-aweb/skills/VENDORED.md +6 -1
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +24 -14
- package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +1 -1
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +216 -0
- package/docs/capabilities.md +3 -4
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +2 -0
- package/docs/design/2026-09-24-phase-d-plan.md +2 -0
- package/docs/design/2026-09-25-teams-contract.md +36 -4
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +4 -4
- package/docs/official-catalog.md +1 -1
- package/docs/packages.md +4 -4
- package/docs/release-notes/v0.29.2.md +51 -0
- package/docs/release-notes/v0.29.3.md +53 -0
- package/docs/workspaces.md +3 -3
- package/lib/core.mjs +3 -3
- package/lib/resolve.mjs +2 -2
- package/lib/workspace.mjs +3 -3
- package/package-catalog.json +2 -2
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* identity joined moments before the failure must still be deletable.
|
|
38
38
|
*/
|
|
39
39
|
import { execFileSync } from "node:child_process";
|
|
40
|
-
import { chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
40
|
+
import { chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
41
41
|
import { hostname } from "node:os";
|
|
42
42
|
import { join, dirname, resolve, delimiter, isAbsolute } from "node:path";
|
|
43
43
|
import { loadCapturedAwebExecution, requireCapturedAwebAction } from "../lib/captured-execution.mjs";
|
|
@@ -45,17 +45,23 @@ import { assessCapturedSessionReadiness, querySelectedKernel } from "../lib/sess
|
|
|
45
45
|
import { runCapturedNative } from "../lib/captured-native.mjs";
|
|
46
46
|
import { CUSTODY_ATTACH_MIN, grantYamlCustodySocket, parseBindingJson } from "../lib/binding-wire.mjs";
|
|
47
47
|
import { custodyPreflight } from "../lib/grant-custody.mjs";
|
|
48
|
+
import { runtimeDeliveryFor, wakeRegistration } from "../lib/wake-receive.mjs";
|
|
48
49
|
|
|
49
50
|
/** Run a command as ARGV — never a shell string. Team ids, aliases, instance
|
|
50
51
|
* names and invite tokens all flow through here; quoting them correctly is a
|
|
51
52
|
* property of one helper staying correct forever, while argv removes the class.
|
|
52
53
|
* This hook is a REQUIRED spawn hook, so it gates every spawn, which is reason
|
|
53
54
|
* enough not to rely on quoting. */
|
|
54
|
-
const run = (argv, cwd, timeout = 45000, { secrets = [], secretSafe = false, env: extraEnv, unsetEnv = [] } = {}) => {
|
|
55
|
+
const run = (argv, cwd, timeout = 45000, { secrets = [], secretSafe = false, env: extraEnv, unsetEnv = [], input } = {}) => {
|
|
55
56
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// An inherited AWEB_IDENTITY_HOME (every aweb instance session carries its
|
|
58
|
+
// own) is never this hook's identity: aw would act as the CALLER — refusing
|
|
59
|
+
// cwd-rooted commands such as team invite/list, or deleting the caller's
|
|
60
|
+
// workspace when a lead retires a worker. Only an explicit env sets one.
|
|
61
|
+
const childEnv = { ...process.env, ...(extraEnv || {}) };
|
|
62
|
+
if (!extraEnv || !Object.hasOwn(extraEnv, "AWEB_IDENTITY_HOME")) delete childEnv.AWEB_IDENTITY_HOME;
|
|
63
|
+
for (const name of unsetEnv) delete childEnv[name];
|
|
64
|
+
return execFileSync(argv[0], argv.slice(1), { cwd, encoding: "utf8", stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"], ...(input === undefined ? {} : { input }), timeout, env: childEnv }).trim();
|
|
59
65
|
} catch (e) {
|
|
60
66
|
// execFileSync puts the WHOLE ARGV in e.message ("Command failed: aw team
|
|
61
67
|
// join <token> …"). This hook's failures are reported by the kernel and land
|
|
@@ -118,7 +124,43 @@ function onPath(cmd) {
|
|
|
118
124
|
}
|
|
119
125
|
return false;
|
|
120
126
|
}
|
|
121
|
-
|
|
127
|
+
// Kernel home operations (`oats operation run messaging:teams|join|leave`, run
|
|
128
|
+
// with OATS_OPERATION and --json) read stdout as EXACTLY ONE JSON-v1 envelope
|
|
129
|
+
// whose `ok` agrees with the exit status (oats bin/oats.mjs finishOperation):
|
|
130
|
+
// {schemaVersion:1, ok:true, result} with exit 0, or {schemaVersion:1, ok:false,
|
|
131
|
+
// error:{code, message}} with a nonzero exit. Under OATS_OPERATION every other
|
|
132
|
+
// stdout write goes to stderr and every way out answers one envelope. Without
|
|
133
|
+
// it (`oats aweb teams --json` from a shell) the bare document is unchanged.
|
|
134
|
+
const OPERATION_COMMANDS = ["teams", "join", "leave"];
|
|
135
|
+
const operation = process.env.OATS_OPERATION && OPERATION_COMMANDS.includes(process.env.OATS_EVENT || process.argv[2]) ? process.env.OATS_OPERATION : undefined;
|
|
136
|
+
let operationAnswered = false;
|
|
137
|
+
let lastStderr = "";
|
|
138
|
+
const operationEnvelopeFailure = (code, message, details) => ({ schemaVersion: 1, ok: false, error: { code: code || "E_OPERATION_FAILED", message: String(message || "failed").slice(0, 1000), ...(details ? { details } : {}) } });
|
|
139
|
+
function answerOperation(envelope, exitCode) {
|
|
140
|
+
operationAnswered = true;
|
|
141
|
+
writeSync(1, JSON.stringify(envelope) + "\n");
|
|
142
|
+
process.exit(exitCode);
|
|
143
|
+
}
|
|
144
|
+
const operationOk = (result) => answerOperation({ schemaVersion: 1, ok: true, result }, 0);
|
|
145
|
+
const operationFail = (code, message, details) => answerOperation(operationEnvelopeFailure(code, message, details), 1);
|
|
146
|
+
if (operation) {
|
|
147
|
+
const toStderr = process.stderr.write.bind(process.stderr);
|
|
148
|
+
process.stderr.write = (chunk, ...rest) => { if (String(chunk).trim()) lastStderr = String(chunk).trim(); return toStderr(chunk, ...rest); };
|
|
149
|
+
process.stdout.write = (chunk, ...rest) => process.stderr.write(chunk, ...rest);
|
|
150
|
+
// Any exit that did not answer (a refusal printed to stderr, an uncaught
|
|
151
|
+
// error) still answers one failure envelope, and never exits 0.
|
|
152
|
+
process.on("exit", (code) => {
|
|
153
|
+
if (operationAnswered) return;
|
|
154
|
+
operationAnswered = true;
|
|
155
|
+
writeSync(1, JSON.stringify(operationEnvelopeFailure("E_OPERATION_FAILED", lastStderr || `oats-aweb ${operation} exited ${code} without an answer`)) + "\n");
|
|
156
|
+
if (!code) process.exitCode = 1;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const out = (o, code = 0) => {
|
|
160
|
+
if (operation) operationFail("E_OPERATION_FAILED", String(o?.warning || o?.problems?.[0]?.message || "failed").replace(/^oats-aweb: /, ""));
|
|
161
|
+
process.stdout.write(JSON.stringify(o) + "\n");
|
|
162
|
+
process.exit(code);
|
|
163
|
+
};
|
|
122
164
|
const warn = (m) => out({ warning: `oats-aweb: ${String(m).slice(0, 300)}` });
|
|
123
165
|
/** Fatal for a REQUIRED spawn hook: emit metadata for compensation, then exit
|
|
124
166
|
* nonzero so the kernel rolls the spawn back. `meta` carries whatever external
|
|
@@ -455,7 +497,7 @@ function globalGrantSpawn() {
|
|
|
455
497
|
const teamWarnings = [];
|
|
456
498
|
const unmappedPrimary = !team ? unmappedPrimaryRow() : undefined;
|
|
457
499
|
if (!team) team = activeTeamAt(custody);
|
|
458
|
-
if (unmappedPrimary && team) teamWarnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using
|
|
500
|
+
if (unmappedPrimary && team) teamWarnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using the default team ${team}`);
|
|
459
501
|
if (!team) fatal("identity.mode \"global\" requires settings.oats.aweb.team or an active team at the resident custody root before minting a grant");
|
|
460
502
|
const grantHome = join(home, ".aweb-identity");
|
|
461
503
|
if (existsSync(grantHome)) fatal(`${grantHome} already exists; refusing to overwrite an existing aweb session grant home`);
|
|
@@ -715,9 +757,9 @@ function validateJoinLabels(labels, { action = "join" } = {}) {
|
|
|
715
757
|
const eligible = eligibleTeams();
|
|
716
758
|
const byLabel = new Map(eligible.map((t) => [t.label, t]));
|
|
717
759
|
for (const label of labels) {
|
|
718
|
-
if (action === "leave" && label === "
|
|
719
|
-
const error = new Error(`
|
|
720
|
-
error.code = "
|
|
760
|
+
if (action === "leave" && label === "default") {
|
|
761
|
+
const error = new Error(`E_TEAM_DEFAULT: ${label} is the workspace's default team and cannot be left`);
|
|
762
|
+
error.code = "E_TEAM_DEFAULT";
|
|
721
763
|
throw error;
|
|
722
764
|
}
|
|
723
765
|
if (!LABEL_RE.test(label) || !byLabel.has(label)) {
|
|
@@ -735,14 +777,14 @@ const providerTeamsFile = () => join(providerStateDir(), "teams.json");
|
|
|
735
777
|
function readProviderTeamsState(meta = {}) {
|
|
736
778
|
try {
|
|
737
779
|
const doc = JSON.parse(readFileSync(providerTeamsFile(), "utf8"));
|
|
738
|
-
return { joinedTeams: joinedTeamsOf(doc) };
|
|
739
|
-
} catch { return { joinedTeams: joinedTeamsOf(meta) }; }
|
|
780
|
+
return { joinedTeams: joinedTeamsOf(doc), wakeJoined: doc.wakeJoined === true };
|
|
781
|
+
} catch { return { joinedTeams: joinedTeamsOf(meta), wakeJoined: meta.wakeJoined === true }; }
|
|
740
782
|
}
|
|
741
783
|
function writeProviderTeamsState(meta) {
|
|
742
784
|
mkdirSync(providerStateDir(), { recursive: true, mode: 0o700 });
|
|
743
|
-
writeFileSync(providerTeamsFile(), JSON.stringify({ joinedTeams: joinedTeamsOf(meta) }, null, 2) + "\n", { mode: 0o600 });
|
|
785
|
+
writeFileSync(providerTeamsFile(), JSON.stringify({ joinedTeams: joinedTeamsOf(meta), wakeJoined: meta.wakeJoined === true }, null, 2) + "\n", { mode: 0o600 });
|
|
744
786
|
}
|
|
745
|
-
function withProviderTeams(meta = {}) { return { ...meta, joinedTeams:
|
|
787
|
+
function withProviderTeams(meta = {}) { const state = readProviderTeamsState(meta); return { ...meta, joinedTeams: state.joinedTeams, wakeJoined: state.wakeJoined }; }
|
|
746
788
|
function identityHomeForLabel(label) { return join(home, `.aweb-identity-${label}`); }
|
|
747
789
|
function awWithIdentity(identityHome, args) { return ["aw", "--identity-home", identityHome, ...args]; }
|
|
748
790
|
function commandOutput(e) { return [e?.stdout, e?.stderr, e?.message].filter(Boolean).join("\n"); }
|
|
@@ -758,14 +800,19 @@ function readCapabilityMeta() {
|
|
|
758
800
|
if (process.env.OATS_META) { try { return withProviderTeams(JSON.parse(process.env.OATS_META || "{}")); } catch { return withProviderTeams({}); } }
|
|
759
801
|
try { return withProviderTeams(JSON.parse(readFileSync(join(home, "instance.json"), "utf8")).capabilityMeta?.["oats.aweb"] || {}); } catch { return withProviderTeams({}); }
|
|
760
802
|
}
|
|
803
|
+
function defaultTeamSource(meta = {}) {
|
|
804
|
+
const recorded = meta.defaultTeam?.source;
|
|
805
|
+
if (recorded === "setting" || recorded === "root") return recorded;
|
|
806
|
+
return payloadTeam().team ? "setting" : "root";
|
|
807
|
+
}
|
|
761
808
|
function teamsDocument(meta = readCapabilityMeta()) {
|
|
762
809
|
const teams = parseOatsTeams();
|
|
763
810
|
const joined = joinedTeamsOf(meta);
|
|
764
811
|
const joinedLabels = new Set(joined.map((j) => j.label));
|
|
765
812
|
const primary = primaryTeamLabel();
|
|
766
|
-
const
|
|
813
|
+
const defaultTeam = meta.defaultTeam?.team || meta.team || meta.identity?.team || payloadTeam().team || null;
|
|
767
814
|
return {
|
|
768
|
-
|
|
815
|
+
defaultTeam: { team: defaultTeam, source: defaultTeamSource(meta) },
|
|
769
816
|
primary,
|
|
770
817
|
eligible: teams.filter((t) => t.mapped && t.team).map((t) => ({ label: t.label, team: t.team, joined: joinedLabels.has(t.label) })),
|
|
771
818
|
joined: joined.map((j) => ({ label: j.label, team: j.team, identityHome: j.identityHome, receive: j.receive || "poll", since: j.since })),
|
|
@@ -845,7 +892,7 @@ function parseHomeCommandArgs(argv = process.argv.slice(3)) {
|
|
|
845
892
|
}
|
|
846
893
|
function outputTeamsDocument(doc, json) {
|
|
847
894
|
if (json) { console.log(JSON.stringify(doc)); return; }
|
|
848
|
-
console.log(`
|
|
895
|
+
console.log(`default team: ${doc.defaultTeam.team || "unknown"}`);
|
|
849
896
|
for (const row of doc.eligible) console.log(`${row.joined ? "joined" : "eligible"}: ${row.label} (${row.team})`);
|
|
850
897
|
for (const label of doc.unmapped) console.log(`unmapped: ${label}`);
|
|
851
898
|
}
|
|
@@ -855,36 +902,129 @@ function runTeamsCommand(kind) {
|
|
|
855
902
|
const actions = [];
|
|
856
903
|
const warnings = [];
|
|
857
904
|
if (kind === "join" || kind === "leave") {
|
|
905
|
+
if (identityMode === "global" || meta.identity?.mode === "global") {
|
|
906
|
+
const error = new Error('joined teams need local per-team identities; this home acts as a resident identity through a session grant (identity.mode "global")');
|
|
907
|
+
error.code = "E_TEAM_GLOBAL_MODE";
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
858
910
|
const rows = validateJoinLabels(args.labels, { action: kind });
|
|
859
|
-
if (!rows.length)
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
911
|
+
if (!rows.length) { const error = new Error("labels are required"); error.code = "E_BAD_ARGS"; throw error; }
|
|
912
|
+
// Record every completed label even when a later one fails: a confirmed
|
|
913
|
+
// leave deleted its home, and a join created a remote identity.
|
|
914
|
+
try {
|
|
915
|
+
for (const row of rows) {
|
|
916
|
+
const result = kind === "join" ? mintJoinedTeam(row, meta) : leaveJoinedTeam(row.label, meta);
|
|
917
|
+
meta = result.meta;
|
|
918
|
+
writeProviderTeamsState(meta);
|
|
919
|
+
actions.push({ action: kind, label: row.label, ...(result.released ? { released: result.released } : {}), ...(result.receipt ? { receipt: result.receipt } : {}), ...(result.warning ? { warning: result.warning } : {}) });
|
|
920
|
+
if (result.warning) warnings.push(`oats-aweb: ${result.warning}`);
|
|
921
|
+
}
|
|
922
|
+
} catch (e) {
|
|
923
|
+
// What already happened travels with the failure, so a caller can
|
|
924
|
+
// reconcile (the operation envelope carries it as error.details).
|
|
925
|
+
e.partial = { actions };
|
|
926
|
+
throw e;
|
|
927
|
+
} finally {
|
|
928
|
+
const synced = syncWakeReceive(meta);
|
|
929
|
+
meta = synced.meta;
|
|
930
|
+
for (const w of synced.warnings) warnings.push(`oats-aweb: ${w}`);
|
|
931
|
+
writeProviderTeamsState(meta);
|
|
865
932
|
}
|
|
866
|
-
writeProviderTeamsState(meta);
|
|
867
933
|
}
|
|
868
934
|
const doc = { ...teamsDocument(meta), ...(actions.length ? { actions } : {}), ...(warnings.length ? { warnings } : {}) };
|
|
935
|
+
if (operation) operationOk(doc);
|
|
869
936
|
outputTeamsDocument(doc, args.json);
|
|
870
937
|
}
|
|
871
938
|
|
|
939
|
+
/** The primary identity's team and the root that mints it, exactly as 1.14.2:
|
|
940
|
+
* the config's `team:` (id, then name) wins, else the root's active team.
|
|
941
|
+
* Exits through fatal() on refusal. */
|
|
942
|
+
function resolvePrimaryTeam() {
|
|
943
|
+
const warnings = [];
|
|
944
|
+
const root = awebRoot();
|
|
945
|
+
if (!root) fatal(`${awebRootProblem(rootSettingCandidate())}, so no identity could be minted and this instance would have no messaging`);
|
|
946
|
+
let team = payloadTeam().team;
|
|
947
|
+
const source = team ? "setting" : "root";
|
|
948
|
+
const unmappedPrimary = !team ? unmappedPrimaryRow() : undefined;
|
|
949
|
+
if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
950
|
+
if (unmappedPrimary && team) warnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using the default team ${team}`);
|
|
951
|
+
if (!team) fatal(`cannot determine target team, so no identity could be minted — ${teamConfigRemedy()}, or activate a team at the aweb root`);
|
|
952
|
+
// A bare team name (no namespace) resolves against the root's memberships.
|
|
953
|
+
if (!team.includes(":")) {
|
|
954
|
+
const teams = JSON.parse(run(["aw", "team", "list", "--json"], root));
|
|
955
|
+
const match = teamIdsOf(teams).filter((tid) => String(tid).startsWith(`${team}:`));
|
|
956
|
+
if (match.length === 1) team = match[0];
|
|
957
|
+
else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted — ${teamConfigRemedy()}`);
|
|
958
|
+
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()}`);
|
|
959
|
+
}
|
|
960
|
+
return { team, root, source, warnings };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** The home's runtime. Spawn and launch run in the session's own env; a
|
|
964
|
+
* join/leave may be run by another agent whose OATS_RUNTIME is its own. */
|
|
965
|
+
function instanceRuntime(meta = {}) {
|
|
966
|
+
const own = ["spawn", "launch"].includes(event) ? process.env.OATS_RUNTIME : undefined;
|
|
967
|
+
if (own) return own;
|
|
968
|
+
if (meta.runtime) return meta.runtime;
|
|
969
|
+
try { const r = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")).runtime; if (r) return r; } catch { /* fall through */ }
|
|
970
|
+
return process.env.OATS_RUNTIME || undefined;
|
|
971
|
+
}
|
|
972
|
+
function primaryIdentityHomeOf(meta = {}) {
|
|
973
|
+
return meta.identity?.mode === "global" && meta.identity?.grant?.home ? meta.identity.grant.home : join(home, ".aw");
|
|
974
|
+
}
|
|
975
|
+
/** Keep the host wake broker's registration for this home in step with the
|
|
976
|
+
* joined teams, and record each joined team's receive mode. Never throws: a
|
|
977
|
+
* refused registration leaves that team poll-only with a warning. */
|
|
978
|
+
function syncWakeReceive(meta) {
|
|
979
|
+
// A joined home that no longer exists would make aw refuse the whole
|
|
980
|
+
// registration; it is stale provider state, not a receive identity.
|
|
981
|
+
const joined = joinedTeamsOf(meta).filter((j) => existsSync(j.identityHome));
|
|
982
|
+
const delivery = meta.delivery || deliveryMode;
|
|
983
|
+
const runtime = instanceRuntime(meta);
|
|
984
|
+
const primary = primaryIdentityHomeOf(meta);
|
|
985
|
+
const doc = wakeRegistration({ home, primaryIdentityHome: primary, delivery, runtime, joined, backend: process.env.OATS_BACKEND });
|
|
986
|
+
const warnings = [];
|
|
987
|
+
let receive = "poll", wakeJoined = false;
|
|
988
|
+
if (doc) {
|
|
989
|
+
try { run(["aw", "wake", "register", "--registration-json", "-"], home, 60000, { input: JSON.stringify(doc) }); receive = "native"; wakeJoined = true; }
|
|
990
|
+
catch (e) { warnings.push(`joined teams stay poll-only: aw wake register refused the multi-identity registration (${e.message || e})`); }
|
|
991
|
+
} else if (joined.length && !runtimeDeliveryFor({ delivery, runtime })) {
|
|
992
|
+
warnings.push(`joined teams receive by polling: runtime ${runtime || "unknown"} has no native presentation surface for the host wake broker`);
|
|
993
|
+
}
|
|
994
|
+
if (!wakeJoined && meta.wakeJoined) {
|
|
995
|
+
// Back to one identity: a session home keeps its legacy registration, a
|
|
996
|
+
// native home leaves the broker entirely.
|
|
997
|
+
if (delivery === "session") { try { wakeRegister(home, primary); } catch (e) { warnings.push(String(e.message || e)); } }
|
|
998
|
+
else if (!wakeDeregister(home)) warnings.push("aw wake deregister failed; the broker treats a stale registration as inactive on its own");
|
|
999
|
+
}
|
|
1000
|
+
const next = { ...meta, ...(runtime ? { runtime } : {}), wakeJoined, joinedTeams: joinedTeamsOf(meta).map((j) => ({ ...j, receive: joined.includes(j) ? receive : "poll" })) };
|
|
1001
|
+
return { meta: next, warnings };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
872
1004
|
if (event === "launch") {
|
|
873
1005
|
if (identityMode === "global" || grantRenewMode() === "launch") globalGrantRenew();
|
|
874
1006
|
let oldMeta = withProviderTeams(JSON.parse(process.env.OATS_META || "{}"));
|
|
875
1007
|
const joined = joinedTeamsOf(oldMeta);
|
|
876
1008
|
if (joined.length && process.env.OATS_TEAMS_SOURCE === "live") {
|
|
877
1009
|
const eligible = new Set(eligibleTeams().map((t) => t.label));
|
|
878
|
-
let changed = false;
|
|
879
1010
|
const warnings = [];
|
|
880
1011
|
for (const row of joined) if (!eligible.has(row.label)) {
|
|
881
|
-
try { oldMeta = leaveJoinedTeam(row.label, oldMeta).meta;
|
|
1012
|
+
try { oldMeta = leaveJoinedTeam(row.label, oldMeta).meta; }
|
|
882
1013
|
catch (e) { warnings.push(`joined team ${row.label} cleanup failed: ${e.message || e}`); }
|
|
883
1014
|
}
|
|
884
|
-
|
|
885
|
-
|
|
1015
|
+
// Re-register what remains: the runtime may differ from the last session.
|
|
1016
|
+
const synced = syncWakeReceive(oldMeta);
|
|
1017
|
+
oldMeta = synced.meta;
|
|
1018
|
+
warnings.push(...synced.warnings);
|
|
1019
|
+
writeProviderTeamsState(oldMeta);
|
|
1020
|
+
out({ meta: oldMeta, ...retainedLaunchOutput(oldMeta), ...(warnings.length ? { warning: `oats-aweb: ${warnings.join(" | ")}` } : {}) });
|
|
1021
|
+
}
|
|
1022
|
+
if (joined.length) {
|
|
1023
|
+
const synced = syncWakeReceive(oldMeta);
|
|
1024
|
+
oldMeta = synced.meta;
|
|
1025
|
+
writeProviderTeamsState(oldMeta);
|
|
1026
|
+
out({ meta: oldMeta, ...retainedLaunchOutput(oldMeta), warning: ["oats-aweb: teams-unverified — keeping joined team memberships because live eligible teams are unavailable", ...synced.warnings.map((w) => `oats-aweb: ${w}`)].join(" | ") });
|
|
886
1027
|
}
|
|
887
|
-
if (joined.length && process.env.OATS_TEAMS_SOURCE !== "live") out({ ...retainedLaunchOutput(oldMeta), warning: "oats-aweb: teams-unverified — keeping joined team memberships because live eligible teams are unavailable" });
|
|
888
1028
|
out(retainedLaunchOutput(oldMeta));
|
|
889
1029
|
} else if (event === "spawn") {
|
|
890
1030
|
if (identityMode === "global" && identitySettings.source) fatal('identity.mode "global" cannot be combined with identity.source; use identity.mode "local" with identity.source for a retained seat, or identity.mode "global" with identity.resident for a resident grant');
|
|
@@ -895,32 +1035,16 @@ if (event === "launch") {
|
|
|
895
1035
|
try { joinRows = validateJoinLabels(requestedJoinLabels()); }
|
|
896
1036
|
catch (e) { fatal(e.message || e); }
|
|
897
1037
|
let minted; // external identity, once `aw team join` succeeds
|
|
898
|
-
|
|
899
|
-
if (!root) {
|
|
900
|
-
const candidate = rootSettingCandidate();
|
|
901
|
-
fatal(`${awebRootProblem(candidate)}, so no identity could be minted and this instance would have no messaging`);
|
|
902
|
-
}
|
|
1038
|
+
let spawnMeta; // with joined teams, once any is accepted
|
|
903
1039
|
try {
|
|
904
1040
|
// Team correctness: the config's `team:` block wins (id, then name), else the
|
|
905
1041
|
// root's active team. ALWAYS pass --team-id explicitly — never inherit whatever
|
|
906
1042
|
// team happens to be active at mint time — and verify the joined cert matches.
|
|
907
1043
|
// The instance name IS the discoverable alias (the team roster doubles as the
|
|
908
1044
|
// cross-machine instance directory).
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
const warnings = [];
|
|
912
|
-
const unmappedPrimary = !team ? unmappedPrimaryRow() : undefined;
|
|
913
|
-
if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
914
|
-
if (unmappedPrimary && team) warnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using personal team ${team}`);
|
|
915
|
-
if (!team) fatal(`cannot determine target team, so no identity could be minted — ${teamConfigRemedy()}, or activate a team at the aweb root`);
|
|
916
|
-
// A bare team name (no namespace) resolves against the root's memberships.
|
|
917
|
-
if (!team.includes(":")) {
|
|
918
|
-
const teams = JSON.parse(run(["aw", "team", "list", "--json"], root));
|
|
919
|
-
const match = teamIdsOf(teams).filter((tid) => String(tid).startsWith(`${team}:`));
|
|
920
|
-
if (match.length === 1) team = match[0];
|
|
921
|
-
else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted — ${teamConfigRemedy()}`);
|
|
922
|
-
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()}`);
|
|
923
|
-
}
|
|
1045
|
+
const primary = resolvePrimaryTeam();
|
|
1046
|
+
const { team, root } = primary;
|
|
1047
|
+
const warnings = [...primary.warnings];
|
|
924
1048
|
// Both of these carry the invite token — one mints it, the other spends it —
|
|
925
1049
|
// so neither their output nor their diagnostics may reach a log.
|
|
926
1050
|
const inv = parseSecretJson(run(["aw", "team", "invite", "--team-id", team, "--json"], root, 45000, { secretSafe: true }), "aw team invite");
|
|
@@ -987,15 +1111,19 @@ if (event === "launch") {
|
|
|
987
1111
|
const deliveryBrief = deliveryMode === "session"
|
|
988
1112
|
? ` Notification delivery: external (AWEB_DELIVERY=session): the host wake broker (aw wake) is registered for this home and nudges you when mail or chat arrives; the native aweb channel is not running. If you have waited long with nothing arriving, check \`aw mail inbox\` and \`aw chat pending\` yourself at task boundaries.`
|
|
989
1113
|
: "";
|
|
990
|
-
let meta = { team: joined.team_id, alias, delivery: deliveryMode, identity: identityMeta({ mode: "local", alias, team: joined.team_id }) };
|
|
1114
|
+
let meta = { team: joined.team_id, alias, delivery: deliveryMode, defaultTeam: { team: joined.team_id, source: primary.source }, ...(process.env.OATS_RUNTIME ? { runtime: process.env.OATS_RUNTIME } : {}), identity: identityMeta({ mode: "local", alias, team: joined.team_id }) };
|
|
991
1115
|
const joinFloorProblem = joinRows.length ? joinedTeamsAwFloorProblem() : undefined;
|
|
992
1116
|
if (joinFloorProblem) warnings.push(`oats-aweb: ${joinFloorProblem}`);
|
|
993
|
-
else for (const row of joinRows) { const result = mintJoinedTeam(row, meta); meta = result.meta; if (result.warning) warnings.push(`oats-aweb: ${result.warning}`); }
|
|
1117
|
+
else for (const row of joinRows) { const result = mintJoinedTeam(row, meta); meta = result.meta; spawnMeta = meta; writeProviderTeamsState(meta); if (result.warning) warnings.push(`oats-aweb: ${result.warning}`); }
|
|
1118
|
+
if (joinedTeamsOf(meta).length) { const synced = syncWakeReceive(meta); meta = synced.meta; for (const w of synced.warnings) warnings.push(`oats-aweb: ${w}`); }
|
|
994
1119
|
writeProviderTeamsState(meta);
|
|
1120
|
+
const defaultTeamBrief = meta.defaultTeam.source === "setting" ? "the team this deployment configured for you" : "the workspace's default team (the messaging root's active team)";
|
|
1121
|
+
const joinedNow = joinedTeamsOf(meta);
|
|
1122
|
+
const joinedBrief = joinedNow.length ? ` Joined teams: ${joinedNow.map((j) => `${j.label} (${j.team}, receive ${j.receive}, send with \`aw --identity-home ${j.identityHome} mail|chat ...\`)`).join("; ")}.` : "";
|
|
995
1123
|
out({
|
|
996
1124
|
meta,
|
|
997
1125
|
env,
|
|
998
|
-
brief: `Comms: you have an aweb identity — alias "${alias}" on team ${joined.team_id}.${mismatch}${deliveryBrief}
|
|
1126
|
+
brief: `Comms: you have an aweb identity — alias "${alias}" on team ${joined.team_id}, ${defaultTeamBrief}.${mismatch}${deliveryBrief}${joinedBrief} Load the oats-aweb skill before messaging: \`oats aweb teams --json\` shows your teams, \`oats aweb roster\` who you can reach. Coordination stays in your deployment's task layer.`,
|
|
999
1127
|
...(launch ? { launch } : {}),
|
|
1000
1128
|
...(joined.team_id !== team ? { warning: `oats-aweb: team mismatch — joined ${joined.team_id}, expected ${team}` } : warnings.length ? { warning: warnings.join(" | ") } : channelWarning ? { warning: channelWarning } : {}),
|
|
1001
1129
|
});
|
|
@@ -1003,7 +1131,7 @@ if (event === "launch") {
|
|
|
1003
1131
|
// A join may already have created a REMOTE identity before the failure.
|
|
1004
1132
|
// Hand it back as meta so the kernel's compensation can delete it — losing
|
|
1005
1133
|
// it here would strand a roster entry no one owns.
|
|
1006
|
-
fatal(`identity minting failed: ${e.message || e}`, minted);
|
|
1134
|
+
fatal(`identity minting failed: ${e.message || e}`, minted && joinedTeamsOf(spawnMeta).length ? { ...minted, joinedTeams: joinedTeamsOf(spawnMeta) } : minted);
|
|
1007
1135
|
}
|
|
1008
1136
|
} else if (event === "retire") {
|
|
1009
1137
|
let meta = withProviderTeams(JSON.parse(process.env.OATS_META || "{}"));
|
|
@@ -1017,6 +1145,10 @@ if (event === "launch") {
|
|
|
1017
1145
|
try { meta = leaveJoinedTeam(joined.label, meta).meta; }
|
|
1018
1146
|
catch (e) { retireWarnings.push(`joined team ${joined.label} cleanup failed: ${e.message || e}`); }
|
|
1019
1147
|
}
|
|
1148
|
+
// A native (channel/pi) home registered with the broker only for its joined
|
|
1149
|
+
// teams; a session home was deregistered above.
|
|
1150
|
+
if (meta.wakeJoined && meta.delivery !== "session") { if (!wakeDeregister(home)) retireWarnings.push("aw wake deregister failed; the broker treats a retired home as inactive on its own"); }
|
|
1151
|
+
meta = { ...meta, wakeJoined: false };
|
|
1020
1152
|
writeProviderTeamsState(meta);
|
|
1021
1153
|
if (meta.retained) {
|
|
1022
1154
|
if (meta.lock) { try { rmSync(meta.lock, { force: true }); } catch { /* the lock may already be gone */ } }
|
|
@@ -1065,15 +1197,35 @@ if (event === "launch") {
|
|
|
1065
1197
|
}
|
|
1066
1198
|
} else if (["teams", "join", "leave"].includes(event)) {
|
|
1067
1199
|
try { runTeamsCommand(event); process.exit(0); }
|
|
1068
|
-
catch (e) {
|
|
1200
|
+
catch (e) {
|
|
1201
|
+
console.error(e.code ? `${e.code}: ${e.message}` : `oats aweb ${event}: ${e.message || e}`);
|
|
1202
|
+
if (operation) operationFail(e.code, e.message || String(e), e.partial ? { ...e.partial, joined: teamsDocument(readCapabilityMeta()).joined } : undefined);
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1069
1205
|
} else if (event === "roster") {
|
|
1070
1206
|
// Cross-machine directory: every OATS-spawned instance joins the team with
|
|
1071
1207
|
// alias = instance name, so the team's member roster lists live instances
|
|
1072
1208
|
// wherever they run (plus human members). Local liveness comes from
|
|
1073
1209
|
// `oats status` in the deployment; this is the network view.
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1210
|
+
// Default: this instance's default team, listed from the root that minted
|
|
1211
|
+
// it. `--label <label>` lists an eligible (joined or not) workspace team from
|
|
1212
|
+
// the host root that holds it.
|
|
1213
|
+
const argv = process.argv.slice(3);
|
|
1214
|
+
const labelAt = argv.indexOf("--label");
|
|
1215
|
+
const label = labelAt >= 0 ? argv[labelAt + 1] : (argv.find((a) => a.startsWith("--label=")) || "").slice("--label=".length) || undefined;
|
|
1216
|
+
const meta = readCapabilityMeta();
|
|
1217
|
+
let team, root;
|
|
1218
|
+
if (label) {
|
|
1219
|
+
const row = eligibleTeams().find((t) => t.label === label);
|
|
1220
|
+
if (!row) { console.error(`E_TEAM_NOT_ELIGIBLE: ${label} is not an eligible team label for this instance (eligible: ${eligibleTeams().map((t) => t.label).join(", ") || "(none)"})`); process.exit(1); }
|
|
1221
|
+
team = row.team; root = awebRootForTeam(team);
|
|
1222
|
+
if (!root) { console.error(`oats aweb roster: ${awebRootProblem(rootSettingCandidate(team))}, so the roster of ${label} cannot be read from this host`); process.exit(1); }
|
|
1223
|
+
} else {
|
|
1224
|
+
team = meta.defaultTeam?.team || meta.identity?.team || meta.team || process.env.OATS_TEAM_ID;
|
|
1225
|
+
root = awebRoot();
|
|
1226
|
+
if (!root) { console.error(`oats aweb roster: ${awebRootProblem(rootSettingCandidate())}`); process.exit(1); }
|
|
1227
|
+
if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
1228
|
+
}
|
|
1077
1229
|
if (!team) { console.error(`oats aweb roster: cannot determine team (${teamConfigRemedy()}, or activate a team at the aweb root)`); process.exit(1); }
|
|
1078
1230
|
const teamFlag = team.includes(":") ? ["--team-id", team] : ["--team", team];
|
|
1079
1231
|
const r = JSON.parse(run(["aw", "id", "team", "members", ...teamFlag, "--json"], root, 60000));
|
|
@@ -1082,7 +1234,7 @@ if (event === "launch") {
|
|
|
1082
1234
|
const members = r.members || [];
|
|
1083
1235
|
if (!members.length) console.log(" (no member certificates visible from this workspace)");
|
|
1084
1236
|
for (const m of members) console.log(` ${m.alias || m.name || m.did || JSON.stringify(m)}`);
|
|
1085
|
-
console.log(
|
|
1237
|
+
console.log(`\nAliases minted by OATS are instance names; message one with \`aw mail send --to <alias> --subject "..." --body-file <file>\`${label ? ` as this team: \`aw --identity-home <identityHome> mail send ...\` (identityHome from \`oats aweb teams --json\`)` : ""}.`);
|
|
1086
1238
|
process.exit(0);
|
|
1087
1239
|
} else if (event === "setup") {
|
|
1088
1240
|
// Guided onboarding — idempotent, prints what it finds and can run one
|
|
@@ -1,82 +1,47 @@
|
|
|
1
1
|
## Messaging: aweb
|
|
2
2
|
|
|
3
|
-
Your messaging layer is **aweb**.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Your messaging layer is **aweb**. Your identity's alias is your instance name,
|
|
4
|
+
and it lives in the workspace's default team (the `Comms:` line of your TASK.md
|
|
5
|
+
names it). Wider workspace teams are joined explicitly, each with its own
|
|
6
|
+
identity home.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
message you sent shows unverified at the receiver, the attachment is missing:
|
|
13
|
-
report it, do not retry. If `aw mail` or `aw chat` reports a terminal grant condition —
|
|
14
|
-
`grant_expired`, `grant_revoked`, `grant_subject_inactive`,
|
|
15
|
-
`grant_issuer_revoked`, or `grant_freshness_unavailable` — report the exact
|
|
16
|
-
condition to your coordinator/human and stop using messaging. The host restarts
|
|
17
|
-
you with a fresh grant when renewal is available.
|
|
8
|
+
**Load the `oats-aweb` skill before your first `aw mail`/`aw chat` of a session,
|
|
9
|
+
whenever an aweb wake or channel event arrives, and whenever messaging or a
|
|
10
|
+
team command looks wrong.** It covers your teams, the roster, sending and
|
|
11
|
+
replying, how wakes work, etiquette and troubleshooting. Do not work from
|
|
12
|
+
memory; if a flag looks wrong run `aw <command> --help`.
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- **Before your first `aw mail`/`aw chat` of a session**, load the
|
|
22
|
-
**aweb-messaging** skill. It is the
|
|
23
|
-
playbook for sending, replying, and chat etiquette.
|
|
24
|
-
- **When an aweb channel event awakens you**, read the injected event
|
|
25
|
-
metadata first, then load the **aweb-messaging** skill ("Read the event
|
|
26
|
-
first" section) before responding — continue the existing conversation,
|
|
27
|
-
never
|
|
28
|
-
start a new thread when a `message_id`/`conversation_id` is provided.
|
|
29
|
-
- For team/roster/certificate questions, load **aweb-team-membership**; for
|
|
30
|
-
identity/key questions, load **aweb-identity**.
|
|
31
|
-
- If a command errors or a flag looks wrong, re-read the skill or run
|
|
32
|
-
`aw <cmd> --help` — never invent flags.
|
|
33
|
-
|
|
34
|
-
Quick crib (the skill has the full craft; run from your instance home):
|
|
14
|
+
Quick crib (run from your instance home, never from `./work`):
|
|
35
15
|
|
|
36
16
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
aw mail
|
|
40
|
-
aw mail send --to <alias> --subject "..." --body-file <f>
|
|
41
|
-
aw mail reply <message-id> --body
|
|
42
|
-
aw chat send
|
|
17
|
+
oats aweb teams --json # defaultTeam, eligible, joined teams
|
|
18
|
+
oats aweb roster # who you can reach
|
|
19
|
+
aw mail inbox # UNREAD mail only (--show-all: history)
|
|
20
|
+
aw mail send --to <alias> --subject "..." --body-file <f> # recipient needs --to
|
|
21
|
+
aw mail reply <message-id> --body-file <f> # stay in the thread
|
|
22
|
+
aw chat send-and-wait <alias> --body-file <f> --start-conversation # blocking question
|
|
23
|
+
aw chat pending # chats waiting on you
|
|
24
|
+
aw --identity-home <identityHome> mail|chat ... # act as a joined team
|
|
43
25
|
```
|
|
44
26
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
broker registers you, nothing wakes you: check `aw mail inbox` and
|
|
67
|
-
`aw chat pending` at every task boundary. Otherwise the rule below applies.
|
|
68
|
-
|
|
69
|
-
**Never sleep, poll, or busy-wait for another agent's reply.** Send your
|
|
70
|
-
message, finish your turn, and go idle when a delivery channel is configured
|
|
71
|
-
(you saw `✓ aweb connected` at startup). Native Codex has no aweb channel:
|
|
72
|
-
check inbox and pending chat at task boundaries or when the operator asks,
|
|
73
|
-
as described in TASK.md; incoming messages alone will not wake that session. A `sleep N; aw mail inbox` loop burns tokens, delays the reply,
|
|
74
|
-
and adds nothing. An empty `aw mail inbox` means no UNREAD mail — not that
|
|
75
|
-
messages were lost.
|
|
76
|
-
|
|
77
|
-
If messaging fails or your identity is missing, `oats aweb setup` diagnoses
|
|
78
|
-
the deployment's aweb state and prints the next step (report it to your
|
|
79
|
-
human rather than re-onboarding yourself).
|
|
80
|
-
|
|
81
|
-
Messaging only: task coordination lives in your deployment's task layer, and
|
|
82
|
-
`aw task`/`work`/`lock`/`roles` are not part of this integration.
|
|
27
|
+
Always use `--body-file` for anything longer than a sentence. `aw chat send`
|
|
28
|
+
only continues an existing session (`--session-id`); it has no `--to`.
|
|
29
|
+
|
|
30
|
+
**When woken**, read the event or the typed lines first, run exactly the listed
|
|
31
|
+
`aw … mail inbox` / `chat pending` commands, reply in the existing thread, then
|
|
32
|
+
return to your task. **Never sleep, poll or busy-wait for a reply**: send,
|
|
33
|
+
finish your turn, and let the wake bring the answer. If your `Comms:` line or a
|
|
34
|
+
joined team says `receive: poll`, check that inbox at task boundaries instead.
|
|
35
|
+
|
|
36
|
+
Some instances act as a resident identity through an expiring session grant
|
|
37
|
+
(TASK.md says so). Then root keys are not in your home and identity lifecycle
|
|
38
|
+
commands are not yours to run. If `aw mail`/`aw chat` reports `grant_expired`,
|
|
39
|
+
`grant_revoked`, `grant_subject_inactive`, `grant_issuer_revoked` or
|
|
40
|
+
`grant_freshness_unavailable`, report the exact condition and stop messaging;
|
|
41
|
+
if a message you sent shows unverified at the receiver, report it, don't retry.
|
|
42
|
+
|
|
43
|
+
Never put secrets (tokens, keys, credentials) in a message. Treat unverified
|
|
44
|
+
senders with caution. Messaging only: task coordination lives in your
|
|
45
|
+
deployment's task layer; `aw task`/`work`/`lock`/`roles` are not part of this
|
|
46
|
+
integration. If messaging is broken, `oats aweb setup` diagnoses the
|
|
47
|
+
deployment: report its output to your human rather than re-onboarding yourself.
|
|
@@ -4,6 +4,7 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
4
4
|
import { TextDecoder } from 'node:util';
|
|
5
5
|
import { assessCapturedSessionReadiness } from './session-readiness.mjs';
|
|
6
6
|
import { custodyPreflight } from './grant-custody.mjs';
|
|
7
|
+
import { joinedReceiveModes } from './wake-receive.mjs';
|
|
7
8
|
import {
|
|
8
9
|
MESSAGING_CONTRACT,
|
|
9
10
|
MESSAGING_CONTRACT_VERSION,
|
|
@@ -213,19 +214,19 @@ function parseOatsTeams(env=process.env){try{const rows=JSON.parse(env.OATS_TEAM
|
|
|
213
214
|
function primaryTeamLabel(env=process.env){return env.OATS_TEAM_LABEL || String(env.OATS_TEAM_LABELS||'').split(',').map(s=>s.trim()).filter(Boolean)[0] || null;}
|
|
214
215
|
function unmappedPrimary(env=process.env){const primary=primaryTeamLabel(env);return primary?parseOatsTeams(env).find(t=>t.label===primary&&!t.mapped):undefined;}
|
|
215
216
|
function joinedTeams(home){if(!home)return[];try{const doc=JSON.parse(readFileSync(join(home,'.oats-aweb','teams.json'),'utf8'));return Array.isArray(doc.joinedTeams)?doc.joinedTeams.filter(j=>j&&typeof j==='object'&&j.label&&j.team&&j.identityHome):[];}catch{return[];}}
|
|
216
|
-
function teamsReadiness({home,team,env=process.env}){const teams=parseOatsTeams(env),joined=joinedTeams(home),joinedLabels=new Set(joined.map(j=>j.label));return{personal:{team:team||null},primary:primaryTeamLabel(env),eligible:teams.filter(t=>t.mapped&&t.team).map(t=>({label:t.label,team:t.team,joined:joinedLabels.has(t.label)})),joined:joined.map(j=>({label:j.label,team:j.team,identityHome:j.identityHome,receive:j.receive||'poll',since:j.since})),unmapped:teams.filter(t=>!t.mapped).map(t=>t.label),at:new Date().toISOString()};}
|
|
217
217
|
function readinessDetails(settings,{deployment,env=process.env}={}) {
|
|
218
218
|
if(classicEnv(env)) return {team:undefined,candidate:null,warnings:[],result:{status:'needs-configuration',problems:[{code:'needs-configuration',message:CLASSIC_REFUSAL}]}};
|
|
219
219
|
const initialTeam=typeof settings.team==='string' && settings.team.trim()?settings.team.trim():undefined;
|
|
220
220
|
const candidate=rootCandidate(settings,initialTeam,{deployment,env}),team=teamFromSettings(settings,candidate,{env}),problems=[],warnings=[];
|
|
221
221
|
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}`});
|
|
222
|
-
const unmapped=unmappedPrimary(env);if(unmapped&&team)warnings.push({code:'team-unmapped',message:`workspace label ${unmapped.label} is not mapped; using
|
|
222
|
+
const unmapped=unmappedPrimary(env);if(unmapped&&team)warnings.push({code:'team-unmapped',message:`workspace label ${unmapped.label} is not mapped; using the default team ${team}`});
|
|
223
223
|
if(!team) problems.push({code:'needs-configuration',message:'no team: set settings.oats.aweb.team or keep an active team at the aweb root'});
|
|
224
224
|
return {team,candidate,warnings,result:checkProblems(problems) || {status:'ready',problems:[]}};
|
|
225
225
|
}
|
|
226
226
|
function readinessFromSettings(settings,options) {return readinessDetails(settings,options).result;}
|
|
227
227
|
function runAw(argv,cwd,{unsetEnv=[],timeout=60000}={}) {
|
|
228
|
-
|
|
228
|
+
// An inherited AWEB_IDENTITY_HOME is the caller's identity, never this check's.
|
|
229
|
+
const env={...process.env};delete env.AWEB_IDENTITY_HOME;for(const name of unsetEnv) delete env[name];
|
|
229
230
|
try {return execFileSync(argv[0],argv.slice(1),{cwd,env,encoding:'utf8',stdio:['ignore','pipe','pipe'],timeout}).trim();}
|
|
230
231
|
catch(e) {throw new Error(`${argv.slice(0,3).join(' ')} failed${e.status===undefined?'':` (exit ${e.status})`}`);}
|
|
231
232
|
}
|
|
@@ -264,8 +265,16 @@ function workspaceReadinessPhase(req) {
|
|
|
264
265
|
catch(e) {problems.push({code:'custody',message:e.message});}
|
|
265
266
|
}
|
|
266
267
|
}
|
|
267
|
-
const
|
|
268
|
-
|
|
268
|
+
const joined=joinedTeams(ctx.home);
|
|
269
|
+
if(joined.length) {
|
|
270
|
+
let status;try{status=JSON.parse(runAw(['aw','wake','status','--json'],ctx.home,{timeout:10000}));}catch{status=undefined;}
|
|
271
|
+
const why={'home-not-registered':'this home is not registered with the host wake broker','not-registered-with-broker':'its identity home is not registered with the host wake broker','wake-daemon-not-running':'the host wake daemon is not running','stream-not-admitted':'the host wake broker has not admitted its stream'};
|
|
272
|
+
for(const mode of joinedReceiveModes(status,{home:ctx.home,joined})) {
|
|
273
|
+
const row=joined.find(j=>j.label===mode.label);
|
|
274
|
+
if(mode.receive==='native') warnings.push({code:'joined-team-receive',message:`joined team ${mode.label} receives native through the host wake broker (stream ${mode.phase})`});
|
|
275
|
+
else warnings.push({code:'joined-team-poll-only',message:`joined team ${mode.label} receives by polling: ${status?why[mode.reason]||mode.reason:'aw wake status is unavailable'}${mode.detail?` (${mode.detail})`:''}; check aw --identity-home ${row.identityHome} mail inbox and chat pending at task boundaries`});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
269
278
|
const wake=String(req.settings.delivery||'channel')==='session'?wakeReadiness(ctx.home,{reliedOn:true}):{problems:[],warnings:[]};
|
|
270
279
|
problems.push(...wake.problems);warnings.push(...wake.warnings);
|
|
271
280
|
const result=checkProblems(problems) || {status:'ready',problems:[]};
|