a2acalling 0.6.0 → 0.6.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/README.md +33 -9
- package/SKILL.md +67 -5
- package/bin/cli.js +468 -151
- package/docs/protocol.md +24 -14
- package/package.json +1 -1
- package/scripts/install-openclaw.js +64 -68
- package/src/dashboard/public/app.js +765 -28
- package/src/dashboard/public/index.html +57 -13
- package/src/dashboard/public/style.css +16 -0
- package/src/lib/callbook.js +358 -0
- package/src/lib/client.js +1 -2
- package/src/lib/config.js +67 -15
- package/src/lib/external-ip.js +18 -7
- package/src/lib/invite-host.js +26 -41
- package/src/lib/logger.js +26 -14
- package/src/lib/tokens.js +314 -113
- package/src/routes/a2a.js +11 -2
- package/src/routes/callbook.js +142 -0
- package/src/routes/dashboard.js +557 -25
- package/src/server.js +6 -0
|
@@ -26,19 +26,26 @@
|
|
|
26
26
|
<h2>Contacts</h2>
|
|
27
27
|
<button id="refresh-contacts">Refresh</button>
|
|
28
28
|
</div>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
|
|
30
|
+
<div class="card">
|
|
31
|
+
<h3>Add Contact</h3>
|
|
32
|
+
<form id="add-contact-form">
|
|
33
|
+
<label>Invite URL <input id="add-contact-url" type="text" placeholder="a2a://host/token" required></label>
|
|
34
|
+
<label>Agent name <input id="add-contact-name" type="text" placeholder="Agent name (optional)"></label>
|
|
35
|
+
<label>Owner name <input id="add-contact-owner" type="text" placeholder="Human owner (optional)"></label>
|
|
36
|
+
<label><input id="add-contact-mine" type="checkbox"> Mark as mine (personal agent)</label>
|
|
37
|
+
<label>Server name (my agents only) <input id="add-contact-server-name" type="text" placeholder="Server label (optional)"></label>
|
|
38
|
+
<label>Tags <input id="add-contact-tags" type="text" placeholder="comma,separated (optional)"></label>
|
|
39
|
+
<label>Notes <textarea id="add-contact-notes" rows="3" placeholder="Notes (optional)"></textarea></label>
|
|
40
|
+
<label>Fields (JSON) <textarea id="add-contact-fields" rows="4" placeholder='{"email":"...","company":"..."} (optional)'></textarea></label>
|
|
41
|
+
<div class="row">
|
|
42
|
+
<button type="submit">Add</button>
|
|
43
|
+
</div>
|
|
44
|
+
</form>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div id="contacts-sections"></div>
|
|
48
|
+
<div id="contact-detail" class="card"></div>
|
|
42
49
|
</section>
|
|
43
50
|
|
|
44
51
|
<section id="tab-calls" class="panel">
|
|
@@ -160,6 +167,43 @@
|
|
|
160
167
|
<button type="submit">Create Tier</button>
|
|
161
168
|
</div>
|
|
162
169
|
</form>
|
|
170
|
+
|
|
171
|
+
<h3>Remote Callbook</h3>
|
|
172
|
+
<div id="callbook-status" class="card"></div>
|
|
173
|
+
|
|
174
|
+
<form id="callbook-provision-form" class="card">
|
|
175
|
+
<div class="row">
|
|
176
|
+
<button type="submit">Create Install Link (24h)</button>
|
|
177
|
+
<button id="callbook-refresh" type="button">Refresh</button>
|
|
178
|
+
<button id="callbook-logout" type="button">Logout This Browser</button>
|
|
179
|
+
</div>
|
|
180
|
+
<label>Device label <input id="callbook-label" type="text" value="Callbook Remote"></label>
|
|
181
|
+
<label>Install URL<textarea id="callbook-install-url" rows="3" readonly></textarea></label>
|
|
182
|
+
<div class="row">
|
|
183
|
+
<button id="callbook-copy-url" type="button">Copy Link</button>
|
|
184
|
+
</div>
|
|
185
|
+
<div id="callbook-warnings" class="mono"></div>
|
|
186
|
+
</form>
|
|
187
|
+
|
|
188
|
+
<div class="card">
|
|
189
|
+
<div class="row">
|
|
190
|
+
<strong>Paired Devices</strong>
|
|
191
|
+
<button id="callbook-refresh-devices" type="button">Refresh Devices</button>
|
|
192
|
+
</div>
|
|
193
|
+
<table id="callbook-devices-table">
|
|
194
|
+
<thead>
|
|
195
|
+
<tr>
|
|
196
|
+
<th>Label</th>
|
|
197
|
+
<th>Created</th>
|
|
198
|
+
<th>Last Used</th>
|
|
199
|
+
<th>Sessions</th>
|
|
200
|
+
<th>Revoked</th>
|
|
201
|
+
<th>Action</th>
|
|
202
|
+
</tr>
|
|
203
|
+
</thead>
|
|
204
|
+
<tbody></tbody>
|
|
205
|
+
</table>
|
|
206
|
+
</div>
|
|
163
207
|
</section>
|
|
164
208
|
|
|
165
209
|
<section id="tab-invites" class="panel">
|
|
@@ -140,6 +140,18 @@ button:hover {
|
|
|
140
140
|
color: var(--accent);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
.btn-link {
|
|
144
|
+
border: none;
|
|
145
|
+
background: none;
|
|
146
|
+
padding: 0;
|
|
147
|
+
color: var(--accent);
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.btn-link:hover {
|
|
152
|
+
text-decoration: underline;
|
|
153
|
+
}
|
|
154
|
+
|
|
143
155
|
table {
|
|
144
156
|
width: 100%;
|
|
145
157
|
border-collapse: collapse;
|
|
@@ -159,6 +171,10 @@ th {
|
|
|
159
171
|
background: #f8fafc;
|
|
160
172
|
}
|
|
161
173
|
|
|
174
|
+
tr[data-selected="1"] td {
|
|
175
|
+
background: #f0f7ff;
|
|
176
|
+
}
|
|
177
|
+
|
|
162
178
|
.summary {
|
|
163
179
|
max-width: 500px;
|
|
164
180
|
white-space: pre-wrap;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callbook Remote Auth
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Allow a remote owner UI ("Callbook Remote") to manage the dashboard safely.
|
|
6
|
+
* - Provisioning codes are short-lived and one-time use (safe-ish to share as an install link).
|
|
7
|
+
* - Sessions are long-lived (no expiration by default) and revocable.
|
|
8
|
+
*
|
|
9
|
+
* Storage:
|
|
10
|
+
* - SQLite DB at ~/.config/openclaw/a2a-callbook.db (or $A2A_CONFIG_DIR).
|
|
11
|
+
*
|
|
12
|
+
* Notes:
|
|
13
|
+
* - Secrets are never stored plaintext: only SHA-256 hashes are persisted.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
|
|
21
|
+
process.env.OPENCLAW_CONFIG_DIR ||
|
|
22
|
+
path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
|
|
23
|
+
|
|
24
|
+
const DB_FILENAME = 'a2a-callbook.db';
|
|
25
|
+
|
|
26
|
+
function nowIso() {
|
|
27
|
+
return new Date().toISOString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sha256Hex(value) {
|
|
31
|
+
return crypto.createHash('sha256').update(String(value || '')).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function randomToken(prefix = '') {
|
|
35
|
+
const token = crypto.randomBytes(32).toString('base64url');
|
|
36
|
+
return prefix ? `${prefix}${token}` : token;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class CallbookStore {
|
|
40
|
+
constructor(configDir = DEFAULT_CONFIG_DIR, options = {}) {
|
|
41
|
+
this.configDir = configDir;
|
|
42
|
+
this.dbPath = options.dbPath || path.join(configDir, DB_FILENAME);
|
|
43
|
+
this.db = null;
|
|
44
|
+
this._dbError = null;
|
|
45
|
+
this._stmts = null;
|
|
46
|
+
this._ensureDir();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_ensureDir() {
|
|
50
|
+
if (!fs.existsSync(this.configDir)) {
|
|
51
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_initDb() {
|
|
56
|
+
if (this.db) return this.db;
|
|
57
|
+
if (this._dbError) return null;
|
|
58
|
+
try {
|
|
59
|
+
const Database = require('better-sqlite3');
|
|
60
|
+
this.db = new Database(this.dbPath);
|
|
61
|
+
try {
|
|
62
|
+
fs.chmodSync(this.dbPath, 0o600);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// best effort
|
|
65
|
+
}
|
|
66
|
+
this._migrate();
|
|
67
|
+
this._prepareStatements();
|
|
68
|
+
return this.db;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
this._dbError = err && err.message ? err.message : 'failed_to_initialize_callbook_db';
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_migrate() {
|
|
76
|
+
this.db.exec(`
|
|
77
|
+
PRAGMA journal_mode = WAL;
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS provision_codes (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
code_hash TEXT NOT NULL UNIQUE,
|
|
82
|
+
label TEXT,
|
|
83
|
+
created_at TEXT NOT NULL,
|
|
84
|
+
expires_at TEXT NOT NULL,
|
|
85
|
+
used_at TEXT
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_provision_codes_expires ON provision_codes(expires_at);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_provision_codes_used ON provision_codes(used_at);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
label TEXT,
|
|
94
|
+
created_at TEXT NOT NULL,
|
|
95
|
+
revoked_at TEXT,
|
|
96
|
+
last_used_at TEXT
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_devices_revoked ON devices(revoked_at);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
device_id TEXT NOT NULL,
|
|
104
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
105
|
+
created_at TEXT NOT NULL,
|
|
106
|
+
revoked_at TEXT,
|
|
107
|
+
last_used_at TEXT,
|
|
108
|
+
FOREIGN KEY(device_id) REFERENCES devices(id)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_device_id ON sessions(device_id);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_revoked ON sessions(revoked_at);
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_prepareStatements() {
|
|
117
|
+
this._stmts = {
|
|
118
|
+
insertProvision: this.db.prepare(
|
|
119
|
+
`INSERT INTO provision_codes (id, code_hash, label, created_at, expires_at, used_at)
|
|
120
|
+
VALUES (@id, @code_hash, @label, @created_at, @expires_at, NULL)`
|
|
121
|
+
),
|
|
122
|
+
findProvisionByHash: this.db.prepare(
|
|
123
|
+
`SELECT * FROM provision_codes WHERE code_hash = ?`
|
|
124
|
+
),
|
|
125
|
+
markProvisionUsed: this.db.prepare(
|
|
126
|
+
`UPDATE provision_codes SET used_at = @used_at WHERE id = @id AND used_at IS NULL`
|
|
127
|
+
),
|
|
128
|
+
|
|
129
|
+
insertDevice: this.db.prepare(
|
|
130
|
+
`INSERT INTO devices (id, label, created_at, revoked_at, last_used_at)
|
|
131
|
+
VALUES (@id, @label, @created_at, NULL, NULL)`
|
|
132
|
+
),
|
|
133
|
+
getDevice: this.db.prepare(
|
|
134
|
+
`SELECT * FROM devices WHERE id = ?`
|
|
135
|
+
),
|
|
136
|
+
listDevices: this.db.prepare(
|
|
137
|
+
`SELECT
|
|
138
|
+
d.id,
|
|
139
|
+
d.label,
|
|
140
|
+
d.created_at,
|
|
141
|
+
d.revoked_at,
|
|
142
|
+
d.last_used_at,
|
|
143
|
+
(SELECT COUNT(*) FROM sessions s WHERE s.device_id = d.id AND s.revoked_at IS NULL) AS active_sessions
|
|
144
|
+
FROM devices d
|
|
145
|
+
ORDER BY d.created_at DESC
|
|
146
|
+
LIMIT ?`
|
|
147
|
+
),
|
|
148
|
+
updateDeviceLastUsed: this.db.prepare(
|
|
149
|
+
`UPDATE devices SET last_used_at = ? WHERE id = ?`
|
|
150
|
+
),
|
|
151
|
+
revokeDevice: this.db.prepare(
|
|
152
|
+
`UPDATE devices SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL`
|
|
153
|
+
),
|
|
154
|
+
|
|
155
|
+
insertSession: this.db.prepare(
|
|
156
|
+
`INSERT INTO sessions (id, device_id, token_hash, created_at, revoked_at, last_used_at)
|
|
157
|
+
VALUES (@id, @device_id, @token_hash, @created_at, NULL, NULL)`
|
|
158
|
+
),
|
|
159
|
+
findSessionByHash: this.db.prepare(
|
|
160
|
+
`SELECT * FROM sessions WHERE token_hash = ?`
|
|
161
|
+
),
|
|
162
|
+
updateSessionLastUsed: this.db.prepare(
|
|
163
|
+
`UPDATE sessions SET last_used_at = ? WHERE id = ?`
|
|
164
|
+
),
|
|
165
|
+
revokeSessionsByDevice: this.db.prepare(
|
|
166
|
+
`UPDATE sessions SET revoked_at = ? WHERE device_id = ? AND revoked_at IS NULL`
|
|
167
|
+
)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
isAvailable() {
|
|
172
|
+
return Boolean(this._initDb());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getDbError() {
|
|
176
|
+
this._initDb();
|
|
177
|
+
return this._dbError;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create a one-time provisioning code (default TTL: 24 hours).
|
|
182
|
+
* Returns { code, record } where code is plaintext (show once).
|
|
183
|
+
*/
|
|
184
|
+
createProvisionCode(options = {}) {
|
|
185
|
+
const db = this._initDb();
|
|
186
|
+
if (!db) {
|
|
187
|
+
return { success: false, error: 'callbook_storage_unavailable', message: this._dbError };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const label = options.label ? String(options.label).trim().slice(0, 120) : null;
|
|
191
|
+
const ttlMs = Number.isFinite(options.ttlMs) ? options.ttlMs : (24 * 60 * 60 * 1000);
|
|
192
|
+
const createdAt = nowIso();
|
|
193
|
+
const expiresAt = new Date(Date.now() + Math.max(1, ttlMs)).toISOString();
|
|
194
|
+
|
|
195
|
+
const code = randomToken('cbk_');
|
|
196
|
+
const record = {
|
|
197
|
+
id: `cbkprov_${crypto.randomBytes(10).toString('hex')}`,
|
|
198
|
+
code_hash: sha256Hex(code),
|
|
199
|
+
label,
|
|
200
|
+
created_at: createdAt,
|
|
201
|
+
expires_at: expiresAt
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
this._stmts.insertProvision.run(record);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
success: true,
|
|
208
|
+
code,
|
|
209
|
+
record: {
|
|
210
|
+
id: record.id,
|
|
211
|
+
label: record.label,
|
|
212
|
+
created_at: record.created_at,
|
|
213
|
+
expires_at: record.expires_at,
|
|
214
|
+
used_at: null
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Exchange a provisioning code for a long-lived session token.
|
|
221
|
+
* Returns { sessionToken, device } on success.
|
|
222
|
+
*/
|
|
223
|
+
exchangeProvisionCode(code, options = {}) {
|
|
224
|
+
const db = this._initDb();
|
|
225
|
+
if (!db) {
|
|
226
|
+
return { success: false, error: 'callbook_storage_unavailable', message: this._dbError };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const raw = String(code || '').trim();
|
|
230
|
+
if (!raw) {
|
|
231
|
+
return { success: false, error: 'missing_code' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const codeHash = sha256Hex(raw);
|
|
235
|
+
const found = this._stmts.findProvisionByHash.get(codeHash);
|
|
236
|
+
if (!found) {
|
|
237
|
+
return { success: false, error: 'invalid_code' };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
const expiresMs = Date.parse(found.expires_at);
|
|
242
|
+
if (Number.isFinite(expiresMs) && now > expiresMs) {
|
|
243
|
+
return { success: false, error: 'code_expired' };
|
|
244
|
+
}
|
|
245
|
+
if (found.used_at) {
|
|
246
|
+
return { success: false, error: 'code_already_used' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const usedAt = nowIso();
|
|
250
|
+
const tx = db.transaction(() => {
|
|
251
|
+
const marked = this._stmts.markProvisionUsed.run({ id: found.id, used_at: usedAt });
|
|
252
|
+
if (!marked || marked.changes !== 1) {
|
|
253
|
+
// Another request raced us.
|
|
254
|
+
return { success: false, error: 'code_already_used' };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const deviceId = `cbkdev_${crypto.randomBytes(10).toString('hex')}`;
|
|
258
|
+
const deviceLabel = (options.label ? String(options.label).trim().slice(0, 120) : null) || found.label || null;
|
|
259
|
+
this._stmts.insertDevice.run({
|
|
260
|
+
id: deviceId,
|
|
261
|
+
label: deviceLabel,
|
|
262
|
+
created_at: usedAt
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const sessionToken = randomToken('cbksess_');
|
|
266
|
+
const sessionId = `cbks_${crypto.randomBytes(10).toString('hex')}`;
|
|
267
|
+
this._stmts.insertSession.run({
|
|
268
|
+
id: sessionId,
|
|
269
|
+
device_id: deviceId,
|
|
270
|
+
token_hash: sha256Hex(sessionToken),
|
|
271
|
+
created_at: usedAt
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
sessionToken,
|
|
277
|
+
device: {
|
|
278
|
+
id: deviceId,
|
|
279
|
+
label: deviceLabel,
|
|
280
|
+
created_at: usedAt,
|
|
281
|
+
revoked_at: null,
|
|
282
|
+
last_used_at: null
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return tx();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Validate a session token from a cookie.
|
|
292
|
+
* Returns { valid, session, device }.
|
|
293
|
+
*/
|
|
294
|
+
validateSession(sessionToken) {
|
|
295
|
+
const db = this._initDb();
|
|
296
|
+
if (!db) {
|
|
297
|
+
return { valid: false, error: 'callbook_storage_unavailable' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const raw = String(sessionToken || '').trim();
|
|
301
|
+
if (!raw) return { valid: false, error: 'missing_session' };
|
|
302
|
+
|
|
303
|
+
const session = this._stmts.findSessionByHash.get(sha256Hex(raw));
|
|
304
|
+
if (!session) return { valid: false, error: 'invalid_session' };
|
|
305
|
+
if (session.revoked_at) return { valid: false, error: 'session_revoked' };
|
|
306
|
+
|
|
307
|
+
const device = this._stmts.getDevice.get(session.device_id);
|
|
308
|
+
if (!device) return { valid: false, error: 'device_not_found' };
|
|
309
|
+
if (device.revoked_at) return { valid: false, error: 'device_revoked' };
|
|
310
|
+
|
|
311
|
+
const usedAt = nowIso();
|
|
312
|
+
try {
|
|
313
|
+
this._stmts.updateSessionLastUsed.run(usedAt, session.id);
|
|
314
|
+
this._stmts.updateDeviceLastUsed.run(usedAt, device.id);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
// best effort
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { valid: true, session: { id: session.id, device_id: session.device_id }, device };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
listDevices(options = {}) {
|
|
323
|
+
const db = this._initDb();
|
|
324
|
+
if (!db) {
|
|
325
|
+
return { success: false, error: 'callbook_storage_unavailable', message: this._dbError, devices: [] };
|
|
326
|
+
}
|
|
327
|
+
const limit = Math.min(500, Math.max(1, Number.parseInt(String(options.limit || '200'), 10) || 200));
|
|
328
|
+
const rows = this._stmts.listDevices.all(limit);
|
|
329
|
+
const includeRevoked = Boolean(options.includeRevoked);
|
|
330
|
+
const devices = includeRevoked ? rows : rows.filter(r => !r.revoked_at);
|
|
331
|
+
return { success: true, devices };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
revokeDevice(deviceId) {
|
|
335
|
+
const db = this._initDb();
|
|
336
|
+
if (!db) {
|
|
337
|
+
return { success: false, error: 'callbook_storage_unavailable', message: this._dbError };
|
|
338
|
+
}
|
|
339
|
+
const id = String(deviceId || '').trim();
|
|
340
|
+
if (!id) return { success: false, error: 'device_id_required' };
|
|
341
|
+
const revokedAt = nowIso();
|
|
342
|
+
const tx = db.transaction(() => {
|
|
343
|
+
const dev = this._stmts.getDevice.get(id);
|
|
344
|
+
if (!dev) return { success: false, error: 'device_not_found' };
|
|
345
|
+
this._stmts.revokeDevice.run(revokedAt, id);
|
|
346
|
+
this._stmts.revokeSessionsByDevice.run(revokedAt, id);
|
|
347
|
+
return { success: true, device: { ...dev, revoked_at: revokedAt } };
|
|
348
|
+
});
|
|
349
|
+
return tx();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = {
|
|
354
|
+
DEFAULT_CONFIG_DIR,
|
|
355
|
+
DB_FILENAME,
|
|
356
|
+
CallbookStore
|
|
357
|
+
};
|
|
358
|
+
|
package/src/lib/client.js
CHANGED
|
@@ -60,8 +60,7 @@ class A2AClient {
|
|
|
60
60
|
* Parse an a2a:// URL
|
|
61
61
|
*/
|
|
62
62
|
static parseInvite(inviteUrl) {
|
|
63
|
-
|
|
64
|
-
const match = inviteUrl.match(/^(?:a2a|oclaw):\/\/([^/]+)\/(.+)$/);
|
|
63
|
+
const match = inviteUrl.match(/^a2a:\/\/([^/]+)\/(.+)$/);
|
|
65
64
|
if (!match) {
|
|
66
65
|
throw new Error(`Invalid invite URL: ${inviteUrl}`);
|
|
67
66
|
}
|
package/src/lib/config.js
CHANGED
|
@@ -15,9 +15,31 @@ const CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
|
|
|
15
15
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'a2a-config.json');
|
|
16
16
|
const logger = createLogger({ component: 'a2a.config' });
|
|
17
17
|
|
|
18
|
+
function deepMerge(base, override) {
|
|
19
|
+
const baseIsObject = base && typeof base === 'object' && !Array.isArray(base);
|
|
20
|
+
const overrideIsObject = override && typeof override === 'object' && !Array.isArray(override);
|
|
21
|
+
if (!overrideIsObject) {
|
|
22
|
+
return override === undefined ? base : override;
|
|
23
|
+
}
|
|
24
|
+
const out = baseIsObject ? { ...base } : {};
|
|
25
|
+
for (const [key, value] of Object.entries(override)) {
|
|
26
|
+
const baseValue = baseIsObject ? base[key] : undefined;
|
|
27
|
+
const bothObjects = baseValue && typeof baseValue === 'object' && !Array.isArray(baseValue) &&
|
|
28
|
+
value && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
out[key] = bothObjects ? deepMerge(baseValue, value) : value;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
const DEFAULT_CONFIG = {
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
onboarding: {
|
|
36
|
+
version: 2,
|
|
37
|
+
step: 'not_started', // not_started|tiers|ingress|verify|complete
|
|
38
|
+
tiers_confirmed: false,
|
|
39
|
+
ingress_confirmed: false,
|
|
40
|
+
verify_confirmed: false,
|
|
41
|
+
last_run_at: null
|
|
42
|
+
},
|
|
21
43
|
|
|
22
44
|
// Permission tiers
|
|
23
45
|
tiers: {
|
|
@@ -25,7 +47,7 @@ const DEFAULT_CONFIG = {
|
|
|
25
47
|
name: 'Public',
|
|
26
48
|
description: 'Basic networking - safe for anyone',
|
|
27
49
|
capabilities: ['context-read'],
|
|
28
|
-
topics: [],
|
|
50
|
+
topics: ['chat'],
|
|
29
51
|
goals: [],
|
|
30
52
|
disclosure: 'minimal',
|
|
31
53
|
examples: ['calendar availability', 'public social posts', 'general questions']
|
|
@@ -34,19 +56,19 @@ const DEFAULT_CONFIG = {
|
|
|
34
56
|
name: 'Friends',
|
|
35
57
|
description: 'Most capabilities, no sensitive financial data',
|
|
36
58
|
capabilities: ['context-read', 'calendar.read', 'email.read', 'search'],
|
|
37
|
-
topics: [],
|
|
59
|
+
topics: ['chat', 'search', 'openclaw', 'a2a'],
|
|
38
60
|
goals: [],
|
|
39
61
|
disclosure: 'public',
|
|
40
62
|
examples: ['email summaries', 'schedule meetings', 'project discussions']
|
|
41
63
|
},
|
|
42
|
-
|
|
43
|
-
name: '
|
|
44
|
-
description: 'Full access - only for
|
|
64
|
+
family: {
|
|
65
|
+
name: 'Family',
|
|
66
|
+
description: 'Full access - only for your inner circle',
|
|
45
67
|
capabilities: ['context-read', 'calendar', 'email', 'search', 'tools', 'memory'],
|
|
46
|
-
topics: [],
|
|
68
|
+
topics: ['chat', 'search', 'openclaw', 'a2a', 'tools', 'memory'],
|
|
47
69
|
goals: [],
|
|
48
70
|
disclosure: 'public',
|
|
49
|
-
examples: ['
|
|
71
|
+
examples: ['deep collaboration', 'private project context', 'personal notes']
|
|
50
72
|
},
|
|
51
73
|
custom: {
|
|
52
74
|
name: 'Custom',
|
|
@@ -99,7 +121,7 @@ class A2AConfig {
|
|
|
99
121
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
100
122
|
try {
|
|
101
123
|
const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
102
|
-
return
|
|
124
|
+
return deepMerge(JSON.parse(JSON.stringify(DEFAULT_CONFIG)), saved);
|
|
103
125
|
} catch (e) {
|
|
104
126
|
logger.error('A2A config is corrupted, using defaults', {
|
|
105
127
|
event: 'a2a_config_corrupt',
|
|
@@ -110,10 +132,10 @@ class A2AConfig {
|
|
|
110
132
|
config_file: CONFIG_FILE
|
|
111
133
|
}
|
|
112
134
|
});
|
|
113
|
-
return
|
|
135
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
114
136
|
}
|
|
115
137
|
}
|
|
116
|
-
return
|
|
138
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
117
139
|
}
|
|
118
140
|
|
|
119
141
|
_save() {
|
|
@@ -133,18 +155,48 @@ class A2AConfig {
|
|
|
133
155
|
|
|
134
156
|
// Check if onboarding is complete
|
|
135
157
|
isOnboarded() {
|
|
136
|
-
return this.config.
|
|
158
|
+
return this.config.onboarding &&
|
|
159
|
+
this.config.onboarding.version === 2 &&
|
|
160
|
+
this.config.onboarding.step === 'complete';
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
// Mark onboarding complete
|
|
140
164
|
completeOnboarding() {
|
|
141
|
-
this.config.
|
|
165
|
+
this.config.onboarding = this.config.onboarding || {};
|
|
166
|
+
this.config.onboarding.version = 2;
|
|
167
|
+
this.config.onboarding.step = 'complete';
|
|
168
|
+
this.config.onboarding.tiers_confirmed = true;
|
|
169
|
+
this.config.onboarding.ingress_confirmed = true;
|
|
170
|
+
this.config.onboarding.verify_confirmed = true;
|
|
171
|
+
this.config.onboarding.last_run_at = new Date().toISOString();
|
|
142
172
|
this._save();
|
|
143
173
|
}
|
|
144
174
|
|
|
145
175
|
// Reset to run onboarding again
|
|
146
176
|
resetOnboarding() {
|
|
147
|
-
this.config.
|
|
177
|
+
this.config.onboarding = this.config.onboarding || {};
|
|
178
|
+
this.config.onboarding.version = 2;
|
|
179
|
+
this.config.onboarding.step = 'not_started';
|
|
180
|
+
this.config.onboarding.tiers_confirmed = false;
|
|
181
|
+
this.config.onboarding.ingress_confirmed = false;
|
|
182
|
+
this.config.onboarding.verify_confirmed = false;
|
|
183
|
+
this.config.onboarding.last_run_at = new Date().toISOString();
|
|
184
|
+
this._save();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getOnboarding() {
|
|
188
|
+
const ob = (this.config && this.config.onboarding && typeof this.config.onboarding === 'object')
|
|
189
|
+
? this.config.onboarding
|
|
190
|
+
: {};
|
|
191
|
+
return deepMerge(DEFAULT_CONFIG.onboarding, ob);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setOnboarding(patch = {}) {
|
|
195
|
+
const current = this.getOnboarding();
|
|
196
|
+
const merged = deepMerge(current, patch);
|
|
197
|
+
merged.version = 2;
|
|
198
|
+
merged.last_run_at = new Date().toISOString();
|
|
199
|
+
this.config.onboarding = merged;
|
|
148
200
|
this._save();
|
|
149
201
|
}
|
|
150
202
|
|