@awebai/oats 0.25.9 → 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 +279 -93
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
- 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 +8 -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-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 +34 -1
- package/docs/design/2026-09-24-phase-d-plan.md +57 -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 +537 -261
- 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 +64 -33
- 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.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 +6 -6
- 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)) {
|
|
@@ -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;
|
|
@@ -283,8 +294,23 @@ function wakeRegister(instanceHome, identityHome) {
|
|
|
283
294
|
function wakeDeregister(instanceHome) {
|
|
284
295
|
try { run(["aw", "wake", "deregister", "--home", instanceHome], instanceHome, 60000); return true; } catch { return false; }
|
|
285
296
|
}
|
|
286
|
-
|
|
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"];
|
|
287
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);
|
|
288
314
|
function resolveResidentCustody(name) {
|
|
289
315
|
if (!name) fatal(`identity.mode "global" requires identity.resident; set ${residentKeyHint("<name>")} to the absolute custody directory for that resident identity`);
|
|
290
316
|
const residents = settings.residents && typeof settings.residents === "object" && !Array.isArray(settings.residents) ? settings.residents : {};
|
|
@@ -314,6 +340,21 @@ function revokeGrant(custody, grantId) {
|
|
|
314
340
|
throw e;
|
|
315
341
|
}
|
|
316
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
|
+
}
|
|
317
358
|
function recoverGrantHome(grantHome) {
|
|
318
359
|
try {
|
|
319
360
|
const text = readFileSync(join(grantHome, "grant.yaml"), "utf8");
|
|
@@ -321,9 +362,120 @@ function recoverGrantHome(grantHome) {
|
|
|
321
362
|
const m = text.match(new RegExp(`^\\s*${key}:\\s*["']?([^"'\\n#]+)["']?\\s*$`, "m"));
|
|
322
363
|
return m ? m[1].trim() : undefined;
|
|
323
364
|
};
|
|
324
|
-
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) };
|
|
325
366
|
} catch { return {}; }
|
|
326
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
|
+
}
|
|
327
479
|
function globalGrantSpawn() {
|
|
328
480
|
const { team, payload, env: envTeam } = payloadTeam();
|
|
329
481
|
if (!team) fatal("identity.mode \"global\" requires settings.oats.aweb.team (or OATS_TEAM_ID/OATS_TEAM_NAME) before minting a grant");
|
|
@@ -331,37 +483,40 @@ function globalGrantSpawn() {
|
|
|
331
483
|
const custody = resolveResidentCustody(resident);
|
|
332
484
|
const grantHome = join(home, ".aweb-identity");
|
|
333
485
|
if (existsSync(grantHome)) fatal(`${grantHome} already exists; refusing to overwrite an existing aweb session grant home`);
|
|
334
|
-
const scopes =
|
|
486
|
+
const scopes = grantScopes();
|
|
335
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}`);
|
|
336
491
|
let meta;
|
|
337
492
|
const cleanup = () => { try { rmSync(grantHome, { recursive: true, force: true }); } catch { /* best effort */ } };
|
|
338
493
|
const failAfterMint = (message, code = 1) => { cleanup(); out({ ...(meta ? { meta } : {}), warning: `oats-aweb: ${String(message).slice(0, 300)}` }, code); };
|
|
339
494
|
try {
|
|
340
|
-
const raw = run(
|
|
495
|
+
const raw = run(grantMintArgv({ team, scopes, ttl, grantHome, custodySocket }), custody, 60000, { unsetEnv: ["AWEB_IDENTITY_HOME"] });
|
|
341
496
|
let minted;
|
|
342
|
-
try { minted =
|
|
497
|
+
try { minted = parseMintedGrant(raw, grantHome).minted; }
|
|
343
498
|
catch (parseError) {
|
|
344
499
|
const recovered = recoverGrantHome(grantHome);
|
|
345
500
|
if (recovered.grantId) {
|
|
346
|
-
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 } }) };
|
|
347
502
|
try { revokeGrant(custody, recovered.grantId); failAfterMint(`${parseError.message}; recovered grant ${recovered.grantId} from grant.yaml, revoked it, and removed the grant home`); }
|
|
348
503
|
catch (revokeError) { failAfterMint(`${parseError.message}; recovered grant ${recovered.grantId} from grant.yaml, but revoke failed: ${revokeError.message || revokeError}`); }
|
|
349
504
|
}
|
|
350
505
|
throw parseError;
|
|
351
506
|
}
|
|
352
|
-
const grantId
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
if (!grantId || !expiresAt || !mintedTeam || !mintedOut) throw new Error("aw id grant mint JSON lacked grant_id, expires_at, team_id, or out");
|
|
357
|
-
if (resolve(mintedOut) !== resolve(grantHome)) throw new Error(`aw id grant mint wrote ${mintedOut}, not ${grantHome}`);
|
|
358
|
-
const alias = typeof minted.alias === "string" && minted.alias ? minted.alias : resident;
|
|
359
|
-
const address = typeof minted.address === "string" && minted.address ? minted.address : null;
|
|
360
|
-
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 } }) };
|
|
361
511
|
if (mintedTeam !== team) {
|
|
362
512
|
try { revokeGrant(custody, grantId); failAfterMint(`minted grant team ${mintedTeam} differs from ${team}; the grant was revoked and nothing was kept`); }
|
|
363
513
|
catch (e) { failAfterMint(`minted grant team ${mintedTeam} differs from ${team}; revoke failed: ${e.message || e}`); }
|
|
364
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
|
+
}
|
|
365
520
|
const launch = (process.env.OATS_RUNTIME || "") === "claude" && deliveryMode === "channel"
|
|
366
521
|
? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" }
|
|
367
522
|
: undefined;
|
|
@@ -373,15 +528,16 @@ function globalGrantSpawn() {
|
|
|
373
528
|
catch (revokeError) { failAfterMint(`session delivery registration failed for minted grant ${grantId}: ${e.message || e}; revoke failed: ${revokeError.message || revokeError}`); }
|
|
374
529
|
}
|
|
375
530
|
}
|
|
376
|
-
const warnings = [];
|
|
531
|
+
const warnings = [...preflight.warnings];
|
|
377
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(" ")}` : "";
|
|
378
534
|
const deliveryBrief = deliveryMode === "session"
|
|
379
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.`
|
|
380
536
|
: "";
|
|
381
537
|
out({
|
|
382
538
|
meta,
|
|
383
539
|
env,
|
|
384
|
-
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.`,
|
|
385
541
|
...(launch ? { launch } : {}),
|
|
386
542
|
...(warnings.length ? { warning: warnings.join(" | ") } : {}),
|
|
387
543
|
});
|
|
@@ -398,8 +554,10 @@ function globalGrantRetire(meta) {
|
|
|
398
554
|
const custody = resolveResidentCustody(resident);
|
|
399
555
|
try {
|
|
400
556
|
revokeGrant(custody, id);
|
|
557
|
+
removeGrantHomes();
|
|
401
558
|
out({ meta: { retired: true, identityRevoked: true, grant: id } });
|
|
402
559
|
} catch (e) {
|
|
560
|
+
removeGrantHomes();
|
|
403
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);
|
|
404
562
|
}
|
|
405
563
|
}
|
|
@@ -407,7 +565,7 @@ const seatLockPath = (source) => join(dirname(source), ".aw-retained-seat.json")
|
|
|
407
565
|
/** The alias a home's .aw/workspace.yaml records under memberships (indented),
|
|
408
566
|
* or undefined. Read only when the hook has no alias of its own. */
|
|
409
567
|
const workspaceAliasOf = (homeDir) => {
|
|
410
|
-
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; }
|
|
411
569
|
catch { return undefined; }
|
|
412
570
|
};
|
|
413
571
|
/** A join that the CLI reported as failed (or that this hook killed on
|
|
@@ -513,7 +671,7 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
513
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`);
|
|
514
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`);
|
|
515
673
|
const aliasRaw = String(ws.alias || st.alias || (expectedAddress || "").split("/").pop() || instance);
|
|
516
|
-
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`);
|
|
517
675
|
const alias = aliasRaw;
|
|
518
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`);
|
|
519
677
|
writeFileSync(lockPath, JSON.stringify({ home, instance, alias, team, takenAt: new Date().toISOString(), host: hostname(), ...(takenOver ? { tookOverFrom: takenOver } : {}) }, null, 2) + "\n", { mode: 0o600 });
|
|
@@ -541,7 +699,10 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
541
699
|
}
|
|
542
700
|
}
|
|
543
701
|
|
|
544
|
-
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") {
|
|
545
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');
|
|
546
707
|
if (identityMode === "global") globalGrantSpawn();
|
|
547
708
|
if (identityMode === "local" && settings.identity && typeof settings.identity === "object" && settings.identity.source) retainedSeatSpawn(String(settings.identity.source), settings.identity.takeOver === true);
|
|
@@ -594,7 +755,7 @@ if (event === "spawn") {
|
|
|
594
755
|
// re-spawn under the same name is refused by AWID. Say that, and the
|
|
595
756
|
// remedy, instead of relaying a bare join error.
|
|
596
757
|
if (e.aliasConflict) {
|
|
597
|
-
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}`);
|
|
598
759
|
}
|
|
599
760
|
throw e;
|
|
600
761
|
}
|
|
@@ -607,7 +768,7 @@ if (event === "spawn") {
|
|
|
607
768
|
// token; otherwise fall back to what WE asked for, which is always known.
|
|
608
769
|
const clean = (v) => (typeof v === "string" && v.trim() && !v.includes(inv.token) ? v.trim() : undefined);
|
|
609
770
|
const joined = {
|
|
610
|
-
alias: (() => { const a = clean(raw.alias); return a &&
|
|
771
|
+
alias: (() => { const a = clean(raw.alias); return a && AWEB_ALIAS_RE.test(a) ? a : instance; })(),
|
|
611
772
|
// Team ids are "<name>:<domain>"; anything else is not one, and the
|
|
612
773
|
// requested team is the honest fallback.
|
|
613
774
|
team_id: (() => { const t = clean(raw.team_id); return t && /^[^\s:]+:[^\s:]+$/.test(t) ? t : team; })(),
|
|
@@ -656,7 +817,7 @@ if (event === "spawn") {
|
|
|
656
817
|
// A retained seat: release the lock and leave the identity alone. Never
|
|
657
818
|
// aw workspace delete (it would soft-delete the standing identity's row)
|
|
658
819
|
// and never team retire; the source .aw stays until a human removes it.
|
|
659
|
-
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"); }
|
|
660
821
|
if (meta.retained) {
|
|
661
822
|
if (meta.lock) { try { rmSync(meta.lock, { force: true }); } catch { /* the lock may already be gone */ } }
|
|
662
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}` : ""}` });
|
|
@@ -688,14 +849,14 @@ if (event === "spawn") {
|
|
|
688
849
|
// aw 1.36.1 prints the cause as alias_released_reason (workspace.go,
|
|
689
850
|
// workspace_self_retire.go); `reason` is tolerated for a later rename.
|
|
690
851
|
const reason = typeof doc?.alias_released_reason === "string" ? doc.alias_released_reason : typeof doc?.reason === "string" ? doc.reason : (doc ? "unstated" : "no JSON answer");
|
|
691
|
-
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` }) });
|
|
692
853
|
}
|
|
693
854
|
run(["aw", "workspace", "delete", meta.alias], home);
|
|
694
855
|
// Honest: the workspace row is deleted, but a hosted local member cannot
|
|
695
856
|
// revoke its own AWID certificate (aweb-abim), so the alias is NOT
|
|
696
857
|
// reusable. retired stays true because the cleanup is as complete as the
|
|
697
858
|
// platform allows; the field and the line carry the truth.
|
|
698
|
-
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` });
|
|
699
860
|
} catch (e) {
|
|
700
861
|
// Exit nonzero: during a required-hook rollback this is the signal that
|
|
701
862
|
// compensation did NOT complete, so the spawn is not reported as cleanly
|
|
@@ -721,80 +882,105 @@ if (event === "spawn") {
|
|
|
721
882
|
console.log("\nAliases minted by OATS are instance names; message one with `aw mail send --to <alias> --subject \"...\" --body \"...\"`.");
|
|
722
883
|
process.exit(0);
|
|
723
884
|
} else if (event === "setup") {
|
|
724
|
-
// Guided onboarding — idempotent, prints what it finds and
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
console.log(` cd ${scope} && aw init`);
|
|
738
|
-
console.log(" First time on aweb? `aw init` walks you through creating a hosted aweb.ai account.");
|
|
739
|
-
console.log(" Own your domain? Use `aw init --byod` (see the aweb-team-membership skill).");
|
|
740
|
-
process.exit(0);
|
|
741
|
-
}
|
|
742
|
-
let teams = { memberships: [] };
|
|
743
|
-
try { teams = JSON.parse(run(["aw", "team", "list", "--json"], scope)); } catch { /* fall through */ }
|
|
744
|
-
const want = teamId || teamName;
|
|
745
|
-
const match = teamIdsOf(teams).find((tid) => String(tid) === want || String(tid).startsWith(`${want}:`));
|
|
746
|
-
if (match) {
|
|
747
|
-
console.log(`✓ aweb workspace initialized and member of ${match}.`);
|
|
748
|
-
if (teams.active_team && teams.active_team !== match) console.log(` Note: active team is ${teams.active_team}; instances join ${match} explicitly, but consider \`aw team switch ${match}\`.`);
|
|
749
|
-
console.log(" Done — spawned instances will join this team automatically (alias = instance name).");
|
|
750
|
-
console.log(" Roster: `oats aweb roster` · local: `oats status --team`");
|
|
751
|
-
} else {
|
|
752
|
-
console.log(`Workspace initialized, but no membership matching "${want}".`);
|
|
753
|
-
console.log(` Create the team: cd ${scope} && aw team create ${teamName}`);
|
|
754
|
-
console.log(" Or join an existing one: get an invite token from a member, then `aw team join <token>`");
|
|
755
|
-
console.log(" (details: aweb-team-membership skill)");
|
|
756
|
-
}
|
|
757
|
-
process.exit(0);
|
|
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);
|
|
758
898
|
}
|
|
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
|
+
|
|
759
903
|
const resolvedTeam = payloadTeam();
|
|
760
904
|
const teamName = resolvedTeam.team;
|
|
761
905
|
const teamId = typeof settings.team === "string" && settings.team.trim() ? settings.team.trim() : process.env.OATS_TEAM_ID;
|
|
762
906
|
const candidate = rootSettingCandidate(teamName);
|
|
763
|
-
const scope =
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
process.exit(0);
|
|
769
|
-
}
|
|
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`);
|
|
770
912
|
if (!isAbsolute(scope)) {
|
|
771
913
|
console.log(`${candidate?.key || "settings.oats.aweb.root"} must be an absolute directory whose .aw is the aweb minting root.`);
|
|
772
914
|
process.exit(0);
|
|
773
915
|
}
|
|
774
|
-
|
|
775
|
-
console.log(`No aweb workspace at the messaging root yet (${candidate?.key || "settings.oats.aweb.root"}). Initialize it (interactive — creates or connects an aweb account):`);
|
|
776
|
-
console.log(` cd ${scope} && aw init`);
|
|
777
|
-
console.log(" Or set settings.oats.aweb.root to an absolute directory whose .aw is the aweb minting root, then re-run `oats aweb setup`.");
|
|
778
|
-
console.log(" First time on aweb? `aw init` walks you through creating a hosted aweb.ai account.");
|
|
779
|
-
console.log(" Own your domain? Use `aw init --byod` (see the aweb-team-membership skill).");
|
|
780
|
-
process.exit(0);
|
|
781
|
-
}
|
|
782
|
-
let teams = { memberships: [] };
|
|
783
|
-
try { teams = JSON.parse(run(["aw", "team", "list", "--json"], scope)); } catch { /* fall through */ }
|
|
916
|
+
|
|
784
917
|
const want = teamId || teamName;
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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);
|
|
796
983
|
}
|
|
797
|
-
process.exit(0);
|
|
798
984
|
} else {
|
|
799
985
|
warn(`unknown event "${event}" (expected spawn|retire)`);
|
|
800
986
|
}
|
|
@@ -8,8 +8,13 @@ Some instances serve a resident global identity through an expiring session
|
|
|
8
8
|
grant instead of holding their own root keys. In that mode, your TASK.md and
|
|
9
9
|
`instance.json` identify the resident alias, scopes, and expiry; root keys are
|
|
10
10
|
not in your home, and mint/revoke/join/identity-lifecycle commands are not yours
|
|
11
|
-
to run.
|
|
12
|
-
|
|
11
|
+
to run. Your grant home is attached to the resident's custody service; if a
|
|
12
|
+
message you sent shows unverified at the receiver, the attachment is missing:
|
|
13
|
+
report it, do not retry. If `aw mail` or `aw chat` reports a terminal grant condition —
|
|
14
|
+
`grant_expired`, `grant_revoked`, `grant_subject_inactive`,
|
|
15
|
+
`grant_issuer_revoked`, or `grant_freshness_unavailable` — report the exact
|
|
16
|
+
condition to your coordinator/human and stop using messaging. The host restarts
|
|
17
|
+
you with a fresh grant when renewal is available.
|
|
13
18
|
|
|
14
19
|
**Load the skills at the right moments — do not work from memory:**
|
|
15
20
|
|