@seasonkoh/webaz 0.1.26 → 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.
Files changed (99) hide show
  1. package/LICENSE +2 -2
  2. package/NOTICE +24 -3
  3. package/README.md +74 -330
  4. package/README.zh-CN.md +419 -0
  5. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
  6. package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
  7. package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +69 -0
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +270 -82
  10. package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
  11. package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
  12. package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
  13. package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
  14. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
  15. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
  16. package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
  20. package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
  21. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
  22. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
  23. package/dist/ledger.js +1 -1
  24. package/dist/pwa/admin-audit.js +38 -0
  25. package/dist/pwa/anti-abuse-thresholds.js +135 -0
  26. package/dist/pwa/cf-origin-guard.js +33 -0
  27. package/dist/pwa/contract-fingerprint.js +1 -0
  28. package/dist/pwa/data/onboarding-cases.js +2 -2
  29. package/dist/pwa/data/onboarding-quiz.js +1 -1
  30. package/dist/pwa/economic-participation.js +2 -2
  31. package/dist/pwa/integration-contract.js +46 -4
  32. package/dist/pwa/internal/pv-settlement.js +12 -0
  33. package/dist/pwa/internal/wallet-signer.js +26 -0
  34. package/dist/pwa/public/app-account.js +977 -0
  35. package/dist/pwa/public/app-admin.js +608 -0
  36. package/dist/pwa/public/app-agents.js +63 -0
  37. package/dist/pwa/public/app-ai.js +2162 -0
  38. package/dist/pwa/public/app-contribution.js +836 -0
  39. package/dist/pwa/public/app-discover.js +1296 -0
  40. package/dist/pwa/public/app-listings.js +226 -0
  41. package/dist/pwa/public/app-profile.js +1692 -0
  42. package/dist/pwa/public/app-seller.js +199 -0
  43. package/dist/pwa/public/app-shop.js +1145 -0
  44. package/dist/pwa/public/app.js +15075 -23960
  45. package/dist/pwa/public/i18n.js +31 -28
  46. package/dist/pwa/public/index.html +11 -1
  47. package/dist/pwa/public/openapi.json +4851 -2776
  48. package/dist/pwa/pv-kill-switch.js +31 -0
  49. package/dist/pwa/routes/admin-admins.js +48 -1
  50. package/dist/pwa/routes/admin-analytics.js +1 -10
  51. package/dist/pwa/routes/admin-atomic.js +4 -17
  52. package/dist/pwa/routes/admin-operator-claims.js +280 -0
  53. package/dist/pwa/routes/admin-reports.js +4 -26
  54. package/dist/pwa/routes/admin-tokenomics.js +2 -76
  55. package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
  56. package/dist/pwa/routes/admin-users-query.js +23 -1
  57. package/dist/pwa/routes/admin-wallet-ops.js +1 -1
  58. package/dist/pwa/routes/agent-grants.js +255 -0
  59. package/dist/pwa/routes/auth-read.js +1 -5
  60. package/dist/pwa/routes/auth-register.js +3 -13
  61. package/dist/pwa/routes/build-task-quota.js +113 -0
  62. package/dist/pwa/routes/claim-verify.js +15 -11
  63. package/dist/pwa/routes/contribution-facts.js +18 -0
  64. package/dist/pwa/routes/dispute-cases.js +5 -4
  65. package/dist/pwa/routes/growth.js +3 -3
  66. package/dist/pwa/routes/orders-action.js +27 -10
  67. package/dist/pwa/routes/orders-create.js +1 -1
  68. package/dist/pwa/routes/products-meta.js +19 -6
  69. package/dist/pwa/routes/profile-placement.js +1 -1
  70. package/dist/pwa/routes/promoter.js +10 -29
  71. package/dist/pwa/routes/public-build-tasks.js +5 -1
  72. package/dist/pwa/routes/public-utils.js +9 -12
  73. package/dist/pwa/routes/referral.js +5 -26
  74. package/dist/pwa/routes/rewards-apply.js +3 -2
  75. package/dist/pwa/routes/share-redirects.js +1 -1
  76. package/dist/pwa/routes/shareables-interactions.js +2 -1
  77. package/dist/pwa/routes/task-proposals.js +85 -9
  78. package/dist/pwa/routes/users-public.js +1 -4
  79. package/dist/pwa/routes/wallet-read.js +2 -14
  80. package/dist/pwa/routes/webauthn.js +7 -2
  81. package/dist/pwa/server-schema.js +9 -0
  82. package/dist/pwa/server.js +319 -2034
  83. package/dist/runtime/agent-grant-scopes.js +128 -0
  84. package/dist/runtime/agent-grant-verifier.js +67 -0
  85. package/dist/runtime/agent-pairing.js +60 -0
  86. package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
  87. package/dist/runtime/webaz-schema-helpers.js +1848 -0
  88. package/dist/settlement-math.js +3 -3
  89. package/dist/version.js +6 -4
  90. package/package.json +43 -8
  91. package/dist/index.js +0 -182
  92. package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
  93. package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
  94. package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
  95. package/dist/test-dispute.js +0 -153
  96. package/dist/test-manifest.js +0 -61
  97. package/dist/test-mcp-tools.js +0 -135
  98. package/dist/test-reputation.js +0 -116
  99. package/dist/test-skill-market.js +0 -101
