@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.
- package/LICENSE +2 -2
- package/NOTICE +24 -3
- package/README.md +74 -330
- package/README.zh-CN.md +419 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +69 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +270 -82
- package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
- package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
- package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
- package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
- package/dist/ledger.js +1 -1
- package/dist/pwa/admin-audit.js +38 -0
- package/dist/pwa/anti-abuse-thresholds.js +135 -0
- package/dist/pwa/cf-origin-guard.js +33 -0
- package/dist/pwa/contract-fingerprint.js +1 -0
- package/dist/pwa/data/onboarding-cases.js +2 -2
- package/dist/pwa/data/onboarding-quiz.js +1 -1
- package/dist/pwa/economic-participation.js +2 -2
- package/dist/pwa/integration-contract.js +46 -4
- package/dist/pwa/internal/pv-settlement.js +12 -0
- package/dist/pwa/internal/wallet-signer.js +26 -0
- 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 +15075 -23960
- package/dist/pwa/public/i18n.js +31 -28
- package/dist/pwa/public/index.html +11 -1
- package/dist/pwa/public/openapi.json +4851 -2776
- package/dist/pwa/pv-kill-switch.js +31 -0
- package/dist/pwa/routes/admin-admins.js +48 -1
- package/dist/pwa/routes/admin-analytics.js +1 -10
- package/dist/pwa/routes/admin-atomic.js +4 -17
- package/dist/pwa/routes/admin-operator-claims.js +280 -0
- package/dist/pwa/routes/admin-reports.js +4 -26
- package/dist/pwa/routes/admin-tokenomics.js +2 -76
- package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
- package/dist/pwa/routes/admin-users-query.js +23 -1
- package/dist/pwa/routes/admin-wallet-ops.js +1 -1
- package/dist/pwa/routes/agent-grants.js +255 -0
- package/dist/pwa/routes/auth-read.js +1 -5
- package/dist/pwa/routes/auth-register.js +3 -13
- package/dist/pwa/routes/build-task-quota.js +113 -0
- package/dist/pwa/routes/claim-verify.js +15 -11
- package/dist/pwa/routes/contribution-facts.js +18 -0
- package/dist/pwa/routes/dispute-cases.js +5 -4
- package/dist/pwa/routes/growth.js +3 -3
- package/dist/pwa/routes/orders-action.js +27 -10
- package/dist/pwa/routes/orders-create.js +1 -1
- package/dist/pwa/routes/products-meta.js +19 -6
- package/dist/pwa/routes/profile-placement.js +1 -1
- package/dist/pwa/routes/promoter.js +10 -29
- package/dist/pwa/routes/public-build-tasks.js +5 -1
- package/dist/pwa/routes/public-utils.js +9 -12
- package/dist/pwa/routes/referral.js +5 -26
- package/dist/pwa/routes/rewards-apply.js +3 -2
- package/dist/pwa/routes/share-redirects.js +1 -1
- package/dist/pwa/routes/shareables-interactions.js +2 -1
- package/dist/pwa/routes/task-proposals.js +85 -9
- package/dist/pwa/routes/users-public.js +1 -4
- package/dist/pwa/routes/wallet-read.js +2 -14
- package/dist/pwa/routes/webauthn.js +7 -2
- package/dist/pwa/server-schema.js +9 -0
- package/dist/pwa/server.js +319 -2034
- 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/dist/settlement-math.js +3 -3
- package/dist/version.js +6 -4
- package/package.json +43 -8
- package/dist/index.js +0 -182
- package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
- package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
- package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
- package/dist/test-dispute.js +0 -153
- package/dist/test-manifest.js +0 -61
- package/dist/test-mcp-tools.js +0 -135
- package/dist/test-reputation.js +0 -116
- 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
|
-
//
|
|
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
|
|
@@ -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,
|
|
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 —
|
|
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/
|
|
1524
|
-
const baseRepo = c.expected_pr_base_repo || c.canonical_repository_full_name || '
|
|
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/
|
|
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 →
|
|
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
|
|
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
|
-
// ─── 推广 /
|
|
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
|
-
|
|
3500
|
-
//
|
|
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
|
-
|
|
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() && !
|
|
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;
|