@awebai/oats 0.27.0 → 0.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +272 -72
- package/capabilities/oats-aweb/injects/aweb.md +9 -1
- package/capabilities/oats-aweb/lib/binding-wire.mjs +42 -11
- package/capabilities/oats-aweb/oats.json +47 -5
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +79 -301
- package/docs/capabilities.md +3 -1
- package/docs/desktop-cli-api.md +6 -0
- package/docs/packages.md +1 -1
- package/docs/release-notes/v0.27.1.md +29 -0
- package/docs/release-notes/v0.27.2.md +51 -0
- package/docs/souls-and-instances.md +12 -0
- package/lib/core.mjs +102 -6
- package/package-catalog.json +1 -1
- package/package.json +1 -1
|
@@ -83,10 +83,11 @@ export function parseBindingJson(bytes,limits=BINDING_WIRE_LIMITS) {
|
|
|
83
83
|
function settings(value,{phase}={}) {
|
|
84
84
|
if(!obj(value)) wireError('invalid-binding');
|
|
85
85
|
if(phase!=='check' && Object.hasOwn(value,'identity')) wireError('provider-not-qualified');
|
|
86
|
-
keys(value,phase==='check'?['delivery','team','root','roots','identity','residents']:['delivery','team','root','roots'],[]);
|
|
86
|
+
keys(value,phase==='check'?['delivery','team','root','roots','identity','residents','join']:['delivery','team','root','roots','join'],[]);
|
|
87
87
|
if(value.delivery!==undefined && !['channel','session'].includes(value.delivery)) wireError('needs-configuration');
|
|
88
88
|
if(value.team!==undefined && (typeof value.team!=='string' || !value.team.trim())) wireError('needs-configuration');
|
|
89
89
|
if(value.root!==undefined && (typeof value.root!=='string' || !value.root.trim())) wireError('needs-configuration');
|
|
90
|
+
if(value.join!==undefined && typeof value.join!=='string') wireError('needs-configuration');
|
|
90
91
|
if(value.roots!==undefined && !obj(value.roots)) wireError('needs-configuration');
|
|
91
92
|
if(obj(value.roots)) for(const [team,root] of Object.entries(value.roots)) if(!team || typeof root!=='string' || !root.trim()) wireError('needs-configuration');
|
|
92
93
|
if(value.identity!==undefined && !obj(value.identity)) wireError('needs-configuration');
|
|
@@ -164,6 +165,9 @@ function workspaceReadinessContext(value) {
|
|
|
164
165
|
}
|
|
165
166
|
function yamlScalar(text,key){const m=String(text).match(new RegExp(`^${key}:\\s*["']?([^"'\\n#]+)["']?\\s*$`,'m'));return m?m[1].trim():undefined;}
|
|
166
167
|
export const CUSTODY_ATTACH_MIN = '1.36.3';
|
|
168
|
+
export const WAKE_STREAM_MIN = '1.36.5';
|
|
169
|
+
const CLASSIC_REFUSAL = 'oats.aweb 1.14 needs OATS 0.26.0 or newer (workspace model); on an older kernel pin oats.aweb v1.13.x';
|
|
170
|
+
function classicEnv(env=process.env) {return !!env.OATS_TEAM_SCOPE && !(env.OATS_WORKSPACE_KEY || env.OATS_WORKSPACE_NAME || env.OATS_TEAM_LABEL);}
|
|
167
171
|
export function grantYamlCustodySocket(text) {
|
|
168
172
|
const lines=String(text??'').split(/\r?\n/);let inCustody=false,baseIndent=0;
|
|
169
173
|
for(const line of lines) {
|
|
@@ -189,27 +193,35 @@ function grantAttachmentProblem(home) {
|
|
|
189
193
|
const id=yamlScalar(text,'grant_id')||'<unknown>';
|
|
190
194
|
return {code:'custody',message:`grant ${id} is not attached to custody; retire and respawn on aw >= ${CUSTODY_ATTACH_MIN}`};
|
|
191
195
|
}
|
|
192
|
-
function activeTeamAt(root){try{
|
|
196
|
+
function activeTeamAt(root){try{const text=readFileSync(join(resolve(root),'.aw','teams.yaml'),'utf8');return yamlScalar(text,'active_team')||yamlScalar(text,'active');}catch{return undefined;}}
|
|
197
|
+
function residentCustodyRoot(settings){const identity=obj(settings.identity)?settings.identity:{},residents=obj(settings.residents)?settings.residents:{};const name=typeof identity.resident==='string'?identity.resident:'';const root=name&&typeof residents[name]==='string'?residents[name]:undefined;return identity.mode==='global'&&root&&isAbsolute(root)?root:undefined;}
|
|
193
198
|
function teamFromSettings(settings,candidate,{env=process.env}={}) {
|
|
194
|
-
const configured=typeof settings.team==='string' && settings.team.trim()?settings.team.trim():
|
|
195
|
-
if(configured
|
|
199
|
+
const configured=typeof settings.team==='string' && settings.team.trim()?settings.team.trim():undefined;
|
|
200
|
+
if(configured) return configured;
|
|
201
|
+
const custody=residentCustodyRoot(settings);if(custody)return activeTeamAt(custody);
|
|
196
202
|
return candidate?.root && isAbsolute(candidate.root) ? activeTeamAt(candidate.root) : undefined;
|
|
197
203
|
}
|
|
198
|
-
function classicEnv(env=process.env) {return !!env.OATS_TEAM_SCOPE && !(env.OATS_WORKSPACE_KEY || env.OATS_WORKSPACE_NAME || env.OATS_TEAM_LABEL);}
|
|
199
204
|
function rootCandidate(settings,team,{deployment,env=process.env}={}) {
|
|
200
205
|
const roots=obj(settings.roots)?settings.roots:{};
|
|
201
206
|
if(team && typeof roots[team]==='string' && roots[team].trim()) return {root:roots[team].trim(),key:`settings.oats.aweb.roots[${JSON.stringify(team)}]`,declared:true};
|
|
202
207
|
if(typeof settings.root==='string' && settings.root.trim()) return {root:settings.root.trim(),key:'settings.oats.aweb.root',declared:true};
|
|
203
|
-
const candidates=
|
|
208
|
+
const candidates=[env.OATS_WORKSPACE || deployment || process.cwd()];
|
|
204
209
|
for(const root of candidates) if(isAbsolute(root) && existsSync(join(resolve(root),'.aw'))) return {root,key:'settings.oats.aweb.root',declared:false};
|
|
205
210
|
return {root:candidates[0] || process.cwd(),key:'settings.oats.aweb.root',declared:false};
|
|
206
211
|
}
|
|
212
|
+
function parseOatsTeams(env=process.env){try{const rows=JSON.parse(env.OATS_TEAMS||'[]');return Array.isArray(rows)?rows.filter(r=>r&&typeof r==='object').map(r=>({label:String(r.label||''),team:typeof r.team==='string'?r.team:null,mapped:r.mapped===true,payload:obj(r.payload)?r.payload:{}})):[];}catch{return [];}}
|
|
213
|
+
function primaryTeamLabel(env=process.env){return env.OATS_TEAM_LABEL || String(env.OATS_TEAM_LABELS||'').split(',').map(s=>s.trim()).filter(Boolean)[0] || null;}
|
|
214
|
+
function unmappedPrimary(env=process.env){const primary=primaryTeamLabel(env);return primary?parseOatsTeams(env).find(t=>t.label===primary&&!t.mapped):undefined;}
|
|
215
|
+
function joinedTeams(home){if(!home)return[];try{const doc=JSON.parse(readFileSync(join(home,'.oats-aweb','teams.json'),'utf8'));return Array.isArray(doc.joinedTeams)?doc.joinedTeams.filter(j=>j&&typeof j==='object'&&j.label&&j.team&&j.identityHome):[];}catch{return[];}}
|
|
216
|
+
function teamsReadiness({home,team,env=process.env}){const teams=parseOatsTeams(env),joined=joinedTeams(home),joinedLabels=new Set(joined.map(j=>j.label));return{personal:{team:team||null},primary:primaryTeamLabel(env),eligible:teams.filter(t=>t.mapped&&t.team).map(t=>({label:t.label,team:t.team,joined:joinedLabels.has(t.label)})),joined:joined.map(j=>({label:j.label,team:j.team,identityHome:j.identityHome,receive:j.receive||'poll',since:j.since})),unmapped:teams.filter(t=>!t.mapped).map(t=>t.label),at:new Date().toISOString()};}
|
|
207
217
|
function readinessDetails(settings,{deployment,env=process.env}={}) {
|
|
208
|
-
|
|
209
|
-
const
|
|
218
|
+
if(classicEnv(env)) return {team:undefined,candidate:null,warnings:[],result:{status:'needs-configuration',problems:[{code:'needs-configuration',message:CLASSIC_REFUSAL}]}};
|
|
219
|
+
const initialTeam=typeof settings.team==='string' && settings.team.trim()?settings.team.trim():undefined;
|
|
220
|
+
const candidate=rootCandidate(settings,initialTeam,{deployment,env}),team=teamFromSettings(settings,candidate,{env}),problems=[],warnings=[];
|
|
210
221
|
if(!candidate.root || !isAbsolute(candidate.root) || !existsSync(join(resolve(candidate.root),'.aw'))) problems.push({code:'needs-configuration',message:`no messaging root at ${candidate.root?resolve(candidate.root):process.cwd()}: run oats aweb setup there or set ${candidate.key}`});
|
|
211
|
-
if(
|
|
212
|
-
|
|
222
|
+
const unmapped=unmappedPrimary(env);if(unmapped&&team)warnings.push({code:'team-unmapped',message:`workspace label ${unmapped.label} is not mapped; using personal team ${team}`});
|
|
223
|
+
if(!team) problems.push({code:'needs-configuration',message:'no team: set settings.oats.aweb.team or keep an active team at the aweb root'});
|
|
224
|
+
return {team,candidate,warnings,result:checkProblems(problems) || {status:'ready',problems:[]}};
|
|
213
225
|
}
|
|
214
226
|
function readinessFromSettings(settings,options) {return readinessDetails(settings,options).result;}
|
|
215
227
|
function runAw(argv,cwd,{unsetEnv=[],timeout=60000}={}) {
|
|
@@ -217,10 +229,25 @@ function runAw(argv,cwd,{unsetEnv=[],timeout=60000}={}) {
|
|
|
217
229
|
try {return execFileSync(argv[0],argv.slice(1),{cwd,env,encoding:'utf8',stdio:['ignore','pipe','pipe'],timeout}).trim();}
|
|
218
230
|
catch(e) {throw new Error(`${argv.slice(0,3).join(' ')} failed${e.status===undefined?'':` (exit ${e.status})`}`);}
|
|
219
231
|
}
|
|
232
|
+
function semverLt(a,b) {const A=String(a||'0.0.0').split('.').map(n=>Number(n)||0),B=String(b).split('.').map(n=>Number(n)||0);for(let i=0;i<3;i++){if((A[i]||0)!==(B[i]||0)) return (A[i]||0)<(B[i]||0);}return false;}
|
|
233
|
+
function wakeReadiness(home,{reliedOn=false}={}) {
|
|
234
|
+
if(!home || !reliedOn) return {problems:[],warnings:[]};
|
|
235
|
+
try {
|
|
236
|
+
const doc=JSON.parse(runAw(['aw','wake','status','--json'],home,{timeout:10000}));
|
|
237
|
+
const state=doc.daemon_version_state || (doc.daemon_running===false?'not_running':doc.daemon_version?'reported':'unknown');
|
|
238
|
+
if(state==='reported') {
|
|
239
|
+
const running=String(doc.daemon_version||'unknown');
|
|
240
|
+
if(semverLt(running,WAKE_STREAM_MIN)) return {problems:[{code:'wake-daemon-outdated',message:`host wake daemon is running ${running}; required ${WAKE_STREAM_MIN}; upgrade aw, then restart the host wake daemon`}],warnings:[]};
|
|
241
|
+
return {problems:[],warnings:[]};
|
|
242
|
+
}
|
|
243
|
+
if(state==='not_running') return {problems:[{code:'wake-daemon-not-running',message:'host wake daemon is not running; session delivery relies on it'}],warnings:[]};
|
|
244
|
+
return {problems:[],warnings:[{code:'wake-daemon-version-unknown',message:`host wake daemon version is unknown; compatibility unproven; required ${WAKE_STREAM_MIN}; upgrade aw, then restart the host wake daemon`}]};
|
|
245
|
+
} catch {return {problems:[],warnings:[{code:'wake-daemon-version-unknown',message:`host wake daemon version is unknown; compatibility unproven; required ${WAKE_STREAM_MIN}; upgrade aw, then restart the host wake daemon`}]};}
|
|
246
|
+
}
|
|
220
247
|
function workspaceReadinessPhase(req) {
|
|
221
248
|
const ctx=workspaceReadinessContext(req.input.context);
|
|
222
249
|
if(!obj(req.input.action) || req.input.action.kind!=='readiness') wireError('invalid-binding');
|
|
223
|
-
const details=readinessDetails(req.settings,{deployment:ctx.deployment}),problems=[...details.result.problems],warnings=[];
|
|
250
|
+
const details=readinessDetails(req.settings,{deployment:ctx.deployment}),problems=[...details.result.problems],warnings=[...details.warnings];
|
|
224
251
|
const identity=obj(req.settings.identity)?req.settings.identity:{},mode=identity.mode===undefined || identity.mode===null || identity.mode===''?'local':String(identity.mode);
|
|
225
252
|
if(mode==='global') {
|
|
226
253
|
const grantProblem=grantAttachmentProblem(ctx.home);if(grantProblem) problems.push(grantProblem);
|
|
@@ -237,6 +264,10 @@ function workspaceReadinessPhase(req) {
|
|
|
237
264
|
catch(e) {problems.push({code:'custody',message:e.message});}
|
|
238
265
|
}
|
|
239
266
|
}
|
|
267
|
+
const teams=process.env.OATS_TEAMS?teamsReadiness({home:ctx.home,team:details.team,env:process.env}):undefined;
|
|
268
|
+
for(const joined of teams?.joined||[]) if(joined.receive==='poll') warnings.push({code:'joined-team-poll-only',message:`joined team ${joined.label} receives by polling in oats.aweb 1.14; check aw --identity-home ${joined.identityHome} mail inbox/chat pending`});
|
|
269
|
+
const wake=String(req.settings.delivery||'channel')==='session'?wakeReadiness(ctx.home,{reliedOn:true}):{problems:[],warnings:[]};
|
|
270
|
+
problems.push(...wake.problems);warnings.push(...wake.warnings);
|
|
240
271
|
const result=checkProblems(problems) || {status:'ready',problems:[]};
|
|
241
272
|
return {...result,warnings};
|
|
242
273
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"capability": "oats.aweb",
|
|
3
3
|
"command": "aweb",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.14.2",
|
|
5
5
|
"compatibility": {
|
|
6
|
-
"oats": ">=0.
|
|
6
|
+
"oats": ">=0.26.0"
|
|
7
7
|
},
|
|
8
8
|
"layer": "messaging",
|
|
9
9
|
"description": "Messaging layer via aweb: per-instance team identities + native aw mail/chat skills + cross-machine team roster.",
|
|
@@ -71,7 +71,10 @@
|
|
|
71
71
|
"setup": "bin/oats-aweb.mjs setup",
|
|
72
72
|
"binding-normalize": "bin/oats-aweb-binding.mjs normalize",
|
|
73
73
|
"binding-bind": "bin/oats-aweb-binding.mjs bind",
|
|
74
|
-
"binding-check": "bin/oats-aweb-binding.mjs check"
|
|
74
|
+
"binding-check": "bin/oats-aweb-binding.mjs check",
|
|
75
|
+
"teams": "bin/oats-aweb.mjs teams",
|
|
76
|
+
"join": "bin/oats-aweb.mjs join",
|
|
77
|
+
"leave": "bin/oats-aweb.mjs leave"
|
|
75
78
|
},
|
|
76
79
|
"binding": {
|
|
77
80
|
"version": 1,
|
|
@@ -138,7 +141,7 @@
|
|
|
138
141
|
"description": "channel: the native aweb channel packages wake the instance (default). session: delivery is external (AWEB_DELIVERY=session), no channel flag; the host wake broker registers the instance once it exists."
|
|
139
142
|
},
|
|
140
143
|
"team": {
|
|
141
|
-
"description": "Target aweb team id for identity lifecycle. In workspace v2 spawns this payload value wins over OATS_TEAM_ID
|
|
144
|
+
"description": "Target aweb team id for the primary personal identity lifecycle. In workspace v2 spawns this payload value wins over OATS_TEAM_ID; if both are set and differ the hook warns and uses this setting."
|
|
142
145
|
},
|
|
143
146
|
"root": {
|
|
144
147
|
"hostOnly": true,
|
|
@@ -157,9 +160,48 @@
|
|
|
157
160
|
"residents": {
|
|
158
161
|
"hostOnly": true,
|
|
159
162
|
"description": "Host-owned map for global mode: resident name to absolute custody directory whose .aw holds the resident root keys and team certificate. Put this only in oats-local.yaml settings.oats.aweb.residents; committed workspace or soul files must never carry custody paths."
|
|
163
|
+
},
|
|
164
|
+
"join": {
|
|
165
|
+
"description": "comma-separated eligible team labels to join at spawn; values are workspace-defined labels and the default is absent"
|
|
160
166
|
}
|
|
161
167
|
},
|
|
162
168
|
"environmentNamespaces": [
|
|
163
169
|
"AWEB_"
|
|
164
|
-
]
|
|
170
|
+
],
|
|
171
|
+
"operations": {
|
|
172
|
+
"teams": {
|
|
173
|
+
"kind": "action",
|
|
174
|
+
"command": "teams",
|
|
175
|
+
"context": "home",
|
|
176
|
+
"description": "Read-only structured JSON summary of eligible and joined aweb teams for this home."
|
|
177
|
+
},
|
|
178
|
+
"join": {
|
|
179
|
+
"kind": "action",
|
|
180
|
+
"command": "join",
|
|
181
|
+
"context": "home",
|
|
182
|
+
"description": "Join comma-separated eligible aweb team labels for this home.",
|
|
183
|
+
"args": [
|
|
184
|
+
{
|
|
185
|
+
"name": "labels",
|
|
186
|
+
"flag": "--labels",
|
|
187
|
+
"required": true,
|
|
188
|
+
"description": "Comma-separated eligible team labels to join."
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
"leave": {
|
|
193
|
+
"kind": "action",
|
|
194
|
+
"command": "leave",
|
|
195
|
+
"context": "home",
|
|
196
|
+
"description": "Leave comma-separated joined aweb team labels for this home.",
|
|
197
|
+
"args": [
|
|
198
|
+
{
|
|
199
|
+
"name": "labels",
|
|
200
|
+
"flag": "--labels",
|
|
201
|
+
"required": true,
|
|
202
|
+
"description": "Comma-separated joined team labels to leave."
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
165
207
|
}
|
|
@@ -1,328 +1,106 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aweb-team-membership
|
|
3
|
-
description: This skill should be used when
|
|
4
|
-
allowed-tools: "Bash(aw *)"
|
|
3
|
+
description: This skill should be used when reasoning about which aweb/OATS teams an agent belongs to, checking team certificates and active-team diagnostics, or using the OATS provider's team operations (`oats aweb teams|join|leave`). Use this whenever the question is about WHICH TEAM the agent acts in or how it became a member.
|
|
4
|
+
allowed-tools: "Bash(aw workspace status), Bash(aw team list), Bash(aw id cert show), Bash(oats aweb *)"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# aweb Team Membership
|
|
7
|
+
# aweb Team Membership for OATS agents
|
|
8
8
|
|
|
9
|
-
Use this skill when the question is about teams
|
|
9
|
+
Use this skill when the question is about teams: current membership, eligible
|
|
10
|
+
workspace teams, joined wider teams, team certificates, or why a message/command
|
|
11
|
+
is landing in the wrong team. For identity keys, `did:key`/`did:aw`, custody,
|
|
12
|
+
addressability, inbound mode, contacts, or key rotation, load `aweb-identity`.
|
|
13
|
+
For mail/chat policy, load `aweb-messaging`.
|
|
10
14
|
|
|
11
|
-
##
|
|
15
|
+
## OATS owns agent team changes
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
For an OATS-managed instance, do **not** manually run native `aw team` mutation
|
|
18
|
+
commands to join, switch, invite, or leave teams. The `oats.aweb` provider owns
|
|
19
|
+
those lifecycle effects so it can keep per-team identity homes, provider state,
|
|
20
|
+
retire cleanup, readiness, and Desktop operations consistent.
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
- **Team certificate** — a signed statement that a specific `did:key` is a member of a specific team, with an alias and metadata. Public; replicated in AWID. Stored locally in `.aw/team-certs/*.pem`. A certificate proves team membership; it is not a message-decryption key.
|
|
17
|
-
- **Team id** — canonical form is `<name>:<namespace>` (e.g. `personal:acme.com`, `aweb:juan.aweb.ai`). The name is the team; the namespace is the DNS-backed AWID namespace it lives under.
|
|
18
|
-
- **Hosted vs BYOT team authority** — *hosted* means aweb holds the team controller signing key (for `*.aweb.ai` namespaces). *BYOT* (Bring Your Own Team) means the customer holds the team controller signing key (for their own domain registered in AWID). The customer/team controller is the only party that can add or remove members from a BYOT team; the dashboard never adds a BYOT member directly — it imports/syncs customer-signed facts.
|
|
19
|
-
|
|
20
|
-
## Team-related files in `.aw/`
|
|
21
|
-
|
|
22
|
-
For identity files (`signing.key`, `workspace.yaml` server URL), see `aweb-identity`. Team-specific files:
|
|
23
|
-
|
|
24
|
-
- `teams.yaml` — local index of teams this identity is a member of. Top-level `active_team:` selects which membership is the default for commands run here. `aw team list` reads this; `aw team switch <team-id>` updates it.
|
|
25
|
-
- `team-certs/*.pem` — public team certificates this identity has been issued (one `.pem` per team membership). `teams.yaml` and `workspace.yaml` reference these by `cert_path`.
|
|
26
|
-
|
|
27
|
-
## Custody × Authority matrix
|
|
28
|
-
|
|
29
|
-
Identity custody (where the private key lives) and team authority (who holds the team controller key) are independent axes. Use the matrix to pick the right joining path:
|
|
30
|
-
|
|
31
|
-
| Team authority | Identity custody | Meaning |
|
|
32
|
-
| --- | --- | --- |
|
|
33
|
-
| Hosted | Custodial | aweb manages team authority AND holds hosted identity signing key material (browser/MCP). Messaging in this mode is server-readable hosted messaging, not E2E. |
|
|
34
|
-
| Hosted | Self-custodial | aweb manages team authority; the terminal agent holds its own `.aw/signing.key`. |
|
|
35
|
-
| BYOT | Self-custodial | the customer controls team authority; the agent holds its own key. |
|
|
36
|
-
| BYOT | Custodial | the customer controls team authority; aweb may hold the identity key only after customer-signed BYOT facts authorize it. |
|
|
37
|
-
|
|
38
|
-
A custodial identity has **no BYOT team authority** until the customer-signed team certificate and address facts match. Do not infer team authority from identity custody.
|
|
39
|
-
|
|
40
|
-
For E2E messaging, custody and team membership are still not enough by themselves: the recipient's encryption public key must be identity-authorized as described in `docs/e2e-messaging-contract.md`. Team/namespace authority may distribute that assertion, but it must not replace the member's key. If an encryption-key check fails, do not suggest a team-controller workaround or plaintext fallback; stop and route the user to the approved identity/key setup or recovery flow.
|
|
41
|
-
|
|
42
|
-
## Readiness checks (membership level)
|
|
43
|
-
|
|
44
|
-
Start with:
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
aw workspace status
|
|
48
|
-
aw team list
|
|
49
|
-
aw id cert show
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Interpret failures by what's missing (for self-custodial CLI workspaces; custodial browser/MCP identities live entirely in the hosted account):
|
|
53
|
-
|
|
54
|
-
- **`.aw/teams.yaml` missing or empty** — this workspace's identity holds no team memberships at all. Join one (see paths below) before attempting team coordination.
|
|
55
|
-
- **No `.aw/team-certs/<team>.pem` for `teams.yaml`'s active team** — identity exists but holds no cert for the active team. Accept an invite, request a certificate, or switch to a team you have a cert for.
|
|
56
|
-
- **Active team mismatch** — `teams.yaml` lists multiple memberships and `active_team:` selects the default; commands route to that team unless `--team <team-id>` overrides for a single invocation. If commands appear to land in the wrong team, fix `active_team:` (run `aw team switch <team-id>`).
|
|
57
|
-
- **Workspace not bound to a server** — `workspace.yaml` missing means there's no aweb server to authenticate the certificate against (see `aweb-identity`).
|
|
58
|
-
|
|
59
|
-
## Joining a team — match the path to team authority
|
|
60
|
-
|
|
61
|
-
The right joining path depends entirely on **who holds the team controller signing key**. Pick by authority, not by the word "invite" alone.
|
|
62
|
-
|
|
63
|
-
### Hosted teams (aweb holds the team controller key)
|
|
64
|
-
|
|
65
|
-
Three distinct paths exist; they are NOT interchangeable.
|
|
66
|
-
|
|
67
|
-
**Path 1 — Fresh identity at init time.** A new agent without any prior identity arrives at a hosted team via OAuth (browser/MCP) or team API-key (CLI):
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
AWEB_API_KEY=<team-api-key> aw init
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
The hosted service provisions the identity and a team certificate together. Browser/MCP harnesses do this through OAuth without a CLI. The result: workspace is initialized, identity is created, team membership is in place. Verify with `aw workspace status` and `aw team list`.
|
|
74
|
-
|
|
75
|
-
**Path 2 — Existing global identity → "Add existing identity" in the dashboard.** When an agent already has a `did:aw` registered in AWID and wants to join an existing hosted team:
|
|
76
|
-
|
|
77
|
-
In the app.aweb.ai dashboard, an owner/admin clicks "Add existing identity" on the team, supplies the global identity's address or `did:aw`, and the backend mints a team certificate with the cloud-held team controller key, registers it in AWID, and prints commands like:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
aw id team fetch-cert --namespace <namespace> --team <team> --cert-id <cert-id>
|
|
81
|
-
aw team switch <team>:<namespace>
|
|
82
|
-
aw init # if the joining directory still needs server binding
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
No token round-trip. Direct controller-mint. Available only for hosted teams because BYOT controller keys aren't held by aweb.
|
|
86
|
-
|
|
87
|
-
**Path 3 — CLI invite-token, for hosted self-custodial local or global identities.** Hosted CLI invite tokens are redeemed through the cloud, but the accepting directory keeps its own signing key. Use the default form when the owner wants to invite a new local-workspace identity by token:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
# Owner side (in a workspace with the necessary authority):
|
|
91
|
-
aw team invite # local-workspace member token
|
|
92
|
-
|
|
93
|
-
# share the printed <token>
|
|
94
|
-
|
|
95
|
-
# Joiner side (in a clean target directory):
|
|
96
|
-
aw team join <token> --name <name>
|
|
97
|
-
aw init # finish wiring the new workspace if instructed
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`accept-invite` refuses to overwrite an existing `.aw/` identity and generates a fresh local self-custodial identity in the target directory before requesting the certificate. For a hosted global identity, accept the hosted token with `--address <domain>/<name>`:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
aw team join <token> --address <domain>/<name>
|
|
104
|
-
aw init # finish wiring the new workspace if instructed
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
In the hosted `--address` case, the CLI creates a fresh self-custodial global identity for the address, registers it through the hosted service, and installs the hosted team certificate. The hosted service signs the team certificate; it does not receive the accepting directory's private signing key. If the user already has a global identity and only needs a certificate for an existing hosted team, Path 2 (dashboard Add existing identity + `fetch-cert`) remains valid.
|
|
108
|
-
|
|
109
|
-
This is **not** the cross-machine BYOT path. Do not present it as the normal way to join a BYOT team from another machine.
|
|
110
|
-
|
|
111
|
-
**Do not run `aw id team add-member` for a hosted team** — `add-member` signs a certificate with the team controller key, which you do not hold for hosted teams. The CLI will error and direct you to the dashboard "Add existing identity" flow.
|
|
112
|
-
|
|
113
|
-
### BYOT teams (customer holds the team controller key)
|
|
114
|
-
|
|
115
|
-
The dashboard cannot add BYOT members directly. The customer's team controller must sign. Three cases:
|
|
116
|
-
|
|
117
|
-
**Case 1 — Self-custodial identity, cross-machine join.** The joining machine doesn't hold the team controller key:
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
# Joining identity machine
|
|
121
|
-
aw id team request --team <team>:<namespace> --name <name>
|
|
122
|
-
|
|
123
|
-
# Controller machine — runs the exact command the request printed:
|
|
124
|
-
aw id team add-member ...
|
|
125
|
-
|
|
126
|
-
# Back on the joining identity machine
|
|
127
|
-
aw id team fetch-cert --namespace <namespace> --team <team> --cert-id <id>
|
|
128
|
-
aw team switch <team>:<namespace>
|
|
129
|
-
aw init # if needed
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
The team controller private key never leaves the controller machine.
|
|
133
|
-
|
|
134
|
-
**Case 2 — Custodial browser identity into a BYOT team.** Start from the dashboard's "Create custodial request" action. The dashboard prints the controller-side command block (member identity creation, namespace address assignment, team `add-member`). The team controller runs that block on their machine, then syncs the signed team state into aweb cloud:
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
aw id team import-request --team <team> --namespace <namespace> --cloud-team-id <cloud-team-id> --apply
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
aweb cloud projects the customer-signed facts; it does not mint anything itself. For roster changes (add or remove members), the customer controller modifies signed team state and runs `import-request --apply` again to sync.
|
|
141
|
-
|
|
142
|
-
**Case 3 — Same-machine local-controller invite-token convenience.** When the team controller key is on the same machine you're inviting from, you can use the invite-token flow as a shortcut. Two variants:
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
# Owner side, same machine, local-controller key present:
|
|
146
|
-
aw team invite # local-workspace member (default)
|
|
147
|
-
aw team invite --global # global-member token (requires existing global identity in the accepting directory)
|
|
148
|
-
|
|
149
|
-
# Joiner side (still same machine, different directory):
|
|
150
|
-
|
|
151
|
-
# For a local invite:
|
|
152
|
-
aw team join <token> --name <name>
|
|
153
|
-
|
|
154
|
-
# For a global invite, the accepting directory must already have a global identity:
|
|
155
|
-
aw id create --domain <domain> --name <name>
|
|
156
|
-
aw team join <token> --address <namespace>/<name>
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
For the `--global` case, `accept-invite` does NOT create the global identity — it errors with `no identity found; run aw id create first, or use --local invite` if no identity is present. `--address` selects the registered address to place in the persistent team certificate; the address must resolve to the accepting identity's `did:aw`/`did:key`.
|
|
160
|
-
|
|
161
|
-
This is the local-controller convenience case only. For cross-machine BYOT joins, use Case 1.
|
|
162
|
-
|
|
163
|
-
### Not a membership path: human dashboard invites
|
|
164
|
-
|
|
165
|
-
`/api/v1/teams/.../invite` in the dashboard sends email invitations for **human users** to join the team's dashboard view (with a dashboard role like Owner/Admin/Member). That is independent of AWID agent membership certificates. Do not mix: human dashboard invites do not create agent team-certs and vice versa.
|
|
166
|
-
|
|
167
|
-
## Accepting an invite vs fetching a certificate
|
|
168
|
-
|
|
169
|
-
Two distinct local actions install a membership:
|
|
170
|
-
|
|
171
|
-
- **`aw team join <token>`** (human-facing alias for `aw id team accept-invite <token>`) — redeems a CLI invite token. For hosted invites (Path 3), generates a fresh self-custodial identity in the current directory (refusing to overwrite) and installs the certificate; default is local, while `--address <domain>/<name>` creates/registers a fresh global identity through the hosted service before certificate install. For local-controller same-machine invites (BYOT Case 3), local-member behaves the same way as hosted local; global accepts require an existing global identity (from `aw id create`) and attach a team certificate to it via `--address <namespace>/<name>`.
|
|
172
|
-
- **`aw id team fetch-cert --namespace <namespace> --team <team> --cert-id <id>`** — installs a certificate that has already been minted server-side (by hosted "Add existing identity") or signed by a controller (BYOT `add-member`). Used for hosted Path 2 and BYOT Case 1.
|
|
173
|
-
|
|
174
|
-
If you have a token, use `aw team join` (or the underlying primitive `aw id team accept-invite`). If you have a `cert-id` printed by the dashboard or controller, use `fetch-cert`.
|
|
175
|
-
|
|
176
|
-
## Multiple team memberships
|
|
177
|
-
|
|
178
|
-
One identity can hold multiple team certificates simultaneously — one per team — all stored in `.aw/team-certs/`. Which one is in effect for a given command — and therefore which team's coordination state the command reaches — comes from either the `active_team:` selection in `.aw/teams.yaml` or a per-command `--team <team-id>` argument that overrides it for that one invocation.
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
aw team list # see memberships
|
|
182
|
-
aw team switch <team>:<namespace> # update teams.yaml's active_team
|
|
183
|
-
aw <verb> --team <team>:<namespace> ... # one-off override for team-scoped commands
|
|
184
|
-
aw team leave <team>:<namespace> # remove a local membership
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Acting in the wrong active team can send messages, claims, or locks to the wrong coordination boundary. Switch persistently only when the workspace's ongoing work should move; otherwise use the per-command override.
|
|
188
|
-
|
|
189
|
-
If the recipient's `inbound_mode` is `team-and-contacts`, valid same-team membership is one of the authorization paths for delivery to them. The full inbound-mode model is in `aweb-identity`.
|
|
190
|
-
|
|
191
|
-
## Fresh BYOT setup into aweb cloud
|
|
192
|
-
|
|
193
|
-
Use this flow when the user controls DNS for a domain and wants to create a customer-controlled AWID team, add agents, and import/sync it into app.aweb.ai.
|
|
194
|
-
|
|
195
|
-
Vocabulary:
|
|
196
|
-
|
|
197
|
-
- The namespace is the domain, e.g. `juanreyero.com`.
|
|
198
|
-
- The team is named inside that namespace, e.g. `personal`; its AWID team id is `personal:juanreyero.com`.
|
|
199
|
-
- Agents have addresses under the namespace, e.g. `juanreyero.com/alpha`.
|
|
200
|
-
- Do not call `personal:juanreyero.com` an agent; it is the team id.
|
|
201
|
-
|
|
202
|
-
Before starting, confirm `aw version` includes `aw id namespace prepare-controller` and `aw id namespace check-txt`; older `aw` versions make this flow hard to drive from non-interactive harnesses.
|
|
203
|
-
|
|
204
|
-
**Use `aw id create`, NOT `aw init --byod --global`, for the identity-prep commands in this section.** `aw init --byod --global` is workspace onboarding: it bootstraps the directory and connects it to the `default:<domain>` team on app.aweb.ai (the team created during BYOD onboarding), writing `workspace.yaml`, joining that team, and minting a team certificate. That short-circuits the controller-signed team-state import this section is about. `aw id create` only mints the identity in AWID and writes `.aw/identity.yaml` + `.aw/signing.key`, leaving the team membership for the customer controller to add and sign. If a user already ran the wrong command and needs to recover, see the matching diagnostic bullet in `aweb-identity`'s readiness checks.
|
|
205
|
-
|
|
206
|
-
Namespace controller setup (does not create an identity or team):
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
aw id namespace prepare-controller --domain <domain>
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Pause and have the human add the printed `_awid.<domain>` TXT record. Do not invent DNS values. Tell the human to back up `~/.awid` now; it contains the namespace controller key. After DNS propagates, verify it:
|
|
213
|
-
|
|
214
|
-
```bash
|
|
215
|
-
aw id namespace check-txt --domain <domain>
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Then create the BYOT team with the namespace controller key:
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
aw id team create --namespace <domain> --name <team> --display-name "<display name>"
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
After team creation, tell the human to back up `~/.awid` again; it now also contains the team controller key under `~/.awid/team-keys/<domain>/<team>.key`.
|
|
225
|
-
|
|
226
|
-
Add initial global agents:
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
aw id create --domain <domain> --name alpha
|
|
230
|
-
aw id team add-member --team <team> --namespace <domain> --did <alpha_did_key> --name alpha --global --did-aw <alpha_did_aw>
|
|
231
|
-
|
|
232
|
-
aw id create --domain <domain> --name beta
|
|
233
|
-
aw id team add-member --team <team> --namespace <domain> --did <beta_did_key> --name beta --global --did-aw <beta_did_aw>
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Use the actual `did`/`did_aw` values printed by `aw id create`. Do not guess them.
|
|
237
|
-
|
|
238
|
-
Register with aweb cloud without using the dashboard:
|
|
239
|
-
|
|
240
|
-
```bash
|
|
241
|
-
aw id team register --service https://app.aweb.ai --team <team>:<domain>
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
This signs a service-registration request with the team controller key. It creates or syncs an aweb projection of the AWID team, but it does not upload controller private keys, create identities, or initialize any agent workspace. Read the returned next steps and run the required workspace connection command from each already-certified agent directory:
|
|
245
|
-
|
|
246
|
-
```bash
|
|
247
|
-
aw workspace connect --service https://app.aweb.ai --team <team>:<domain>
|
|
248
|
-
# equivalent service-oriented primitive: aw service init --service https://app.aweb.ai --team <team>:<domain>
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
`aw workspace connect` / `aw service init` requires the local `.aw/signing.key`, `.aw/teams.yaml`, and `.aw/team-certs/*.pem` for that agent. If the certificate is not installed yet, fetch it first with the certificate id returned by `aw id team add-member`:
|
|
22
|
+
Use the provider commands from the instance home (or with `--home <path>`):
|
|
252
23
|
|
|
253
24
|
```bash
|
|
254
|
-
|
|
25
|
+
oats aweb teams --json # personal, eligible, joined, unmapped
|
|
26
|
+
oats aweb join --labels <label>[,<label>] # join eligible workspace labels
|
|
27
|
+
oats aweb leave --labels <label>[,<label>] # leave joined wider-team labels
|
|
255
28
|
```
|
|
256
29
|
|
|
257
|
-
|
|
30
|
+
- The personal team cannot be left; attempting it is `E_TEAM_PERSONAL`.
|
|
31
|
+
- A label that is not eligible for this soul/workspace is `E_TEAM_NOT_ELIGIBLE`.
|
|
32
|
+
- Joined wider teams use a local identity home such as
|
|
33
|
+
`<home>/.aweb-identity-<label>` and receive by polling in oats.aweb 1.14. Joined teams require aw >= 1.36.12. The provider creates joined homes with `aw id team accept-invite` under `--identity-home`, verifies the root auto-connected, and does not run `aw init` inside the per-team home.
|
|
34
|
+
- Send as a joined team with exactly:
|
|
258
35
|
|
|
259
36
|
```bash
|
|
260
|
-
aw
|
|
37
|
+
aw --identity-home <identityHome> mail|chat ...
|
|
261
38
|
```
|
|
262
39
|
|
|
263
|
-
`
|
|
40
|
+
`oats aweb teams --json` prints each joined entry's `identityHome` and
|
|
41
|
+
`receive` mode.
|
|
264
42
|
|
|
265
|
-
|
|
43
|
+
## Readiness checks
|
|
266
44
|
|
|
267
|
-
|
|
268
|
-
2. Open the BYOT import flow. Prefer the command shown by the dashboard because it contains the correct `--organization-id`.
|
|
269
|
-
3. First preview:
|
|
45
|
+
Start with read-only diagnostics:
|
|
270
46
|
|
|
271
47
|
```bash
|
|
272
|
-
aw
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
4. If the preview is correct, regenerate an apply request:
|
|
278
|
-
|
|
279
|
-
```bash
|
|
280
|
-
aw id team import-request --team <team> --namespace <domain> --organization-id <org-id> --apply
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
Paste it and use Import / sync.
|
|
284
|
-
|
|
285
|
-
Sync later changes:
|
|
286
|
-
|
|
287
|
-
- After the team exists in aweb cloud, use `--cloud-team-id <cloud-team-id>` instead of `--organization-id`.
|
|
288
|
-
- The dashboard Connect / Sync page should show the exact command. Prefer that command.
|
|
289
|
-
|
|
290
|
-
```bash
|
|
291
|
-
aw id team import-request --team <team> --namespace <domain> --cloud-team-id <cloud-team-id> --apply
|
|
48
|
+
aw workspace status
|
|
49
|
+
oats aweb teams --json
|
|
50
|
+
aw team list
|
|
51
|
+
aw id cert show
|
|
292
52
|
```
|
|
293
53
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
-
|
|
305
|
-
- `
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
54
|
+
Interpret common states:
|
|
55
|
+
|
|
56
|
+
- `teams.personal.team` is the primary personal team identity wired to the
|
|
57
|
+
harness.
|
|
58
|
+
- `eligible[]` are labels this soul/workspace may explicitly join; the primary
|
|
59
|
+
label may appear here and is joinable/leavable like any other wider team.
|
|
60
|
+
- `joined[]` are provider-created wider-team memberships; each has an
|
|
61
|
+
`identityHome`, `since`, and `receive` (`poll` in 1.14).
|
|
62
|
+
- `unmapped[]` labels are present on the soul but not mapped by the workspace.
|
|
63
|
+
An unmapped primary falls back to the personal/root active team with a
|
|
64
|
+
`team-unmapped` warning; it is not a spawn blocker.
|
|
65
|
+
- `teams-unverified` on launch means the kernel supplied recorded/unknown team
|
|
66
|
+
data, so the provider kept memberships instead of leaving anything.
|
|
67
|
+
|
|
68
|
+
## Team vocabulary
|
|
69
|
+
|
|
70
|
+
- **Team id**: canonical form `<name>:<namespace>` (for example
|
|
71
|
+
`default:oats.aweb.ai`).
|
|
72
|
+
- **Team certificate**: a signed membership statement for an identity; stored in
|
|
73
|
+
`.aw/team-certs/` for native identities.
|
|
74
|
+
- **Personal team**: the default team for the instance's primary identity. In
|
|
75
|
+
1.14.1, until per-workspace personal teams are available, this may be the
|
|
76
|
+
person's default team as a stand-in.
|
|
77
|
+
- **Joined team**: an explicit wider team joined through `oats aweb join`, with a
|
|
78
|
+
separate local identity home in this release.
|
|
79
|
+
|
|
80
|
+
## Hosted vs BYOT authority (diagnostic context)
|
|
81
|
+
|
|
82
|
+
Hosted teams are signed by aweb-held team authority; BYOT teams are signed by a
|
|
83
|
+
customer-held controller. This matters when diagnosing why a human or provider
|
|
84
|
+
cannot mint a certificate, but ordinary OATS agents should still use
|
|
85
|
+
`oats aweb join|leave` rather than native membership mutation commands. If a
|
|
86
|
+
join reports authorization failure, ask the team's owner/admin for the needed
|
|
87
|
+
invite or mapping; do not invent a native workaround.
|
|
88
|
+
|
|
89
|
+
## Wrong team symptoms
|
|
90
|
+
|
|
91
|
+
If commands appear to use the wrong team:
|
|
92
|
+
|
|
93
|
+
1. Run `oats aweb teams --json` and confirm which identity home should send.
|
|
94
|
+
2. For the primary identity, run `aw workspace status` and `aw team list`.
|
|
95
|
+
3. For a joined team, run `aw --identity-home <identityHome> mail inbox` or
|
|
96
|
+
`aw --identity-home <identityHome> chat pending` and send with the same
|
|
97
|
+
`--identity-home`.
|
|
98
|
+
4. If the provider state and native files disagree, report the exact output to a
|
|
99
|
+
coordinator; do not hand-edit `.aw` or `.oats-aweb/teams.json`.
|
|
320
100
|
|
|
321
101
|
## References
|
|
322
102
|
|
|
323
|
-
Read
|
|
103
|
+
Read only when deeper context is needed:
|
|
324
104
|
|
|
325
|
-
- `references/team-membership-reference.md`: detailed hosted/BYOT and diagnostic notes.
|
|
326
105
|
- <https://aweb.ai/docs/teams/>: team model.
|
|
327
|
-
- <https://
|
|
328
|
-
- <https://aweb.ai/docs/agent-guide/>: full agent guide.
|
|
106
|
+
- <https://aweb.ai/docs/agent-guide/>: agent messaging guide.
|
package/docs/capabilities.md
CHANGED
|
@@ -118,7 +118,9 @@ A self-contained package has an `oats.json`:
|
|
|
118
118
|
record can never be empty: a quarantine exists because something is outstanding,
|
|
119
119
|
and one claiming otherwise would give the retry nothing to prove. A marker failing any of
|
|
120
120
|
that is no more retryable than a missing one, and is treated as missing so the
|
|
121
|
-
escape hatch works.
|
|
121
|
+
escape hatch works. `--force` never skips work preservation, which runs
|
|
122
|
+
before the retire hooks and refuses with `E_WORK_PRESERVATION_FAILED` when
|
|
123
|
+
its recovery cannot be verified.
|
|
122
124
|
- A retry clears the quarantine only by **proving the outstanding work happened**:
|
|
123
125
|
every retire hook the marker records as owing cleanup must have run and reported
|
|
124
126
|
success, and every Git step it records must be re-run and verified. A retry that resolves no
|