@seasonkoh/webaz 0.1.27 → 0.1.28
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 +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +69 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +234 -54
- package/dist/pwa/public/app-account.js +977 -0
- package/dist/pwa/public/app-admin.js +608 -0
- package/dist/pwa/public/app-agents.js +63 -0
- package/dist/pwa/public/app-ai.js +2162 -0
- package/dist/pwa/public/app-contribution.js +836 -0
- package/dist/pwa/public/app-discover.js +1296 -0
- package/dist/pwa/public/app-listings.js +226 -0
- package/dist/pwa/public/app-profile.js +1692 -0
- package/dist/pwa/public/app-seller.js +199 -0
- package/dist/pwa/public/app-shop.js +1145 -0
- package/dist/pwa/public/app.js +9956 -18841
- package/dist/pwa/public/i18n.js +16 -0
- package/dist/pwa/public/index.html +10 -0
- package/dist/pwa/public/openapi.json +85 -1
- package/dist/pwa/routes/agent-grants.js +255 -0
- package/dist/pwa/routes/webauthn.js +6 -1
- package/dist/pwa/server-schema.js +9 -0
- package/dist/pwa/server.js +157 -1559
- package/dist/runtime/agent-grant-scopes.js +128 -0
- package/dist/runtime/agent-grant-verifier.js +67 -0
- package/dist/runtime/agent-pairing.js +60 -0
- package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
- package/dist/runtime/webaz-schema-helpers.js +1848 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ WebAZ deliberately separates **principle** (permanent) from **mechanism** (tunab
|
|
|
79
79
|
- Money flows are fully parameterized and on-chain-auditable. The operator earns only an explicit platform fee + risk-event slashing — most of it rerouted to a public-good fund. Full walk-through: [`docs/ECONOMIC-MODEL.md`](docs/ECONOMIC-MODEL.md).
|
|
80
80
|
- The constitution (meta-rules) and amendment thresholds live in [`docs/CHARTER.md`](docs/CHARTER.md) / [`docs/META-RULES-FULL.md`](docs/META-RULES-FULL.md).
|
|
81
81
|
|
|
82
|
-
>
|
|
82
|
+
> Matching rewards are disabled by default; PV / position is a participation record only — not income, not redeemable, no entitlement. See [`docs/REWARD-ENGINES-DECOUPLING.md`](docs/REWARD-ENGINES-DECOUPLING.md).
|
|
83
83
|
|
|
84
84
|
**Fund split** (current `protocol_params` defaults, DAO-adjustable; 100-unit shop order):
|
|
85
85
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-003 — MCP NETWORK-mode tool gating (pure, testable; no I/O).
|
|
3
|
+
*
|
|
4
|
+
* In `network` / `network_readonly` mode the dispatcher hard-fails any tool that is
|
|
5
|
+
* NOT allowed through, so an un-migrated tool can't silently fall back to the local
|
|
6
|
+
* sandbox (a write would become a phantom op). This module holds the allow-sets +
|
|
7
|
+
* the predicate so the gate decision is unit-testable without booting the stdio
|
|
8
|
+
* server. (Extracted from server.ts; behavior unchanged except adding webaz_pair.)
|
|
9
|
+
*/
|
|
10
|
+
// Tools that talk to the live webaz.xyz network (Bearer api_key where needed).
|
|
11
|
+
// Un-listed tools run sandbox (local). `_mode` annotation = network for these.
|
|
12
|
+
export const NETWORK_TOOLS = new Set([
|
|
13
|
+
// RFC-020 onboarding/auth: pairing is the no-key credential bootstrap — it MUST be
|
|
14
|
+
// reachable in the default network_readonly install (you pair to GET a credential).
|
|
15
|
+
'webaz_pair',
|
|
16
|
+
'webaz_price_history',
|
|
17
|
+
'webaz_leaderboard',
|
|
18
|
+
'webaz_verify_price',
|
|
19
|
+
'webaz_place_order',
|
|
20
|
+
'webaz_list_product',
|
|
21
|
+
'webaz_update_order',
|
|
22
|
+
'webaz_search',
|
|
23
|
+
'webaz_get_status',
|
|
24
|
+
'webaz_feedback',
|
|
25
|
+
'webaz_contribute',
|
|
26
|
+
// Batch 1(只读 + 低危自身写):走 webaz.xyz Bearer api_key。
|
|
27
|
+
'webaz_notifications',
|
|
28
|
+
'webaz_nearby',
|
|
29
|
+
'webaz_profile',
|
|
30
|
+
'webaz_shareables',
|
|
31
|
+
'webaz_mykey',
|
|
32
|
+
// Batch 2(低危写,无钱无 escrow):走 webaz.xyz Bearer api_key。
|
|
33
|
+
'webaz_follows',
|
|
34
|
+
'webaz_like',
|
|
35
|
+
'webaz_blocklist',
|
|
36
|
+
'webaz_default_address',
|
|
37
|
+
'webaz_chat',
|
|
38
|
+
'webaz_rfq',
|
|
39
|
+
'webaz_referral',
|
|
40
|
+
// Batch 3(商务):
|
|
41
|
+
'webaz_secondhand',
|
|
42
|
+
'webaz_skill',
|
|
43
|
+
'webaz_skill_market',
|
|
44
|
+
'webaz_auction',
|
|
45
|
+
// Batch 4(资金/质押,守恒由服务端 RFC-014 保证;wallet 只读,写=Passkey 仅 PWA):
|
|
46
|
+
'webaz_wallet',
|
|
47
|
+
'webaz_trial',
|
|
48
|
+
'webaz_charity',
|
|
49
|
+
'webaz_bid',
|
|
50
|
+
'webaz_auto_bid',
|
|
51
|
+
// Batch 5(铁律/敏感):
|
|
52
|
+
'webaz_dispute',
|
|
53
|
+
'webaz_claim_verify',
|
|
54
|
+
'webaz_rotate_key',
|
|
55
|
+
'webaz_revoke_key',
|
|
56
|
+
// #1122:share_link 现有服务端端点 /api/share-link,可走网络。
|
|
57
|
+
'webaz_share_link',
|
|
58
|
+
]);
|
|
59
|
+
// Not in NETWORK_TOOLS but still allowed to run in NETWORK mode as self-aware /
|
|
60
|
+
// onboarding tools (non-data ops): info = local introspection; register = redirect
|
|
61
|
+
// the human to webaz.xyz. Everything else un-migrated hard-fails.
|
|
62
|
+
export const NETWORK_SELF_AWARE = new Set(['webaz_info', 'webaz_register']);
|
|
63
|
+
/**
|
|
64
|
+
* In NETWORK / network_readonly mode, may this tool proceed (vs migration-pending
|
|
65
|
+
* hard-fail)? True for migrated network tools and the self-aware onboarding tools.
|
|
66
|
+
*/
|
|
67
|
+
export function toolAllowedInNetworkMode(tool) {
|
|
68
|
+
return NETWORK_TOOLS.has(tool) || NETWORK_SELF_AWARE.has(tool);
|
|
69
|
+
}
|
|
@@ -23,6 +23,13 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSche
|
|
|
23
23
|
GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
24
24
|
import { initDatabase, generateId } from '../../layer0-foundation/L0-1-database/schema.js';
|
|
25
25
|
import { setSeamDb } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam(本进程注入)
|
|
26
|
+
import { applyWebazRuntimeSchema } from '../../runtime/apply-webaz-runtime-schema.js'; // 与 PWA 同源的纯 schema 桥(防 MCP fresh DB 漂移)
|
|
27
|
+
import { generateCodeVerifier, pkceChallengeS256 } from '../../runtime/agent-pairing.js'; // RFC-020 PR-C1 — PKCE 配对
|
|
28
|
+
import { NETWORK_TOOLS, NETWORK_SELF_AWARE, toolAllowedInNetworkMode } from './network-mode.js'; // RFC-003 网络门(可单测)
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { join as pathJoin } from 'node:path';
|
|
31
|
+
import { existsSync as fsExists, mkdirSync, writeFileSync, readFileSync, unlinkSync, chmodSync } from 'node:fs';
|
|
32
|
+
import { execFileSync } from 'node:child_process';
|
|
26
33
|
import { transition, getOrderStatus, initSystemUser, } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
27
34
|
import { initDisputeSchema, createDispute, respondToDispute, getDisputeDetails, getOrderDispute, getOpenDisputes, } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js';
|
|
28
35
|
import { initNotificationSchema, notifyTransition, getNotifications, getUnreadCount, markRead, } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
@@ -63,56 +70,6 @@ const MODE = WEBAZ_MODE_ENV === 'network' ? 'network'
|
|
|
63
70
|
: (WEBAZ_API_KEY ? 'network' : 'network_readonly');
|
|
64
71
|
// network 或 network_readonly 都"走真网络"(后者无 Bearer)。sandbox 才是本地。
|
|
65
72
|
const isNetworkMode = () => MODE === 'network' || MODE === 'network_readonly';
|
|
66
|
-
// 已迁移到 NETWORK 的工具名。P1/P2 逐个加入;未在集合里的工具仍走 sandbox(本地)。
|
|
67
|
-
// P1(读工具): 纯公开读,无写无 Passkey,作"MCP 连得上生产网络"的首验证。
|
|
68
|
-
const NETWORK_TOOLS = new Set([
|
|
69
|
-
'webaz_price_history',
|
|
70
|
-
'webaz_leaderboard',
|
|
71
|
-
'webaz_verify_price',
|
|
72
|
-
'webaz_place_order',
|
|
73
|
-
'webaz_list_product',
|
|
74
|
-
'webaz_update_order',
|
|
75
|
-
'webaz_search',
|
|
76
|
-
'webaz_get_status',
|
|
77
|
-
'webaz_feedback',
|
|
78
|
-
'webaz_contribute',
|
|
79
|
-
// Batch 1(只读 + 低危自身写):走 webaz.xyz Bearer api_key。
|
|
80
|
-
'webaz_notifications',
|
|
81
|
-
'webaz_nearby',
|
|
82
|
-
'webaz_profile',
|
|
83
|
-
'webaz_shareables',
|
|
84
|
-
'webaz_mykey',
|
|
85
|
-
// Batch 2(低危写,无钱无 escrow):走 webaz.xyz Bearer api_key。
|
|
86
|
-
// 注:share_link 暂不迁(无对应服务端端点,需新建,留待后续)。
|
|
87
|
-
'webaz_follows',
|
|
88
|
-
'webaz_like',
|
|
89
|
-
'webaz_blocklist',
|
|
90
|
-
'webaz_default_address',
|
|
91
|
-
'webaz_chat',
|
|
92
|
-
'webaz_rfq',
|
|
93
|
-
'webaz_referral',
|
|
94
|
-
// Batch 3(商务):secondhand/skill_market/auction 纯 pwaApi(mode-aware 自动走网络);
|
|
95
|
-
// skill 直连本地引擎,加了显式 apiCall network 分支。
|
|
96
|
-
'webaz_secondhand',
|
|
97
|
-
'webaz_skill',
|
|
98
|
-
'webaz_skill_market',
|
|
99
|
-
'webaz_auction',
|
|
100
|
-
// Batch 4(资金/质押,守恒由服务端 RFC-014 保证;wallet 只读,写=Passkey 仅 PWA):
|
|
101
|
-
'webaz_wallet',
|
|
102
|
-
'webaz_trial',
|
|
103
|
-
'webaz_charity',
|
|
104
|
-
'webaz_bid',
|
|
105
|
-
'webaz_auto_bid',
|
|
106
|
-
// Batch 5(铁律/敏感):claim_verify 纯 pwaApi(真人门由服务端 require_human_presence 强制);
|
|
107
|
-
// dispute view/list_open/respond/add_evidence 走网络,arbitrate 仅返回 Passkey 指引;
|
|
108
|
-
// rotate_key/revoke_key 仅返回 Passkey 指引(不本地校验)。
|
|
109
|
-
'webaz_dispute',
|
|
110
|
-
'webaz_claim_verify',
|
|
111
|
-
'webaz_rotate_key',
|
|
112
|
-
'webaz_revoke_key',
|
|
113
|
-
// #1122:share_link 现有服务端端点 /api/share-link,可走网络。
|
|
114
|
-
'webaz_share_link',
|
|
115
|
-
]);
|
|
116
73
|
const recentCalls = [];
|
|
117
74
|
function pushRecentCall(c) {
|
|
118
75
|
recentCalls.push(c);
|
|
@@ -124,9 +81,8 @@ function pushRecentCall(c) {
|
|
|
124
81
|
function toolBackend(tool) {
|
|
125
82
|
return (isNetworkMode() && NETWORK_TOOLS.has(tool)) ? 'network' : 'sandbox';
|
|
126
83
|
}
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
const NETWORK_SELF_AWARE = new Set(['webaz_info', 'webaz_register']);
|
|
84
|
+
// NETWORK_SELF_AWARE imported from ./network-mode.ts (info = local introspection;
|
|
85
|
+
// register = redirect human to webaz.xyz). Everything else un-migrated hard-fails.
|
|
130
86
|
// RFC-003 Batch 0 安全网:NETWORK 模式下调用【未迁移】工具时的诚实拒绝(而非静默落本地沙盒)。
|
|
131
87
|
// 否则带 key 的用户调未迁工具会被悄悄喂本地结果——写操作=幻影操作(根本没到 webaz.xyz)。
|
|
132
88
|
function networkMigrationPending(tool) {
|
|
@@ -180,6 +136,200 @@ async function apiCall(path, opts = {}) {
|
|
|
180
136
|
return { error: `网络错误:${msg}`, network_error: true };
|
|
181
137
|
}
|
|
182
138
|
}
|
|
139
|
+
// ─────────────────────────── RFC-020 PR-C1: webaz_pair (pairing + local credential storage) ───────────────────────────
|
|
140
|
+
// C1 scope: pairing / consent / retrieval / LOCAL credential storage ONLY. The grant is
|
|
141
|
+
// NOT wired into any business tool (that is PR-C2 — per-request scope enforcement + audit).
|
|
142
|
+
// The raw bearer appears exactly once, in the server→MCP retrieval response; it is written
|
|
143
|
+
// straight to the OS secret store (Keychain → 0600 file fallback) and NEVER returned to the
|
|
144
|
+
// user / chat / tool-response / log. webaz_pair returns only a credential handle + status.
|
|
145
|
+
const WEBAZ_DIR = pathJoin(homedir(), '.webaz');
|
|
146
|
+
const PENDING_PATH = pathJoin(WEBAZ_DIR, 'pairing-pending.json'); // holds the PKCE verifier between start→complete
|
|
147
|
+
const CRED_FALLBACK_PATH = pathJoin(WEBAZ_DIR, 'credentials'); // 0600 fallback when Keychain unavailable
|
|
148
|
+
const KEYCHAIN_SERVICE = 'webaz-grant';
|
|
149
|
+
function ensureWebazDir() { if (!fsExists(WEBAZ_DIR))
|
|
150
|
+
mkdirSync(WEBAZ_DIR, { recursive: true, mode: 0o700 }); }
|
|
151
|
+
function write0600(path, data) { ensureWebazDir(); writeFileSync(path, data, { mode: 0o600 }); try {
|
|
152
|
+
chmodSync(path, 0o600);
|
|
153
|
+
}
|
|
154
|
+
catch { /* best effort */ } }
|
|
155
|
+
/** Store the raw bearer in the OS secret store; never log it. Returns an opaque handle. */
|
|
156
|
+
function storeGrantCredential(grantId, token) {
|
|
157
|
+
// macOS Keychain first (non-interactive upsert). Any failure → strict-perms file fallback.
|
|
158
|
+
if (process.platform === 'darwin') {
|
|
159
|
+
try {
|
|
160
|
+
execFileSync('security', ['add-generic-password', '-U', '-s', KEYCHAIN_SERVICE, '-a', grantId, '-w', token], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
161
|
+
return `keychain:${KEYCHAIN_SERVICE}/${grantId}`;
|
|
162
|
+
}
|
|
163
|
+
catch { /* fall through to file */ }
|
|
164
|
+
}
|
|
165
|
+
let store = {};
|
|
166
|
+
try {
|
|
167
|
+
if (fsExists(CRED_FALLBACK_PATH))
|
|
168
|
+
store = JSON.parse(readFileSync(CRED_FALLBACK_PATH, 'utf8'));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
store = {};
|
|
172
|
+
}
|
|
173
|
+
store[grantId] = { token, stored_at: new Date().toISOString() };
|
|
174
|
+
write0600(CRED_FALLBACK_PATH, JSON.stringify(store, null, 2));
|
|
175
|
+
return `file:~/.webaz/credentials#${grantId}`;
|
|
176
|
+
}
|
|
177
|
+
// Non-secret index of the current grant (grant_id + handle + metadata) so the stored
|
|
178
|
+
// credential can be resolved back without re-pairing. The SECRET (token) lives only in
|
|
179
|
+
// the OS secret store / 0600 file — never in this pointer.
|
|
180
|
+
const CRED_POINTER_PATH = pathJoin(WEBAZ_DIR, 'grant-current.json');
|
|
181
|
+
function saveGrantPointer(grantId, handle, capabilities, expiresAt) {
|
|
182
|
+
write0600(CRED_POINTER_PATH, JSON.stringify({ grant_id: grantId, handle, capabilities: capabilities ?? null, expires_at: expiresAt ?? null, stored_at: new Date().toISOString() }));
|
|
183
|
+
}
|
|
184
|
+
function readGrantPointer() {
|
|
185
|
+
try {
|
|
186
|
+
return fsExists(CRED_POINTER_PATH) ? JSON.parse(readFileSync(CRED_POINTER_PATH, 'utf8')) : null;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Resolve the stored grant bearer (reverse of storeGrantCredential). Returns null if not paired.
|
|
194
|
+
* ⚠️ The returned `token` is the RAW bearer — pass it only as an Authorization header; NEVER log it,
|
|
195
|
+
* print it, or return it in a tool response.
|
|
196
|
+
*/
|
|
197
|
+
export function resolveGrantCredential() {
|
|
198
|
+
const ptr = readGrantPointer();
|
|
199
|
+
if (!ptr?.grant_id)
|
|
200
|
+
return null;
|
|
201
|
+
let token = '';
|
|
202
|
+
if (typeof ptr.handle === 'string' && ptr.handle.startsWith('keychain:') && process.platform === 'darwin') {
|
|
203
|
+
try {
|
|
204
|
+
token = execFileSync('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-a', ptr.grant_id, '-w'], { encoding: 'utf8' }).trim();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
token = '';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!token) { // file fallback (also covers keychain-read failure)
|
|
211
|
+
try {
|
|
212
|
+
const store = JSON.parse(readFileSync(CRED_FALLBACK_PATH, 'utf8'));
|
|
213
|
+
token = store[ptr.grant_id]?.token || '';
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
token = '';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!token)
|
|
220
|
+
return null;
|
|
221
|
+
return { grant_id: ptr.grant_id, token, handle: ptr.handle, capabilities: ptr.capabilities, expires_at: ptr.expires_at };
|
|
222
|
+
}
|
|
223
|
+
function savePending(pairingId, codeVerifier) {
|
|
224
|
+
write0600(PENDING_PATH, JSON.stringify({ pairing_id: pairingId, code_verifier: codeVerifier, started_at: new Date().toISOString() }));
|
|
225
|
+
}
|
|
226
|
+
function readPending() {
|
|
227
|
+
try {
|
|
228
|
+
return fsExists(PENDING_PATH) ? JSON.parse(readFileSync(PENDING_PATH, 'utf8')) : null;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function clearPending() { try {
|
|
235
|
+
if (fsExists(PENDING_PATH))
|
|
236
|
+
unlinkSync(PENDING_PATH);
|
|
237
|
+
}
|
|
238
|
+
catch { /* best effort */ } }
|
|
239
|
+
export async function handlePair(args) {
|
|
240
|
+
// Mode isolation (fail-closed): every webaz_pair action (start / complete / verify) talks to the
|
|
241
|
+
// LIVE webaz.xyz API. In explicit sandbox mode (local-only) we must NOT touch the live network.
|
|
242
|
+
if (!isNetworkMode()) {
|
|
243
|
+
return {
|
|
244
|
+
_mode: MODE,
|
|
245
|
+
error: 'webaz_pair requires NETWORK mode — you are in sandbox (local-only, isolated from webaz.xyz). Unset WEBAZ_MODE (or set WEBAZ_MODE=network / network_readonly) to pair with / verify against a real human account.',
|
|
246
|
+
error_code: 'PAIRING_REQUIRES_NETWORK',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const action = args.action || 'start';
|
|
250
|
+
if (action === 'start') {
|
|
251
|
+
const caps = Array.isArray(args.capabilities) && args.capabilities.length
|
|
252
|
+
? args.capabilities.map(c => ({ capability: String(c) }))
|
|
253
|
+
: [{ capability: 'read_public' }, { capability: 'search' }]; // default: minimal safe scopes
|
|
254
|
+
const verifier = generateCodeVerifier();
|
|
255
|
+
const challenge = pkceChallengeS256(verifier);
|
|
256
|
+
const resp = await apiCall('/api/agent-grants/pair/start', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
body: {
|
|
259
|
+
code_challenge: challenge,
|
|
260
|
+
capabilities: caps,
|
|
261
|
+
agent_label: typeof args.agent_label === 'string' ? args.agent_label : undefined,
|
|
262
|
+
reason: typeof args.reason === 'string' ? args.reason : undefined, // free-text reason only
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
if (resp.error)
|
|
266
|
+
return resp;
|
|
267
|
+
savePending(String(resp.pairing_id), verifier); // verifier stays LOCAL; never sent until retrieve
|
|
268
|
+
return {
|
|
269
|
+
status: 'awaiting_human_approval',
|
|
270
|
+
pairing_id: resp.pairing_id,
|
|
271
|
+
user_code: resp.user_code,
|
|
272
|
+
approve_url: `${WEBAZ_API_URL}${String(resp.approve_url || '')}`,
|
|
273
|
+
expires_at: resp.expires_at,
|
|
274
|
+
next: `Ask the human to open approve_url (logged in at webaz.xyz) and approve. Then call webaz_pair again with action="complete".`,
|
|
275
|
+
capabilities_requested: caps.map(c => c.capability),
|
|
276
|
+
note: 'Safe scopes only. The credential is delivered once on completion and stored in your OS secret store — it is never shown here.',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (action === 'complete') {
|
|
280
|
+
const pending = readPending();
|
|
281
|
+
if (!pending)
|
|
282
|
+
return { error: 'no pending pairing — run webaz_pair (action="start") first', error_code: 'NO_PENDING_PAIRING' };
|
|
283
|
+
const resp = await apiCall(`/api/agent-grants/pair/${pending.pairing_id}/retrieve`, {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
body: { code_verifier: pending.code_verifier },
|
|
286
|
+
});
|
|
287
|
+
if (resp.error) {
|
|
288
|
+
// not approved yet → keep pending so the human can still approve + a later retry works
|
|
289
|
+
if (resp.http_status === 409)
|
|
290
|
+
return { status: 'not_ready', detail: resp.error, hint: 'Human has not approved yet (or it expired). Approve, then call action="complete" again.' };
|
|
291
|
+
clearPending();
|
|
292
|
+
return resp;
|
|
293
|
+
}
|
|
294
|
+
const token = String(resp.token || '');
|
|
295
|
+
const grantId = String(resp.grant_id || '');
|
|
296
|
+
if (!token || !grantId) {
|
|
297
|
+
clearPending();
|
|
298
|
+
return { error: 'retrieval returned no credential', error_code: 'RETRIEVE_EMPTY' };
|
|
299
|
+
}
|
|
300
|
+
const handle = storeGrantCredential(grantId, token); // raw bearer goes straight to secret store
|
|
301
|
+
saveGrantPointer(grantId, handle, resp.capabilities, resp.expires_at); // non-secret index for later resolve
|
|
302
|
+
clearPending();
|
|
303
|
+
return {
|
|
304
|
+
status: 'stored',
|
|
305
|
+
credential_handle: handle, // opaque handle — NOT the token
|
|
306
|
+
grant_id: grantId,
|
|
307
|
+
capabilities: resp.capabilities,
|
|
308
|
+
expires_at: resp.expires_at,
|
|
309
|
+
note: 'Credential stored in your OS secret store (raw token NOT shown; server keeps only a hash). Use webaz_pair action="verify" to confirm it; the server enforces active/expiry/revoked/scope on every call. Safe scopes only.',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (action === 'verify') {
|
|
313
|
+
// Consume the stored grant against a SAFE grant-gated route (read_public). The SERVER is authoritative:
|
|
314
|
+
// it re-checks active/expiry/revoked/subject-suspension/scope + audits on EVERY call. We resolve the
|
|
315
|
+
// bearer from the secret store and attach it; the raw token is never printed. Safe scopes only — no
|
|
316
|
+
// business tool and no risk scope is wired to grants here.
|
|
317
|
+
const cred = resolveGrantCredential();
|
|
318
|
+
if (!cred)
|
|
319
|
+
return { status: 'not_paired', error_code: 'NO_GRANT_CREDENTIAL', hint: 'No stored grant. Run webaz_pair action="start", have the human approve, then action="complete".' };
|
|
320
|
+
const resp = await apiCall('/api/agent-grants/whoami', { method: 'GET', apiKey: cred.token });
|
|
321
|
+
if (resp.error) {
|
|
322
|
+
return { status: 'grant_invalid', grant_id: cred.grant_id, error: resp.error, error_code: resp.error_code, hint: 'Grant is no longer valid (revoked / expired / suspended). Re-pair with webaz_pair action="start".' };
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
status: 'active',
|
|
326
|
+
grant: resp.grant, // SERVER-AUTHORITATIVE principal (grant_id/human_id/agent_label/capability)
|
|
327
|
+
local_cache: { capabilities: cred.capabilities, expires_at: cred.expires_at }, // advisory only, may be stale — NOT authoritative
|
|
328
|
+
note: 'Grant verified LIVE by the server (per-call: active/expiry/revoked/subject-suspension/scope + audited). `grant` is authoritative; `local_cache` is advisory. Safe scopes only; no business tool or risk scope consumes this grant.',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return { error: `unknown action "${action}" — use "start" | "complete" | "verify"`, error_code: 'BAD_ACTION' };
|
|
332
|
+
}
|
|
183
333
|
// 启动 banner(stderr)+ status 声明用 —— 让用户/agent 一眼知道现在是真网络还是沙盒
|
|
184
334
|
function modeBanner() {
|
|
185
335
|
if (MODE === 'network') {
|
|
@@ -195,6 +345,13 @@ function modeBanner() {
|
|
|
195
345
|
// ─── 初始化 ──────────────────────────────────────────────────
|
|
196
346
|
const db = initDatabase();
|
|
197
347
|
setSeamDb(db); // RFC-016 Phase 1:注入异步 DB seam(本进程)—— 共享引擎迁 seam 后 MCP 进程也能用,否则 dbOne/dbAll 抛"未初始化"
|
|
348
|
+
// MCP fresh-DB schema bridge: apply the SAME pure schema helpers the PWA boot
|
|
349
|
+
// runs (incl. product_aliases + users.permanent_code/handle/region), so an
|
|
350
|
+
// MCP-initialized DB is schema-complete for the sandbox tool path WITHOUT first
|
|
351
|
+
// booting the PWA server. Pure idempotent DDL only — no business writes, no
|
|
352
|
+
// money/order/status path. (MCP_PRODUCT_COLS below now overlaps the products
|
|
353
|
+
// columns and is redundant-but-harmless; left untouched to avoid scope creep.)
|
|
354
|
+
applyWebazRuntimeSchema(db);
|
|
198
355
|
initSystemUser(db);
|
|
199
356
|
initDisputeSchema(db);
|
|
200
357
|
initNotificationSchema(db);
|
|
@@ -325,6 +482,26 @@ No auth required, no parameters needed.
|
|
|
325
482
|
properties: {},
|
|
326
483
|
},
|
|
327
484
|
},
|
|
485
|
+
{
|
|
486
|
+
name: 'webaz_pair',
|
|
487
|
+
description: `Pair this agent with a human's WebAZ account via a Passkey-approved, scoped, short-lived **delegation grant** (RFC-020) — NOT by pasting a permanent api_key. Two steps:
|
|
488
|
+
1. action="start" → returns an approve_url + short user_code. The human opens it at webaz.xyz (logged in) and approves the server-shown consent (safe scopes only).
|
|
489
|
+
2. action="complete" → retrieves the credential ONCE (PKCE) and stores it in your OS secret store (macOS Keychain → ~/.webaz/credentials 0600 fallback). Returns only a credential_handle + status — the raw token is never shown.
|
|
490
|
+
3. action="verify" → uses the stored grant against a safe read endpoint to confirm it is still valid. The server re-checks active/expiry/revoked/suspension/scope and audits the call every time. Returns the grant principal/status; never the raw token.
|
|
491
|
+
|
|
492
|
+
Scopes: SAFE only (read_public, profile_read, search, list_product_draft, product_publish_request, draft_order). Risk scopes (place_order/wallet/refund/...) and never-delegable actions (withdraw/key-change/vote/...) are hard-rejected.
|
|
493
|
+
|
|
494
|
+
NOTE: this consumes the grant only on safe read paths; no business tool and no risk scope is wired to grants. No account is created (registration stays human-only at webaz.xyz).`,
|
|
495
|
+
inputSchema: {
|
|
496
|
+
type: 'object',
|
|
497
|
+
properties: {
|
|
498
|
+
action: { type: 'string', enum: ['start', 'complete', 'verify'], description: 'start a pairing, complete it after the human approves, or verify the stored grant (default: start)' },
|
|
499
|
+
capabilities: { type: 'array', items: { type: 'string' }, description: 'Requested SAFE scopes (default: read_public, search). Non-safe scopes are rejected.' },
|
|
500
|
+
agent_label: { type: 'string', description: 'Human-friendly name for this agent (shown in the consent screen)' },
|
|
501
|
+
reason: { type: 'string', description: 'Free-text reason shown to the human (you cannot relabel the scopes)' },
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
},
|
|
328
505
|
{
|
|
329
506
|
name: 'webaz_register',
|
|
330
507
|
// was ~1732 chars, now ~780 chars
|
|
@@ -4948,7 +5125,7 @@ export async function startMCPServer() {
|
|
|
4948
5125
|
// ─── RFC-003 Batch 0 安全网:NETWORK 模式下未迁移的工具【硬失败】,不静默落本地沙盒 ───
|
|
4949
5126
|
// 例外:info / register(NETWORK_SELF_AWARE)有专门 network-aware 处理,照常放行。
|
|
4950
5127
|
let handled = false;
|
|
4951
|
-
if (isNetworkMode() && !
|
|
5128
|
+
if (isNetworkMode() && !toolAllowedInNetworkMode(name)) {
|
|
4952
5129
|
result = networkMigrationPending(name);
|
|
4953
5130
|
handled = true;
|
|
4954
5131
|
}
|
|
@@ -4957,6 +5134,9 @@ export async function startMCPServer() {
|
|
|
4957
5134
|
case 'webaz_info':
|
|
4958
5135
|
result = await handleInfo();
|
|
4959
5136
|
break;
|
|
5137
|
+
case 'webaz_pair':
|
|
5138
|
+
result = await handlePair(args);
|
|
5139
|
+
break;
|
|
4960
5140
|
case 'webaz_register':
|
|
4961
5141
|
result = handleRegister(args);
|
|
4962
5142
|
break;
|