@@ -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
- // 未在 NETWORK_TOOLS 名单、但 NETWORK 模式下仍可本地运行的"自省/引导"工具(非数据操作)。
128
- // info = 本地自省(并拉 live 网络状态);register = 引导真人去 webaz.xyz。其余未迁工具一律硬失败。
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
@@ -521,7 +698,7 @@ Options:
521
698
  },
522
699
  promoter_api_key: {
523
700
  type: 'string',
524
- description: "Referrer api_key (optional). ⚠️ Only L1 recorded (direct referrer, 70% commission); L2/L3 can't be inferred via MCP, redirects per region rule (singapore-like high max_levels charity chain_gap; global max_levels=1 → global_fund region cap). Full 7:2:1 three-tier chain requires buyer clicking ?ref= URL from webaz_share_link (creates product_share_attribution).",
701
+ description: "Referrer api_key (optional). ⚠️ Only L1 recorded (direct referrer, 70% commission); L2/L3 can't be inferred via MCP, so the undelivered L2/L3 portions go to commission_reserve (protocol reserve, in-only). Full 7:2:1 three-tier chain requires buyer clicking ?ref= URL from webaz_share_link (creates product_share_attribution).",
525
702
  },
526
703
  // B2 隐私购物
527
704
  anonymous_recipient: {
@@ -1444,7 +1621,8 @@ Discovery + suggesting need NO api_key (anyone / any agent can browse and propos
1444
1621
  Actions:
1445
1622
  - list_open (default): open public tasks (opt. filters: area / risk_level / auto_claimable / required_capabilities / agent_capabilities / max_duration_minutes / estimated_context_size / estimated_agent_budget — estimated_agent_budget is a resource/effort estimate, NOT a payment). Each task carries its execution boundary + the trusted canonical contribution target. NO api_key needed.
1446
1623
  - detail: one task's full execution boundary (allowed/forbidden paths, prohibited actions, acceptance criteria, verification commands, deliverables, definition_of_done) + the canonical repo to PR to + a copy-ready agent_handoff. NO api_key needed.
1447
- - suggest: propose a NEW task (title + summary/reason; opt. area/expected_outcome/source_ref/github_login). It enters the maintainer inbox — it is a suggestion, NOT a contribution fact / reward / participation, and never auto-becomes a task. NO api_key needed.
1624
+ - suggest: propose a NEW task (title + summary/reason; opt. area/expected_outcome/source_ref/github_login). It enters the maintainer inbox — it is a suggestion, NOT a contribution fact / reward / participation, and never auto-becomes a task. NO api_key needed (but pass your key to LINK it to your account so you can track it via my_suggestions).
1625
+ - my_suggestions: your OWN past proposals + their review status / public_reply / next_action (api_key). Agent-readable 回执 so a proposer-agent can act on the maintainer's decision (needs_info → resubmit; converted → see converted_ref).
1448
1626
  - claim: take an open task (api_key); provenance=human|ai_assisted|ai_authored (self-declared, not detected); auto-expires ~7d if not submitted. Returns a handoff — point a coding agent at it; the human needn't know git but stays accountable (Passkey).
1449
1627
  - submit: mark in_review with pr_ref + verification_summary (api_key). The PR's base repo MUST be the canonical WebAZ repo, and a verification_summary (what you ran/verified) is REQUIRED — both server-enforced. A human maintainer reviews next; done ≠ merge.
1450
1628
  - status: tasks you hold (api_key).
@@ -1454,12 +1632,12 @@ Coordinates + records only — NO merge/reward; acceptance (done) = human mainta
1454
1632
  inputSchema: {
1455
1633
  type: 'object',
1456
1634
  properties: {
1457
- action: { type: 'string', enum: ['list_open', 'detail', 'suggest', 'claim', 'submit', 'status', 'profile'], description: 'list_open (default) | detail | suggest | claim | submit | status | profile' },
1635
+ action: { type: 'string', enum: ['list_open', 'detail', 'suggest', 'my_suggestions', 'claim', 'submit', 'status', 'profile'], description: 'list_open (default) | detail | suggest | my_suggestions | claim | submit | status | profile' },
1458
1636
  api_key: { type: 'string', description: 'claim/submit/status/profile: your api_key (accountable identity). NOT needed for list_open/detail/suggest. (or set the WEBAZ_API_KEY env var)' },
1459
1637
  task_id: { type: 'string', description: 'detail / claim / submit: the task id' },
1460
1638
  area: { type: 'string', description: 'list_open: area filter / suggest: suggested area (e.g. search / docs / mcp)' },
1461
1639
  risk_level: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'list_open: optional risk filter' },
1462
- auto_claimable: { type: 'boolean', description: 'list_open: optional filter — only auto-claimable (true) or manual-claim (false) tasks' },
1640
+ auto_claimable: { type: 'boolean', description: 'list_open: optional filter — true returns tasks an agent can just do (auto-claimable AND with a real effort estimate; matches the derived claimability=auto_claimable), false returns manual-claim tasks. A task with a placeholder (unknown) estimate is treated as manual_review even if its raw auto_claimable flag is true, so it is excluded from true.' },
1463
1641
  required_capabilities: { type: 'string', description: 'list_open: optional filter — comma-separated; matches tasks that REQUIRE ALL of the listed capabilities (superset/AND match on the task requirement). For "tasks my agent can do", use agent_capabilities instead.' },
1464
1642
  agent_capabilities: { type: 'string', description: 'list_open: optional filter — capabilities your agent HAS (comma-separated); matches tasks whose required_capabilities are a SUBSET of these, i.e. tasks your agent can actually do' },
1465
1643
  max_duration_minutes: { type: 'number', description: 'list_open: optional filter — only tasks whose estimated max duration fits within this many minutes (your idle time)' },
@@ -1518,18 +1696,22 @@ async function handleFeedback(args) {
1518
1696
  }
1519
1697
  // RFC-006 断点1(b)交接:从【可信】canonical 目标(API 响应里,绝不硬编码/不取自 task metadata)构造"怎么真正
1520
1698
  // 动手"。人的编码 agent 做 git/PR;Passkey 真人担责。sandbox 运行 / 本地草稿不算正式参与。
1521
- function buildContributeHandoff(cct, taskId) {
1699
+ function buildContributeHandoff(cct, taskId, caseId) {
1522
1700
  const c = (cct ?? {});
1523
- const repoUrl = c.canonical_github_url || 'https://github.com/seasonsagents-art/webaz';
1524
- const baseRepo = c.expected_pr_base_repo || c.canonical_repository_full_name || 'seasonsagents-art/webaz';
1701
+ const repoUrl = c.canonical_github_url || 'https://github.com/webaz-protocol/webaz';
1702
+ const baseRepo = c.expected_pr_base_repo || c.canonical_repository_full_name || 'webaz-protocol/webaz';
1525
1703
  const baseBranch = c.base_branch || 'main';
1704
+ // case_id threads proposal → task → PR. = the source proposal id when this task came from a proposal,
1705
+ // else the task id itself. Quote it in the PR so the whole case stays traceable end to end.
1706
+ const cid = caseId || taskId;
1526
1707
  return {
1708
+ case_id: cid,
1527
1709
  canonical_repo: baseRepo,
1528
1710
  repo: repoUrl,
1529
1711
  base_branch: baseBranch,
1530
1712
  start_here: 'Read AGENTS.md (project map + before-you-code + PR flow), then CONTRIBUTING.md.',
1531
1713
  do_the_work: 'Point a coding agent (e.g. Claude Code) at the repo on a single-topic branch. The buyer/shopping agent is not the coding agent — hand off to one.',
1532
- submit_pr: `Open a PR whose BASE repo is ${baseRepo} (${repoUrl}), base branch ${baseBranch}. If any target repo differs from this canonical repo, STOP and ask the human — never contribute to a non-canonical repository.`,
1714
+ submit_pr: `Open a PR whose BASE repo is ${baseRepo} (${repoUrl}), base branch ${baseBranch}. Reference case ${cid} in the PR title/body so the proposal → task → PR chain stays traceable. If any target repo differs from this canonical repo, STOP and ask the human — never contribute to a non-canonical repository.`,
1533
1715
  pr_flow: 'Commit with DCO sign-off (git commit -s). If AI-authored, mark the PR per the meta-rule. Humans merge — no auto-merge.',
1534
1716
  then: `When the PR is open, report it back: webaz_contribute action=submit task_id=${taskId} pr_ref=#<N> verification_summary="<the verification_commands you ran + their results>". Both pr_ref and verification_summary are required.`,
1535
1717
  not_participation: 'A sandbox run or a local-only draft is NOT participation and is NOT a contribution; only a merged PR (or recognized issue/task/RFC) on the canonical repo enters the contribution record.',
@@ -1580,7 +1762,7 @@ export async function handleContribute(args) {
1580
1762
  return { error: 'task_id required for action=detail' };
1581
1763
  const r = await apiCall('/api/public/build-tasks/' + encodeURIComponent(tid));
1582
1764
  if (!r.error && r.task)
1583
- r.agent_handoff = buildContributeHandoff(r.canonical_contribution_target, tid);
1765
+ r.agent_handoff = buildContributeHandoff(r.canonical_contribution_target, tid, r.task.case_id);
1584
1766
  return r;
1585
1767
  }
1586
1768
  if (action === 'suggest') {
@@ -1592,6 +1774,7 @@ export async function handleContribute(args) {
1592
1774
  return { error: 'summary (the reason) required for action=suggest' };
1593
1775
  const r = await apiCall('/api/public/task-proposals', {
1594
1776
  method: 'POST',
1777
+ apiKey, // optional — when present, links the proposal to the submitter so it shows up in action=my_suggestions (still works anonymously)
1595
1778
  body: {
1596
1779
  title, summary,
1597
1780
  suggested_area: args.area ?? args.suggested_area,
@@ -1600,6 +1783,8 @@ export async function handleContribute(args) {
1600
1783
  proposer_github_login: args.proposer_github_login,
1601
1784
  },
1602
1785
  });
1786
+ if (!r.error && r.linked_to_account)
1787
+ r._next = 'Track this proposal\'s review status + reply: webaz_contribute action=my_suggestions api_key=<key>.';
1603
1788
  // typed errors (RATE_LIMITED / DUPLICATE_PROPOSAL / validation) are already mapped by apiCall; the
1604
1789
  // success response already carries the route-level `proposal_notice` (suggestion ≠ contribution/reward).
1605
1790
  return r;
@@ -1612,6 +1797,13 @@ export async function handleContribute(args) {
1612
1797
  };
1613
1798
  if (action === 'status')
1614
1799
  return apiCall('/api/build-tasks?mine=1', { apiKey });
1800
+ if (action === 'my_suggestions') {
1801
+ // your OWN past proposals + review status/public_reply/next_action (agent-readable 回执). Own rows only (server-enforced).
1802
+ const r = await apiCall('/api/me/task-proposals', { apiKey });
1803
+ if (!r.error)
1804
+ r._next = 'Each item carries status + public_reply + next_action. needs_info → resubmit via action=suggest referencing the id; converted → see converted_ref.';
1805
+ return r;
1806
+ }
1615
1807
  if (action === 'profile')
1616
1808
  return apiCall('/api/build-reputation/me', { apiKey });
1617
1809
  if (action === 'claim') {
@@ -1730,8 +1922,11 @@ async function handleInfo() {
1730
1922
  // 连接两个场景:用协议(本工具) ↔ 改协议(开发协作)。想改 WebAZ 本身的 agent 从这里进。
1731
1923
  for_contributors: {
1732
1924
  note: 'Want to change WebAZ itself (not just use it)? This is an open, agent-native protocol — AI-authored PRs are welcome, with accountability. / 想改 WebAZ 本身(不只是用)?这是开放的 agent 原生协议,欢迎 AI 提 PR,但需问责。',
1733
- repo: 'https://github.com/seasonsagents-art/webaz',
1925
+ repo: 'https://github.com/webaz-protocol/webaz',
1734
1926
  start_here: 'AGENTS.md (project map + before-you-code + PR flow) → CONTRIBUTING.md (full guide)',
1927
+ // 低门槛路径:无需 api_key、无需 clone 仓库,直接经协议发现任务 / 提建议(对外 well-known 入口也有,见 agent_quickstart)。
1928
+ no_key_path: 'No api_key needed to START contributing: discover open tasks and submit a suggestion via webaz_contribute action=list_open / action=suggest (mirrors GET /api/public/build-tasks + POST /api/public/task-proposals). / 无需 key 即可起步:webaz_contribute action=list_open 发现开放任务、action=suggest 提建议。',
1929
+ contribution_boundary: 'A suggestion is a proposal in the maintainer review inbox — NOT a contribution fact, NOT formal participation, and NOT any economic or redemption right; recorded contribution is facts / evidence / attribution only (RFC-017). / 建议只是进入维护者审阅箱的提议,不是贡献事实、不是正式参与、不构成任何经济或兑现权利;记录的贡献只是事实/证据/归属(RFC-017)。',
1735
1930
  ai_accountability: 'AI-authored PRs: add 🤖🤖🤖 to the PR title; the agent must be triggered by a Passkey-bound human (webazer) who is accountable. / AI 提 PR:标题加 🤖🤖🤖,且须由已绑 Passkey 的真人(webazer)触发并担责。',
1736
1931
  },
1737
1932
  // NETWORK 模式:真网络 live 状态(best-effort 拉自 webaz.xyz);SANDBOX 模式为 null。
@@ -1741,10 +1936,10 @@ async function handleInfo() {
1741
1936
  // 佣金机制 —— 纯功能性描述(怎么运作),不做"自证清白"式辩护。
1742
1937
  commission_model: {
1743
1938
  split: '7:2:1 — L1 70% / L2 20% / L3 10% of an order\'s commission_pool',
1744
- jurisdiction_tiers: 'Tiers are graded by the order region\'s max_levels — NOT a uniform 3 tiers everywhere. e.g. global region max_levels=1 → L1 only; singapore (etc.) max_levels=3 → up to L3. A region may also be 0 (no commission tiers; pool → community fund).',
1939
+ jurisdiction_tiers: 'Tiers are graded by the order region\'s max_levels — NOT a uniform 3 tiers everywhere. e.g. global region max_levels=1 → L1 only; singapore (etc.) max_levels=3 → up to L3. A region may also be 0 (no commission tiers; pool → commission_reserve / protocol reserve).',
1745
1940
  attribution: 'EXPLICIT per-order — commission goes to the promoter attributed at purchase time, not derived from the buyer\'s sponsor chain.',
1746
1941
  how_to_attribute: 'L1: webaz_place_order(promoter_api_key) records the direct promoter. Full L2/L3 chain requires the buyer to arrive via a webaz_share_link /i/<permanent_code> (?ref=<permanent_code>) URL clicked in a browser (builds product_share_attribution).',
1747
- redirect_rules: 'chain_gap (no L / invalid sponsor) charity_fund; level beyond the region cap → global_fund.',
1942
+ redirect_rules: 'all undelivered commission (chain_gap / no L / invalid sponsor / level beyond the region cap / max_levels=0 / opt-out / escrow expiry) commission_reserve (protocol reserve, in-only; use decided by governance).',
1748
1943
  l1_gate: 'the promoter must be a verified buyer (≥1 completed order) to receive commission, otherwise that share redirects.',
1749
1944
  opt_in: 'Participation is opt-in (RFC-002): default = off. A user applies (Passkey + ≥1 completed order); attribution is always recorded. Commission destination is state-dependent: never_activated / auto_downgraded → held in pending_commission_escrow (30d grace), recoverable by (re-)activating within the window, else swept to commission_reserve; deactivated (active opt-out) → future commission goes directly to commission_reserve, NOT escrow and NOT recoverable. Never to charity_fund. See docs/rfcs/RFC-002-rewards-opt-in.md.',
1750
1945
  },
@@ -3438,7 +3633,7 @@ function handleRotateKey(args) {
3438
3633
  },
3439
3634
  };
3440
3635
  }
3441
- // ─── 推广 / 双轨 (Tokenomics) ───────────────────────────────────
3636
+ // ─── 推广 / 推荐网络 (Tokenomics) ───────────────────────────────────
3442
3637
  async function handleReferral(args) {
3443
3638
  // RFC-003 Batch 2:NETWORK 模式 → webaz.xyz 真网络聚合(Bearer api_key);SANDBOX 走本地。
3444
3639
  if (toolBackend('webaz_referral') === 'network') {
@@ -3465,16 +3660,9 @@ async function handleReferral(args) {
3465
3660
  const completed = db.prepare("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
3466
3661
  const override = db.prepare("SELECT l1_share_override FROM users WHERE id = ?").get(userId)?.l1_share_override ?? 0;
3467
3662
  const canL1 = override === 1 || (override === 0 && completed > 0);
3468
- // 原子能双轨
3663
+ // Neutral participation record only — placement position + per-leg PV. Matching-rewards engine excised (#401):
3664
+ // no Score / tier / pair-volume / payout is read or exposed.
3469
3665
  const me = db.prepare("SELECT total_left_pv, total_right_pv, left_child_id, right_child_id, placement_id, placement_side FROM users WHERE id = ?").get(userId);
3470
- const score = db.prepare(`
3471
- SELECT COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score ELSE 0 END),0) as pending,
3472
- COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as settled_waz
3473
- FROM binary_score_records WHERE user_id = ?
3474
- `).get(userId);
3475
- const tiers = db.prepare("SELECT tier, pv_threshold, score_per_hit FROM binary_tier_config WHERE active=1 ORDER BY tier ASC").all();
3476
- const pair = Math.min(Number(me?.total_left_pv ?? 0), Number(me?.total_right_pv ?? 0));
3477
- const nextTier = tiers.find(t => t.pv_threshold > pair);
3478
3666
  // invite / share links use permanent_code ONLY — never usr_xxx. (sandbox users have one from register.)
3479
3667
  const permaCode = db.prepare("SELECT permanent_code FROM users WHERE id = ?").get(userId)?.permanent_code || null;
3480
3668
  return {
@@ -3496,15 +3684,12 @@ async function handleReferral(args) {
3496
3684
  l1: byLevel[1], l2: byLevel[2], l3: byLevel[3],
3497
3685
  grand_total: byLevel[1].total + byLevel[2].total + byLevel[3].total,
3498
3686
  },
3499
- binary: {
3500
- // pre-public 去左右码:只暴露唯一的推荐码;放置侧别由系统自动决定(无 left/right 选择)
3687
+ placement: {
3688
+ // Neutral participation/attribution record: a single referral code + per-leg PV. No matching rewards.
3501
3689
  referral_link: permaCode ? `/i/${permaCode}` : null,
3502
3690
  total_left_pv: Number(me?.total_left_pv ?? 0),
3503
3691
  total_right_pv: Number(me?.total_right_pv ?? 0),
3504
- pair_volume: pair,
3505
- next_tier: nextTier ? { tier: nextTier.tier, pv_threshold: nextTier.pv_threshold, score_per_hit: nextTier.score_per_hit, pv_needed: nextTier.pv_threshold - pair } : null,
3506
- score_pending: score.pending,
3507
- waz_total_earned: score.settled_waz,
3692
+ note: 'total_left_pv / total_right_pv are a participation / attribution record only — not income, not redeemable, no entitlement.',
3508
3693
  },
3509
3694
  rewards_status: (() => {
3510
3695
  // RFC-002 §3.5 — 4 states + pending escrow visibility (PR-4)
@@ -4940,7 +5125,7 @@ export async function startMCPServer() {
4940
5125
  // ─── RFC-003 Batch 0 安全网:NETWORK 模式下未迁移的工具【硬失败】,不静默落本地沙盒 ───
4941
5126
  // 例外:info / register(NETWORK_SELF_AWARE)有专门 network-aware 处理,照常放行。
4942
5127
  let handled = false;
4943
- if (isNetworkMode() && !NETWORK_TOOLS.has(name) && !NETWORK_SELF_AWARE.has(name)) {
5128
+ if (isNetworkMode() && !toolAllowedInNetworkMode(name)) {
4944
5129
  result = networkMigrationPending(name);
4945
5130
  handled = true;
4946
5131
  }
@@ -4949,6 +5134,9 @@ export async function startMCPServer() {
4949
5134
  case 'webaz_info':
4950
5135
  result = await handleInfo();
4951
5136
  break;
5137
+ case 'webaz_pair':
5138
+ result = await handlePair(args);
5139
+ break;
4952
5140
  case 'webaz_register':
4953
5141
  result = handleRegister(args);
4954
5142
  break;