@awebai/oats 0.29.4 → 0.30.0
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/README.md +12 -6
- package/bin/oats.mjs +194 -50
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +538 -204
- package/capabilities/oats-aweb/injects/aweb.md +1 -1
- package/capabilities/oats-aweb/lib/binding-wire.mjs +31 -22
- package/capabilities/oats-aweb/oats.json +5 -12
- package/capabilities/oats-aweb/skills/VENDORED.md +4 -4
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +1 -1
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +83 -13
- package/capabilities/oats-code-review/injects/reviewer.md +26 -0
- package/capabilities/oats-code-review/oats.json +16 -0
- package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +66 -0
- package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +30 -0
- package/capabilities/oats-code-review/skills/security-review/SKILL.md +56 -0
- package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +34 -0
- package/capabilities/oats-developer/injects/developer.md +38 -0
- package/capabilities/oats-developer/oats.json +17 -0
- package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +43 -0
- package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +47 -0
- package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +65 -0
- package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +37 -0
- package/capabilities/oats-developer/skills/worktrees/SKILL.md +36 -0
- package/capabilities/oats-engineering-expert/injects/expert.md +37 -0
- package/capabilities/oats-engineering-expert/oats.json +17 -0
- package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +37 -0
- package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +52 -0
- package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +50 -0
- package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +53 -0
- package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +49 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +8 -4
- package/capabilities/oats-okf/lib/binding-wire.mjs +47 -15
- package/capabilities/oats-okf/lib/inspection.mjs +26 -7
- package/capabilities/oats-okf/lib/sources.mjs +16 -2
- package/capabilities/oats-okf/lib/worker.mjs +5 -16
- package/capabilities/oats-okf/oats.json +6 -3
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +2 -2
- package/capabilities/oats-okf-harvest/oats.json +3 -3
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +6 -6
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +2 -2
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +1 -1
- package/capabilities/oats-okf-maintenance/oats.json +2 -2
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +1 -1
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +11 -23
- package/capabilities/oats-workspace-experts/injects/oats-experts.md +26 -0
- package/capabilities/oats-workspace-experts/oats.json +9 -0
- package/docs/capabilities.md +160 -171
- package/docs/capability-manifest.schema.json +6 -11
- package/docs/configuration.md +213 -64
- package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
- package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
- package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
- package/docs/design/2026-09-27-team-model-v2.md +97 -117
- package/docs/design/2026-09-28-automations-trust.md +38 -0
- package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
- package/docs/design/HISTORY.md +65 -0
- package/docs/design/README.md +23 -54
- package/docs/desktop-cli-api.md +1787 -1777
- package/docs/desktop.md +30 -91
- package/docs/execution-targets.md +146 -292
- package/docs/first-team.md +31 -17
- package/docs/implementation.md +76 -288
- package/docs/integrations.md +118 -320
- package/docs/knowledge-capability-authoring.md +25 -52
- package/docs/knowledge-reference/acceptance.md +3 -3
- package/docs/knowledge-reference/adoption.md +1 -1
- package/docs/knowledge-reference/harvester.md +2 -2
- package/docs/knowledge-reference/package-craft.md +3 -3
- package/docs/knowledge-reference/provider-mapping.md +3 -6
- package/docs/knowledge-reference/reader-capture.md +3 -3
- package/docs/knowledge-theory.md +62 -166
- package/docs/knowledge.md +225 -404
- package/docs/layers.md +42 -97
- package/docs/oats-local.schema.json +58 -5
- package/docs/oats-membership.schema.json +1 -8
- package/docs/oats-package.schema.json +5 -5
- package/docs/oats-workspace.schema.json +8 -22
- package/docs/official-catalog.md +25 -28
- package/docs/packages.md +45 -63
- package/docs/plans/0.30-close-out.md +61 -0
- package/docs/release-lane.md +77 -0
- package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
- package/docs/release-notes/v0.19.0.md +48 -147
- package/docs/release-notes/v0.19.1.md +2 -3
- package/docs/release-notes/v0.19.3.md +2 -15
- package/docs/release-notes/v0.20.0.md +0 -15
- package/docs/release-notes/v0.22.0.md +71 -138
- package/docs/release-notes/v0.22.1.md +42 -90
- package/docs/release-notes/v0.22.10.md +1 -1
- package/docs/release-notes/v0.22.11.md +1 -47
- package/docs/release-notes/v0.22.12.md +4 -13
- package/docs/release-notes/v0.22.13.md +1 -42
- package/docs/release-notes/v0.22.14.md +3 -11
- package/docs/release-notes/v0.22.15.md +1 -46
- package/docs/release-notes/v0.22.16.md +6 -8
- package/docs/release-notes/v0.22.18.md +1 -99
- package/docs/release-notes/v0.22.19.md +3 -14
- package/docs/release-notes/v0.22.2.md +6 -15
- package/docs/release-notes/v0.22.3.md +0 -1
- package/docs/release-notes/v0.22.4.md +1 -14
- package/docs/release-notes/v0.22.5.md +2 -12
- package/docs/release-notes/v0.22.6.md +0 -3
- package/docs/release-notes/v0.23.0.md +9 -25
- package/docs/release-notes/v0.23.1.md +9 -25
- package/docs/release-notes/v0.23.2.md +2 -4
- package/docs/release-notes/v0.24.0.md +56 -97
- package/docs/release-notes/v0.24.1.md +7 -11
- package/docs/release-notes/v0.24.10.md +34 -45
- package/docs/release-notes/v0.24.11.md +12 -20
- package/docs/release-notes/v0.24.12.md +35 -48
- package/docs/release-notes/v0.24.13.md +34 -41
- package/docs/release-notes/v0.24.2.md +9 -13
- package/docs/release-notes/v0.24.3.md +7 -11
- package/docs/release-notes/v0.24.4.md +6 -6
- package/docs/release-notes/v0.24.5.md +6 -10
- package/docs/release-notes/v0.24.6.md +2 -5
- package/docs/release-notes/v0.24.7.md +46 -75
- package/docs/release-notes/v0.24.8.md +58 -96
- package/docs/release-notes/v0.24.9.md +38 -54
- package/docs/release-notes/v0.25.0.md +59 -76
- package/docs/release-notes/v0.25.1.md +57 -81
- package/docs/release-notes/v0.25.2.md +51 -70
- package/docs/release-notes/v0.25.3.md +11 -13
- package/docs/release-notes/v0.25.4.md +9 -13
- package/docs/release-notes/v0.25.5.md +3 -5
- package/docs/release-notes/v0.25.6.md +20 -29
- package/docs/release-notes/v0.25.7.md +5 -7
- package/docs/release-notes/v0.25.8.md +26 -39
- package/docs/release-notes/v0.26.0.md +175 -646
- package/docs/release-notes/v0.27.0.md +4 -5
- package/docs/release-notes/v0.27.1.md +4 -6
- package/docs/release-notes/v0.27.2.md +1 -1
- package/docs/release-notes/v0.28.0.md +57 -124
- package/docs/release-notes/v0.29.0.md +89 -208
- package/docs/release-notes/v0.29.1.md +1 -1
- package/docs/release-notes/v0.29.2.md +3 -4
- package/docs/release-notes/v0.30.0.md +205 -0
- package/docs/schedules.md +280 -363
- package/docs/servers.md +99 -117
- package/docs/soul.schema.json +2 -9
- package/docs/souls-and-instances.md +145 -158
- package/docs/workspaces.md +132 -215
- package/lib/automations.mjs +21 -6
- package/lib/core.mjs +226 -74
- package/lib/instance-events.mjs +1 -1
- package/lib/instance-inspect.mjs +109 -34
- package/lib/instance-lifecycle.mjs +14 -1
- package/lib/instance-resolution.mjs +26 -27
- package/lib/launch-preference.mjs +87 -0
- package/lib/materialize.mjs +3 -3
- package/lib/resolve.mjs +29 -87
- package/lib/schedule.mjs +1 -1
- package/lib/teams-verbs.mjs +195 -0
- package/lib/teams.mjs +190 -0
- package/lib/triggers.mjs +2 -2
- package/lib/workspace.mjs +54 -147
- package/package-catalog.json +9 -15
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +25 -13
- package/capabilities/oats-review/injects/review.md +0 -69
- package/capabilities/oats-review/oats.json +0 -10
- package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
- package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
- package/docs/conventions.md +0 -90
- package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
- package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
- package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
- package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
- package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
- package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
- package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
- package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
- package/docs/design/2026-09-15-captured-dispatch.md +0 -127
- package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
- package/docs/design/2026-09-15-package-preparation.md +0 -100
- package/docs/design/2026-09-15-portable-data-contract.md +0 -121
- package/docs/design/2026-09-15-portable-declarations.md +0 -189
- package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
- package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
- package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
- package/docs/design/2026-09-15-source-observation.md +0 -119
- package/docs/design/2026-09-16-captured-admission.md +0 -77
- package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
- package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
- package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
- package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
- package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
- package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
- package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
- package/docs/design/2026-09-16-portable-onboarding.md +0 -179
- package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
- package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
- package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
- package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
- package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
- package/docs/design/2026-09-17-captured-native-start.md +0 -58
- package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
- package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
- package/docs/design/2026-09-17-public-captured-start.md +0 -108
- package/docs/design/2026-09-17-public-prepare-request.md +0 -90
- package/docs/design/2026-09-18-captured-pi-host.md +0 -205
- package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
- package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
- package/docs/design/2026-09-20-redesign-program-board.md +0 -142
- package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
- package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
- package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
- package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
- package/docs/design/2026-09-24-phase-d-plan.md +0 -305
- package/docs/design/2026-09-25-teams-contract.md +0 -258
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
- package/docs/design/desktop-ux-plan.md +0 -362
- package/docs/design/launch-configurations.md +0 -168
- package/docs/design/okf-mirror-provenance.md +0 -105
- package/docs/design/operations-contract.md +0 -141
- package/docs/oats-member.schema.json +0 -38
- package/skills/integration-authoring/SKILL.md +0 -84
- package/skills/oats-support/SKILL.md +0 -79
- package/skills/skill-craft/SKILL.md +0 -109
- package/skills/soul-craft/SKILL.md +0 -116
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* OATS_CONTEXT resolution context dir (the soul's repo / agents root parent)
|
|
18
18
|
* OATS_WORKSPACE the agents root's parent — the team boundary
|
|
19
19
|
* OATS_SETTINGS JSON of the provider's `settings:` block
|
|
20
|
-
*
|
|
20
|
+
* OATS_DEFAULT_TEAM/OATS_DEFAULT_TEAM_ID/OATS_DEFAULT_TEAM_FROM default team facts
|
|
21
|
+
* OATS_TEAMS JSON rows {label, team, default, from} for mapped eligible teams
|
|
21
22
|
* OATS_META JSON persisted from this hook's previous spawn output (retire only)
|
|
22
23
|
*
|
|
23
24
|
* Output (spawn, stdout JSON):
|
|
@@ -37,13 +38,13 @@
|
|
|
37
38
|
* identity joined moments before the failure must still be deletable.
|
|
38
39
|
*/
|
|
39
40
|
import { execFileSync } from "node:child_process";
|
|
40
|
-
import { chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
41
|
+
import { appendFileSync, chmodSync, cpSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
41
42
|
import { hostname } from "node:os";
|
|
42
|
-
import { join, dirname, resolve, delimiter, isAbsolute } from "node:path";
|
|
43
|
+
import { join, dirname, resolve, delimiter, isAbsolute, relative } from "node:path";
|
|
43
44
|
import { loadCapturedAwebExecution, requireCapturedAwebAction } from "../lib/captured-execution.mjs";
|
|
44
45
|
import { assessCapturedSessionReadiness, querySelectedKernel } from "../lib/session-readiness.mjs";
|
|
45
46
|
import { runCapturedNative } from "../lib/captured-native.mjs";
|
|
46
|
-
import {
|
|
47
|
+
import { AW_MIN, grantYamlCustodySocket, parseBindingJson } from "../lib/binding-wire.mjs";
|
|
47
48
|
import { custodyPreflight } from "../lib/grant-custody.mjs";
|
|
48
49
|
import { runtimeDeliveryFor, wakeRegistration } from "../lib/wake-receive.mjs";
|
|
49
50
|
|
|
@@ -100,21 +101,18 @@ const parseSecretJson = (text, what) => {
|
|
|
100
101
|
* `command -v`, which is a SHELL BUILTIN — spawning it as a program depends on
|
|
101
102
|
* a /usr/bin/command binary that many systems do not ship, and its absence
|
|
102
103
|
* would read as "aw is missing" on every such host. */
|
|
103
|
-
/** The installed aw
|
|
104
|
-
* undefined when it cannot be read; compared as numeric triples. */
|
|
104
|
+
/** The installed aw version from `aw version`, or undefined when it cannot be read. */
|
|
105
105
|
function awVersionTriple() {
|
|
106
106
|
try { return /aw\s+v?(\d+)\.(\d+)\.(\d+)/.exec(run(["aw", "version"], undefined, 10000)); } catch { return undefined; }
|
|
107
107
|
}
|
|
108
|
-
function
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
const a = v.slice(1, 4).map(Number), b = floor.split(".").map(Number);
|
|
112
|
-
for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] > b[i]; }
|
|
108
|
+
function semverAtLeast(version, floor) {
|
|
109
|
+
const a = version.split(".").map(Number), b = floor.split(".").map(Number);
|
|
110
|
+
for (let i = 0; i < 3; i++) { if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0); }
|
|
113
111
|
return true;
|
|
114
112
|
}
|
|
115
113
|
function awVersionLabel() {
|
|
116
114
|
const v = awVersionTriple();
|
|
117
|
-
return v ? v.slice(1, 4).join(".") :
|
|
115
|
+
return v ? v.slice(1, 4).join(".") : undefined;
|
|
118
116
|
}
|
|
119
117
|
|
|
120
118
|
function onPath(cmd) {
|
|
@@ -188,7 +186,7 @@ try {
|
|
|
188
186
|
const manifest = JSON.parse(readFileSync(new URL("../oats.json", import.meta.url), "utf8"));
|
|
189
187
|
const selected = requireCapturedAwebAction(loaded, event, manifest);
|
|
190
188
|
const settings = parseBindingJson(Buffer.from(process.env.OATS_SETTINGS || "{}"));
|
|
191
|
-
if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k => !["delivery", "
|
|
189
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k => !["delivery", "root", "roots"].includes(k))) throw new Error("captured settings support delivery/root readiness only; no identity copying or ambient fallback");
|
|
192
190
|
const checked = assessCapturedSessionReadiness({ binding: selected.binding, invocation: selected.context, settings }, {
|
|
193
191
|
query(args, options) { selected.assertCurrent(); const result = querySelectedKernel(args, options); selected.assertCurrent(); return result; },
|
|
194
192
|
});
|
|
@@ -207,7 +205,7 @@ const AWEB_ALIAS_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
|
207
205
|
const AWEB_ALIAS_RULE = "invalid alias: aweb aliases must be 1-64 characters, start with a letter or digit, and then contain only letters, digits, '-' or '_'";
|
|
208
206
|
const ALIAS_REUSE_REMEDY = "spawn with a different --name (kernels 0.26.0+) or a different --purpose";
|
|
209
207
|
const CLASSIC_REFUSAL = "oats.aweb 1.14 needs OATS 0.26.0 or newer (workspace model); on an older kernel pin oats.aweb v1.13.x";
|
|
210
|
-
const hasWorkspaceV2Facts = () => !!(process.env.OATS_WORKSPACE_KEY || process.env.OATS_WORKSPACE_NAME || process.env.
|
|
208
|
+
const hasWorkspaceV2Facts = () => !!(process.env.OATS_WORKSPACE_KEY || process.env.OATS_WORKSPACE_NAME || process.env.OATS_DEFAULT_TEAM);
|
|
211
209
|
const isClassicEnvironment = () => !!process.env.OATS_TEAM_SCOPE && !hasWorkspaceV2Facts();
|
|
212
210
|
// Effective capability settings, injected by kernel dispatch (OATS_SETTINGS).
|
|
213
211
|
// delivery: "channel" (default) keeps the native channel packages waking the
|
|
@@ -216,6 +214,9 @@ const isClassicEnvironment = () => !!process.env.OATS_TEAM_SCOPE && !hasWorkspac
|
|
|
216
214
|
// flag is omitted, and nothing wakes the instance until the broker exists.
|
|
217
215
|
let settings = {};
|
|
218
216
|
try { settings = JSON.parse(process.env.OATS_SETTINGS || "{}"); } catch { settings = {}; }
|
|
217
|
+
const TEAM_SETTING_MESSAGE = "teams are not a setting since oats.aweb 1.17 / OATS 0.30: use oats teams / oats soul teams";
|
|
218
|
+
const hasStaleTeamSetting = settings && typeof settings === "object" && !Array.isArray(settings) && Object.hasOwn(settings, "team");
|
|
219
|
+
if (hasStaleTeamSetting && ["spawn", "launch", "setup", "teams", "join", "leave", "roster"].includes(event)) fatal(TEAM_SETTING_MESSAGE);
|
|
219
220
|
const deliveryMode = (() => {
|
|
220
221
|
const v = settings.delivery === undefined || settings.delivery === null || settings.delivery === "" ? "channel" : String(settings.delivery);
|
|
221
222
|
return v === "session" ? "session" : "channel";
|
|
@@ -226,21 +227,22 @@ if (isClassicEnvironment() && ["spawn", "setup"].includes(event)) {
|
|
|
226
227
|
if (event === "setup") { console.error(CLASSIC_REFUSAL); process.exit(1); }
|
|
227
228
|
fatal(CLASSIC_REFUSAL);
|
|
228
229
|
}
|
|
229
|
-
if (!["local", "global"].includes(identityMode) && ["spawn"
|
|
230
|
+
if (!["local", "global"].includes(identityMode) && ["spawn"].includes(event)) fatal(`identity.mode must be either "local" or "global" (got ${JSON.stringify(identitySettings.mode)})`);
|
|
230
231
|
if (event === "spawn" && identityMode === "local" && !identitySettings.source && (!instance || !AWEB_ALIAS_RE.test(instance))) fatal(`${AWEB_ALIAS_RULE}; OATS_INSTANCE is ${instance ? "not valid" : "missing"}, so no identity could be minted`);
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
const NO_TEAMS_MESSAGE = "no teams configured: run `oats aweb setup`";
|
|
233
|
+
const unmappedDefaultMessage = (label = defaultTeamLabel()) => `the default team ${label} has no provider id yet: its owner runs oats aweb setup, then commits the id, or choose another default with oats teams default`;
|
|
234
|
+
const defaultTeamLabel = () => typeof process.env.OATS_DEFAULT_TEAM === "string" && process.env.OATS_DEFAULT_TEAM.trim() ? process.env.OATS_DEFAULT_TEAM.trim() : undefined;
|
|
235
|
+
const defaultTeamId = () => typeof process.env.OATS_DEFAULT_TEAM_ID === "string" && process.env.OATS_DEFAULT_TEAM_ID.trim() ? process.env.OATS_DEFAULT_TEAM_ID.trim() : undefined;
|
|
236
|
+
const defaultTeamFromEnv = () => process.env.OATS_DEFAULT_TEAM_FROM === "soul" ? "soul" : "deployment";
|
|
235
237
|
const identityMeta = ({ mode = "local", alias, team, address = null, resident = null, grant }) => ({ mode, alias, team, address: address || null, resident: resident || null, ...(grant ? { grant } : {}) });
|
|
236
|
-
const teamConfigRemedy = () =>
|
|
237
|
-
function declaredRootCandidate(team =
|
|
238
|
+
const teamConfigRemedy = () => defaultTeamLabel() && !defaultTeamId() ? unmappedDefaultMessage() : NO_TEAMS_MESSAGE;
|
|
239
|
+
function declaredRootCandidate(team = defaultTeamId()) {
|
|
238
240
|
const roots = settings.roots && typeof settings.roots === "object" && !Array.isArray(settings.roots) ? settings.roots : {};
|
|
239
241
|
if (team && typeof roots[team] === "string" && roots[team].trim()) return { root: roots[team].trim(), key: `settings.oats.aweb.roots[${JSON.stringify(team)}]`, declared: true };
|
|
240
242
|
if (typeof settings.root === "string" && settings.root.trim()) return { root: settings.root.trim(), key: "settings.oats.aweb.root", declared: true };
|
|
241
243
|
return undefined;
|
|
242
244
|
}
|
|
243
|
-
function rootSettingCandidate(team =
|
|
245
|
+
function rootSettingCandidate(team = defaultTeamId()) {
|
|
244
246
|
const declared = declaredRootCandidate(team);
|
|
245
247
|
if (declared) return declared;
|
|
246
248
|
const fallback = process.env.OATS_WORKSPACE || process.cwd();
|
|
@@ -268,14 +270,134 @@ function awebRoot() { return resolveAwebRoot(); }
|
|
|
268
270
|
* name-only team config into a blocked spawn (reviewer-602627c). */
|
|
269
271
|
const teamMemberships = (listed) => listed?.memberships || listed?.teams || [];
|
|
270
272
|
const teamIdsOf = (listed) => teamMemberships(listed).map((m) => m.team_id || m.id || m);
|
|
273
|
+
function normalizeAwebTeamName(label) {
|
|
274
|
+
const normalized = String(label || "").toLowerCase().replace(/[_.]+/g, "-").replace(/^-+|-+$/g, "");
|
|
275
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(normalized) || normalized.length > 128) throw new Error(`team label ${JSON.stringify(label)} does not normalize to a valid aweb team name`);
|
|
276
|
+
return normalized;
|
|
277
|
+
}
|
|
278
|
+
const AWEB_TEAM_ID_MESSAGE = "aweb team ids must have shape <name>:<namespace> (name matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$; namespace is a hostname)";
|
|
279
|
+
function validHostname(value) { const s = String(value || ""); return s.length <= 253 && s.split(".").every((label) => /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(label)); }
|
|
280
|
+
function validAwebTeamId(value) { const m = /^([A-Za-z0-9][A-Za-z0-9._-]{0,127}):([^:]+)$/.exec(String(value || "")); return !!m && validHostname(m[2]); }
|
|
281
|
+
function invalidAwebTeamId(env = process.env) {
|
|
282
|
+
const id = defaultTeamId();
|
|
283
|
+
if (id && !validAwebTeamId(id)) return id;
|
|
284
|
+
try { const rows = JSON.parse(env.OATS_TEAMS || "[]"); if (Array.isArray(rows)) for (const r of rows) if (r && typeof r.team === "string" && r.team.trim() && !validAwebTeamId(r.team.trim())) return r.team.trim(); } catch { /* malformed OATS_TEAMS is handled as no eligible teams */ }
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const flagEq = (name, value) => `${name}=${String(value)}`;
|
|
288
|
+
function assertNotFlag(value, what) {
|
|
289
|
+
if (String(value || "").startsWith("-")) throw new Error(`${what} must not start with '-'`);
|
|
290
|
+
}
|
|
291
|
+
const yamlQuote = (value) => JSON.stringify(String(value));
|
|
292
|
+
function findOatsLocal(start = process.env.OATS_WORKSPACE || process.cwd()) {
|
|
293
|
+
let dir = resolve(start);
|
|
294
|
+
for (;;) {
|
|
295
|
+
const file = join(dir, "oats-local.yaml");
|
|
296
|
+
if (existsSync(file)) return file;
|
|
297
|
+
const parent = dirname(dir);
|
|
298
|
+
if (parent === dir) return join(resolve(start), "oats-local.yaml");
|
|
299
|
+
dir = parent;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function blockEnd(lines, start, indent) {
|
|
303
|
+
let end = lines.length;
|
|
304
|
+
for (let i = start + 1; i < lines.length; i++) if (lines[i].trim() && !lines[i].startsWith(" ".repeat(indent + 1))) { end = i; break; }
|
|
305
|
+
return end;
|
|
306
|
+
}
|
|
307
|
+
function ensureYamlBlock(lines, parentStart, parentEnd, indent, header) {
|
|
308
|
+
const row = `${" ".repeat(indent)}${header}:`;
|
|
309
|
+
for (let i = parentStart + 1; i < parentEnd; i++) if (lines[i].trimEnd() === row) return i;
|
|
310
|
+
lines.splice(parentEnd, 0, row);
|
|
311
|
+
return parentEnd;
|
|
312
|
+
}
|
|
313
|
+
function atomicWrite(file, text) {
|
|
314
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
315
|
+
const tmp = join(dirname(file), `.${basenameForTemp(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
316
|
+
writeFileSync(tmp, text, { mode: 0o600 });
|
|
317
|
+
renameSync(tmp, file);
|
|
318
|
+
}
|
|
319
|
+
function basenameForTemp(file) { return file.split(/[\\/]/).pop() || "oats-local.yaml"; }
|
|
320
|
+
function unsupportedLocalYaml(file, detail, team, rootDir) {
|
|
321
|
+
throw new Error(`${file}: cannot safely update settings.oats.aweb.roots automatically (${detail}); add this line by hand under block-style settings.oats.aweb.roots: ${yamlQuote(team)}: ${yamlQuote(rootDir)}`);
|
|
322
|
+
}
|
|
323
|
+
function findBlockHeader(lines, start, end, indent, names) {
|
|
324
|
+
const pad = " ".repeat(indent);
|
|
325
|
+
for (let i = start; i < end; i++) {
|
|
326
|
+
const line = lines[i];
|
|
327
|
+
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
|
328
|
+
if (!line.startsWith(pad) || line.startsWith(pad + " ")) continue;
|
|
329
|
+
const trimmed = line.slice(indent).trimEnd();
|
|
330
|
+
for (const name of names) if (trimmed === `${name}:`) return i;
|
|
331
|
+
for (const name of names) if (trimmed.startsWith(`${name}:`)) return { unsupported: i, line };
|
|
332
|
+
}
|
|
333
|
+
return -1;
|
|
334
|
+
}
|
|
335
|
+
function assertAwebRootSettingRecordable(team, rootDir, { start = process.env.OATS_WORKSPACE || process.cwd() } = {}) {
|
|
336
|
+
const file = findOatsLocal(start);
|
|
337
|
+
if (!existsSync(file)) return;
|
|
338
|
+
const lines = readFileSync(file, "utf8").split(/\r?\n/);
|
|
339
|
+
let settings = findBlockHeader(lines, 0, lines.length, 0, ["settings"]);
|
|
340
|
+
if (typeof settings === "object") unsupportedLocalYaml(file, `line ${settings.unsupported + 1} is not a block-style settings: mapping`, team, rootDir);
|
|
341
|
+
if (settings < 0) return;
|
|
342
|
+
const settingsEnd = blockEnd(lines, settings, 0);
|
|
343
|
+
let aweb = findBlockHeader(lines, settings + 1, settingsEnd, 2, ["oats.aweb", '"oats.aweb"', "'oats.aweb'"]);
|
|
344
|
+
if (typeof aweb === "object") unsupportedLocalYaml(file, `line ${aweb.unsupported + 1} is not a block-style oats.aweb: mapping`, team, rootDir);
|
|
345
|
+
if (aweb < 0) return;
|
|
346
|
+
const awebEnd = blockEnd(lines, aweb, 2);
|
|
347
|
+
const roots = findBlockHeader(lines, aweb + 1, awebEnd, 4, ["roots"]);
|
|
348
|
+
if (typeof roots === "object") unsupportedLocalYaml(file, `line ${roots.unsupported + 1} is not a block-style roots: mapping`, team, rootDir);
|
|
349
|
+
}
|
|
350
|
+
function recordAwebRootSetting(team, rootDir, { start = process.env.OATS_WORKSPACE || process.cwd() } = {}) {
|
|
351
|
+
const file = findOatsLocal(start);
|
|
352
|
+
const existed = existsSync(file);
|
|
353
|
+
const lines = existed ? readFileSync(file, "utf8").split(/\r?\n/) : ["schemaVersion: 2", "workspace: local"];
|
|
354
|
+
while (lines.length && lines.at(-1) === "") lines.pop();
|
|
355
|
+
let settings = findBlockHeader(lines, 0, lines.length, 0, ["settings"]);
|
|
356
|
+
if (typeof settings === "object") unsupportedLocalYaml(file, `line ${settings.unsupported + 1} is not a block-style settings: mapping`, team, rootDir);
|
|
357
|
+
if (settings < 0) { lines.push("settings:"); settings = lines.length - 1; }
|
|
358
|
+
let settingsEnd = blockEnd(lines, settings, 0);
|
|
359
|
+
let aweb = findBlockHeader(lines, settings + 1, settingsEnd, 2, ["oats.aweb", '"oats.aweb"', "'oats.aweb'"]);
|
|
360
|
+
if (typeof aweb === "object") unsupportedLocalYaml(file, `line ${aweb.unsupported + 1} is not a block-style oats.aweb: mapping`, team, rootDir);
|
|
361
|
+
if (aweb < 0) { aweb = ensureYamlBlock(lines, settings, settingsEnd, 2, "oats.aweb"); settingsEnd++; }
|
|
362
|
+
let awebEnd = blockEnd(lines, aweb, 2);
|
|
363
|
+
let roots = findBlockHeader(lines, aweb + 1, awebEnd, 4, ["roots"]);
|
|
364
|
+
if (typeof roots === "object") unsupportedLocalYaml(file, `line ${roots.unsupported + 1} is not a block-style roots: mapping`, team, rootDir);
|
|
365
|
+
if (roots < 0) { roots = ensureYamlBlock(lines, aweb, awebEnd, 4, "roots"); awebEnd++; }
|
|
366
|
+
const rootsEnd = blockEnd(lines, roots, 4);
|
|
367
|
+
const key = yamlQuote(team), value = yamlQuote(rootDir), row = ` ${key}: ${value}`;
|
|
368
|
+
const existing = lines.findIndex((l, i) => i > roots && i < rootsEnd && l.trimStart().startsWith(`${key}:`));
|
|
369
|
+
if (existing >= 0) lines[existing] = row; else lines.splice(rootsEnd, 0, row);
|
|
370
|
+
atomicWrite(file, `${lines.join("\n")}\n`);
|
|
371
|
+
}
|
|
372
|
+
function perTeamRoot(base, label) { return join(resolve(base), ".aweb-roots", normalizeAwebTeamName(label)); }
|
|
373
|
+
|
|
374
|
+
function awFloorProblem() {
|
|
375
|
+
if (!onPath("aw")) return `aw CLI not on PATH; install aw >= ${AW_MIN}`;
|
|
376
|
+
const version = awVersionLabel();
|
|
377
|
+
if (!version) return `aw version could not be read; install aw >= ${AW_MIN}`;
|
|
378
|
+
if (semverAtLeast(version, AW_MIN)) return undefined;
|
|
379
|
+
return `aw ${version} is older than required ${AW_MIN}; install aw >= ${AW_MIN}`;
|
|
380
|
+
}
|
|
271
381
|
|
|
272
382
|
const AW_INSTALL = "install the aw CLI first — see https://aweb.ai/docs (or `oats aweb setup` for guided onboarding)";
|
|
273
383
|
const isCommand = ["roster", "setup", "teams", "join", "leave"].includes(event);
|
|
384
|
+
const invalidConfiguredTeamId = invalidAwebTeamId();
|
|
385
|
+
if (invalidConfiguredTeamId && (isCommand || ["spawn", "launch"].includes(event))) {
|
|
386
|
+
if (isCommand) { console.error(`oats aweb ${event}: ${AWEB_TEAM_ID_MESSAGE}`); process.exit(1); }
|
|
387
|
+
fatal(AWEB_TEAM_ID_MESSAGE);
|
|
388
|
+
}
|
|
274
389
|
if (!onPath("aw")) {
|
|
275
390
|
if (isCommand) { console.error(`oats aweb ${event}: aw CLI not on PATH — ${AW_INSTALL}`); process.exit(1); }
|
|
276
391
|
if (event === "spawn") fatal(`aw CLI not on PATH, so no identity could be minted and this instance would have no messaging — ${AW_INSTALL}`);
|
|
277
392
|
warn(`aw CLI not on PATH — no identity minted; ${AW_INSTALL}`);
|
|
278
393
|
}
|
|
394
|
+
if (isCommand || event === "spawn") {
|
|
395
|
+
const floorProblem = awFloorProblem();
|
|
396
|
+
if (floorProblem) {
|
|
397
|
+
if (isCommand) { console.error(`oats aweb ${event}: ${floorProblem}`); process.exit(1); }
|
|
398
|
+
fatal(`${floorProblem}, so no identity could be minted and this instance would not meet the messaging contract`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
279
401
|
|
|
280
402
|
// ---------------------------------------------------------------------------
|
|
281
403
|
// Retained identity (explicit per-soul opt-in): a standing seat keeps its
|
|
@@ -307,10 +429,6 @@ function wakeRegister(instanceHome, identityHome) {
|
|
|
307
429
|
function wakeDeregister(instanceHome) {
|
|
308
430
|
try { run(["aw", "wake", "deregister", "--home", instanceHome], instanceHome, 60000); return true; } catch { return false; }
|
|
309
431
|
}
|
|
310
|
-
// The first aw release whose grant subtree accepts `aw id grant mint --team
|
|
311
|
-
// <team-id>` (and the full normal/reviewer scope lists): aw 1.36.2, paired with
|
|
312
|
-
// aweb server 1.27.5 for complete custody operations. aw 1.36.1 has neither.
|
|
313
|
-
const GRANT_TEAM_FLAG_MIN = "1.36.2";
|
|
314
432
|
const NORMAL_GRANT_SCOPES = ["mail.read", "mail.send", "chat.read", "chat.send", "events.read", "coord.read", "coord.write", "presence.write", "contacts.read", "contacts.write"];
|
|
315
433
|
const REVIEWER_GRANT_SCOPES = ["mail.read", "chat.read", "events.read", "coord.read", "presence.write"];
|
|
316
434
|
const residentKeyHint = (name) => `oats-local.yaml settings.oats.aweb.residents.${name || "<name>"}`;
|
|
@@ -428,7 +546,7 @@ function retainedLaunchOutput(meta = {}, identityHome = priorGrantHome(meta)) {
|
|
|
428
546
|
return { ...(Object.keys(env).length ? { env } : {}), ...(launch ? { launch } : {}) };
|
|
429
547
|
}
|
|
430
548
|
function grantMintArgv({ team, scopes, ttl, grantHome, custodySocket }) {
|
|
431
|
-
return ["aw", "id", "grant", "mint",
|
|
549
|
+
return ["aw", "id", "grant", "mint", flagEq("--team", team), flagEq("--scope", scopes.join(",")), flagEq("--ttl", ttl), flagEq("--label", `oats:${instance}`), flagEq("--out", grantHome), flagEq("--custody-socket", custodySocket), "--json"];
|
|
432
550
|
}
|
|
433
551
|
function validateMintedGrant(minted, grantHome) {
|
|
434
552
|
const grantId = typeof minted.grant_id === "string" ? minted.grant_id : undefined;
|
|
@@ -448,8 +566,8 @@ function globalGrantRenew() {
|
|
|
448
566
|
if (oldMeta.identity?.mode !== "global" || !oldMeta.identity?.grant?.id) out(retainedLaunchOutput(oldMeta));
|
|
449
567
|
const resident = String(identitySettings.resident || oldMeta.identity.resident || "");
|
|
450
568
|
const custody = resolveResidentCustody(resident);
|
|
451
|
-
const team =
|
|
452
|
-
if (!team) fatal(
|
|
569
|
+
const team = defaultTeamId() || oldMeta.identity.team;
|
|
570
|
+
if (!team) fatal(teamConfigRemedy());
|
|
453
571
|
const scopes = grantScopes();
|
|
454
572
|
const ttl = identitySettings.ttl === undefined || identitySettings.ttl === null || identitySettings.ttl === "" ? "8h" : String(identitySettings.ttl);
|
|
455
573
|
const oldHome = priorGrantHome(oldMeta);
|
|
@@ -458,7 +576,6 @@ function globalGrantRenew() {
|
|
|
458
576
|
catch (e) { out({ meta: oldMeta, ...retainedLaunchOutput(oldMeta, oldHome), warning: `oats-aweb: renewal custody preflight failed (${e.message || e}); keeping previous grant ${oldMeta.identity.grant.id}` }); }
|
|
459
577
|
const custodySocket = preflightCustodySocket(preflight);
|
|
460
578
|
if (!custodySocket) out({ meta: oldMeta, ...retainedLaunchOutput(oldMeta, oldHome), warning: `oats-aweb: renewal custody preflight reported no socket_path, so the grant cannot be attached to custody; keeping previous grant ${oldMeta.identity.grant.id}` });
|
|
461
|
-
if (!awAtLeast(CUSTODY_ATTACH_MIN)) out({ meta: oldMeta, ...retainedLaunchOutput(oldMeta, oldHome), warning: `oats-aweb: aw ${awVersionLabel()} cannot attach a grant to custody (--custody-socket); grants need aw >= ${CUSTODY_ATTACH_MIN}; keeping previous grant ${oldMeta.identity.grant.id}` });
|
|
462
579
|
let stamp = Math.floor(Date.now() / 1000);
|
|
463
580
|
let grantHome = join(home, `.aweb-identity-${stamp}`);
|
|
464
581
|
while (existsSync(grantHome)) grantHome = join(home, `.aweb-identity-${++stamp}`);
|
|
@@ -490,22 +607,17 @@ function globalGrantRenew() {
|
|
|
490
607
|
out({ meta: newMeta, ...retainedLaunchOutput(newMeta, grantHome), ...(warning ? { warning } : {}) });
|
|
491
608
|
}
|
|
492
609
|
function globalGrantSpawn() {
|
|
493
|
-
const resolvedTeam = payloadTeam();
|
|
494
610
|
const resident = String(identitySettings.resident || "");
|
|
495
611
|
const custody = resolveResidentCustody(resident);
|
|
496
|
-
let team =
|
|
612
|
+
let team = defaultTeamId();
|
|
497
613
|
const teamWarnings = [];
|
|
498
|
-
|
|
499
|
-
if (!team) team = activeTeamAt(custody);
|
|
500
|
-
if (unmappedPrimary && team) teamWarnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using the default team ${team}`);
|
|
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");
|
|
614
|
+
if (!team) fatal(teamConfigRemedy());
|
|
502
615
|
const grantHome = join(home, ".aweb-identity");
|
|
503
616
|
if (existsSync(grantHome)) fatal(`${grantHome} already exists; refusing to overwrite an existing aweb session grant home`);
|
|
504
617
|
const scopes = grantScopes();
|
|
505
618
|
const ttl = identitySettings.ttl === undefined || identitySettings.ttl === null || identitySettings.ttl === "" ? "8h" : String(identitySettings.ttl);
|
|
506
619
|
const preflight = custodyPreflight({ custody, resident, team, e2eeRequired: grantE2eeRequired(), runAw: (argv, cwd, options) => run(argv, cwd, 60000, options), fatal });
|
|
507
620
|
const custodySocket = requirePreflightCustodySocket(preflight);
|
|
508
|
-
if (!awAtLeast(CUSTODY_ATTACH_MIN)) fatal(`aw ${awVersionLabel()} cannot attach a grant to custody (--custody-socket); grants need aw >= ${CUSTODY_ATTACH_MIN}`);
|
|
509
621
|
let meta;
|
|
510
622
|
const cleanup = () => { try { rmSync(grantHome, { recursive: true, force: true }); } catch { /* best effort */ } };
|
|
511
623
|
const failAfterMint = (message, code = 1) => { cleanup(); out({ ...(meta ? { meta } : {}), warning: `oats-aweb: ${String(message).slice(0, 300)}` }, code); };
|
|
@@ -525,7 +637,7 @@ function globalGrantSpawn() {
|
|
|
525
637
|
const { grantId, expiresAt, mintedTeam, alias: mintedAlias, address } = validateMintedGrant(minted, grantHome);
|
|
526
638
|
const recovered = recoverGrantHome(grantHome);
|
|
527
639
|
const alias = recovered.subjectAlias || mintedAlias || resident;
|
|
528
|
-
meta = { delivery: deliveryMode, identity: identityMeta({ mode: "global", alias, team: mintedTeam, address, resident, grant: { id: grantId, expiresAt, scopes, home: grantHome } }) };
|
|
640
|
+
meta = { delivery: deliveryMode, defaultTeam: { label: defaultTeamLabel(), team: mintedTeam, from: defaultTeamFromEnv() }, identity: identityMeta({ mode: "global", alias, team: mintedTeam, address, resident, grant: { id: grantId, expiresAt, scopes, home: grantHome } }) };
|
|
529
641
|
if (mintedTeam !== team) {
|
|
530
642
|
try { revokeGrant(custody, grantId); failAfterMint(`minted grant team ${mintedTeam} differs from ${team}; the grant was revoked and nothing was kept`); }
|
|
531
643
|
catch (e) { failAfterMint(`minted grant team ${mintedTeam} differs from ${team}; revoke failed: ${e.message || e}`); }
|
|
@@ -590,22 +702,6 @@ const workspaceAliasOf = (homeDir) => {
|
|
|
590
702
|
* signing key, a team certificate and a workspace binding. */
|
|
591
703
|
const joinedLate = (homeDir) => existsSync(join(homeDir, ".aw", "signing.key")) && existsSync(join(homeDir, ".aw", "team-certs")) && !!workspaceAliasOf(homeDir);
|
|
592
704
|
const JOIN_TIMEOUT_MS = Number(process.env.OATS_AWEB_JOIN_TIMEOUT_MS) > 0 ? Number(process.env.OATS_AWEB_JOIN_TIMEOUT_MS) : 120000;
|
|
593
|
-
// The first aw whose joined-team external homes are fully operable: local
|
|
594
|
-
// accept-invite under --identity-home, hosted workspace auto-connect, self-release
|
|
595
|
-
// and joined-root E2E key publication.
|
|
596
|
-
const JOINED_TEAMS_AW_MIN = "1.36.12";
|
|
597
|
-
function joinedTeamsAwFloorProblem() {
|
|
598
|
-
if (/^\d+\.\d+\.\d+$/.test(JOINED_TEAMS_AW_MIN) && awAtLeast(JOINED_TEAMS_AW_MIN)) return undefined;
|
|
599
|
-
if (!/^\d+\.\d+\.\d+$/.test(JOINED_TEAMS_AW_MIN)) return `E_TEAM_AW_FLOOR: joined-team identities need an aw release that admits local accept-invite under --identity-home, auto-connects the joined workspace and publishes the joined-root E2E key; installed aw is ${awVersionLabel()}. join= and oats aweb join are gated until that aw release exists`;
|
|
600
|
-
return `E_TEAM_AW_FLOOR: joined-team identities require aw >= ${JOINED_TEAMS_AW_MIN}; installed aw is ${awVersionLabel()}. join= and oats aweb join are gated until aw admits local accept-invite under --identity-home, auto-connects the joined workspace and publishes the joined-root E2E key`;
|
|
601
|
-
}
|
|
602
|
-
function requireJoinedTeamsAwFloor() {
|
|
603
|
-
const problem = joinedTeamsAwFloorProblem();
|
|
604
|
-
if (!problem) return;
|
|
605
|
-
const error = new Error(problem.replace(/^E_TEAM_AW_FLOOR: /, ""));
|
|
606
|
-
error.code = "E_TEAM_AW_FLOOR";
|
|
607
|
-
throw error;
|
|
608
|
-
}
|
|
609
705
|
const yamlScalar = (text, key) => {
|
|
610
706
|
const m = String(text).match(new RegExp(`^${key}:\\s*["']?([^"'\\n#]+)["']?\\s*$`, "m"));
|
|
611
707
|
return m ? m[1].trim() : undefined;
|
|
@@ -638,7 +734,7 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
638
734
|
const service = process.env.OATS_AWEB_URL || yamlScalar(srcWorkspace, "aweb_url");
|
|
639
735
|
if (!service) fatal(`cannot determine the aweb service for ${source} (no aweb_url in its workspace.yaml)`);
|
|
640
736
|
const role = yamlScalar(srcWorkspace, "role_name");
|
|
641
|
-
let team =
|
|
737
|
+
let team = defaultTeamId();
|
|
642
738
|
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");
|
|
643
739
|
if (!team || !team.includes(":")) fatal(`cannot determine the team for the retained identity (${teamConfigRemedy()}, or keep an active team in ${join(source, "teams.yaml")})`);
|
|
644
740
|
const dest = join(home, ".aw");
|
|
@@ -662,7 +758,7 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
662
758
|
// copy; if the restore fails, KEEP the copy so the seat stays recoverable.
|
|
663
759
|
let restored = !connected;
|
|
664
760
|
if (connected) {
|
|
665
|
-
try { run(["aw", "workspace", "connect", "--service", service, "--team", team, ...(role ? ["--role", role] : [])], legacyHome, 60000); restored = true; }
|
|
761
|
+
try { run(["aw", "workspace", "connect", flagEq("--service", service), flagEq("--team", team), ...(role ? [flagEq("--role", role)] : [])], legacyHome, 60000); restored = true; }
|
|
666
762
|
catch { restored = false; }
|
|
667
763
|
}
|
|
668
764
|
if (restored) { try { rmSync(dest, { recursive: true, force: true }); } catch { /* best effort */ } try { rmSync(lockPath, { force: true }); } catch { /* best effort */ } }
|
|
@@ -683,7 +779,7 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
683
779
|
for (const forbidden of ["workspace.yaml", "context", "interaction-log.jsonl", "channel-delivered-ids.json", "chat-delivered-ids.json"]) {
|
|
684
780
|
if (existsSync(join(dest, forbidden))) rmSync(join(dest, forbidden), { recursive: true, force: true });
|
|
685
781
|
}
|
|
686
|
-
run(["aw", "workspace", "connect", "--service", service, "--team", team, ...(role ? ["--role", role] : [])], home, 60000);
|
|
782
|
+
run(["aw", "workspace", "connect", flagEq("--service", service), flagEq("--team", team), ...(role ? [flagEq("--role", role)] : [])], home, 60000);
|
|
687
783
|
connected = true;
|
|
688
784
|
run(["aw", "check", "--online"], home, 60000);
|
|
689
785
|
run(["aw", "heartbeat"], home, 60000);
|
|
@@ -742,23 +838,24 @@ const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
|
742
838
|
function parseOatsTeams(env = process.env) {
|
|
743
839
|
try {
|
|
744
840
|
const rows = JSON.parse(env.OATS_TEAMS || "[]");
|
|
745
|
-
return Array.isArray(rows) ? rows.filter((r) => r && typeof r === "object").map((r) => ({
|
|
841
|
+
return Array.isArray(rows) ? rows.filter((r) => r && typeof r === "object").map((r) => ({
|
|
842
|
+
label: String(r.label || ""),
|
|
843
|
+
team: typeof r.team === "string" ? r.team : null,
|
|
844
|
+
default: r.default === true,
|
|
845
|
+
from: r.from === "shared" ? "shared" : "local",
|
|
846
|
+
})) : [];
|
|
746
847
|
} catch { return []; }
|
|
747
848
|
}
|
|
748
|
-
function eligibleTeams() { return parseOatsTeams().filter((t) => t.
|
|
849
|
+
function eligibleTeams() { return parseOatsTeams().filter((t) => !t.default && t.label && t.team); }
|
|
749
850
|
function labelsCsv(text) { return String(text || "").split(",").map((s) => s.trim()).filter(Boolean); }
|
|
750
|
-
function primaryTeamLabel() { return
|
|
851
|
+
function primaryTeamLabel() { return defaultTeamLabel() || null; }
|
|
751
852
|
function requestedJoinLabels() { return labelsCsv(settings.join); }
|
|
752
|
-
function unmappedPrimaryRow() {
|
|
753
|
-
const primary = primaryTeamLabel();
|
|
754
|
-
return primary ? parseOatsTeams().find((t) => t.label === primary && !t.mapped) : undefined;
|
|
755
|
-
}
|
|
756
853
|
function validateJoinLabels(labels, { action = "join" } = {}) {
|
|
757
854
|
const eligible = eligibleTeams();
|
|
758
855
|
const byLabel = new Map(eligible.map((t) => [t.label, t]));
|
|
759
856
|
for (const label of labels) {
|
|
760
|
-
if (action === "leave" && label ===
|
|
761
|
-
const error = new Error(`E_TEAM_DEFAULT: ${label} is the
|
|
857
|
+
if (action === "leave" && label === defaultTeamLabel()) {
|
|
858
|
+
const error = new Error(`E_TEAM_DEFAULT: ${label} is the default team and cannot be left`);
|
|
762
859
|
error.code = "E_TEAM_DEFAULT";
|
|
763
860
|
throw error;
|
|
764
861
|
}
|
|
@@ -772,99 +869,133 @@ function validateJoinLabels(labels, { action = "join" } = {}) {
|
|
|
772
869
|
return labels.map((label) => byLabel.get(label));
|
|
773
870
|
}
|
|
774
871
|
const joinedTeamsOf = (meta = {}) => Array.isArray(meta.joinedTeams) ? meta.joinedTeams.filter((j) => j && typeof j === "object" && j.label && j.team && j.identityHome) : [];
|
|
872
|
+
const leftTeamsOf = (meta = {}) => Array.isArray(meta.left) ? meta.left.filter((j) => j && typeof j === "object" && j.label && j.team && j.at && j.reason).slice(-20) : [];
|
|
775
873
|
const providerStateDir = () => join(home, ".oats-aweb");
|
|
776
874
|
const providerTeamsFile = () => join(providerStateDir(), "teams.json");
|
|
777
875
|
function readProviderTeamsState(meta = {}) {
|
|
778
876
|
try {
|
|
779
877
|
const doc = JSON.parse(readFileSync(providerTeamsFile(), "utf8"));
|
|
780
|
-
return { joinedTeams: joinedTeamsOf(doc), wakeJoined: doc.wakeJoined === true };
|
|
781
|
-
} catch { return { joinedTeams: joinedTeamsOf(meta), wakeJoined: meta.wakeJoined === true }; }
|
|
878
|
+
return { joinedTeams: joinedTeamsOf(doc), wakeJoined: doc.wakeJoined === true, left: leftTeamsOf(doc) };
|
|
879
|
+
} catch { return { joinedTeams: joinedTeamsOf(meta), wakeJoined: meta.wakeJoined === true, left: leftTeamsOf(meta) }; }
|
|
782
880
|
}
|
|
783
881
|
function writeProviderTeamsState(meta) {
|
|
784
882
|
mkdirSync(providerStateDir(), { recursive: true, mode: 0o700 });
|
|
785
|
-
writeFileSync(providerTeamsFile(), JSON.stringify({ joinedTeams: joinedTeamsOf(meta), wakeJoined: meta.wakeJoined === true }, null, 2) + "\n", { mode: 0o600 });
|
|
883
|
+
writeFileSync(providerTeamsFile(), JSON.stringify({ joinedTeams: joinedTeamsOf(meta), wakeJoined: meta.wakeJoined === true, left: leftTeamsOf(meta) }, null, 2) + "\n", { mode: 0o600 });
|
|
786
884
|
}
|
|
787
|
-
function withProviderTeams(meta = {}) { const state = readProviderTeamsState(meta); return { ...meta, joinedTeams: state.joinedTeams, wakeJoined: state.wakeJoined }; }
|
|
885
|
+
function withProviderTeams(meta = {}) { const state = readProviderTeamsState(meta); return { ...meta, joinedTeams: state.joinedTeams, wakeJoined: state.wakeJoined, left: state.left }; }
|
|
788
886
|
function identityHomeForLabel(label) { return join(home, `.aweb-identity-${label}`); }
|
|
789
887
|
function awWithIdentity(identityHome, args) { return ["aw", "--identity-home", identityHome, ...args]; }
|
|
790
888
|
function commandOutput(e) { return [e?.stdout, e?.stderr, e?.message].filter(Boolean).join("\n"); }
|
|
791
|
-
function workspaceConnectRecovery(text) {
|
|
792
|
-
const m = String(text || "").match(/aw\s+--identity-home\s+\S+\s+workspace\s+connect\s+--service\s+\S+(?:\s+--json)?|aw\s+workspace\s+connect\s+--service\s+\S+(?:\s+--json)?|workspace\s+connect\s+--service\s+\S+(?:\s+--json)?/i);
|
|
793
|
-
return m ? m[0].trim() : undefined;
|
|
794
|
-
}
|
|
795
889
|
function joinedWorkspacePresent(identityHome) { return existsSync(join(identityHome, "workspace.yaml")); }
|
|
890
|
+
function serviceForJoinedTeam(root, label, accepted, ...docs) {
|
|
891
|
+
const fromDocs = [...docs, accepted].map((d) => d && (d.service || d.service_url || d.aweb_url || d.workspace?.service || d.workspace?.service_url || d.workspace?.aweb_url)).find(Boolean);
|
|
892
|
+
if (fromDocs) return fromDocs;
|
|
893
|
+
const rootWorkspace = root ? join(resolve(root), ".aw", "workspace.yaml") : undefined;
|
|
894
|
+
const fromRoot = rootWorkspace && existsSync(rootWorkspace) ? yamlScalar(readFileSync(rootWorkspace, "utf8"), "aweb_url") : undefined;
|
|
895
|
+
if (fromRoot) return fromRoot;
|
|
896
|
+
throw new Error(`cannot determine the aweb service for joined team ${label} from ${root || "(unknown root)"}; pass --service <url> (ask the team owner; hosted aweb is https://app.aweb.ai/api)`);
|
|
897
|
+
}
|
|
796
898
|
function releaseReceiptStatus(doc) {
|
|
797
899
|
return doc && typeof doc === "object" && doc.alias_released === true ? "released" : undefined;
|
|
798
900
|
}
|
|
901
|
+
function cleanupJoinedIdentity(entry, fallbackAlias, cwd = home) {
|
|
902
|
+
let receipt, released;
|
|
903
|
+
const alias = entry.alias || fallbackAlias;
|
|
904
|
+
if (!alias) throw new Error(`failed to leave team ${entry.label}; no alias is known for ${entry.identityHome}`);
|
|
905
|
+
try {
|
|
906
|
+
receipt = parseAwJson(run(awWithIdentity(entry.identityHome, ["workspace", "delete", alias, "--json"]), cwd, 60000), "aw workspace delete");
|
|
907
|
+
released = releaseReceiptStatus(receipt);
|
|
908
|
+
} catch (e) {
|
|
909
|
+
throw new Error(`failed to leave team ${entry.label}; kept ${entry.identityHome} so cleanup can be retried: ${String(commandOutput(e)).slice(0, 300)}`);
|
|
910
|
+
}
|
|
911
|
+
if (!released) throw new Error(`failed to leave team ${entry.label}; workspace delete did not report alias_released: true; kept ${entry.identityHome} so cleanup can be retried: ${JSON.stringify(receipt)}`);
|
|
912
|
+
try { rmSync(entry.identityHome, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
913
|
+
return { released, receipt };
|
|
914
|
+
}
|
|
915
|
+
function acceptedTeamMembership(identityHome, expectedTeam) {
|
|
916
|
+
const listed = parseAwJson(run(awWithIdentity(identityHome, ["id", "team", "list", "--json"]), dirname(identityHome), 60000), "aw id team list");
|
|
917
|
+
const ids = teamIdsOf(listed).map(String);
|
|
918
|
+
if (expectedTeam) return ids.includes(expectedTeam) ? expectedTeam : undefined;
|
|
919
|
+
return ids.length === 1 ? ids[0] : undefined;
|
|
920
|
+
}
|
|
921
|
+
function connectExistingJoinedTeam({ label, identityHome, expectedTeam, root, cwd = home, serviceDocs = [], resumeCommand }) {
|
|
922
|
+
const team = acceptedTeamMembership(identityHome, expectedTeam);
|
|
923
|
+
if (!team) throw new Error(`team root ${dirname(identityHome)} already holds an aweb identity, but aw id team list does not show ${expectedTeam || "exactly one team membership"}; choose a different label or remove the stale root deliberately`);
|
|
924
|
+
try {
|
|
925
|
+
if (!joinedWorkspacePresent(identityHome)) {
|
|
926
|
+
const service = serviceForJoinedTeam(root, label, {}, ...serviceDocs);
|
|
927
|
+
run(awWithIdentity(identityHome, ["workspace", "connect", flagEq("--service", service), flagEq("--team", team), "--json"]), cwd, 60000, { secretSafe: true });
|
|
928
|
+
}
|
|
929
|
+
if (!joinedWorkspacePresent(identityHome)) throw new Error(`workspace connect did not write ${join(identityHome, "workspace.yaml")}`);
|
|
930
|
+
} catch (e) {
|
|
931
|
+
throw new Error(`joined team ${label} resume failed: ${e.message || e}; kept ${identityHome} for retry${resumeCommand ? `; resume with: ${resumeCommand}` : ""}`);
|
|
932
|
+
}
|
|
933
|
+
return { label, team, identityHome, receive: "poll", since: new Date().toISOString() };
|
|
934
|
+
}
|
|
935
|
+
function acceptConnectVerifyJoinedTeam({ label, token, identityHome, alias: requestedAlias, expectedTeam, root, cwd = home, serviceDocs = [], cleanupOnFailure = true, resumeCommand, useAcceptedService = true }) {
|
|
936
|
+
if (!token || typeof token !== "string") throw new Error(`aw id team accept-invite received no invite token for ${label}`);
|
|
937
|
+
const raw = parseSecretJson(run(awWithIdentity(identityHome, ["id", "team", "accept-invite", token, flagEq("--name", requestedAlias), "--local", "--json"]), cwd, JOIN_TIMEOUT_MS, { secrets: [token], secretSafe: true }), "aw id team accept-invite");
|
|
938
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`aw id team accept-invite returned no usable result for joined team ${label}`);
|
|
939
|
+
const alias = typeof raw.alias === "string" && AWEB_ALIAS_RE.test(raw.alias) ? raw.alias : requestedAlias;
|
|
940
|
+
const team = typeof raw.team_id === "string" && raw.team_id ? raw.team_id : expectedTeam;
|
|
941
|
+
if (!team) throw new Error(`aw id team accept-invite returned no team_id for joined team ${label}`);
|
|
942
|
+
if (expectedTeam && team !== expectedTeam) throw new Error(`joined team ${team} differs from requested ${expectedTeam}`);
|
|
943
|
+
const joined = { label, team, identityHome, receive: "poll", since: new Date().toISOString(), alias };
|
|
944
|
+
try {
|
|
945
|
+
if (!joinedWorkspacePresent(identityHome)) {
|
|
946
|
+
const service = serviceForJoinedTeam(root, label, useAcceptedService ? raw : {}, ...serviceDocs);
|
|
947
|
+
run(awWithIdentity(identityHome, ["workspace", "connect", flagEq("--service", service), flagEq("--team", team), "--json"]), cwd, 60000, { secretSafe: true });
|
|
948
|
+
}
|
|
949
|
+
if (!joinedWorkspacePresent(identityHome)) throw new Error(`workspace connect did not write ${join(identityHome, "workspace.yaml")}`);
|
|
950
|
+
} catch (e) {
|
|
951
|
+
if (!cleanupOnFailure) throw new Error(`joined team ${label} was accepted but workspace connect/verification failed: ${e.message || e}; kept ${identityHome} for retry${resumeCommand ? `; resume with: ${resumeCommand}` : ""}`);
|
|
952
|
+
let cleanupNote;
|
|
953
|
+
try { cleanupJoinedIdentity(joined, requestedAlias, cwd); cleanupNote = "; the joined identity was deleted"; }
|
|
954
|
+
catch (cleanupError) { cleanupNote = `; cleanup failed: ${cleanupError.message || cleanupError}`; }
|
|
955
|
+
throw new Error(`joined team ${label} was accepted but workspace connect/verification failed: ${e.message || e}${cleanupNote}`);
|
|
956
|
+
}
|
|
957
|
+
return { raw, joined };
|
|
958
|
+
}
|
|
799
959
|
function readCapabilityMeta() {
|
|
800
960
|
if (process.env.OATS_META) { try { return withProviderTeams(JSON.parse(process.env.OATS_META || "{}")); } catch { return withProviderTeams({}); } }
|
|
801
961
|
try { return withProviderTeams(JSON.parse(readFileSync(join(home, "instance.json"), "utf8")).capabilityMeta?.["oats.aweb"] || {}); } catch { return withProviderTeams({}); }
|
|
802
962
|
}
|
|
803
|
-
function
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
963
|
+
function defaultTeamObject(meta = {}) {
|
|
964
|
+
const label = meta.defaultTeam?.label || defaultTeamLabel() || null;
|
|
965
|
+
const team = meta.defaultTeam?.team || meta.team || meta.identity?.team || defaultTeamId() || null;
|
|
966
|
+
if (!label && !team) return null;
|
|
967
|
+
const from = meta.defaultTeam?.from === "soul" ? "soul" : "deployment";
|
|
968
|
+
return { label, team, from };
|
|
807
969
|
}
|
|
808
970
|
function teamsDocument(meta = readCapabilityMeta()) {
|
|
809
971
|
const teams = parseOatsTeams();
|
|
810
972
|
const joined = joinedTeamsOf(meta);
|
|
811
973
|
const joinedLabels = new Set(joined.map((j) => j.label));
|
|
812
|
-
const primary = primaryTeamLabel();
|
|
813
|
-
const defaultTeam = meta.defaultTeam?.team || meta.team || meta.identity?.team || payloadTeam().team || null;
|
|
814
974
|
return {
|
|
815
|
-
defaultTeam:
|
|
816
|
-
|
|
817
|
-
eligible: teams.filter((t) => t.mapped && t.team).map((t) => ({ label: t.label, team: t.team, joined: joinedLabels.has(t.label) })),
|
|
975
|
+
defaultTeam: defaultTeamObject(meta),
|
|
976
|
+
eligible: teams.filter((t) => !t.default && t.team).map((t) => ({ label: t.label, team: t.team, joined: joinedLabels.has(t.label), from: t.from })),
|
|
818
977
|
joined: joined.map((j) => ({ label: j.label, team: j.team, identityHome: j.identityHome, receive: j.receive || "poll", since: j.since })),
|
|
819
|
-
|
|
978
|
+
left: leftTeamsOf(meta),
|
|
820
979
|
at: new Date().toISOString(),
|
|
821
980
|
};
|
|
822
981
|
}
|
|
823
982
|
function mintJoinedTeam(row, meta) {
|
|
824
983
|
const existing = joinedTeamsOf(meta).find((j) => j.label === row.label);
|
|
825
984
|
if (existing) return { meta, joined: existing, changed: false };
|
|
826
|
-
requireJoinedTeamsAwFloor();
|
|
827
985
|
const identityHome = identityHomeForLabel(row.label);
|
|
828
986
|
const root = awebRootForTeam(row.team);
|
|
829
987
|
if (!root) throw new Error(`${awebRootProblem(rootSettingCandidate(row.team))}, so team ${row.label} could not be joined`);
|
|
830
|
-
const inv = parseSecretJson(run(["aw", "team", "invite", "--team-id", row.team, "--json"], root, 45000, { secretSafe: true }), "aw team invite");
|
|
988
|
+
const inv = parseSecretJson(run(["aw", "team", "invite", flagEq("--team-id", row.team), "--json"], root, 45000, { secretSafe: true }), "aw team invite");
|
|
831
989
|
if (!inv?.token || typeof inv.token !== "string") throw new Error(`aw team invite returned no usable token for ${row.label}`);
|
|
832
|
-
|
|
833
|
-
try {
|
|
834
|
-
raw = parseSecretJson(run(awWithIdentity(identityHome, ["id", "team", "accept-invite", inv.token, "--name", instance || meta.alias, "--local", "--json"]), home, JOIN_TIMEOUT_MS, { secrets: [inv.token], secretSafe: true }), "aw id team accept-invite");
|
|
835
|
-
} catch (e) {
|
|
836
|
-
const output = commandOutput(e);
|
|
837
|
-
const recovery = workspaceConnectRecovery(output);
|
|
838
|
-
let accepted;
|
|
839
|
-
try { accepted = parseAwJson(e.stdout || "", "aw id team accept-invite"); } catch { accepted = undefined; }
|
|
840
|
-
if (!accepted?.team_id) throw new Error(`aw id team accept-invite failed for joined team ${row.label}${recovery ? `; recovery: ${recovery}` : ""}`);
|
|
841
|
-
raw = accepted;
|
|
842
|
-
acceptWarning = `joined team ${row.label} was accepted but workspace connect failed${recovery ? `; recovery: ${recovery}` : "; rerun the aw workspace connect recovery command printed by aw"}`;
|
|
843
|
-
}
|
|
844
|
-
const alias = typeof raw.alias === "string" && AWEB_ALIAS_RE.test(raw.alias) ? raw.alias : (instance || meta.alias);
|
|
845
|
-
const team = typeof raw.team_id === "string" && raw.team_id ? raw.team_id : row.team;
|
|
846
|
-
if (team !== row.team) throw new Error(`joined team ${team} differs from requested ${row.team}`);
|
|
847
|
-
const joined = { label: row.label, team, identityHome, receive: "poll", since: new Date().toISOString(), alias };
|
|
990
|
+
const { joined } = acceptConnectVerifyJoinedTeam({ label: row.label, token: inv.token, identityHome, alias: instance || meta.alias, expectedTeam: row.team, root, cwd: home, serviceDocs: [inv] });
|
|
848
991
|
const next = { ...meta, joinedTeams: [...joinedTeamsOf(meta), joined] };
|
|
849
|
-
|
|
850
|
-
const recovery = workspaceConnectRecovery(JSON.stringify(raw)) || raw.recovery_command || raw.recoveryCommand;
|
|
851
|
-
return { meta: next, joined, changed: true, warning: acceptWarning || `joined team ${row.label} was accepted but no workspace connection was written under ${identityHome}${recovery ? `; recovery: ${recovery}` : "; rerun the aw workspace connect recovery command printed by aw"}` };
|
|
852
|
-
}
|
|
853
|
-
return { meta: next, joined, changed: true, ...(acceptWarning ? { warning: acceptWarning } : {}) };
|
|
992
|
+
return { meta: next, joined, changed: true };
|
|
854
993
|
}
|
|
855
994
|
function leaveJoinedTeam(label, meta) {
|
|
856
995
|
const joined = joinedTeamsOf(meta);
|
|
857
996
|
const entry = joined.find((j) => j.label === label);
|
|
858
997
|
if (!entry) return { meta, changed: false };
|
|
859
|
-
|
|
860
|
-
try {
|
|
861
|
-
receipt = parseAwJson(run(awWithIdentity(entry.identityHome, ["workspace", "delete", entry.alias || meta.alias || instance, "--json"]), home, 60000), "aw workspace delete");
|
|
862
|
-
released = releaseReceiptStatus(receipt);
|
|
863
|
-
} catch (e) {
|
|
864
|
-
throw new Error(`failed to leave team ${label}; kept ${entry.identityHome} so cleanup can be retried: ${String(commandOutput(e)).slice(0, 300)}`);
|
|
865
|
-
}
|
|
866
|
-
if (!released) throw new Error(`failed to leave team ${label}; workspace delete did not report alias_released: true; kept ${entry.identityHome} so cleanup can be retried: ${JSON.stringify(receipt)}`);
|
|
867
|
-
try { rmSync(entry.identityHome, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
998
|
+
const { released, receipt } = cleanupJoinedIdentity(entry, meta.alias || instance, home);
|
|
868
999
|
return { meta: { ...meta, joinedTeams: joined.filter((j) => j.label !== label) }, changed: true, released, receipt };
|
|
869
1000
|
}
|
|
870
1001
|
function awebRootForTeam(team) {
|
|
@@ -875,7 +1006,19 @@ function awebRootForTeam(team) {
|
|
|
875
1006
|
if (candidate && isAbsolute(candidate.root) && existsSync(join(resolve(candidate.root), ".aw"))) return resolve(candidate.root);
|
|
876
1007
|
return undefined;
|
|
877
1008
|
}
|
|
1009
|
+
let forwardedSoulArg;
|
|
1010
|
+
function stripForwardedSoul(argv = process.argv.slice(3)) {
|
|
1011
|
+
const out = [];
|
|
1012
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1013
|
+
const arg = argv[i];
|
|
1014
|
+
if (arg === "--soul") { if (i + 1 < argv.length) forwardedSoulArg = argv[++i]; continue; }
|
|
1015
|
+
if (arg.startsWith("--soul=")) { forwardedSoulArg = arg.slice("--soul=".length); continue; }
|
|
1016
|
+
out.push(arg);
|
|
1017
|
+
}
|
|
1018
|
+
return out;
|
|
1019
|
+
}
|
|
878
1020
|
function parseHomeCommandArgs(argv = process.argv.slice(3)) {
|
|
1021
|
+
argv = stripForwardedSoul(argv);
|
|
879
1022
|
const rest = [];
|
|
880
1023
|
let json = false, labels;
|
|
881
1024
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -892,9 +1035,76 @@ function parseHomeCommandArgs(argv = process.argv.slice(3)) {
|
|
|
892
1035
|
}
|
|
893
1036
|
function outputTeamsDocument(doc, json) {
|
|
894
1037
|
if (json) { console.log(JSON.stringify(doc)); return; }
|
|
895
|
-
console.log(`default team: ${doc.defaultTeam.team || "
|
|
1038
|
+
console.log(`default team: ${doc.defaultTeam ? `${doc.defaultTeam.label} (${doc.defaultTeam.team || "unmapped"})` : "none"}`);
|
|
896
1039
|
for (const row of doc.eligible) console.log(`${row.joined ? "joined" : "eligible"}: ${row.label} (${row.team})`);
|
|
897
|
-
for (const
|
|
1040
|
+
for (const row of doc.joined || []) if (!(doc.eligible || []).some((e) => e.label === row.label)) console.log(`joined: ${row.label} (${row.team})`);
|
|
1041
|
+
for (const row of doc.left || []) console.log(`left: ${row.label} (${row.team}) ${row.reason}`);
|
|
1042
|
+
for (const warning of doc.warnings || []) console.log(`warning: ${warning}`);
|
|
1043
|
+
}
|
|
1044
|
+
const actionWarning = (warning) => String(warning || "").slice(0, 300);
|
|
1045
|
+
const registryOrigin = (value) => typeof value === "string" && /^https?:\/\//i.test(value.trim()) ? value.trim() : undefined;
|
|
1046
|
+
function registryOriginFromJoinedState(row, match, listed) {
|
|
1047
|
+
const fromJson = registryOrigin(match?.registry_origin || match?.registry_url || match?.registry || match?.awid_registry_url || listed?.registry_origin || listed?.registry_url || listed?.registry || listed?.awid_registry_url);
|
|
1048
|
+
if (fromJson) return { registry: fromJson };
|
|
1049
|
+
for (const file of ["identity.yaml", "teams.yaml", "workspace.yaml", "registry.yaml", "config.yaml"]) {
|
|
1050
|
+
try {
|
|
1051
|
+
const text = readFileSync(join(row.identityHome, file), "utf8");
|
|
1052
|
+
for (const key of ["awid_registry_url", "registry_origin", "registry_url", "registry"]) {
|
|
1053
|
+
const found = registryOrigin(yamlScalar(text, key));
|
|
1054
|
+
if (found) return { registry: found };
|
|
1055
|
+
}
|
|
1056
|
+
} catch { /* keep looking */ }
|
|
1057
|
+
}
|
|
1058
|
+
const fromEnv = registryOrigin(process.env.AWID_REGISTRY_URL);
|
|
1059
|
+
if (fromEnv) return { registry: fromEnv };
|
|
1060
|
+
return { registry: null, registryError: "registry origin could not be determined from the joined identity; command omits --registry and aw will use its default" };
|
|
1061
|
+
}
|
|
1062
|
+
function certificateIdFromJoinedCertificateFile(row) {
|
|
1063
|
+
const certFile = join(row.identityHome, "team-certs", `${String(row.team || "").replace(/:/g, "__")}.pem`);
|
|
1064
|
+
if (!existsSync(certFile)) return { found: false };
|
|
1065
|
+
try {
|
|
1066
|
+
const cert = JSON.parse(readFileSync(certFile, "utf8"));
|
|
1067
|
+
if (cert?.team_id !== row.team) return { found: false, error: `certificate file ${certFile} is for ${cert?.team_id || "unknown team"}, not ${row.team}` };
|
|
1068
|
+
const certificateId = typeof cert?.certificate_id === "string" ? cert.certificate_id.trim() : "";
|
|
1069
|
+
if (!certificateId) return { found: false, error: `certificate file ${certFile} did not contain a certificate_id for ${row.team}` };
|
|
1070
|
+
return { found: true, certificateId };
|
|
1071
|
+
} catch (e) {
|
|
1072
|
+
return { found: false, error: `could not read certificate file ${certFile}: ${e.message || e}` };
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function certificateIdForJoinedTeam(row) {
|
|
1076
|
+
const fromFile = certificateIdFromJoinedCertificateFile(row);
|
|
1077
|
+
let listed, match, listError;
|
|
1078
|
+
try {
|
|
1079
|
+
listed = parseAwJson(run(awWithIdentity(row.identityHome, ["id", "team", "list", "--json"]), home, 60000), "aw id team list");
|
|
1080
|
+
const memberships = teamMemberships(listed);
|
|
1081
|
+
match = memberships.find((m) => String(m?.team_id || m?.id || m?.team || m) === row.team);
|
|
1082
|
+
} catch (e) {
|
|
1083
|
+
listError = actionWarning(e.message || e);
|
|
1084
|
+
}
|
|
1085
|
+
const registry = registryOriginFromJoinedState(row, match, listed);
|
|
1086
|
+
if (fromFile.found) return { certificateId: fromFile.certificateId, ...registry };
|
|
1087
|
+
if (match) {
|
|
1088
|
+
const certificateId = match && typeof match === "object" ? (match.certificate_id || match.cert_id || match.certId || match.certificate?.id || match.cert?.id) : undefined;
|
|
1089
|
+
if (typeof certificateId === "string" && certificateId.trim()) return { certificateId: certificateId.trim(), ...registry };
|
|
1090
|
+
return { certificateId: null, certificateIdError: fromFile.error || `certificate id for ${row.team} was not present in the joined certificate file or aw id team list --json`, ...registry };
|
|
1091
|
+
}
|
|
1092
|
+
return { certificateId: null, certificateIdError: fromFile.error || listError || `aw id team list --json returned no membership for ${row.team}`, ...registry };
|
|
1093
|
+
}
|
|
1094
|
+
function failedLeaveDisposition(row, error) {
|
|
1095
|
+
const text = String(error?.message || error || "");
|
|
1096
|
+
const reason = /team_not_hosted/i.test(text) ? "team_not_hosted" : actionWarning(text);
|
|
1097
|
+
const data = { label: row.label, team: row.team, alias: row.alias || instance || null, ...certificateIdForJoinedTeam(row), at: new Date().toISOString(), reason, ...(reason === "team_not_hosted" ? { cleanup: "controller" } : {}) };
|
|
1098
|
+
const teamName = String(row.team || "").split(":")[0] || row.team;
|
|
1099
|
+
const namespace = String(row.team || "").includes(":") ? String(row.team).split(":").slice(1).join(":") : "<namespace>";
|
|
1100
|
+
const registryArg = data.registry ? ` --registry ${data.registry}` : "";
|
|
1101
|
+
const command = reason === "team_not_hosted" && data.certificateId ? `aw id team remove-member --namespace ${namespace} --team ${teamName} --cert-id ${data.certificateId}${registryArg} --json` : undefined;
|
|
1102
|
+
const ownerCommand = command
|
|
1103
|
+
? `; controller cleanup: ${command}${data.registryError ? ` (${data.registryError})` : ""}`
|
|
1104
|
+
: reason === "team_not_hosted"
|
|
1105
|
+
? `; controller cleanup needs the certificate id, but ${data.certificateIdError || "it could not be read"}${data.registryError ? ` (${data.registryError})` : ""}`
|
|
1106
|
+
: "";
|
|
1107
|
+
return { data: { ...data, ...(command ? { command } : {}) }, warning: `joined team ${row.label} cleanup failed: ${data.reason}${ownerCommand}` };
|
|
898
1108
|
}
|
|
899
1109
|
function runTeamsCommand(kind) {
|
|
900
1110
|
const args = parseHomeCommandArgs();
|
|
@@ -916,8 +1126,9 @@ function runTeamsCommand(kind) {
|
|
|
916
1126
|
const result = kind === "join" ? mintJoinedTeam(row, meta) : leaveJoinedTeam(row.label, meta);
|
|
917
1127
|
meta = result.meta;
|
|
918
1128
|
writeProviderTeamsState(meta);
|
|
919
|
-
|
|
920
|
-
|
|
1129
|
+
const boundedWarning = result.warning ? actionWarning(result.warning) : undefined;
|
|
1130
|
+
actions.push({ action: kind, label: row.label, ...(result.released ? { released: result.released } : {}), ...(result.receipt ? { receipt: result.receipt } : {}), ...(boundedWarning ? { warning: boundedWarning } : {}) });
|
|
1131
|
+
if (boundedWarning) warnings.push(`oats-aweb: ${boundedWarning}`);
|
|
921
1132
|
}
|
|
922
1133
|
} catch (e) {
|
|
923
1134
|
// What already happened travels with the failure, so a caller can
|
|
@@ -936,28 +1147,21 @@ function runTeamsCommand(kind) {
|
|
|
936
1147
|
outputTeamsDocument(doc, args.json);
|
|
937
1148
|
}
|
|
938
1149
|
|
|
939
|
-
/** The primary identity's team
|
|
940
|
-
*
|
|
941
|
-
* Exits through fatal() on refusal. */
|
|
1150
|
+
/** The primary identity's team comes only from the kernel's team-model v2
|
|
1151
|
+
* default env. Never fall back to the root's active team. */
|
|
942
1152
|
function resolvePrimaryTeam() {
|
|
943
1153
|
const warnings = [];
|
|
944
|
-
const
|
|
945
|
-
if (!
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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 };
|
|
1154
|
+
const team = defaultTeamId();
|
|
1155
|
+
if (!team) fatal(teamConfigRemedy());
|
|
1156
|
+
const root = awebRootForTeam(team);
|
|
1157
|
+
if (!root) fatal(`${awebRootProblem(rootSettingCandidate(team))}, so default team ${defaultTeamLabel() || team} (${team}) could not mint this instance; run oats aweb setup or ask the team's owner for an invite`);
|
|
1158
|
+
return { team, root, from: defaultTeamFromEnv(), label: defaultTeamLabel(), warnings };
|
|
1159
|
+
}
|
|
1160
|
+
function appendProviderEvent(kind, data) {
|
|
1161
|
+
try {
|
|
1162
|
+
const row = { eventsApi: 2, at: new Date().toISOString(), instance, home, incarnation: null, producer: "oats.aweb", kind, data };
|
|
1163
|
+
appendFileSync(join(home, ".oats-events.jsonl"), JSON.stringify(row) + "\n");
|
|
1164
|
+
} catch { /* evidence only */ }
|
|
961
1165
|
}
|
|
962
1166
|
|
|
963
1167
|
/** The home's runtime. Spawn and launch run in the session's own env; a
|
|
@@ -1009,8 +1213,18 @@ if (event === "launch") {
|
|
|
1009
1213
|
const eligible = new Set(eligibleTeams().map((t) => t.label));
|
|
1010
1214
|
const warnings = [];
|
|
1011
1215
|
for (const row of joined) if (!eligible.has(row.label)) {
|
|
1012
|
-
try {
|
|
1013
|
-
|
|
1216
|
+
try {
|
|
1217
|
+
oldMeta = leaveJoinedTeam(row.label, oldMeta).meta;
|
|
1218
|
+
const left = { label: row.label, team: row.team, at: new Date().toISOString(), reason: "no-longer-eligible" };
|
|
1219
|
+
oldMeta = { ...oldMeta, left: [...leftTeamsOf(oldMeta), left].slice(-20) };
|
|
1220
|
+
appendProviderEvent("aweb-team-left", left);
|
|
1221
|
+
warnings.push(`left joined team ${row.label} because it is no longer eligible`);
|
|
1222
|
+
}
|
|
1223
|
+
catch (e) {
|
|
1224
|
+
const failed = failedLeaveDisposition(row, e);
|
|
1225
|
+
appendProviderEvent("aweb-team-leave-failed", failed.data);
|
|
1226
|
+
warnings.push(failed.warning);
|
|
1227
|
+
}
|
|
1014
1228
|
}
|
|
1015
1229
|
// Re-register what remains: the runtime may differ from the last session.
|
|
1016
1230
|
const synced = syncWakeReceive(oldMeta);
|
|
@@ -1037,9 +1251,9 @@ if (event === "launch") {
|
|
|
1037
1251
|
let minted; // external identity, once `aw team join` succeeds
|
|
1038
1252
|
let spawnMeta; // with joined teams, once any is accepted
|
|
1039
1253
|
try {
|
|
1040
|
-
// Team correctness: the
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1254
|
+
// Team correctness: the kernel's default team id is the only source. ALWAYS
|
|
1255
|
+
// pass --team-id explicitly — never inherit whatever team happens to be
|
|
1256
|
+
// active at mint time — and verify the joined cert matches.
|
|
1043
1257
|
// The instance name IS the discoverable alias (the team roster doubles as the
|
|
1044
1258
|
// cross-machine instance directory).
|
|
1045
1259
|
const primary = resolvePrimaryTeam();
|
|
@@ -1047,13 +1261,13 @@ if (event === "launch") {
|
|
|
1047
1261
|
const warnings = [...primary.warnings];
|
|
1048
1262
|
// Both of these carry the invite token — one mints it, the other spends it —
|
|
1049
1263
|
// so neither their output nor their diagnostics may reach a log.
|
|
1050
|
-
const inv = parseSecretJson(run(["aw", "team", "invite", "--team-id", team, "--json"], root, 45000, { secretSafe: true }), "aw team invite");
|
|
1264
|
+
const inv = parseSecretJson(run(["aw", "team", "invite", flagEq("--team-id", team), "--json"], root, 45000, { secretSafe: true }), "aw team invite");
|
|
1051
1265
|
if (!inv?.token || typeof inv.token !== "string") fatal("aw team invite returned no usable token, so no identity could be minted");
|
|
1052
1266
|
let raw;
|
|
1053
1267
|
try {
|
|
1054
1268
|
// 120 s: a join on a slow or flapping link is slow, not broken; a killed
|
|
1055
1269
|
// join that completed server-side is caught below.
|
|
1056
|
-
raw = parseSecretJson(run(["aw", "team", "join", inv.token, "--name", instance, "--json"], home, JOIN_TIMEOUT_MS, { secrets: [inv.token], secretSafe: true }), "aw team join");
|
|
1270
|
+
raw = parseSecretJson(run(["aw", "team", "join", inv.token, flagEq("--name", instance), "--json"], home, JOIN_TIMEOUT_MS, { secrets: [inv.token], secretSafe: true }), "aw team join");
|
|
1057
1271
|
} catch (e) {
|
|
1058
1272
|
// The join may have completed after the CLI was killed or reported a
|
|
1059
1273
|
// failure: if the home now holds a bound identity, that identity EXISTS
|
|
@@ -1111,13 +1325,11 @@ if (event === "launch") {
|
|
|
1111
1325
|
const deliveryBrief = deliveryMode === "session"
|
|
1112
1326
|
? ` 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.`
|
|
1113
1327
|
: "";
|
|
1114
|
-
let meta = { team: joined.team_id, alias, delivery: deliveryMode, defaultTeam: { team: joined.team_id,
|
|
1115
|
-
const
|
|
1116
|
-
if (joinFloorProblem) warnings.push(`oats-aweb: ${joinFloorProblem}`);
|
|
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}`); }
|
|
1328
|
+
let meta = { team: joined.team_id, alias, delivery: deliveryMode, defaultTeam: { label: primary.label, team: joined.team_id, from: primary.from }, left: [], ...(process.env.OATS_RUNTIME ? { runtime: process.env.OATS_RUNTIME } : {}), identity: identityMeta({ mode: "local", alias, team: joined.team_id }) };
|
|
1329
|
+
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
1330
|
if (joinedTeamsOf(meta).length) { const synced = syncWakeReceive(meta); meta = synced.meta; for (const w of synced.warnings) warnings.push(`oats-aweb: ${w}`); }
|
|
1119
1331
|
writeProviderTeamsState(meta);
|
|
1120
|
-
const defaultTeamBrief = meta.defaultTeam.
|
|
1332
|
+
const defaultTeamBrief = meta.defaultTeam.from === "soul" ? "the default team configured for this soul" : "this deployment's default team";
|
|
1121
1333
|
const joinedNow = joinedTeamsOf(meta);
|
|
1122
1334
|
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("; ")}.` : "";
|
|
1123
1335
|
out({
|
|
@@ -1136,6 +1348,12 @@ if (event === "launch") {
|
|
|
1136
1348
|
} else if (event === "retire") {
|
|
1137
1349
|
let meta = withProviderTeams(JSON.parse(process.env.OATS_META || "{}"));
|
|
1138
1350
|
const retireWarnings = [];
|
|
1351
|
+
const pendingControllerCleanup = [];
|
|
1352
|
+
const rememberControllerCleanup = (data) => {
|
|
1353
|
+
if (data?.cleanup === "controller") pendingControllerCleanup.push({ label: data.label, team: data.team, alias: data.alias, certificateId: data.certificateId, ...(data.command ? { command: data.command } : {}) });
|
|
1354
|
+
};
|
|
1355
|
+
const retiredMeta = (fields = {}) => ({ ...fields, ...(pendingControllerCleanup.length ? { pendingControllerCleanup } : {}) });
|
|
1356
|
+
if (hasStaleTeamSetting) retireWarnings.push(TEAM_SETTING_MESSAGE);
|
|
1139
1357
|
// A retained seat: release the lock and leave the identity alone. Never
|
|
1140
1358
|
// aw workspace delete (it would soft-delete the standing identity's row)
|
|
1141
1359
|
// and never team retire; the source .aw stays until a human removes it.
|
|
@@ -1143,7 +1361,7 @@ if (event === "launch") {
|
|
|
1143
1361
|
if (meta.identity?.mode === "global" && !meta.retained) globalGrantRetire(meta);
|
|
1144
1362
|
for (const joined of joinedTeamsOf(meta)) {
|
|
1145
1363
|
try { meta = leaveJoinedTeam(joined.label, meta).meta; }
|
|
1146
|
-
catch (e) {
|
|
1364
|
+
catch (e) { const failed = failedLeaveDisposition(joined, e); retireWarnings.push(failed.warning); rememberControllerCleanup(failed.data); }
|
|
1147
1365
|
}
|
|
1148
1366
|
// A native (channel/pi) home registered with the broker only for its joined
|
|
1149
1367
|
// teams; a session home was deregistered above.
|
|
@@ -1153,7 +1371,7 @@ if (event === "launch") {
|
|
|
1153
1371
|
if (meta.retained) {
|
|
1154
1372
|
if (meta.lock) { try { rmSync(meta.lock, { force: true }); } catch { /* the lock may already be gone */ } }
|
|
1155
1373
|
const retainedWarning = `released the retained identity "${meta.alias}" (lock ${meta.lock || "?"} removed); the identity itself and ${meta.source || "its source"} are untouched${meta.tookOverFrom ? `; this seat had taken over from ${meta.tookOverFrom}` : ""}`;
|
|
1156
|
-
out({ meta: { retired: true, retained: true, identityReleased: true, joinedTeams: joinedTeamsOf(meta), ...(meta.tookOverFrom ? { tookOverFrom: meta.tookOverFrom } : {}) }, warning: `oats-aweb: ${[...retireWarnings, retainedWarning].join(" | ")}` });
|
|
1374
|
+
out({ meta: retiredMeta({ retired: true, retained: true, identityReleased: true, joinedTeams: joinedTeamsOf(meta), ...(meta.tookOverFrom ? { tookOverFrom: meta.tookOverFrom } : {}) }), warning: `oats-aweb: ${[...retireWarnings, retainedWarning].join(" | ")}` });
|
|
1157
1375
|
}
|
|
1158
1376
|
// No alias means the spawn hook never reported an identity: nothing exists to
|
|
1159
1377
|
// undo, which is completion. An alias WITH no local `.aw` is the opposite —
|
|
@@ -1163,37 +1381,29 @@ if (event === "launch") {
|
|
|
1163
1381
|
// timeout that completed anyway) still carries the alias in its workspace
|
|
1164
1382
|
// binding: use it rather than leaving the workspace orphaned.
|
|
1165
1383
|
if (!meta.alias) { const late = workspaceAliasOf(home); if (late) meta = { ...meta, alias: late, aliasFromHome: true }; }
|
|
1166
|
-
if (!meta.alias) out({ meta: { retired: false, reason: "nothing-to-delete" } });
|
|
1384
|
+
if (!meta.alias) out({ meta: retiredMeta({ retired: false, reason: "nothing-to-delete" }) });
|
|
1167
1385
|
if (!existsSync(join(home, ".aw"))) {
|
|
1168
|
-
out({ meta: { retired: false, reason: "no-local-identity-key" }, warning: `oats-aweb: alias "${meta.alias}" was minted but ${join(home, ".aw")} is gone, so the remote record cannot be self-deleted and will linger until stale` }, 1);
|
|
1386
|
+
out({ meta: retiredMeta({ retired: false, reason: "no-local-identity-key" }), warning: `oats-aweb: alias "${meta.alias}" was minted but ${join(home, ".aw")} is gone, so the remote record cannot be self-deleted and will linger until stale` }, 1);
|
|
1169
1387
|
}
|
|
1170
1388
|
try {
|
|
1171
1389
|
// Self-delete from inside the home, authenticated by its own key — a remote
|
|
1172
|
-
// delete would 409 until the server marks the workspace stale.
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
// workspace_self_retire.go); `reason` is tolerated for a later rename.
|
|
1183
|
-
const reason = typeof doc?.alias_released_reason === "string" ? doc.alias_released_reason : typeof doc?.reason === "string" ? doc.reason : (doc ? "unstated" : "no JSON answer");
|
|
1184
|
-
out({ meta: { retired: true, aliasReusable: released, aliasReason: reason, joinedTeams: joinedTeamsOf(meta) }, ...(retireWarnings.length ? { warning: `oats-aweb: ${retireWarnings.join(" | ")}` } : released ? {} : { warning: `oats-aweb: workspace "${meta.alias}" deleted but its alias was not released (${reason}); spawn successors with a different --name (kernels 0.26.0+) or a different --purpose until it is` }) });
|
|
1390
|
+
// delete would 409 until the server marks the workspace stale. aw >= 1.36.13
|
|
1391
|
+
// reports whether the certificate was revoked and the alias was released.
|
|
1392
|
+
const raw = run(["aw", "workspace", "delete", meta.alias, "--json"], home);
|
|
1393
|
+
let doc; try { doc = JSON.parse(raw); } catch { doc = undefined; }
|
|
1394
|
+
const released = doc?.alias_released === true;
|
|
1395
|
+
const reason = typeof doc?.alias_released_reason === "string" ? doc.alias_released_reason : typeof doc?.reason === "string" ? doc.reason : (doc ? "unstated" : "no JSON answer");
|
|
1396
|
+
if (!released && /team_not_hosted/i.test(reason)) {
|
|
1397
|
+
const failed = failedLeaveDisposition({ label: "default", team: meta.team || meta.defaultTeam?.team || meta.identity?.team || defaultTeamId(), alias: meta.alias, identityHome: join(home, ".aw") }, reason);
|
|
1398
|
+
retireWarnings.push(failed.warning.replace(/^joined team default cleanup failed:/, "default identity cleanup failed:"));
|
|
1399
|
+
rememberControllerCleanup({ ...failed.data, label: "default" });
|
|
1185
1400
|
}
|
|
1186
|
-
|
|
1187
|
-
// Honest: the workspace row is deleted, but a hosted local member cannot
|
|
1188
|
-
// revoke its own AWID certificate (aweb-abim), so the alias is NOT
|
|
1189
|
-
// reusable. retired stays true because the cleanup is as complete as the
|
|
1190
|
-
// platform allows; the field and the line carry the truth.
|
|
1191
|
-
out({ meta: { retired: true, aliasReusable: false, joinedTeams: joinedTeamsOf(meta) }, warning: `oats-aweb: ${[...retireWarnings, `workspace "${meta.alias}" deleted; its certificate is not revoked (aweb-abim), so the alias is not reusable — spawn successors with a different --name (kernels 0.26.0+) or a different --purpose`].join(" | ")}` });
|
|
1401
|
+
out({ meta: retiredMeta({ retired: true, aliasReusable: released, aliasReason: reason, joinedTeams: joinedTeamsOf(meta) }), ...(retireWarnings.length ? { warning: `oats-aweb: ${retireWarnings.join(" | ")}` } : released ? {} : { warning: `oats-aweb: workspace "${meta.alias}" deleted but its alias was not released (${reason}); spawn successors with a different --name (kernels 0.26.0+) or a different --purpose until it is` }) });
|
|
1192
1402
|
} catch (e) {
|
|
1193
1403
|
// Exit nonzero: during a required-hook rollback this is the signal that
|
|
1194
1404
|
// compensation did NOT complete, so the spawn is not reported as cleanly
|
|
1195
1405
|
// rolled back while a remote identity still exists.
|
|
1196
|
-
out({ meta: { retired: false, reason: "self-delete-failed" }, warning: `oats-aweb: self-delete failed (the remote record will linger until stale): ${e.message || e}` }, 1);
|
|
1406
|
+
out({ meta: retiredMeta({ retired: false, reason: "self-delete-failed" }), warning: `oats-aweb: self-delete failed (the remote record will linger until stale): ${e.message || e}` }, 1);
|
|
1197
1407
|
}
|
|
1198
1408
|
} else if (["teams", "join", "leave"].includes(event)) {
|
|
1199
1409
|
try { runTeamsCommand(event); process.exit(0); }
|
|
@@ -1210,7 +1420,7 @@ if (event === "launch") {
|
|
|
1210
1420
|
// Default: this instance's default team, listed from the root that minted
|
|
1211
1421
|
// it. `--label <label>` lists an eligible (joined or not) workspace team from
|
|
1212
1422
|
// the host root that holds it.
|
|
1213
|
-
const argv = process.argv.slice(3);
|
|
1423
|
+
const argv = stripForwardedSoul(process.argv.slice(3));
|
|
1214
1424
|
const labelAt = argv.indexOf("--label");
|
|
1215
1425
|
const label = labelAt >= 0 ? argv[labelAt + 1] : (argv.find((a) => a.startsWith("--label=")) || "").slice("--label=".length) || undefined;
|
|
1216
1426
|
const meta = readCapabilityMeta();
|
|
@@ -1221,15 +1431,14 @@ if (event === "launch") {
|
|
|
1221
1431
|
team = row.team; root = awebRootForTeam(team);
|
|
1222
1432
|
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
1433
|
} else {
|
|
1224
|
-
team = meta.defaultTeam?.team || meta.identity?.team || meta.team ||
|
|
1225
|
-
root =
|
|
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;
|
|
1434
|
+
team = meta.defaultTeam?.team || meta.identity?.team || meta.team || defaultTeamId();
|
|
1435
|
+
root = awebRootForTeam(team);
|
|
1436
|
+
if (!root) { console.error(`oats aweb roster: ${awebRootProblem(rootSettingCandidate(team))}`); process.exit(1); }
|
|
1228
1437
|
}
|
|
1229
|
-
if (!team) { console.error(`oats aweb roster:
|
|
1230
|
-
const teamFlag = team.includes(":") ?
|
|
1438
|
+
if (!team) { console.error(`oats aweb roster: ${teamConfigRemedy()}`); process.exit(1); }
|
|
1439
|
+
const teamFlag = [flagEq(team.includes(":") ? "--team-id" : "--team", team)];
|
|
1231
1440
|
const r = JSON.parse(run(["aw", "id", "team", "members", ...teamFlag, "--json"], root, 60000));
|
|
1232
|
-
if (
|
|
1441
|
+
if (argv.includes("--json")) { console.log(JSON.stringify(r, null, 2)); process.exit(0); }
|
|
1233
1442
|
console.log(`aweb team ${r.team_id || team} — member roster (cross-machine):`);
|
|
1234
1443
|
const members = r.members || [];
|
|
1235
1444
|
if (!members.length) console.log(" (no member certificates visible from this workspace)");
|
|
@@ -1239,25 +1448,37 @@ if (event === "launch") {
|
|
|
1239
1448
|
} else if (event === "setup") {
|
|
1240
1449
|
// Guided onboarding — idempotent, prints what it finds and can run one
|
|
1241
1450
|
// existing aw primitive when the operator supplies the needed authority.
|
|
1242
|
-
const args = process.argv.slice(3).filter((arg) => arg !== "--json");
|
|
1243
|
-
const usage = "usage: oats aweb setup [--username <hosted-user> | --invite <token>]";
|
|
1244
|
-
let username, invite;
|
|
1451
|
+
const args = stripForwardedSoul(process.argv.slice(3)).filter((arg) => arg !== "--json");
|
|
1452
|
+
const usage = "usage: oats aweb setup [--username <hosted-user> | --create <label> [--namespace <domain>] | --join <label> [--invite <token>] [--service <url>] [--name <alias>]]";
|
|
1453
|
+
let username, invite, createLabel, createNamespace, joinLabel, joinService, joinName;
|
|
1245
1454
|
for (let i = 0; i < args.length; i++) {
|
|
1246
1455
|
const arg = args[i];
|
|
1247
1456
|
if (arg === "--username" && args[i + 1]) { username = args[++i]; continue; }
|
|
1248
1457
|
if (arg.startsWith("--username=") && arg.length > "--username=".length) { username = arg.slice("--username=".length); continue; }
|
|
1249
1458
|
if (arg === "--invite" && args[i + 1]) { invite = args[++i]; continue; }
|
|
1250
1459
|
if (arg.startsWith("--invite=") && arg.length > "--invite=".length) { invite = arg.slice("--invite=".length); continue; }
|
|
1460
|
+
if (arg === "--join" && args[i + 1]) { joinLabel = args[++i]; continue; }
|
|
1461
|
+
if (arg.startsWith("--join=") && arg.length > "--join=".length) { joinLabel = arg.slice("--join=".length); continue; }
|
|
1462
|
+
if (arg === "--create" && args[i + 1]) { createLabel = args[++i]; continue; }
|
|
1463
|
+
if (arg.startsWith("--create=") && arg.length > "--create=".length) { createLabel = arg.slice("--create=".length); continue; }
|
|
1464
|
+
if (arg === "--namespace" && args[i + 1]) { createNamespace = args[++i]; continue; }
|
|
1465
|
+
if (arg.startsWith("--namespace=") && arg.length > "--namespace=".length) { createNamespace = arg.slice("--namespace=".length); continue; }
|
|
1466
|
+
if (arg === "--service" && args[i + 1]) { joinService = args[++i]; continue; }
|
|
1467
|
+
if (arg.startsWith("--service=") && arg.length > "--service=".length) { joinService = arg.slice("--service=".length); continue; }
|
|
1468
|
+
if (arg === "--name" && args[i + 1]) { joinName = args[++i]; continue; }
|
|
1469
|
+
if (arg.startsWith("--name=") && arg.length > "--name=".length) { joinName = arg.slice("--name=".length); continue; }
|
|
1251
1470
|
console.error(`oats aweb setup: ${usage}`);
|
|
1252
1471
|
process.exit(2);
|
|
1253
1472
|
}
|
|
1473
|
+
if (createNamespace && !createLabel) { console.error(`oats aweb setup: --namespace requires --create\n${usage}`); process.exit(2); }
|
|
1474
|
+
if (invite && !joinLabel) { console.error(`oats aweb setup: --invite requires --join <label> so the team gets its own root\n${usage}`); process.exit(2); }
|
|
1475
|
+
if ((joinService || joinName) && !joinLabel) { console.error(`oats aweb setup: --service/--name require --join <label>\n${usage}`); process.exit(2); }
|
|
1254
1476
|
const apiKey = !!process.env.AWEB_API_KEY;
|
|
1255
|
-
const actions = [username ? "--username" : null,
|
|
1477
|
+
const actions = [username ? "--username" : null, joinLabel ? "--join" : null, createLabel ? "--create" : null, apiKey && !createLabel ? "AWEB_API_KEY" : null].filter(Boolean);
|
|
1256
1478
|
if (actions.length > 1) { console.error(`oats aweb setup: choose exactly one onboarding authority (${actions.join(", ")})\n${usage}`); process.exit(2); }
|
|
1257
1479
|
|
|
1258
|
-
const
|
|
1259
|
-
const
|
|
1260
|
-
const teamId = typeof settings.team === "string" && settings.team.trim() ? settings.team.trim() : process.env.OATS_TEAM_ID;
|
|
1480
|
+
const teamName = defaultTeamId();
|
|
1481
|
+
const teamId = defaultTeamId();
|
|
1261
1482
|
const candidate = rootSettingCandidate(teamName);
|
|
1262
1483
|
const scope = candidate?.root ? resolve(candidate.root) : process.cwd();
|
|
1263
1484
|
console.log(`aweb onboarding — messaging root: ${scope}${teamName ? `, team: ${teamName}` : ""}\n`);
|
|
@@ -1268,11 +1489,102 @@ if (event === "launch") {
|
|
|
1268
1489
|
|
|
1269
1490
|
const want = teamId || teamName;
|
|
1270
1491
|
const defaultTeamForUsername = username ? `default:${username}.aweb.ai` : undefined;
|
|
1271
|
-
const
|
|
1272
|
-
try { return parseAwJson(run(["aw", "team", "list", "--json"],
|
|
1492
|
+
const readTeamsAt = (rootDir) => {
|
|
1493
|
+
try { return parseAwJson(run(["aw", "team", "list", "--json"], rootDir), "aw team list"); }
|
|
1273
1494
|
catch { return { memberships: [] }; }
|
|
1274
1495
|
};
|
|
1496
|
+
const readTeams = () => readTeamsAt(scope);
|
|
1275
1497
|
const matchingTeam = (teams) => want ? teamIdsOf(teams).find((tid) => String(tid) === want || String(tid).startsWith(`${want}:`)) : undefined;
|
|
1498
|
+
const rootAlias = () => {
|
|
1499
|
+
try { const who = JSON.parse(run(["aw", "whoami", "--json"], scope)); return who.alias || who.name; } catch { return undefined; }
|
|
1500
|
+
};
|
|
1501
|
+
const setupAlias = () => {
|
|
1502
|
+
const alias = joinName || rootAlias();
|
|
1503
|
+
if (!alias || !AWEB_ALIAS_RE.test(alias)) throw new Error(`--name <alias> is required when no root identity is available; aliases must match the aweb 1-64 character rule`);
|
|
1504
|
+
return alias;
|
|
1505
|
+
};
|
|
1506
|
+
const setupResumeCommand = (label) => `oats aweb setup --soul ${setupSoulArg()} --join ${label} --service <url>`;
|
|
1507
|
+
const setupServiceDocs = (teamRoot) => {
|
|
1508
|
+
const docs = [];
|
|
1509
|
+
if (joinService) docs.push({ aweb_url: joinService });
|
|
1510
|
+
if (process.env.AWEB_URL) docs.push({ aweb_url: process.env.AWEB_URL });
|
|
1511
|
+
const addRoot = (rootDir) => {
|
|
1512
|
+
if (!rootDir || resolve(rootDir) === resolve(teamRoot || "")) return;
|
|
1513
|
+
try { const url = yamlScalar(readFileSync(join(resolve(rootDir), ".aw", "workspace.yaml"), "utf8"), "aweb_url"); if (url) docs.push({ aweb_url: url }); } catch { /* no service there */ }
|
|
1514
|
+
};
|
|
1515
|
+
const roots = settings.roots && typeof settings.roots === "object" && !Array.isArray(settings.roots) ? settings.roots : {};
|
|
1516
|
+
for (const rootDir of Object.values(roots)) if (typeof rootDir === "string") addRoot(rootDir);
|
|
1517
|
+
if (typeof settings.root === "string") addRoot(settings.root);
|
|
1518
|
+
addRoot(scope);
|
|
1519
|
+
return docs;
|
|
1520
|
+
};
|
|
1521
|
+
const hostedCreateUnavailable = () => "creating an additional hosted team needs hosted team creation (aweb-abkh), not yet released in aw or aweb Cloud; use --namespace <domain> for a team you control, or ask the aweb team";
|
|
1522
|
+
const candidateTeamId = (name) => `${name}:${createNamespace}`;
|
|
1523
|
+
const teamExistsError = (e) => e?.status === 409 || /\b409\b|\bconflict\b|\balready exists\b|\bexists\b/i.test(commandOutput(e));
|
|
1524
|
+
const configuredTeamForLabel = (label) => parseOatsTeams().find((t) => t.label === label && t.team)?.team || (label === defaultTeamLabel() ? defaultTeamId() : undefined);
|
|
1525
|
+
const acceptIntoTeamRoot = (label, token, expectedTeam, ...serviceDocs) => {
|
|
1526
|
+
const teamRoot = perTeamRoot(process.env.OATS_WORKSPACE || scope, label);
|
|
1527
|
+
const idHome = join(teamRoot, ".aw");
|
|
1528
|
+
const resumeCommand = setupResumeCommand(label);
|
|
1529
|
+
const docs = [...setupServiceDocs(teamRoot), ...serviceDocs];
|
|
1530
|
+
const recordTeam = expectedTeam || configuredTeamForLabel(label) || `<team for ${label}>`;
|
|
1531
|
+
assertAwebRootSettingRecordable(recordTeam, teamRoot, { start: process.env.OATS_WORKSPACE || scope });
|
|
1532
|
+
let joined;
|
|
1533
|
+
if (existsSync(join(idHome, "identity.yaml"))) {
|
|
1534
|
+
if (existsSync(join(idHome, "workspace.yaml"))) throw new Error(`team root ${teamRoot} already holds a connected aweb identity; choose a different label or remove the stale root deliberately`);
|
|
1535
|
+
joined = connectExistingJoinedTeam({ label, identityHome: idHome, expectedTeam: expectedTeam || configuredTeamForLabel(label), root: scope, cwd: teamRoot, serviceDocs: docs, resumeCommand });
|
|
1536
|
+
} else {
|
|
1537
|
+
if (!token || typeof token !== "string") throw new Error(`--join ${label} needs --invite <token> unless ${teamRoot} already holds an accepted unconnected identity to resume`);
|
|
1538
|
+
mkdirSync(teamRoot, { recursive: true });
|
|
1539
|
+
try {
|
|
1540
|
+
({ joined } = acceptConnectVerifyJoinedTeam({ label, token, identityHome: idHome, alias: setupAlias(), expectedTeam: expectedTeam || configuredTeamForLabel(label), root: scope, cwd: teamRoot, serviceDocs: docs, cleanupOnFailure: false, resumeCommand, useAcceptedService: false }));
|
|
1541
|
+
} catch (e) {
|
|
1542
|
+
if (!existsSync(idHome)) { try { rmSync(teamRoot, { recursive: true, force: true }); } catch { /* best effort after failure before identity creation */ } }
|
|
1543
|
+
throw e;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
recordAwebRootSetting(joined.team, teamRoot, { start: process.env.OATS_WORKSPACE || scope });
|
|
1547
|
+
return { team: joined.team, teamRoot };
|
|
1548
|
+
};
|
|
1549
|
+
const createTeam = (label) => {
|
|
1550
|
+
if (!createNamespace) throw new Error(hostedCreateUnavailable());
|
|
1551
|
+
const base = normalizeAwebTeamName(label);
|
|
1552
|
+
for (let n = 1; n <= 20; n++) {
|
|
1553
|
+
const name = n === 1 ? base : `${base}-${n}`;
|
|
1554
|
+
const expectedTeam = candidateTeamId(name);
|
|
1555
|
+
const createArgs = ["aw", "id", "team", "create", flagEq("--name", name), flagEq("--namespace", createNamespace), "--json"];
|
|
1556
|
+
try {
|
|
1557
|
+
const created = parseSecretJson(run(createArgs, scope, 120000, { secretSafe: true }), "aw id team create");
|
|
1558
|
+
if (!created?.team_id || typeof created.team_id !== "string") throw new Error("aw id team create returned no team_id");
|
|
1559
|
+
const team = created.team_id;
|
|
1560
|
+
const token = created.invite_token || created.invite || created.token;
|
|
1561
|
+
if (!token || typeof token !== "string") throw new Error("aw id team create returned no invite token");
|
|
1562
|
+
return acceptIntoTeamRoot(label, token, team, created);
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
if (!teamExistsError(e)) throw e;
|
|
1565
|
+
const teams = readTeams();
|
|
1566
|
+
if (teamIdsOf(teams).some((tid) => String(tid) === expectedTeam)) { recordAwebRootSetting(expectedTeam, scope, { start: process.env.OATS_WORKSPACE || scope }); return { team: expectedTeam, teamRoot: scope, reused: true }; }
|
|
1567
|
+
if (n === 20) throw new Error(`could not create a unique aweb team for ${JSON.stringify(label)} after suffixing through -20`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
const recordLocalTeam = (label, team) => {
|
|
1572
|
+
const cli = process.env.OATS_CLI_BIN;
|
|
1573
|
+
if (!cli) throw new Error("OATS_CLI_BIN is required to record a new local team");
|
|
1574
|
+
assertNotFlag(label, "team label");
|
|
1575
|
+
run([cli, "teams", "add", label, flagEq("--team", team)], process.cwd(), 60000);
|
|
1576
|
+
};
|
|
1577
|
+
const setupSoulArg = () => forwardedSoulArg || "<soul>";
|
|
1578
|
+
const missingSharedRows = () => parseOatsTeams().filter((t) => {
|
|
1579
|
+
if (t.from !== "shared" || !t.label || !t.team) return false;
|
|
1580
|
+
const teamRoot = awebRootForTeam(t.team);
|
|
1581
|
+
if (!teamRoot) return true;
|
|
1582
|
+
const ids = new Set(teamIdsOf(readTeamsAt(teamRoot)).map(String));
|
|
1583
|
+
return !ids.has(t.team);
|
|
1584
|
+
});
|
|
1585
|
+
const printSharedMissing = (rows) => {
|
|
1586
|
+
for (const row of rows) console.log(`team ${row.label} (${row.team}) is shared: ask its owner for an invite, then run \`oats aweb setup --soul ${setupSoulArg()} --join ${row.label} --invite <token>\``);
|
|
1587
|
+
};
|
|
1276
1588
|
const printVerdict = (teams) => {
|
|
1277
1589
|
const match = matchingTeam(teams);
|
|
1278
1590
|
if (match) {
|
|
@@ -1287,40 +1599,62 @@ if (event === "launch") {
|
|
|
1287
1599
|
if (!want) {
|
|
1288
1600
|
console.log(` no team: ${teamConfigRemedy()}`);
|
|
1289
1601
|
const active = teams.active_team || teamIdsOf(teams)[0];
|
|
1290
|
-
if (active) console.log(` This root is a member of ${active};
|
|
1602
|
+
if (active) console.log(` This root is a member of ${active}; record it with \`oats teams add <label> --team ${active}\` and \`oats teams default <label>\`.`);
|
|
1291
1603
|
else if (defaultTeamForUsername) console.log(` New hosted users create ${defaultTeamForUsername}; map the workspace team to that id if this is the intended team.`);
|
|
1292
1604
|
return;
|
|
1293
1605
|
}
|
|
1294
1606
|
console.log(` Workspace initialized, but no membership matching "${want}".`);
|
|
1295
|
-
if (defaultTeamForUsername) console.log(` New hosted users create ${defaultTeamForUsername};
|
|
1607
|
+
if (defaultTeamForUsername) console.log(` New hosted users create ${defaultTeamForUsername}; record it with \`oats teams add <label> --team ${defaultTeamForUsername}\` and \`oats teams default <label>\`, then re-run setup.`);
|
|
1296
1608
|
console.log(" Existing team path: ask a member for an invite token, then run `oats aweb setup --invite <token>` (uses `aw team join <token>` at the root).");
|
|
1297
1609
|
console.log(" Team API-key path: set AWEB_API_KEY in the environment and run `oats aweb setup` (uses `aw init` at the root; the key is never printed).");
|
|
1298
|
-
console.log(" New hosted-account path: run `oats aweb setup --username <u>` (uses `aw init --username <u>` and creates default:<u>.aweb.ai).");
|
|
1610
|
+
console.log(" New hosted-account path: run `oats aweb setup --username <u>` (uses `aw init --new-account --username <u>` and creates default:<u>.aweb.ai).");
|
|
1299
1611
|
};
|
|
1300
1612
|
|
|
1301
1613
|
try {
|
|
1302
1614
|
const hasRoot = existsSync(join(scope, ".aw"));
|
|
1615
|
+
let teams = hasRoot ? readTeams() : { memberships: [] };
|
|
1616
|
+
const unmappedDefault = !actions.length && defaultTeamLabel() && !defaultTeamId();
|
|
1617
|
+
if (unmappedDefault) {
|
|
1618
|
+
console.log(`team ${defaultTeamLabel()} has no provider id yet: ask its owner to run \`oats aweb setup --create ${defaultTeamLabel()} --namespace <domain>\` and commit the id, or ask the owner for an invite and run \`oats aweb setup --join ${defaultTeamLabel()} --invite <token>\`.`);
|
|
1619
|
+
process.exit(1);
|
|
1620
|
+
}
|
|
1621
|
+
const missingShared = !actions.length ? missingSharedRows() : [];
|
|
1622
|
+
if (missingShared.length) { printSharedMissing(missingShared); process.exit(1); }
|
|
1303
1623
|
if (!hasRoot && !actions.length) {
|
|
1304
1624
|
console.log(`No aweb workspace at the messaging root yet (${candidate?.key || "settings.oats.aweb.root"}).`);
|
|
1305
1625
|
if (!want) console.log(` Also choose the aweb team for this deployment: ${teamConfigRemedy()}.`);
|
|
1306
1626
|
console.log(" Choose one guided setup path:");
|
|
1307
|
-
console.log(" oats aweb setup --username <u> # runs `aw init --username <u>` and creates default:<u>.aweb.ai");
|
|
1627
|
+
console.log(" oats aweb setup --username <u> # runs `aw init --new-account --username <u>` and creates default:<u>.aweb.ai");
|
|
1308
1628
|
console.log(" AWEB_API_KEY=<key> oats aweb setup # runs `aw init` for the hosted team behind the key");
|
|
1309
|
-
console.log(" oats aweb setup --invite <token> #
|
|
1629
|
+
console.log(" oats aweb setup --join <label> --invite <token> # accepts into a new per-team root");
|
|
1310
1630
|
console.log(" Or set settings.oats.aweb.root to an absolute directory whose .aw is the aweb minting root, then re-run setup.");
|
|
1311
1631
|
console.log(" Own your domain? Use the aweb-team-membership skill for BYOT flows.");
|
|
1312
1632
|
process.exit(0);
|
|
1313
1633
|
}
|
|
1314
|
-
|
|
1634
|
+
if (createLabel) {
|
|
1635
|
+
const label = createLabel;
|
|
1636
|
+
if (!createNamespace) throw new Error(hostedCreateUnavailable());
|
|
1637
|
+
mkdirSync(scope, { recursive: true });
|
|
1638
|
+
console.log(`Creating aweb team ${label} in namespace ${createNamespace} for a new per-team root.`);
|
|
1639
|
+
const created = createTeam(label);
|
|
1640
|
+
teams = readTeams();
|
|
1641
|
+
recordLocalTeam(label, created.team);
|
|
1642
|
+
console.log(`✓ created ${created.team}, ${created.reused ? `reused existing member root ${created.teamRoot}` : `accepted it into ${created.teamRoot}`}, recorded roots[${created.team}], and recorded local team ${label} with \`oats teams add ${label} --team ${created.team}\`.`);
|
|
1643
|
+
printVerdict(teams);
|
|
1644
|
+
process.exit(0);
|
|
1645
|
+
}
|
|
1646
|
+
if (joinLabel) {
|
|
1647
|
+
console.log(invite ? `Accepting invite for ${joinLabel} into a new per-team root (token withheld).` : `Resuming join for ${joinLabel} in its per-team root.`);
|
|
1648
|
+
const accepted = acceptIntoTeamRoot(joinLabel, invite, undefined);
|
|
1649
|
+
console.log(`✓ joined ${accepted.team} as ${joinLabel} in ${accepted.teamRoot}; recorded settings.oats.aweb.roots[${accepted.team}].`);
|
|
1650
|
+
process.exit(0);
|
|
1651
|
+
}
|
|
1315
1652
|
if (hasRoot && !matchingTeam(teams) && !actions.length) { printVerdict(teams); process.exit(0); }
|
|
1316
1653
|
if (!hasRoot || !matchingTeam(teams)) {
|
|
1317
1654
|
if (actions.length) mkdirSync(scope, { recursive: true });
|
|
1318
1655
|
if (username) {
|
|
1319
|
-
console.log(`Running aw init --username <u> at ${scope} (username withheld from repeated logs).`);
|
|
1320
|
-
run(["aw", "init", "--username", username], scope, 120000, { secrets: [username], unsetEnv: ["AWEB_API_KEY"] });
|
|
1321
|
-
} else if (invite) {
|
|
1322
|
-
console.log(`Running aw team join <token> at ${scope} (token withheld).`);
|
|
1323
|
-
run(["aw", "team", "join", invite], scope, 120000, { secrets: [invite], secretSafe: true });
|
|
1656
|
+
console.log(`Running aw init --new-account --username <u> at ${scope} (username withheld from repeated logs).`);
|
|
1657
|
+
run(["aw", "init", "--new-account", "--username", username], scope, 120000, { secrets: [username], unsetEnv: ["AWEB_API_KEY"] });
|
|
1324
1658
|
} else if (apiKey) {
|
|
1325
1659
|
console.log(`Running aw init at ${scope} with AWEB_API_KEY from the environment (key withheld).`);
|
|
1326
1660
|
run(["aw", "init"], scope, 120000, { secretSafe: true });
|