@awebai/oats 0.25.8 → 0.26.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 +8 -6
- package/bin/oats.mjs +576 -1714
- package/capabilities/oats-authoring/oats-package.json +2 -2
- package/capabilities/oats-authoring/oats.json +2 -2
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +334 -72
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
- package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
- package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
- package/capabilities/oats-aweb/oats.json +14 -4
- package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
- package/capabilities/oats-jira/oats.json +2 -2
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
- package/capabilities/oats-linear/oats.json +2 -2
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
- package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
- package/capabilities/oats-okf/lib/migration.mjs +2 -2
- package/capabilities/oats-okf/lib/sources.mjs +5 -4
- package/capabilities/oats-okf/lib/stores.mjs +40 -9
- package/capabilities/oats-okf/lib/worker.mjs +3 -3
- package/capabilities/oats-okf/oats.json +1 -1
- package/capabilities/oats-review/oats.json +3 -2
- package/docs/capabilities.md +218 -47
- package/docs/capability-manifest.schema.json +13 -4
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +16 -26
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
- package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
- package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
- package/docs/design/2026-09-20-redesign-program-board.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +54 -10
- package/docs/design/2026-09-24-phase-d-plan.md +77 -0
- package/docs/design/2026-09-25-teams-contract.md +226 -0
- package/docs/design/README.md +3 -3
- package/docs/design/launch-configurations.md +20 -16
- package/docs/design/operations-contract.md +27 -10
- package/docs/desktop-cli-api.md +546 -264
- package/docs/desktop-instance-start.md +1 -1
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +14 -17
- package/docs/implementation.md +28 -59
- package/docs/integrations.md +88 -32
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/knowledge-reference/package-craft.md +10 -8
- package/docs/knowledge-theory.md +1 -1
- package/docs/knowledge.md +10 -11
- package/docs/layers.md +16 -17
- package/docs/oats-local.schema.json +29 -1
- package/docs/oats-membership.schema.json +5 -3
- package/docs/oats-package.schema.json +2 -2
- package/docs/oats-workspace.schema.json +1 -1
- package/docs/{official-marketplace.md → official-catalog.md} +15 -16
- package/docs/packages.md +75 -52
- package/docs/release-notes/v0.22.0.md +1 -1
- package/docs/release-notes/v0.23.1.md +1 -1
- package/docs/release-notes/v0.25.9.md +23 -0
- package/docs/release-notes/v0.26.0.md +670 -0
- package/docs/schedules.md +48 -126
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +56 -43
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +1 -1
- package/injects/work-attached.md +1 -1
- package/injects/work-workspace.md +2 -2
- package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
- package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
- package/lib/capability-contract.mjs +110 -0
- package/lib/config-data.mjs +2 -2
- package/lib/core.mjs +700 -4824
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +396 -0
- package/lib/instance-lifecycle.mjs +3 -4
- package/lib/instance-resolution.mjs +212 -26
- package/lib/instruction-composition.mjs +0 -20
- package/lib/materialize.mjs +6 -4
- package/lib/operator-dispatch.mjs +33 -13
- package/lib/packages.mjs +25 -190
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +97 -272
- package/lib/servers.mjs +13 -13
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +125 -20
- package/package-catalog.json +7 -7
- package/package.json +1 -1
- package/skills/integration-authoring/SKILL.md +48 -40
- package/skills/oats-getting-started/SKILL.md +105 -110
- package/skills/oats-support/SKILL.md +2 -2
- package/skills/soul-craft/SKILL.md +13 -6
- package/bin/oats-pi-sdk-host.mjs +0 -17
- package/docs/2026-09-03-architecture-proposal.md +0 -642
- package/docs/artifact-approvals.schema.json +0 -7
- package/docs/captured-invocation-context.schema.json +0 -7
- package/docs/captured-resolution.schema.json +0 -7
- package/docs/design/package-engine-contract.md +0 -813
- package/docs/design/package-runtime-api.md +0 -588
- package/docs/desktop-succession.md +0 -57
- package/docs/execution-capsule.schema.json +0 -108
- package/docs/first-team-demo.md +0 -92
- package/docs/knowledge-migration.md +0 -147
- package/docs/migration-from-oas.md +0 -103
- package/docs/oats-config.schema.json +0 -172
- package/docs/oats-lock-v3.schema.json +0 -7
- package/docs/oats-lock.schema.json +0 -175
- package/docs/operating-team-migration.md +0 -470
- package/docs/portable.schema.json +0 -2512
- package/docs/provider-check-input.schema.json +0 -7
- package/docs/rebuild-to-v2.md +0 -511
- package/docs/workspace-adoption.md +0 -74
- package/injects/framework-workspace.md +0 -7
- package/injects/local-soul.md +0 -19
- package/injects/oats-portable.md +0 -20
- package/injects/oats.md +0 -11
- package/injects/portable-instance-boundary.md +0 -39
- package/injects/portable-work-directory.md +0 -29
- package/lib/artifact-approvals.mjs +0 -120
- package/lib/artifact-tree.mjs +0 -141
- package/lib/capability-artifacts.mjs +0 -179
- package/lib/capability-execution.mjs +0 -15
- package/lib/capability-inputs.mjs +0 -39
- package/lib/capability-provenance.mjs +0 -231
- package/lib/captured-action-shape.mjs +0 -21
- package/lib/captured-admission-shape.mjs +0 -20
- package/lib/captured-binding-file.mjs +0 -36
- package/lib/captured-dispatch.mjs +0 -66
- package/lib/captured-instance-index.mjs +0 -277
- package/lib/captured-invocation-context.mjs +0 -130
- package/lib/captured-launch-request.mjs +0 -66
- package/lib/captured-operation-process.mjs +0 -15
- package/lib/captured-pi-custody.mjs +0 -29
- package/lib/captured-pi-host.mjs +0 -167
- package/lib/captured-pi-outcome.mjs +0 -172
- package/lib/captured-resolutions.mjs +0 -275
- package/lib/captured-scaffold.mjs +0 -87
- package/lib/captured-selector.mjs +0 -28
- package/lib/captured-session-backend.mjs +0 -52
- package/lib/captured-source-receipt-file.mjs +0 -72
- package/lib/helper-injection-policy.mjs +0 -104
- package/lib/legacy-lock-codec.mjs +0 -106
- package/lib/manifest-settings.mjs +0 -84
- package/lib/package-closure.mjs +0 -48
- package/lib/package-materialization.mjs +0 -83
- package/lib/pi-sdk-host.mjs +0 -229
- package/lib/portable-artifacts.mjs +0 -115
- package/lib/portable-choices.mjs +0 -82
- package/lib/portable-composition.mjs +0 -136
- package/lib/portable-digest.mjs +0 -105
- package/lib/portable-identity.mjs +0 -40
- package/lib/portable-lock.mjs +0 -117
- package/lib/portable-onboarding-request.mjs +0 -49
- package/lib/portable-onboarding.mjs +0 -256
- package/lib/portable-package-preparation.mjs +0 -188
- package/lib/portable-policy.mjs +0 -44
- package/lib/portable-soul.mjs +0 -42
- package/lib/portable-state.mjs +0 -80
- package/lib/prepare-composition.mjs +0 -170
- package/lib/prepared-bindings.mjs +0 -92
- package/lib/prepared-resources.mjs +0 -127
- package/lib/provider-binding-broker.mjs +0 -65
- package/lib/provider-binding-wire.mjs +0 -116
- package/lib/readiness.mjs +0 -225
- package/lib/repository-observation.mjs +0 -226
- package/lib/resolution-shape.mjs +0 -393
- package/lib/schedule-capsule.mjs +0 -206
- package/lib/soul-constraints.mjs +0 -40
- package/lib/source-projection.mjs +0 -84
- package/lib/source-spec.mjs +0 -189
- package/lib/workspace-definition.mjs +0 -126
- package/lib/workspace-discovery.mjs +0 -146
- package/skills/oats/SKILL.md +0 -162
- package/skills/oats-config/SKILL.md +0 -164
- package/skills/oats-packages/SKILL.md +0 -184
- package/skills/oats-portable/SKILL.md +0 -115
- package/skills/oats-portable-artifacts/SKILL.md +0 -63
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* oats-aweb retire gracefully self-delete it (BEFORE the home dir is removed)
|
|
8
8
|
* oats-aweb roster list the aweb team's members — the cross-machine directory
|
|
9
9
|
* of live instances (alias = instance name) and humans
|
|
10
|
-
* oats-aweb setup guided onboarding: check the aw CLI, initialize the
|
|
11
|
-
*
|
|
10
|
+
* oats-aweb setup guided onboarding: check the aw CLI, initialize the
|
|
11
|
+
* messaging root with aw init / aw team join, verify team
|
|
12
12
|
*
|
|
13
13
|
* Env contract (set by the kernel):
|
|
14
14
|
* OATS_EVENT spawn|retire
|
|
@@ -43,7 +43,8 @@ import { join, dirname, resolve, delimiter, isAbsolute } from "node:path";
|
|
|
43
43
|
import { loadCapturedAwebExecution, requireCapturedAwebAction } from "../lib/captured-execution.mjs";
|
|
44
44
|
import { assessCapturedSessionReadiness, querySelectedKernel } from "../lib/session-readiness.mjs";
|
|
45
45
|
import { runCapturedNative } from "../lib/captured-native.mjs";
|
|
46
|
-
import { parseBindingJson } from "../lib/binding-wire.mjs";
|
|
46
|
+
import { CUSTODY_ATTACH_MIN, grantYamlCustodySocket, parseBindingJson } from "../lib/binding-wire.mjs";
|
|
47
|
+
import { custodyPreflight } from "../lib/grant-custody.mjs";
|
|
47
48
|
|
|
48
49
|
/** Run a command as ARGV — never a shell string. Team ids, aliases, instance
|
|
49
50
|
* names and invite tokens all flow through here; quoting them correctly is a
|
|
@@ -93,14 +94,20 @@ const parseSecretJson = (text, what) => {
|
|
|
93
94
|
* would read as "aw is missing" on every such host. */
|
|
94
95
|
/** The installed aw's version from `aw version` ("aw 1.36.1 ..."), or
|
|
95
96
|
* undefined when it cannot be read; compared as numeric triples. */
|
|
97
|
+
function awVersionTriple() {
|
|
98
|
+
try { return /aw\s+v?(\d+)\.(\d+)\.(\d+)/.exec(run(["aw", "version"], undefined, 10000)); } catch { return undefined; }
|
|
99
|
+
}
|
|
96
100
|
function awAtLeast(floor) {
|
|
97
|
-
|
|
98
|
-
try { v = /aw\s+v?(\d+)\.(\d+)\.(\d+)/.exec(run(["aw", "version"], undefined, 10000)); } catch { return false; }
|
|
101
|
+
const v = awVersionTriple();
|
|
99
102
|
if (!v) return false;
|
|
100
103
|
const a = v.slice(1, 4).map(Number), b = floor.split(".").map(Number);
|
|
101
104
|
for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] > b[i]; }
|
|
102
105
|
return true;
|
|
103
106
|
}
|
|
107
|
+
function awVersionLabel() {
|
|
108
|
+
const v = awVersionTriple();
|
|
109
|
+
return v ? v.slice(1, 4).join(".") : "unknown";
|
|
110
|
+
}
|
|
104
111
|
|
|
105
112
|
function onPath(cmd) {
|
|
106
113
|
for (const dir of String(process.env.PATH || "").split(delimiter)) {
|
|
@@ -137,7 +144,7 @@ try {
|
|
|
137
144
|
const manifest = JSON.parse(readFileSync(new URL("../oats.json", import.meta.url), "utf8"));
|
|
138
145
|
const selected = requireCapturedAwebAction(loaded, event, manifest);
|
|
139
146
|
const settings = parseBindingJson(Buffer.from(process.env.OATS_SETTINGS || "{}"));
|
|
140
|
-
if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k =>
|
|
147
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k => !["delivery", "team", "root", "roots"].includes(k))) throw new Error("captured settings support delivery/team/root readiness only; no identity copying or ambient fallback");
|
|
141
148
|
const checked = assessCapturedSessionReadiness({ binding: selected.binding, invocation: selected.context, settings }, {
|
|
142
149
|
query(args, options) { selected.assertCurrent(); const result = querySelectedKernel(args, options); selected.assertCurrent(); return result; },
|
|
143
150
|
});
|
|
@@ -152,6 +159,9 @@ try {
|
|
|
152
159
|
const event = process.env.OATS_EVENT || process.argv[2];
|
|
153
160
|
const instance = process.env.OATS_INSTANCE;
|
|
154
161
|
const home = process.env.OATS_HOME || process.cwd();
|
|
162
|
+
const AWEB_ALIAS_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
163
|
+
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 '_'";
|
|
164
|
+
const ALIAS_REUSE_REMEDY = "spawn with a different --name (kernels 0.26.0+) or a different --purpose";
|
|
155
165
|
// Effective capability settings, injected by kernel dispatch (OATS_SETTINGS).
|
|
156
166
|
// delivery: "channel" (default) keeps the native channel packages waking the
|
|
157
167
|
// instance; "session" hands delivery to the host wake broker (aweb-abil):
|
|
@@ -166,6 +176,7 @@ const deliveryMode = (() => {
|
|
|
166
176
|
const identitySettings = settings.identity && typeof settings.identity === "object" && !Array.isArray(settings.identity) ? settings.identity : {};
|
|
167
177
|
const identityMode = identitySettings.mode === undefined || identitySettings.mode === null || identitySettings.mode === "" ? "local" : String(identitySettings.mode);
|
|
168
178
|
if (!["local", "global"].includes(identityMode) && ["spawn", "retire"].includes(event)) fatal(`identity.mode must be either "local" or "global" (got ${JSON.stringify(identitySettings.mode)})`);
|
|
179
|
+
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`);
|
|
169
180
|
const payloadTeam = () => {
|
|
170
181
|
const fromSettings = typeof settings.team === "string" && settings.team.trim() ? settings.team.trim() : undefined;
|
|
171
182
|
const fromEnv = process.env.OATS_TEAM_ID || process.env.OATS_TEAM_NAME || undefined;
|
|
@@ -194,7 +205,7 @@ function gitRootOf(startDir) {
|
|
|
194
205
|
d = parent;
|
|
195
206
|
}
|
|
196
207
|
}
|
|
197
|
-
function
|
|
208
|
+
function classicAwebRoot() {
|
|
198
209
|
const candidates = [];
|
|
199
210
|
const push = (p) => { if (p && !candidates.includes(resolve(p))) candidates.push(resolve(p)); };
|
|
200
211
|
push(process.env.OATS_TEAM_SCOPE);
|
|
@@ -206,6 +217,36 @@ function awebRoot() {
|
|
|
206
217
|
for (const c of candidates) if (existsSync(join(c, ".aw"))) return c;
|
|
207
218
|
return undefined;
|
|
208
219
|
}
|
|
220
|
+
const hasWorkspaceV2Facts = () => !!(process.env.OATS_WORKSPACE_KEY || process.env.OATS_WORKSPACE_NAME || process.env.OATS_TEAM_LABEL);
|
|
221
|
+
const isClassicDeployment = () => !!process.env.OATS_TEAM_SCOPE && !hasWorkspaceV2Facts();
|
|
222
|
+
const teamConfigRemedy = () => `set messaging.byTeam.<label>.team in the workspace file or settings.oats.aweb.team${isClassicDeployment() ? " (classic: set team.id in oats-config.yaml)" : ""}`;
|
|
223
|
+
function declaredRootCandidate(team = payloadTeam().team) {
|
|
224
|
+
const roots = settings.roots && typeof settings.roots === "object" && !Array.isArray(settings.roots) ? settings.roots : {};
|
|
225
|
+
if (team && typeof roots[team] === "string" && roots[team].trim()) return { root: roots[team].trim(), key: `settings.oats.aweb.roots[${JSON.stringify(team)}]`, declared: true };
|
|
226
|
+
if (typeof settings.root === "string" && settings.root.trim()) return { root: settings.root.trim(), key: "settings.oats.aweb.root", declared: true };
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
function rootSettingCandidate(team = payloadTeam().team) {
|
|
230
|
+
const declared = declaredRootCandidate(team);
|
|
231
|
+
if (declared) return declared;
|
|
232
|
+
const fallback = process.env.OATS_WORKSPACE || (!isClassicDeployment() ? process.env.OATS_TEAM_SCOPE : undefined) || process.cwd();
|
|
233
|
+
return fallback ? { root: fallback, key: "settings.oats.aweb.root", declared: false } : undefined;
|
|
234
|
+
}
|
|
235
|
+
function awebRootProblem(candidate) {
|
|
236
|
+
if (!candidate?.root) return `no messaging root at ${process.cwd()}: run oats aweb setup there or set settings.oats.aweb.root`;
|
|
237
|
+
if (!isAbsolute(candidate.root)) return `${candidate.key} must be an absolute directory whose .aw is the aweb minting root`;
|
|
238
|
+
return `no messaging root at ${resolve(candidate.root)}: run oats aweb setup there or set ${candidate.key}`;
|
|
239
|
+
}
|
|
240
|
+
function resolveAwebRoot() {
|
|
241
|
+
const declared = declaredRootCandidate();
|
|
242
|
+
if (declared && isAbsolute(declared.root) && existsSync(join(resolve(declared.root), ".aw"))) return resolve(declared.root);
|
|
243
|
+
if (declared?.declared) return undefined;
|
|
244
|
+
if (isClassicDeployment()) return classicAwebRoot();
|
|
245
|
+
const candidate = rootSettingCandidate();
|
|
246
|
+
if (candidate && isAbsolute(candidate.root) && existsSync(join(resolve(candidate.root), ".aw"))) return resolve(candidate.root);
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
function awebRoot() { return resolveAwebRoot(); }
|
|
209
250
|
|
|
210
251
|
/** Team memberships from `aw team list --json`. The current CLI returns
|
|
211
252
|
* `memberships`; older output used `teams`. Spawn resolution and `oats aweb
|
|
@@ -253,8 +294,23 @@ function wakeRegister(instanceHome, identityHome) {
|
|
|
253
294
|
function wakeDeregister(instanceHome) {
|
|
254
295
|
try { run(["aw", "wake", "deregister", "--home", instanceHome], instanceHome, 60000); return true; } catch { return false; }
|
|
255
296
|
}
|
|
256
|
-
|
|
297
|
+
// The first aw release whose grant subtree accepts `aw id grant mint --team
|
|
298
|
+
// <team-id>` (and the full normal/reviewer scope lists): aw 1.36.2, paired with
|
|
299
|
+
// aweb server 1.27.5 for complete custody operations. aw 1.36.1 has neither.
|
|
300
|
+
const GRANT_TEAM_FLAG_MIN = "1.36.2";
|
|
301
|
+
const NORMAL_GRANT_SCOPES = ["mail.read", "mail.send", "chat.read", "chat.send", "events.read", "coord.read", "coord.write", "presence.write", "contacts.read", "contacts.write"];
|
|
302
|
+
const REVIEWER_GRANT_SCOPES = ["mail.read", "chat.read", "events.read", "coord.read", "presence.write"];
|
|
257
303
|
const residentKeyHint = (name) => `oats-local.yaml settings.oats.aweb.residents.${name || "<name>"}`;
|
|
304
|
+
const grantProfile = () => identitySettings.profile === undefined || identitySettings.profile === null || identitySettings.profile === "" ? "normal" : String(identitySettings.profile);
|
|
305
|
+
function grantScopes() {
|
|
306
|
+
if (Array.isArray(identitySettings.scopes) && identitySettings.scopes.length) return identitySettings.scopes.map(String);
|
|
307
|
+
const profile = grantProfile();
|
|
308
|
+
if (profile === "normal") return [...NORMAL_GRANT_SCOPES];
|
|
309
|
+
if (profile === "reviewer") return [...REVIEWER_GRANT_SCOPES];
|
|
310
|
+
fatal(`identity.profile must be one of "normal" or "reviewer" (got ${JSON.stringify(identitySettings.profile)})`);
|
|
311
|
+
}
|
|
312
|
+
const grantE2eeRequired = () => identitySettings.e2ee !== false;
|
|
313
|
+
const grantRenewMode = () => identitySettings.renew === undefined || identitySettings.renew === null || identitySettings.renew === "" ? "off" : String(identitySettings.renew);
|
|
258
314
|
function resolveResidentCustody(name) {
|
|
259
315
|
if (!name) fatal(`identity.mode "global" requires identity.resident; set ${residentKeyHint("<name>")} to the absolute custody directory for that resident identity`);
|
|
260
316
|
const residents = settings.residents && typeof settings.residents === "object" && !Array.isArray(settings.residents) ? settings.residents : {};
|
|
@@ -284,6 +340,21 @@ function revokeGrant(custody, grantId) {
|
|
|
284
340
|
throw e;
|
|
285
341
|
}
|
|
286
342
|
}
|
|
343
|
+
function grantYamlNestedScalar(text, sectionName, key) {
|
|
344
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
345
|
+
let inSection = false, baseIndent = 0;
|
|
346
|
+
for (const line of lines) {
|
|
347
|
+
const section = new RegExp(`^(\\s*)${sectionName}:\\s*(?:#.*)?$`).exec(line);
|
|
348
|
+
if (section) { inSection = true; baseIndent = section[1].length; continue; }
|
|
349
|
+
if (inSection) {
|
|
350
|
+
const ind = /^(\s*)/.exec(line)?.[1].length || 0;
|
|
351
|
+
if (line.trim() && ind <= baseIndent) inSection = false;
|
|
352
|
+
const value = new RegExp(`^\\s*${key}:\\s*["']?([^"'\\n#]+)["']?\\s*$`).exec(line);
|
|
353
|
+
if (value) return value[1].trim();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
287
358
|
function recoverGrantHome(grantHome) {
|
|
288
359
|
try {
|
|
289
360
|
const text = readFileSync(join(grantHome, "grant.yaml"), "utf8");
|
|
@@ -291,9 +362,120 @@ function recoverGrantHome(grantHome) {
|
|
|
291
362
|
const m = text.match(new RegExp(`^\\s*${key}:\\s*["']?([^"'\\n#]+)["']?\\s*$`, "m"));
|
|
292
363
|
return m ? m[1].trim() : undefined;
|
|
293
364
|
};
|
|
294
|
-
return { grantId: scalar("grant_id"), team: scalar("team_id"), expiresAt: scalar("expires_at") };
|
|
365
|
+
return { grantId: scalar("grant_id"), team: scalar("team_id"), expiresAt: scalar("expires_at"), subjectAlias: grantYamlNestedScalar(text, "subject", "alias"), custodySocket: grantYamlCustodySocket(text) };
|
|
295
366
|
} catch { return {}; }
|
|
296
367
|
}
|
|
368
|
+
function readGrantYamlCustodySocket(grantHome) {
|
|
369
|
+
const text = readFileSync(join(grantHome, "grant.yaml"), "utf8");
|
|
370
|
+
return grantYamlCustodySocket(text);
|
|
371
|
+
}
|
|
372
|
+
function verifyGrantCustodyAttachment({ grantHome, custodySocket, alias, team }) {
|
|
373
|
+
const recorded = readGrantYamlCustodySocket(grantHome);
|
|
374
|
+
if (!recorded) throw new Error(`grant.yaml custody.socket_path is missing`);
|
|
375
|
+
if (recorded !== custodySocket) throw new Error(`grant.yaml custody.socket_path ${recorded} differs from preflight ${custodySocket}`);
|
|
376
|
+
const raw = run(["aw", "custody", "status", "--json"], grantHome, 20000, { env: { AWEB_IDENTITY_HOME: grantHome }, secretSafe: true });
|
|
377
|
+
const status = parseAwJson(raw, "aw custody status");
|
|
378
|
+
if (String(status.status || "") !== "running") throw new Error(`grant custody status is ${status.status || "unknown"}, not running`);
|
|
379
|
+
if (status.socket_path !== custodySocket) throw new Error(`grant custody status socket_path ${status.socket_path || "<missing>"} differs from preflight ${custodySocket}`);
|
|
380
|
+
if (status.resident?.alias !== alias) throw new Error(`grant custody resident alias ${status.resident?.alias || "<missing>"} differs from ${alias}`);
|
|
381
|
+
const teamRow = (Array.isArray(status.teams) ? status.teams : []).find((t) => t && (t.team_id || t.id) === team);
|
|
382
|
+
if (!teamRow || teamRow.ready !== true) throw new Error(`grant custody team ${team} is not ready`);
|
|
383
|
+
return status;
|
|
384
|
+
}
|
|
385
|
+
function preflightCustodySocket(preflight) {
|
|
386
|
+
return typeof preflight.status?.socket_path === "string" && preflight.status.socket_path.trim() ? preflight.status.socket_path.trim() : undefined;
|
|
387
|
+
}
|
|
388
|
+
function requirePreflightCustodySocket(preflight) {
|
|
389
|
+
const socket = preflightCustodySocket(preflight);
|
|
390
|
+
if (!socket) fatal("custody preflight reported no socket_path, so the grant cannot be attached to custody");
|
|
391
|
+
return socket;
|
|
392
|
+
}
|
|
393
|
+
function grantHomeDirs() {
|
|
394
|
+
try {
|
|
395
|
+
return readdirSync(home)
|
|
396
|
+
.filter((name) => name === ".aweb-identity" || /^\.aweb-identity-\d+$/.test(name))
|
|
397
|
+
.map((name) => join(home, name))
|
|
398
|
+
.filter((p) => { try { return statSync(p).isDirectory(); } catch { return false; } });
|
|
399
|
+
} catch { return []; }
|
|
400
|
+
}
|
|
401
|
+
function removeGrantHomes() { for (const p of grantHomeDirs()) { try { rmSync(p, { recursive: true, force: true }); } catch { /* best effort */ } } }
|
|
402
|
+
function newestGrantHome() {
|
|
403
|
+
const dirs = grantHomeDirs().map((p) => {
|
|
404
|
+
try { return { path: p, mtime: statSync(p).mtimeMs }; } catch { return { path: p, mtime: 0 }; }
|
|
405
|
+
}).sort((a, b) => b.mtime - a.mtime || b.path.localeCompare(a.path));
|
|
406
|
+
return dirs[0]?.path || join(home, ".aweb-identity");
|
|
407
|
+
}
|
|
408
|
+
const priorGrantHome = (meta = {}) => typeof meta.identity?.grant?.home === "string" && meta.identity.grant.home ? meta.identity.grant.home : newestGrantHome();
|
|
409
|
+
function retainedLaunchOutput(meta = {}, identityHome = priorGrantHome(meta)) {
|
|
410
|
+
const delivery = meta.delivery || deliveryMode;
|
|
411
|
+
const env = {};
|
|
412
|
+
if (delivery === "session") env.AWEB_DELIVERY = "session";
|
|
413
|
+
if (meta.identity?.mode === "global" && meta.identity?.grant?.id) env.AWEB_IDENTITY_HOME = identityHome;
|
|
414
|
+
const launch = (process.env.OATS_RUNTIME || "") === "claude" && delivery === "channel" ? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" } : undefined;
|
|
415
|
+
return { ...(Object.keys(env).length ? { env } : {}), ...(launch ? { launch } : {}) };
|
|
416
|
+
}
|
|
417
|
+
function grantMintArgv({ team, scopes, ttl, grantHome, custodySocket }) {
|
|
418
|
+
return ["aw", "id", "grant", "mint", ...(awAtLeast(GRANT_TEAM_FLAG_MIN) ? ["--team", team] : []), "--scope", scopes.join(","), "--ttl", ttl, "--label", `oats:${instance}`, "--out", grantHome, "--custody-socket", custodySocket, "--json"];
|
|
419
|
+
}
|
|
420
|
+
function validateMintedGrant(minted, grantHome) {
|
|
421
|
+
const grantId = typeof minted.grant_id === "string" ? minted.grant_id : undefined;
|
|
422
|
+
const expiresAt = typeof minted.expires_at === "string" ? minted.expires_at : undefined;
|
|
423
|
+
const mintedTeam = typeof minted.team_id === "string" ? minted.team_id : undefined;
|
|
424
|
+
const mintedOut = typeof minted.out === "string" ? minted.out : undefined;
|
|
425
|
+
if (!grantId || !expiresAt || !mintedTeam || !mintedOut) throw new Error("aw id grant mint JSON lacked grant_id, expires_at, team_id, or out");
|
|
426
|
+
if (resolve(mintedOut) !== resolve(grantHome)) throw new Error(`aw id grant mint wrote ${mintedOut}, not ${grantHome}`);
|
|
427
|
+
return { minted, grantId, expiresAt, mintedTeam, alias: typeof minted.alias === "string" && minted.alias ? minted.alias : undefined, address: typeof minted.address === "string" && minted.address ? minted.address : null };
|
|
428
|
+
}
|
|
429
|
+
function parseMintedGrant(raw, grantHome) { return validateMintedGrant(parseAwJson(raw, "aw id grant mint"), grantHome); }
|
|
430
|
+
function globalGrantRenew() {
|
|
431
|
+
const mode = grantRenewMode();
|
|
432
|
+
const oldMeta = JSON.parse(process.env.OATS_META || "{}");
|
|
433
|
+
if (mode === "off") out(retainedLaunchOutput(oldMeta));
|
|
434
|
+
if (mode !== "launch") fatal(`identity.renew must be "off" or "launch" (got ${JSON.stringify(identitySettings.renew)})`);
|
|
435
|
+
if (oldMeta.identity?.mode !== "global" || !oldMeta.identity?.grant?.id) out(retainedLaunchOutput(oldMeta));
|
|
436
|
+
const resident = String(identitySettings.resident || oldMeta.identity.resident || "");
|
|
437
|
+
const custody = resolveResidentCustody(resident);
|
|
438
|
+
const team = payloadTeam().team || oldMeta.identity.team;
|
|
439
|
+
if (!team) fatal("identity.renew launch needs the prior grant team or settings.oats.aweb.team");
|
|
440
|
+
const scopes = grantScopes();
|
|
441
|
+
const ttl = identitySettings.ttl === undefined || identitySettings.ttl === null || identitySettings.ttl === "" ? "8h" : String(identitySettings.ttl);
|
|
442
|
+
const oldHome = priorGrantHome(oldMeta);
|
|
443
|
+
let preflight;
|
|
444
|
+
try { preflight = custodyPreflight({ custody, resident, team, e2eeRequired: grantE2eeRequired(), fatalOnError: false, runAw: (argv, cwd, options) => run(argv, cwd, 60000, options), fatal }); }
|
|
445
|
+
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}` }); }
|
|
446
|
+
const custodySocket = preflightCustodySocket(preflight);
|
|
447
|
+
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}` });
|
|
448
|
+
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}` });
|
|
449
|
+
let stamp = Math.floor(Date.now() / 1000);
|
|
450
|
+
let grantHome = join(home, `.aweb-identity-${stamp}`);
|
|
451
|
+
while (existsSync(grantHome)) grantHome = join(home, `.aweb-identity-${++stamp}`);
|
|
452
|
+
let parsed;
|
|
453
|
+
try {
|
|
454
|
+
parsed = parseMintedGrant(run(grantMintArgv({ team, scopes, ttl, grantHome, custodySocket }), custody, 60000, { unsetEnv: ["AWEB_IDENTITY_HOME"] }), grantHome);
|
|
455
|
+
} catch (e) {
|
|
456
|
+
try { rmSync(grantHome, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
457
|
+
out({ meta: oldMeta, ...retainedLaunchOutput(oldMeta, oldHome), warning: `oats-aweb: renewal mint failed (${e.message || e}); keeping previous grant ${oldMeta.identity.grant.id}` });
|
|
458
|
+
}
|
|
459
|
+
const { grantId, expiresAt, mintedTeam, alias: mintedAlias, address } = parsed;
|
|
460
|
+
const recovered = recoverGrantHome(grantHome);
|
|
461
|
+
const alias = recovered.subjectAlias || mintedAlias || oldMeta.identity.alias || resident;
|
|
462
|
+
const newMeta = { ...oldMeta, delivery: oldMeta.delivery || deliveryMode, identity: identityMeta({ mode: "global", alias, team: mintedTeam, address: address || oldMeta.identity.address || null, resident, grant: { id: grantId, expiresAt, scopes, home: grantHome } }) };
|
|
463
|
+
if (mintedTeam !== team) {
|
|
464
|
+
try { revokeGrant(custody, grantId); } catch { /* minted mismatch expires by TTL if revoke fails */ }
|
|
465
|
+
try { rmSync(grantHome, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
466
|
+
out({ meta: oldMeta, ...retainedLaunchOutput(oldMeta, oldHome), warning: `oats-aweb: renewal minted grant team ${mintedTeam} differs from ${team}; keeping previous grant ${oldMeta.identity.grant.id}` });
|
|
467
|
+
}
|
|
468
|
+
try { verifyGrantCustodyAttachment({ grantHome, custodySocket, alias: newMeta.identity.alias, team: mintedTeam }); }
|
|
469
|
+
catch (e) {
|
|
470
|
+
try { revokeGrant(custody, grantId); } catch { /* new grant expires by TTL if revoke fails */ }
|
|
471
|
+
try { rmSync(grantHome, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
472
|
+
out({ meta: oldMeta, ...retainedLaunchOutput(oldMeta, oldHome), warning: `oats-aweb: renewal grant ${grantId} custody attachment failed (${e.message || e}); keeping previous grant ${oldMeta.identity.grant.id}` });
|
|
473
|
+
}
|
|
474
|
+
let warning;
|
|
475
|
+
try { revokeGrant(custody, oldMeta.identity.grant.id); }
|
|
476
|
+
catch (e) { warning = `oats-aweb: previous grant ${oldMeta.identity.grant.id} was not revoked (${e.message || e}); new grant ${grantId} is kept and the previous grant still expires at ${oldMeta.identity.grant.expiresAt || "its TTL"}`; }
|
|
477
|
+
out({ meta: newMeta, ...retainedLaunchOutput(newMeta, grantHome), ...(warning ? { warning } : {}) });
|
|
478
|
+
}
|
|
297
479
|
function globalGrantSpawn() {
|
|
298
480
|
const { team, payload, env: envTeam } = payloadTeam();
|
|
299
481
|
if (!team) fatal("identity.mode \"global\" requires settings.oats.aweb.team (or OATS_TEAM_ID/OATS_TEAM_NAME) before minting a grant");
|
|
@@ -301,37 +483,40 @@ function globalGrantSpawn() {
|
|
|
301
483
|
const custody = resolveResidentCustody(resident);
|
|
302
484
|
const grantHome = join(home, ".aweb-identity");
|
|
303
485
|
if (existsSync(grantHome)) fatal(`${grantHome} already exists; refusing to overwrite an existing aweb session grant home`);
|
|
304
|
-
const scopes =
|
|
486
|
+
const scopes = grantScopes();
|
|
305
487
|
const ttl = identitySettings.ttl === undefined || identitySettings.ttl === null || identitySettings.ttl === "" ? "8h" : String(identitySettings.ttl);
|
|
488
|
+
const preflight = custodyPreflight({ custody, resident, team, e2eeRequired: grantE2eeRequired(), runAw: (argv, cwd, options) => run(argv, cwd, 60000, options), fatal });
|
|
489
|
+
const custodySocket = requirePreflightCustodySocket(preflight);
|
|
490
|
+
if (!awAtLeast(CUSTODY_ATTACH_MIN)) fatal(`aw ${awVersionLabel()} cannot attach a grant to custody (--custody-socket); grants need aw >= ${CUSTODY_ATTACH_MIN}`);
|
|
306
491
|
let meta;
|
|
307
492
|
const cleanup = () => { try { rmSync(grantHome, { recursive: true, force: true }); } catch { /* best effort */ } };
|
|
308
493
|
const failAfterMint = (message, code = 1) => { cleanup(); out({ ...(meta ? { meta } : {}), warning: `oats-aweb: ${String(message).slice(0, 300)}` }, code); };
|
|
309
494
|
try {
|
|
310
|
-
const raw = run(
|
|
495
|
+
const raw = run(grantMintArgv({ team, scopes, ttl, grantHome, custodySocket }), custody, 60000, { unsetEnv: ["AWEB_IDENTITY_HOME"] });
|
|
311
496
|
let minted;
|
|
312
|
-
try { minted =
|
|
497
|
+
try { minted = parseMintedGrant(raw, grantHome).minted; }
|
|
313
498
|
catch (parseError) {
|
|
314
499
|
const recovered = recoverGrantHome(grantHome);
|
|
315
500
|
if (recovered.grantId) {
|
|
316
|
-
meta = { delivery: deliveryMode, identity: identityMeta({ mode: "global", alias: resident, team: recovered.team || team, resident, grant: { id: recovered.grantId, expiresAt: recovered.expiresAt || "unknown", scopes } }) };
|
|
501
|
+
meta = { delivery: deliveryMode, identity: identityMeta({ mode: "global", alias: resident, team: recovered.team || team, resident, grant: { id: recovered.grantId, expiresAt: recovered.expiresAt || "unknown", scopes, home: grantHome } }) };
|
|
317
502
|
try { revokeGrant(custody, recovered.grantId); failAfterMint(`${parseError.message}; recovered grant ${recovered.grantId} from grant.yaml, revoked it, and removed the grant home`); }
|
|
318
503
|
catch (revokeError) { failAfterMint(`${parseError.message}; recovered grant ${recovered.grantId} from grant.yaml, but revoke failed: ${revokeError.message || revokeError}`); }
|
|
319
504
|
}
|
|
320
505
|
throw parseError;
|
|
321
506
|
}
|
|
322
|
-
const grantId
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
if (!grantId || !expiresAt || !mintedTeam || !mintedOut) throw new Error("aw id grant mint JSON lacked grant_id, expires_at, team_id, or out");
|
|
327
|
-
if (resolve(mintedOut) !== resolve(grantHome)) throw new Error(`aw id grant mint wrote ${mintedOut}, not ${grantHome}`);
|
|
328
|
-
const alias = typeof minted.alias === "string" && minted.alias ? minted.alias : resident;
|
|
329
|
-
const address = typeof minted.address === "string" && minted.address ? minted.address : null;
|
|
330
|
-
meta = { delivery: deliveryMode, identity: identityMeta({ mode: "global", alias, team: mintedTeam, address, resident, grant: { id: grantId, expiresAt, scopes } }) };
|
|
507
|
+
const { grantId, expiresAt, mintedTeam, alias: mintedAlias, address } = validateMintedGrant(minted, grantHome);
|
|
508
|
+
const recovered = recoverGrantHome(grantHome);
|
|
509
|
+
const alias = recovered.subjectAlias || mintedAlias || resident;
|
|
510
|
+
meta = { delivery: deliveryMode, identity: identityMeta({ mode: "global", alias, team: mintedTeam, address, resident, grant: { id: grantId, expiresAt, scopes, home: grantHome } }) };
|
|
331
511
|
if (mintedTeam !== team) {
|
|
332
512
|
try { revokeGrant(custody, grantId); failAfterMint(`minted grant team ${mintedTeam} differs from ${team}; the grant was revoked and nothing was kept`); }
|
|
333
513
|
catch (e) { failAfterMint(`minted grant team ${mintedTeam} differs from ${team}; revoke failed: ${e.message || e}`); }
|
|
334
514
|
}
|
|
515
|
+
try { verifyGrantCustodyAttachment({ grantHome, custodySocket, alias, team: mintedTeam }); }
|
|
516
|
+
catch (e) {
|
|
517
|
+
try { revokeGrant(custody, grantId); failAfterMint(`minted grant ${grantId} custody attachment failed: ${e.message || e}; the grant was revoked and nothing was kept`); }
|
|
518
|
+
catch (revokeError) { failAfterMint(`minted grant ${grantId} custody attachment failed: ${e.message || e}; revoke failed: ${revokeError.message || revokeError}`); }
|
|
519
|
+
}
|
|
335
520
|
const launch = (process.env.OATS_RUNTIME || "") === "claude" && deliveryMode === "channel"
|
|
336
521
|
? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" }
|
|
337
522
|
: undefined;
|
|
@@ -343,15 +528,16 @@ function globalGrantSpawn() {
|
|
|
343
528
|
catch (revokeError) { failAfterMint(`session delivery registration failed for minted grant ${grantId}: ${e.message || e}; revoke failed: ${revokeError.message || revokeError}`); }
|
|
344
529
|
}
|
|
345
530
|
}
|
|
346
|
-
const warnings = [];
|
|
531
|
+
const warnings = [...preflight.warnings];
|
|
347
532
|
if (payload && envTeam && payload !== envTeam) warnings.push(`oats-aweb: settings.oats.aweb.team ${payload} differs from OATS team ${envTeam}; using payload team`);
|
|
533
|
+
const e2eeBrief = preflight.warnings.length ? ` Warning: ${preflight.warnings.join(" ")}` : "";
|
|
348
534
|
const deliveryBrief = deliveryMode === "session"
|
|
349
535
|
? ` 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.`
|
|
350
536
|
: "";
|
|
351
537
|
out({
|
|
352
538
|
meta,
|
|
353
539
|
env,
|
|
354
|
-
brief: `Comms: you act as resident aweb identity "${alias}" on team ${mintedTeam} through a session grant for ${resident}; scopes: ${scopes.join(", ")}; expires: ${expiresAt}. Root keys are not in this home, and identity lifecycle commands are not yours to run.${deliveryBrief} Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill); coordination stays in your deployment's task layer.`,
|
|
540
|
+
brief: `Comms: you act as resident aweb identity "${alias}" on team ${mintedTeam} through a session grant for ${resident}; scopes: ${scopes.join(", ")}; expires: ${expiresAt}. Root keys are not in this home, and identity lifecycle commands are not yours to run; your grant home is attached to the resident's custody service. If a message you sent shows unverified at the receiver, report it, do not retry.${e2eeBrief}${deliveryBrief} Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill); coordination stays in your deployment's task layer.`,
|
|
355
541
|
...(launch ? { launch } : {}),
|
|
356
542
|
...(warnings.length ? { warning: warnings.join(" | ") } : {}),
|
|
357
543
|
});
|
|
@@ -368,8 +554,10 @@ function globalGrantRetire(meta) {
|
|
|
368
554
|
const custody = resolveResidentCustody(resident);
|
|
369
555
|
try {
|
|
370
556
|
revokeGrant(custody, id);
|
|
557
|
+
removeGrantHomes();
|
|
371
558
|
out({ meta: { retired: true, identityRevoked: true, grant: id } });
|
|
372
559
|
} catch (e) {
|
|
560
|
+
removeGrantHomes();
|
|
373
561
|
out({ meta: { retired: false, reason: "grant-revoke-failed", grant: id }, warning: `oats-aweb: grant ${id} was not revoked (${e.message || e}); it still expires at ${meta.identity?.grant?.expiresAt || "its TTL"}` }, 1);
|
|
374
562
|
}
|
|
375
563
|
}
|
|
@@ -377,7 +565,7 @@ const seatLockPath = (source) => join(dirname(source), ".aw-retained-seat.json")
|
|
|
377
565
|
/** The alias a home's .aw/workspace.yaml records under memberships (indented),
|
|
378
566
|
* or undefined. Read only when the hook has no alias of its own. */
|
|
379
567
|
const workspaceAliasOf = (homeDir) => {
|
|
380
|
-
try { const m = readFileSync(join(homeDir, ".aw", "workspace.yaml"), "utf8").match(/^\s*alias:\s*["']?([a-z0-9][a-z0-
|
|
568
|
+
try { const m = readFileSync(join(homeDir, ".aw", "workspace.yaml"), "utf8").match(/^\s*alias:\s*["']?([a-z0-9][a-z0-9_-]{0,63})["']?\s*$/mi); return m ? m[1] : undefined; }
|
|
381
569
|
catch { return undefined; }
|
|
382
570
|
};
|
|
383
571
|
/** A join that the CLI reported as failed (or that this hook killed on
|
|
@@ -411,9 +599,9 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
411
599
|
const service = process.env.OATS_AWEB_URL || yamlScalar(srcWorkspace, "aweb_url");
|
|
412
600
|
if (!service) fatal(`cannot determine the aweb service for ${source} (no aweb_url in its workspace.yaml)`);
|
|
413
601
|
const role = yamlScalar(srcWorkspace, "role_name");
|
|
414
|
-
let team =
|
|
602
|
+
let team = payloadTeam().team;
|
|
415
603
|
if (!team && existsSync(join(source, "teams.yaml"))) team = yamlScalar(readFileSync(join(source, "teams.yaml"), "utf8"), "active_team") || yamlScalar(readFileSync(join(source, "teams.yaml"), "utf8"), "active");
|
|
416
|
-
if (!team || !team.includes(":")) fatal(`cannot determine the team for the retained identity (
|
|
604
|
+
if (!team || !team.includes(":")) fatal(`cannot determine the team for the retained identity (${teamConfigRemedy()}, or keep an active team in ${join(source, "teams.yaml")})`);
|
|
417
605
|
const dest = join(home, ".aw");
|
|
418
606
|
const legacyHome = dirname(source);
|
|
419
607
|
// The lock is taken FIRST: a concurrent second spawn must see it before any
|
|
@@ -483,14 +671,14 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
483
671
|
if (expectedDid && shownDid !== expectedDid) throw new Error(`aw whoami shows did ${shownDid || "(none)"}, not the retained identity's ${expectedDid}; the seat is not the same identity`);
|
|
484
672
|
if (expectedAddress && shownAddress !== expectedAddress) throw new Error(`aw whoami shows address ${shownAddress || "(none)"}, not the retained identity's ${expectedAddress}; the seat is not the same identity`);
|
|
485
673
|
const aliasRaw = String(ws.alias || st.alias || (expectedAddress || "").split("/").pop() || instance);
|
|
486
|
-
if (
|
|
674
|
+
if (!AWEB_ALIAS_RE.test(aliasRaw)) throw new Error(`aw workspace status reports an alias that is not a plausible alias; the seat is not briefed`);
|
|
487
675
|
const alias = aliasRaw;
|
|
488
676
|
if (expectedAddress && !expectedAddress.endsWith(`/${alias}`)) throw new Error(`aw workspace status shows alias ${alias}, not the retained identity's address ${expectedAddress}; the seat is not the same identity`);
|
|
489
677
|
writeFileSync(lockPath, JSON.stringify({ home, instance, alias, team, takenAt: new Date().toISOString(), host: hostname(), ...(takenOver ? { tookOverFrom: takenOver } : {}) }, null, 2) + "\n", { mode: 0o600 });
|
|
490
678
|
const launch = (process.env.OATS_RUNTIME || "") === "claude" && deliveryMode === "channel"
|
|
491
679
|
? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" }
|
|
492
680
|
: undefined;
|
|
493
|
-
const env = deliveryMode === "session" ? { AWEB_DELIVERY: "session" } :
|
|
681
|
+
const env = { ...(deliveryMode === "session" ? { AWEB_DELIVERY: "session" } : {}), AWEB_IDENTITY_HOME: dest };
|
|
494
682
|
const deliveryBrief = deliveryMode === "session"
|
|
495
683
|
? ` Notification delivery: external (AWEB_DELIVERY=session); until the host wake broker registers this instance NOTHING wakes you: check \`aw mail inbox\` and \`aw chat pending\` at every task boundary.`
|
|
496
684
|
: "";
|
|
@@ -500,7 +688,7 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
500
688
|
if (hostNote) warnings.push(`oats-aweb: seated${hostNote}`);
|
|
501
689
|
out({
|
|
502
690
|
meta: { team, alias, retained: true, source, lock: lockPath, delivery: deliveryMode, identity: identityMeta({ mode: "global", alias, team, address: shownAddress || expectedAddress || null }), ...(takenOver ? { tookOverFrom: takenOver } : {}) },
|
|
503
|
-
|
|
691
|
+
env,
|
|
504
692
|
brief: `Comms: you are the retained seat of the existing aweb identity "${alias}" on team ${team} (same did and address as the seat you replace; its contacts, routes and conversations are yours).${deliveryBrief} Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill).`,
|
|
505
693
|
...(launch ? { launch } : {}),
|
|
506
694
|
...(warnings.length ? { warning: warnings.join(" | ") } : {}),
|
|
@@ -511,13 +699,20 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
511
699
|
}
|
|
512
700
|
}
|
|
513
701
|
|
|
514
|
-
if (event === "
|
|
702
|
+
if (event === "launch") {
|
|
703
|
+
if (identityMode === "global" || grantRenewMode() === "launch") globalGrantRenew();
|
|
704
|
+
out(retainedLaunchOutput(JSON.parse(process.env.OATS_META || "{}")));
|
|
705
|
+
} else if (event === "spawn") {
|
|
515
706
|
if (identityMode === "global" && identitySettings.source) fatal('identity.mode "global" cannot be combined with identity.source; use identity.mode "local" with identity.source for a retained seat, or identity.mode "global" with identity.resident for a resident grant');
|
|
516
707
|
if (identityMode === "global") globalGrantSpawn();
|
|
517
708
|
if (identityMode === "local" && settings.identity && typeof settings.identity === "object" && settings.identity.source) retainedSeatSpawn(String(settings.identity.source), settings.identity.takeOver === true);
|
|
518
709
|
let minted; // external identity, once `aw team join` succeeds
|
|
519
710
|
const root = awebRoot();
|
|
520
|
-
if (!root)
|
|
711
|
+
if (!root) {
|
|
712
|
+
const candidate = rootSettingCandidate();
|
|
713
|
+
const classicHint = isClassicDeployment() ? " (classic fallback also checked the bounded team-scope candidates)" : "";
|
|
714
|
+
fatal(`${awebRootProblem(candidate)}, so no identity could be minted and this instance would have no messaging${classicHint}`);
|
|
715
|
+
}
|
|
521
716
|
try {
|
|
522
717
|
// Team correctness: the config's `team:` block wins (id, then name), else the
|
|
523
718
|
// root's active team. ALWAYS pass --team-id explicitly — never inherit whatever
|
|
@@ -527,15 +722,16 @@ if (event === "spawn") {
|
|
|
527
722
|
const resolvedTeam = payloadTeam();
|
|
528
723
|
let team = resolvedTeam.team;
|
|
529
724
|
const teamPayloadMismatch = resolvedTeam.payload && resolvedTeam.env && resolvedTeam.payload !== resolvedTeam.env;
|
|
725
|
+
if (!team && process.env.OATS_TEAM_LABEL) fatal(`cannot determine target team for workspace team label ${JSON.stringify(process.env.OATS_TEAM_LABEL)}, so no identity could be minted — ${teamConfigRemedy()}`);
|
|
530
726
|
if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
531
|
-
if (!team) fatal(
|
|
727
|
+
if (!team) fatal(`cannot determine target team, so no identity could be minted — ${teamConfigRemedy()}, or activate a team at the aweb root`);
|
|
532
728
|
// A bare team name (no namespace) resolves against the root's memberships.
|
|
533
729
|
if (!team.includes(":")) {
|
|
534
730
|
const teams = JSON.parse(run(["aw", "team", "list", "--json"], root));
|
|
535
731
|
const match = teamIdsOf(teams).filter((tid) => String(tid).startsWith(`${team}:`));
|
|
536
732
|
if (match.length === 1) team = match[0];
|
|
537
|
-
else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted —
|
|
538
|
-
else fatal(`no membership matching team "${team}" at ${root}, so no identity could be minted — join or create it first (aweb-team-membership skill), or
|
|
733
|
+
else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted — ${teamConfigRemedy()}`);
|
|
734
|
+
else fatal(`no membership matching team "${team}" at ${root}, so no identity could be minted — join or create it first (aweb-team-membership skill), or ${teamConfigRemedy()}`);
|
|
539
735
|
}
|
|
540
736
|
// Both of these carry the invite token — one mints it, the other spends it —
|
|
541
737
|
// so neither their output nor their diagnostics may reach a log.
|
|
@@ -559,7 +755,7 @@ if (event === "spawn") {
|
|
|
559
755
|
// re-spawn under the same name is refused by AWID. Say that, and the
|
|
560
756
|
// remedy, instead of relaying a bare join error.
|
|
561
757
|
if (e.aliasConflict) {
|
|
562
|
-
fatal(`alias "${instance}" already holds a certificate on ${team} (a retired instance of that name is not reusable until aweb-abim ships), so no identity could be minted —
|
|
758
|
+
fatal(`alias "${instance}" already holds a certificate on ${team} (a retired instance of that name is not reusable until aweb-abim ships), so no identity could be minted — ${ALIAS_REUSE_REMEDY}`);
|
|
563
759
|
}
|
|
564
760
|
throw e;
|
|
565
761
|
}
|
|
@@ -572,7 +768,7 @@ if (event === "spawn") {
|
|
|
572
768
|
// token; otherwise fall back to what WE asked for, which is always known.
|
|
573
769
|
const clean = (v) => (typeof v === "string" && v.trim() && !v.includes(inv.token) ? v.trim() : undefined);
|
|
574
770
|
const joined = {
|
|
575
|
-
alias: (() => { const a = clean(raw.alias); return a &&
|
|
771
|
+
alias: (() => { const a = clean(raw.alias); return a && AWEB_ALIAS_RE.test(a) ? a : instance; })(),
|
|
576
772
|
// Team ids are "<name>:<domain>"; anything else is not one, and the
|
|
577
773
|
// requested team is the honest fallback.
|
|
578
774
|
team_id: (() => { const t = clean(raw.team_id); return t && /^[^\s:]+:[^\s:]+$/.test(t) ? t : team; })(),
|
|
@@ -597,7 +793,7 @@ if (event === "spawn") {
|
|
|
597
793
|
const launch = (process.env.OATS_RUNTIME || "") === "claude" && deliveryMode === "channel"
|
|
598
794
|
? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" }
|
|
599
795
|
: undefined;
|
|
600
|
-
const env = deliveryMode === "session" ? { AWEB_DELIVERY: "session" } :
|
|
796
|
+
const env = { ...(deliveryMode === "session" ? { AWEB_DELIVERY: "session" } : {}), AWEB_IDENTITY_HOME: join(home, ".aw") };
|
|
601
797
|
const channelWarning = undefined;
|
|
602
798
|
if (deliveryMode === "session") wakeRegister(home, join(home, ".aw"));
|
|
603
799
|
const deliveryBrief = deliveryMode === "session"
|
|
@@ -605,7 +801,7 @@ if (event === "spawn") {
|
|
|
605
801
|
: "";
|
|
606
802
|
out({
|
|
607
803
|
meta: { team: joined.team_id, alias, delivery: deliveryMode, identity: identityMeta({ mode: "local", alias, team: joined.team_id }) },
|
|
608
|
-
|
|
804
|
+
env,
|
|
609
805
|
brief: `Comms: you have an aweb identity — alias "${alias}" on team ${joined.team_id}.${mismatch}${deliveryBrief} Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill); coordination stays in your deployment's task layer.`,
|
|
610
806
|
...(launch ? { launch } : {}),
|
|
611
807
|
...(joined.team_id !== team ? { warning: `oats-aweb: team mismatch — joined ${joined.team_id}, expected ${team}` } : teamPayloadMismatch ? { warning: `oats-aweb: settings.oats.aweb.team ${resolvedTeam.payload} differs from OATS team ${resolvedTeam.env}; using payload team` } : channelWarning ? { warning: channelWarning } : {}),
|
|
@@ -621,7 +817,7 @@ if (event === "spawn") {
|
|
|
621
817
|
// A retained seat: release the lock and leave the identity alone. Never
|
|
622
818
|
// aw workspace delete (it would soft-delete the standing identity's row)
|
|
623
819
|
// and never team retire; the source .aw stays until a human removes it.
|
|
624
|
-
if (meta.delivery === "session") { if (!wakeDeregister(home)) process.stderr.write("oats-aweb: aw wake deregister failed; the broker treats a retired home as inactive on its own\n"); }
|
|
820
|
+
if (meta.delivery === "session" && (meta.retained || meta.identity?.mode !== "global")) { if (!wakeDeregister(home)) process.stderr.write("oats-aweb: aw wake deregister failed; the broker treats a retired home as inactive on its own\n"); }
|
|
625
821
|
if (meta.retained) {
|
|
626
822
|
if (meta.lock) { try { rmSync(meta.lock, { force: true }); } catch { /* the lock may already be gone */ } }
|
|
627
823
|
out({ meta: { retired: true, retained: true, identityReleased: true, ...(meta.tookOverFrom ? { tookOverFrom: meta.tookOverFrom } : {}) }, warning: `oats-aweb: 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}` : ""}` });
|
|
@@ -653,14 +849,14 @@ if (event === "spawn") {
|
|
|
653
849
|
// aw 1.36.1 prints the cause as alias_released_reason (workspace.go,
|
|
654
850
|
// workspace_self_retire.go); `reason` is tolerated for a later rename.
|
|
655
851
|
const reason = typeof doc?.alias_released_reason === "string" ? doc.alias_released_reason : typeof doc?.reason === "string" ? doc.reason : (doc ? "unstated" : "no JSON answer");
|
|
656
|
-
out({ meta: { retired: true, aliasReusable: released, aliasReason: reason }, ...(released ? {} : { warning: `oats-aweb: workspace "${meta.alias}" deleted but its alias was not released (${reason}); spawn successors with a
|
|
852
|
+
out({ meta: { retired: true, aliasReusable: released, aliasReason: reason }, ...(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` }) });
|
|
657
853
|
}
|
|
658
854
|
run(["aw", "workspace", "delete", meta.alias], home);
|
|
659
855
|
// Honest: the workspace row is deleted, but a hosted local member cannot
|
|
660
856
|
// revoke its own AWID certificate (aweb-abim), so the alias is NOT
|
|
661
857
|
// reusable. retired stays true because the cleanup is as complete as the
|
|
662
858
|
// platform allows; the field and the line carry the truth.
|
|
663
|
-
out({ meta: { retired: true, aliasReusable: false }, warning: `oats-aweb: workspace "${meta.alias}" deleted; its certificate is not revoked (aweb-abim), so the alias is not reusable — spawn successors with a
|
|
859
|
+
out({ meta: { retired: true, aliasReusable: false }, warning: `oats-aweb: 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` });
|
|
664
860
|
} catch (e) {
|
|
665
861
|
// Exit nonzero: during a required-hook rollback this is the signal that
|
|
666
862
|
// compensation did NOT complete, so the spawn is not reported as cleanly
|
|
@@ -673,9 +869,9 @@ if (event === "spawn") {
|
|
|
673
869
|
// wherever they run (plus human members). Local liveness comes from
|
|
674
870
|
// `oats status --team`; this is the network view.
|
|
675
871
|
const root = awebRoot();
|
|
676
|
-
if (!root) { console.error(
|
|
872
|
+
if (!root) { console.error(`oats aweb roster: ${awebRootProblem(rootSettingCandidate())}`); process.exit(1); }
|
|
677
873
|
const team = process.env.OATS_TEAM_ID || process.env.OATS_TEAM_NAME || JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
678
|
-
if (!team) { console.error(
|
|
874
|
+
if (!team) { console.error(`oats aweb roster: cannot determine team (${teamConfigRemedy()}, or activate a team at the aweb root)`); process.exit(1); }
|
|
679
875
|
const teamFlag = team.includes(":") ? ["--team-id", team] : ["--team", team];
|
|
680
876
|
const r = JSON.parse(run(["aw", "id", "team", "members", ...teamFlag, "--json"], root, 60000));
|
|
681
877
|
if (process.argv.includes("--json")) { console.log(JSON.stringify(r, null, 2)); process.exit(0); }
|
|
@@ -686,39 +882,105 @@ if (event === "spawn") {
|
|
|
686
882
|
console.log("\nAliases minted by OATS are instance names; message one with `aw mail send --to <alias> --subject \"...\" --body \"...\"`.");
|
|
687
883
|
process.exit(0);
|
|
688
884
|
} else if (event === "setup") {
|
|
689
|
-
// Guided onboarding — idempotent, prints what it finds and
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
885
|
+
// Guided onboarding — idempotent, prints what it finds and can run one
|
|
886
|
+
// existing aw primitive when the operator supplies the needed authority.
|
|
887
|
+
const args = process.argv.slice(3).filter((arg) => arg !== "--json");
|
|
888
|
+
const usage = "usage: oats aweb setup [--username <hosted-user> | --invite <token>]";
|
|
889
|
+
let username, invite;
|
|
890
|
+
for (let i = 0; i < args.length; i++) {
|
|
891
|
+
const arg = args[i];
|
|
892
|
+
if (arg === "--username" && args[i + 1]) { username = args[++i]; continue; }
|
|
893
|
+
if (arg.startsWith("--username=") && arg.length > "--username=".length) { username = arg.slice("--username=".length); continue; }
|
|
894
|
+
if (arg === "--invite" && args[i + 1]) { invite = args[++i]; continue; }
|
|
895
|
+
if (arg.startsWith("--invite=") && arg.length > "--invite=".length) { invite = arg.slice("--invite=".length); continue; }
|
|
896
|
+
console.error(`oats aweb setup: ${usage}`);
|
|
897
|
+
process.exit(2);
|
|
698
898
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
899
|
+
const apiKey = !!process.env.AWEB_API_KEY;
|
|
900
|
+
const actions = [username ? "--username" : null, invite ? "--invite" : null, apiKey ? "AWEB_API_KEY" : null].filter(Boolean);
|
|
901
|
+
if (actions.length > 1) { console.error(`oats aweb setup: choose exactly one onboarding authority (${actions.join(", ")})\n${usage}`); process.exit(2); }
|
|
902
|
+
|
|
903
|
+
const resolvedTeam = payloadTeam();
|
|
904
|
+
const teamName = resolvedTeam.team;
|
|
905
|
+
const teamId = typeof settings.team === "string" && settings.team.trim() ? settings.team.trim() : process.env.OATS_TEAM_ID;
|
|
906
|
+
const candidate = rootSettingCandidate(teamName);
|
|
907
|
+
const scope = isClassicDeployment() && settings.root === undefined && settings.roots === undefined && settings.team === undefined
|
|
908
|
+
? resolve(process.env.OATS_TEAM_SCOPE || process.cwd())
|
|
909
|
+
: (candidate?.root ? resolve(candidate.root) : process.cwd());
|
|
910
|
+
const classic = isClassicDeployment() && settings.root === undefined && settings.roots === undefined && settings.team === undefined;
|
|
911
|
+
console.log(`aweb onboarding — ${classic ? "team scope" : "messaging root"}: ${scope}${teamName ? `, team: ${teamName}` : ""}\n`);
|
|
912
|
+
if (!isAbsolute(scope)) {
|
|
913
|
+
console.log(`${candidate?.key || "settings.oats.aweb.root"} must be an absolute directory whose .aw is the aweb minting root.`);
|
|
704
914
|
process.exit(0);
|
|
705
915
|
}
|
|
706
|
-
|
|
707
|
-
try { teams = JSON.parse(run(["aw", "team", "list", "--json"], scope)); } catch { /* fall through */ }
|
|
916
|
+
|
|
708
917
|
const want = teamId || teamName;
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
918
|
+
const defaultTeamForUsername = username ? `default:${username}.aweb.ai` : undefined;
|
|
919
|
+
const readTeams = () => {
|
|
920
|
+
try { return parseAwJson(run(["aw", "team", "list", "--json"], scope), "aw team list"); }
|
|
921
|
+
catch { return { memberships: [] }; }
|
|
922
|
+
};
|
|
923
|
+
const matchingTeam = (teams) => want ? teamIdsOf(teams).find((tid) => String(tid) === want || String(tid).startsWith(`${want}:`)) : undefined;
|
|
924
|
+
const printVerdict = (teams) => {
|
|
925
|
+
const match = matchingTeam(teams);
|
|
926
|
+
if (match) {
|
|
927
|
+
console.log(`readiness: ready`);
|
|
928
|
+
console.log(`✓ aweb workspace initialized and member of ${match}.`);
|
|
929
|
+
if (teams.active_team && teams.active_team !== match) console.log(` Note: active team is ${teams.active_team}; instances join ${match} explicitly, but consider \`aw team switch ${match}\`.`);
|
|
930
|
+
console.log(" Done — spawned instances will join this team automatically (alias = instance name).");
|
|
931
|
+
console.log(" Roster: `oats aweb roster` · local: `oats status --team`");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
console.log(`readiness: needs-configuration`);
|
|
935
|
+
if (!want) {
|
|
936
|
+
console.log(` no team: ${teamConfigRemedy()}`);
|
|
937
|
+
const active = teams.active_team || teamIdsOf(teams)[0];
|
|
938
|
+
if (active) console.log(` This root is a member of ${active}; map the workspace team to that id in the workspace file or settings.oats.aweb.team.`);
|
|
939
|
+
else if (defaultTeamForUsername) console.log(` New hosted users create ${defaultTeamForUsername}; map the workspace team to that id if this is the intended team.`);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
console.log(` Workspace initialized, but no membership matching "${want}".`);
|
|
943
|
+
if (defaultTeamForUsername) console.log(` New hosted users create ${defaultTeamForUsername}; set messaging.byTeam.<label>.team or settings.oats.aweb.team to that id, then re-run setup.`);
|
|
944
|
+
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).");
|
|
945
|
+
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).");
|
|
946
|
+
console.log(" New hosted-account path: run `oats aweb setup --username <u>` (uses `aw init --username <u>` and creates default:<u>.aweb.ai).");
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
try {
|
|
950
|
+
const hasRoot = existsSync(join(scope, ".aw"));
|
|
951
|
+
if (!hasRoot && !actions.length) {
|
|
952
|
+
console.log(`No aweb workspace at the ${classic ? "team scope" : "messaging root"} yet (${candidate?.key || "settings.oats.aweb.root"}).`);
|
|
953
|
+
if (!want) console.log(` Also choose the aweb team for this deployment: ${teamConfigRemedy()}.`);
|
|
954
|
+
console.log(" Choose one guided setup path:");
|
|
955
|
+
console.log(" oats aweb setup --username <u> # runs `aw init --username <u>` and creates default:<u>.aweb.ai");
|
|
956
|
+
console.log(" AWEB_API_KEY=<key> oats aweb setup # runs `aw init` for the hosted team behind the key");
|
|
957
|
+
console.log(" oats aweb setup --invite <token> # runs `aw team join <token>` from an existing-team invite");
|
|
958
|
+
console.log(" Or set settings.oats.aweb.root to an absolute directory whose .aw is the aweb minting root, then re-run setup.");
|
|
959
|
+
console.log(" Own your domain? Use the aweb-team-membership skill for BYOT flows.");
|
|
960
|
+
process.exit(0);
|
|
961
|
+
}
|
|
962
|
+
let teams = hasRoot ? readTeams() : { memberships: [] };
|
|
963
|
+
if (hasRoot && !matchingTeam(teams) && !actions.length) { printVerdict(teams); process.exit(0); }
|
|
964
|
+
if (!hasRoot || !matchingTeam(teams)) {
|
|
965
|
+
if (actions.length) mkdirSync(scope, { recursive: true });
|
|
966
|
+
if (username) {
|
|
967
|
+
console.log(`Running aw init --username <u> at ${scope} (username withheld from repeated logs).`);
|
|
968
|
+
run(["aw", "init", "--username", username], scope, 120000, { secrets: [username], unsetEnv: ["AWEB_API_KEY"] });
|
|
969
|
+
} else if (invite) {
|
|
970
|
+
console.log(`Running aw team join <token> at ${scope} (token withheld).`);
|
|
971
|
+
run(["aw", "team", "join", invite], scope, 120000, { secrets: [invite], secretSafe: true });
|
|
972
|
+
} else if (apiKey) {
|
|
973
|
+
console.log(`Running aw init at ${scope} with AWEB_API_KEY from the environment (key withheld).`);
|
|
974
|
+
run(["aw", "init"], scope, 120000, { secretSafe: true });
|
|
975
|
+
}
|
|
976
|
+
teams = readTeams();
|
|
977
|
+
}
|
|
978
|
+
printVerdict(teams);
|
|
979
|
+
process.exit(0);
|
|
980
|
+
} catch (e) {
|
|
981
|
+
console.error(`oats aweb setup: ${e.message || e}`);
|
|
982
|
+
process.exit(1);
|
|
720
983
|
}
|
|
721
|
-
process.exit(0);
|
|
722
984
|
} else {
|
|
723
985
|
warn(`unknown event "${event}" (expected spawn|retire)`);
|
|
724
986
|
}
|