@wowok/agent-mcp 2.4.1 → 2.4.3

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 (74) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +11 -1624
  3. package/dist/knowledge/guard-ledger.js +14 -0
  4. package/dist/knowledge/guard-lint.d.ts +1 -0
  5. package/dist/knowledge/guard-lint.js +83 -3
  6. package/dist/knowledge/guard-migration.d.ts +48 -0
  7. package/dist/knowledge/guard-migration.js +228 -0
  8. package/dist/knowledge/guard-risk.d.ts +2 -0
  9. package/dist/knowledge/guard-risk.js +827 -57
  10. package/dist/knowledge/guard-templates.d.ts +1 -0
  11. package/dist/knowledge/guard-templates.js +278 -2
  12. package/dist/project/graph.d.ts +1 -0
  13. package/dist/project/graph.js +27 -1
  14. package/dist/project/index.d.ts +1 -0
  15. package/dist/project/index.js +39 -2
  16. package/dist/project/namespace.d.ts +11 -1
  17. package/dist/project/namespace.js +27 -2
  18. package/dist/project/query.d.ts +2 -0
  19. package/dist/project/query.js +56 -15
  20. package/dist/rules.d.ts +12 -0
  21. package/dist/rules.js +9 -0
  22. package/dist/schema/call/allocation.d.ts +10 -10
  23. package/dist/schema/call/base.js +3 -3
  24. package/dist/schema/call/machine.d.ts +38 -38
  25. package/dist/schema/call/semantic.js +1 -1
  26. package/dist/schema/call/service.d.ts +7 -7
  27. package/dist/schema/operations.d.ts +48 -48
  28. package/dist/schema/query/index.d.ts +146 -146
  29. package/dist/schema/query/index.js +15 -9
  30. package/dist/schema/utils/guard-query-utils.d.ts +7 -0
  31. package/dist/schema/utils/guard-query-utils.js +14 -1
  32. package/dist/schemas/bridge_operation.output.json +1 -1
  33. package/dist/schemas/index.json +1 -1
  34. package/dist/schemas/messenger_operation.output.json +1 -1
  35. package/dist/schemas/onchain_events.output.json +1 -1
  36. package/dist/schemas/onchain_operations.output.json +2 -2
  37. package/dist/schemas/onchain_operations.schema.json +11 -20
  38. package/dist/schemas/onchain_operations_allocation.schema.json +7 -10
  39. package/dist/schemas/onchain_operations_machine.schema.json +2 -5
  40. package/dist/schemas/onchain_operations_service.schema.json +2 -5
  41. package/dist/schemas/onchain_table_data.output.json +6 -12
  42. package/dist/tools/handlers/bridge.d.ts +1 -0
  43. package/dist/tools/handlers/bridge.js +1 -0
  44. package/dist/tools/handlers/config.d.ts +2 -0
  45. package/dist/tools/handlers/config.js +71 -0
  46. package/dist/tools/handlers/file-export.d.ts +3 -0
  47. package/dist/tools/handlers/file-export.js +90 -0
  48. package/dist/tools/handlers/local.d.ts +30 -0
  49. package/dist/tools/handlers/local.js +27 -0
  50. package/dist/tools/handlers/messenger.d.ts +16 -0
  51. package/dist/tools/handlers/messenger.js +187 -0
  52. package/dist/tools/handlers/onchain.d.ts +2 -0
  53. package/dist/tools/handlers/onchain.js +246 -0
  54. package/dist/tools/handlers/project.d.ts +2 -0
  55. package/dist/tools/handlers/project.js +53 -0
  56. package/dist/tools/handlers/query.d.ts +5 -0
  57. package/dist/tools/handlers/query.js +256 -0
  58. package/dist/tools/handlers/schema-query.d.ts +2 -0
  59. package/dist/tools/handlers/schema-query.js +92 -0
  60. package/dist/tools/handlers/trust.d.ts +2 -0
  61. package/dist/tools/handlers/trust.js +194 -0
  62. package/dist/tools/handlers/wip.d.ts +2 -0
  63. package/dist/tools/handlers/wip.js +44 -0
  64. package/dist/tools/index.d.ts +13 -0
  65. package/dist/tools/index.js +479 -0
  66. package/dist/tools/rules-hook.d.ts +2 -0
  67. package/dist/tools/rules-hook.js +22 -0
  68. package/dist/tools/shared.d.ts +29 -0
  69. package/dist/tools/shared.js +130 -0
  70. package/dist/tools/types.d.ts +35 -0
  71. package/dist/tools/types.js +1 -0
  72. package/dist/tools/wrap.d.ts +6 -0
  73. package/dist/tools/wrap.js +55 -0
  74. package/package.json +19 -5
@@ -1,3 +1,4 @@
1
+ import { GUARDQUERY } from "@wowok/wowok";
1
2
  function rootStr(ctx) {
2
3
  return JSON.stringify(ctx.root).toLowerCase();
3
4
  }
@@ -31,6 +32,415 @@ function extractWitnessCodes(root) {
31
32
  }
32
33
  return codes;
33
34
  }
35
+ const WITNESS_CHAIN = {
36
+ 100: ["Order", "Progress"],
37
+ 101: ["Order", "Machine"],
38
+ 102: ["Order", "Service"],
39
+ 103: ["Progress", "Machine"],
40
+ 104: ["Arb", "Order"],
41
+ 105: ["Arb", "Arbitration"],
42
+ 106: ["Arb", "Order", "Progress"],
43
+ 107: ["Arb", "Order", "Service"],
44
+ 108: ["Arb", "Order", "Machine"],
45
+ };
46
+ function extractWitnessIdentifierPairs(root) {
47
+ const pairs = [];
48
+ const visited = new WeakSet();
49
+ function walk(node) {
50
+ if (node === null || typeof node !== "object")
51
+ return;
52
+ if (visited.has(node))
53
+ return;
54
+ visited.add(node);
55
+ if (typeof node.convert_witness === "number" &&
56
+ typeof node.identifier === "number") {
57
+ pairs.push({ code: node.convert_witness, identifier: node.identifier });
58
+ }
59
+ for (const key of Object.keys(node)) {
60
+ const child = node[key];
61
+ if (Array.isArray(child)) {
62
+ for (const item of child)
63
+ walk(item);
64
+ }
65
+ else if (child !== null && typeof child === "object") {
66
+ walk(child);
67
+ }
68
+ }
69
+ }
70
+ walk(root);
71
+ return pairs;
72
+ }
73
+ function getChildNodes(node) {
74
+ if (!node || typeof node !== "object")
75
+ return [];
76
+ const children = [];
77
+ if (Array.isArray(node.nodes))
78
+ children.push(...node.nodes);
79
+ if (node.left)
80
+ children.push(node.left);
81
+ if (node.right)
82
+ children.push(node.right);
83
+ if (node.vec)
84
+ children.push(node.vec);
85
+ if (node.target)
86
+ children.push(node.target);
87
+ if (node.object && typeof node.object === "object")
88
+ children.push(node.object);
89
+ if (Array.isArray(node.parameters))
90
+ children.push(...node.parameters);
91
+ return children.filter((c) => c && typeof c === "object");
92
+ }
93
+ function findSignerLogicEqualNodes(root) {
94
+ const results = [];
95
+ function walk(node, parentType) {
96
+ if (!node || typeof node !== "object")
97
+ return;
98
+ if (node.type === "logic_equal") {
99
+ const operands = Array.isArray(node.nodes)
100
+ ? node.nodes
101
+ : node.left && node.right
102
+ ? [node.left, node.right]
103
+ : [];
104
+ if (operands.length === 2) {
105
+ const signerIdx = operands.findIndex((op) => op?.type === "context" && op?.context === "Signer");
106
+ if (signerIdx >= 0) {
107
+ results.push({
108
+ node,
109
+ parentType,
110
+ otherOperand: operands[1 - signerIdx],
111
+ });
112
+ }
113
+ }
114
+ }
115
+ for (const child of getChildNodes(node)) {
116
+ walk(child, node.type);
117
+ }
118
+ }
119
+ walk(root, null);
120
+ return results;
121
+ }
122
+ function detectLevel1StrictBinding(root, table) {
123
+ const signerChecks = findSignerLogicEqualNodes(root);
124
+ for (const check of signerChecks) {
125
+ const other = check.otherOperand;
126
+ if (other?.type === "identifier") {
127
+ const entry = table.find((e) => e.identifier === other.identifier);
128
+ if (entry &&
129
+ !entry.b_submission &&
130
+ entry.value_type === "Address" &&
131
+ typeof entry.value === "string" &&
132
+ entry.value.length > 0) {
133
+ return {
134
+ detected: true,
135
+ entry: {
136
+ identifier: entry.identifier,
137
+ value: entry.value,
138
+ name: entry.name,
139
+ },
140
+ };
141
+ }
142
+ }
143
+ }
144
+ return { detected: false };
145
+ }
146
+ function isSignerCheckInLogicOr(root) {
147
+ const signerChecks = findSignerLogicEqualNodes(root);
148
+ return signerChecks.some((check) => check.parentType === "logic_or");
149
+ }
150
+ const QUERY_INVARIANT_MAP = (() => {
151
+ const map = new Map();
152
+ for (const q of GUARDQUERY) {
153
+ if (q.invariant !== undefined) {
154
+ map.set(q.id, q.invariant);
155
+ }
156
+ }
157
+ return map;
158
+ })();
159
+ function extractQueryIds(root) {
160
+ const ids = [];
161
+ const str = JSON.stringify(root);
162
+ const numMatches = str.matchAll(/"query"\s*:\s*(\d+)/g);
163
+ for (const m of numMatches) {
164
+ const id = parseInt(m[1], 10);
165
+ if (!ids.includes(id))
166
+ ids.push(id);
167
+ }
168
+ const nameMatches = str.matchAll(/"query"\s*:\s*"([^"]+)"/g);
169
+ for (const m of nameMatches) {
170
+ const name = m[1];
171
+ const entry = GUARDQUERY.find((q) => q.name === name);
172
+ if (entry && !ids.includes(entry.id))
173
+ ids.push(entry.id);
174
+ }
175
+ return ids;
176
+ }
177
+ function getQueryInvariant(queryId) {
178
+ return QUERY_INVARIANT_MAP.get(queryId);
179
+ }
180
+ function rootHasNonZeroQuery(root) {
181
+ const ids = extractQueryIds(root);
182
+ return ids.some((id) => QUERY_INVARIANT_MAP.has(id));
183
+ }
184
+ function rootAllQueriesNonZero(root) {
185
+ const ids = extractQueryIds(root);
186
+ if (ids.length === 0)
187
+ return false;
188
+ return ids.every((id) => QUERY_INVARIANT_MAP.has(id));
189
+ }
190
+ const REENTRANCY_SPECS = [
191
+ {
192
+ sceneId: "reward_guard",
193
+ level: "critical",
194
+ existsPrimitives: [1613],
195
+ countPrimitives: [1612],
196
+ addressPrimitives: [1626],
197
+ rationale: "Guard pass distributes reward funds to the Claimant AND creates a reward_record, but the Guard conditions (identity, time, node state) do NOT naturally invalidate after a claim. " +
198
+ "Without an explicit anti-reentrancy primitive, the same Claimant can repeatedly pass the Guard and DRAIN THE ENTIRE POOL. " +
199
+ "This is the most severe reentrancy scene — direct fund loss.",
200
+ mitigation: "Add ONE of the following to the root (wrapped in logic_and with existing conditions):\n" +
201
+ " - logic_not(query('reward.record has', object=reward, parameters=[claimant_address])) — RECOMMENDED (per-user Bool check)\n" +
202
+ " - logic_equal([query('reward.record count', object=reward), identifier(N)]) where table[N].value=0 — total-claim count is 0\n" +
203
+ " - logic_not(query('reward.record.find_first', object=reward, where={...})) — no matching record found\n" +
204
+ "Reference template: MyShop_Advanced Guard 7 condition 4 (logic_not(query_reward_record_exists(where.storeFromId=order_id)))",
205
+ },
206
+ {
207
+ sceneId: "arbitration_voting_guard",
208
+ level: "critical",
209
+ existsPrimitives: [1404, 1406],
210
+ countPrimitives: [1405],
211
+ addressPrimitives: [],
212
+ rationale: "Guard pass counts the vote with the extracted weight. Without anti-reentrancy, the same voter can vote MULTIPLE times, " +
213
+ "multiplying their weight and skewing the arbitration result — which directly determines fund ownership via compensation. " +
214
+ "The voter's eligibility (identity, reputation) does NOT naturally invalidate after voting. " +
215
+ "Note: arb.voted_count (1403) is total voter count (NOT per-voter) and does NOT serve as an anti-reentrancy primitive for an individual voter.",
216
+ mitigation: "Add ONE of the following to the root (wrapped in logic_and):\n" +
217
+ " - logic_not(query('arb.voted has', object=arb, parameters=[voter_address])) — RECOMMENDED (per-voter Bool)\n" +
218
+ " - logic_not(query('arb.voted.agree has', object=arb, parameters=[voter_address, proposition_index])) — per-voter-per-proposition\n" +
219
+ " - logic_equal([query('arb.voted.agree count', object=arb, parameters=[voter_address]), identifier(N)]) where table[N].value=0",
220
+ },
221
+ {
222
+ sceneId: "arbitration_usage_guard",
223
+ level: "high",
224
+ existsPrimitives: [1565],
225
+ countPrimitives: [1564],
226
+ addressPrimitives: [],
227
+ rationale: "Guard pass creates a new Arb case. Without anti-reentrancy, a customer can create multiple Arb cases for the SAME Order " +
228
+ "(up to MAX_DISPUTE_COUNT=10 per order.move L96), harassing the counterparty and inflating arbitration system load. " +
229
+ "Not direct fund loss, but severe system abuse. order::dispute (order.move L93-99) deduplicates by Arb address but does NOT prevent multiple distinct Arbs.",
230
+ mitigation: "Add ONE of the following to the root (wrapped in logic_and):\n" +
231
+ " - logic_not(query('order.dispute has', object=order, parameters=[existing_arb_address])) — per-Arb check\n" +
232
+ " - logic_equal([query('order.dispute count', object=order), identifier(N)]) where table[N].value=0 — STRICT: block any prior dispute\n" +
233
+ " - logic_as_u256_less_or_equal([query('order.dispute count', object=order), identifier(N)]) where table[N].value=K — LENIENT: allow up to K disputes",
234
+ },
235
+ {
236
+ sceneId: "service_order_allocators_guard",
237
+ level: "info",
238
+ existsPrimitives: [1568],
239
+ countPrimitives: [],
240
+ addressPrimitives: [1569],
241
+ rationale: "Protocol-level protection: order::on_allocation (order.move L116-119) asserts ONE Allocation per Order (E_ALLOCATION_ALREADY_SPECIFIED). " +
242
+ "After alloc() executes, typical Rate-mode allocators (sum=10000) and Surplus-mode allocators drain balance to 0, making reentrancy unprofitable. " +
243
+ "EDGE CASE: Amount-mode-only allocators (no Surplus, total < full balance) leave remainder; a second alloc() could extract more — mitigated by preferring Rate/Surplus modes. " +
244
+ "External deposit via receive() is possible but irrational (attacker funds the Allocation they attack).",
245
+ mitigation: "Default: no action needed (protocol-level guard). IF using Amount-mode-only allocators without Surplus: " +
246
+ "consider adding logic_not(query('order.allocation some', object=order)) OR switch to Rate/Surplus mode to drain balance completely.",
247
+ },
248
+ {
249
+ sceneId: "service_buy_guard",
250
+ level: "info",
251
+ existsPrimitives: [],
252
+ countPrimitives: [],
253
+ addressPrimitives: [],
254
+ rationale: "Each purchase creates a fresh Order; no existing Order state is modified. Default: no reentrancy risk. " +
255
+ "IF business intent is 'limit one purchase per user' (limited-edition goods, whitelist single-buy), an anti-reentrancy check is REQUIRED " +
256
+ "but no direct GUARDQUERY primitive exists for per-user purchase history.",
257
+ mitigation: "IF limit-purchase intent: record purchases in a Repository and add logic_not(query('repository.data has', ...)) to root; " +
258
+ "OR revoke EntityRegistrar membership after first buy; OR use an off-chain allowlist with on-chain Merkle proof.",
259
+ },
260
+ {
261
+ sceneId: "repository_write_guard",
262
+ level: "info",
263
+ existsPrimitives: [1166],
264
+ countPrimitives: [],
265
+ addressPrimitives: [],
266
+ rationale: "Default writes are idempotent (same ID overwrites, different ID appends); no fund flow. " +
267
+ "IF business intent is 'one write per ID' (voting, immutable credential, single-attestation), an anti-reentrancy check is needed.",
268
+ mitigation: "IF one-write-per-ID intent: add logic_not(query('repository.data has', object=repository, parameters=[policy_name, user_or_object_id])) to root.",
269
+ },
270
+ {
271
+ sceneId: "gen_passport_guard",
272
+ level: "info",
273
+ existsPrimitives: [],
274
+ countPrimitives: [],
275
+ addressPrimitives: [],
276
+ rationale: "Each gen_passport call generates an independent Passport (one Passport per operation). " +
277
+ "Multiple Passports for the same user are usually valid (each serves a different subsequent operation). " +
278
+ "IF business intent is 'one active Passport per user', an anti-reentrancy check is needed but no direct GUARDQUERY primitive exists.",
279
+ mitigation: "IF one-passport-per-user intent: use off-chain tracking or a Repository to record issued Passport IDs.",
280
+ },
281
+ ];
282
+ const QUERY_TYPE_ALIASES = new Map([
283
+ ["query_reward_record_exists", 1613],
284
+ ["query_reward_record_count", 1612],
285
+ ["query_reward_record_find", 1626],
286
+ ["query_progress_history_find", 1274],
287
+ ["query_progress_history_session_find", 1275],
288
+ ["query_progress_history_session_forward_find", 1276],
289
+ ["query_progress_history_session_count", 1273],
290
+ ["query_progress_history_session_forward_count", 1262],
291
+ ]);
292
+ function detectReentrancyProtection(root, table, spec) {
293
+ const allPrimitives = new Set([
294
+ ...spec.existsPrimitives,
295
+ ...spec.countPrimitives,
296
+ ...spec.addressPrimitives,
297
+ ]);
298
+ if (allPrimitives.size === 0) {
299
+ return {
300
+ state: "absent",
301
+ evidence: "No anti-reentrancy primitive available in GUARDQUERY for this scene",
302
+ };
303
+ }
304
+ const idToName = new Map();
305
+ const nameToId = new Map();
306
+ for (const id of allPrimitives) {
307
+ const entry = GUARDQUERY.find((q) => q.id === id);
308
+ if (entry) {
309
+ idToName.set(id, entry.name);
310
+ nameToId.set(entry.name, id);
311
+ }
312
+ }
313
+ const primitiveNames = Array.from(nameToId.keys());
314
+ function resolvePrimitiveId(node) {
315
+ if (!node || typeof node !== "object")
316
+ return -1;
317
+ if (typeof node.query === "number" && allPrimitives.has(node.query)) {
318
+ return node.query;
319
+ }
320
+ if (typeof node.query === "string" && nameToId.has(node.query)) {
321
+ return nameToId.get(node.query);
322
+ }
323
+ if (typeof node.type === "string" && QUERY_TYPE_ALIASES.has(node.type)) {
324
+ const aliasId = QUERY_TYPE_ALIASES.get(node.type);
325
+ if (allPrimitives.has(aliasId)) {
326
+ return aliasId;
327
+ }
328
+ }
329
+ if (typeof node.type === "string" && nameToId.has(node.type)) {
330
+ return nameToId.get(node.type);
331
+ }
332
+ return -1;
333
+ }
334
+ function isZeroValue(node) {
335
+ if (!node || typeof node !== "object")
336
+ return false;
337
+ if (typeof node.type === "string" &&
338
+ ["number", "u8", "u16", "u32", "u64", "u128", "u256"].includes(node.type)) {
339
+ return node.value === 0 || node.value === "0" || node.value === 0n;
340
+ }
341
+ if (node.type === "identifier" && typeof node.identifier === "number") {
342
+ const entry = table.find((e) => e.identifier === node.identifier);
343
+ if (entry && !entry.b_submission) {
344
+ return entry.value === 0 || entry.value === "0" || entry.value === 0n;
345
+ }
346
+ }
347
+ return false;
348
+ }
349
+ function getSiblings(parent, child) {
350
+ if (!parent || typeof parent !== "object")
351
+ return [];
352
+ const siblings = [];
353
+ if (Array.isArray(parent.nodes)) {
354
+ for (const n of parent.nodes) {
355
+ if (n !== child)
356
+ siblings.push(n);
357
+ }
358
+ }
359
+ if (parent.left && parent.left !== child)
360
+ siblings.push(parent.left);
361
+ if (parent.right && parent.right !== child)
362
+ siblings.push(parent.right);
363
+ return siblings.filter((s) => s && typeof s === "object");
364
+ }
365
+ let foundPrimitive = false;
366
+ let foundProtected = false;
367
+ let pseudoEvidence = "";
368
+ let foundPrimitiveName = "";
369
+ function walk(node, parent) {
370
+ if (!node || typeof node !== "object")
371
+ return;
372
+ const primId = resolvePrimitiveId(node);
373
+ if (primId >= 0) {
374
+ foundPrimitive = true;
375
+ const primName = idToName.get(primId) ?? `query_${primId}`;
376
+ foundPrimitiveName = primName;
377
+ const parentType = parent?.type ?? null;
378
+ let properlyWrapped = false;
379
+ const isExistsStyle = spec.existsPrimitives.includes(primId);
380
+ const isAddressStyle = spec.addressPrimitives.includes(primId);
381
+ const isCountStyle = spec.countPrimitives.includes(primId);
382
+ if ((isExistsStyle || isAddressStyle) && parentType === "logic_not") {
383
+ properlyWrapped = true;
384
+ }
385
+ if (isCountStyle && parentType === "logic_equal") {
386
+ const siblings = getSiblings(parent, node);
387
+ if (siblings.some((sib) => isZeroValue(sib))) {
388
+ properlyWrapped = true;
389
+ }
390
+ }
391
+ if (properlyWrapped) {
392
+ foundProtected = true;
393
+ }
394
+ else if (!pseudoEvidence) {
395
+ const expected = isCountStyle
396
+ ? "logic_equal with a zero operand"
397
+ : "logic_not";
398
+ pseudoEvidence = `Primitive "${primName}" found but its immediate parent is ${parentType ?? "root"} (expected ${expected}). The primitive is present but does NOT actually prevent reentrancy — this is a false sense of security.`;
399
+ }
400
+ }
401
+ const children = [];
402
+ if (Array.isArray(node.nodes))
403
+ children.push(...node.nodes);
404
+ if (node.left)
405
+ children.push(node.left);
406
+ if (node.right)
407
+ children.push(node.right);
408
+ if (node.node)
409
+ children.push(node.node);
410
+ if (node.vec)
411
+ children.push(node.vec);
412
+ if (node.target)
413
+ children.push(node.target);
414
+ if (node.object && typeof node.object === "object")
415
+ children.push(node.object);
416
+ if (Array.isArray(node.parameters))
417
+ children.push(...node.parameters);
418
+ for (const child of children) {
419
+ if (child && typeof child === "object") {
420
+ walk(child, node);
421
+ }
422
+ }
423
+ }
424
+ walk(root, null);
425
+ if (foundProtected) {
426
+ return {
427
+ state: "protected",
428
+ evidence: `Anti-reentrancy primitive "${foundPrimitiveName}" is properly wrapped`,
429
+ foundPrimitiveName,
430
+ };
431
+ }
432
+ if (foundPrimitive) {
433
+ return {
434
+ state: "pseudo",
435
+ evidence: pseudoEvidence,
436
+ foundPrimitiveName,
437
+ };
438
+ }
439
+ return {
440
+ state: "absent",
441
+ evidence: `No anti-reentrancy primitive found in root. Expected one of: ${primitiveNames.join(", ")}`,
442
+ };
443
+ }
34
444
  const RISK_RULES = [
35
445
  {
36
446
  id: "R-C1-01",
@@ -156,57 +566,74 @@ const RISK_RULES = [
156
566
  {
157
567
  id: "R-C2-02",
158
568
  data_source_class: "type2_witness_derived",
159
- trigger: "Guard uses witness but source object's object_type may not match the witness source type",
569
+ trigger: "Guard uses witness but source object's object_type may not match the witness source type, or multi-hop chain has missing intermediate objects",
160
570
  check: (ctx) => {
161
- const codes = extractWitnessCodes(ctx.root);
162
- if (codes.length === 0)
571
+ const pairs = extractWitnessIdentifierPairs(ctx.root);
572
+ const validPairs = pairs.filter((p) => p.code >= 100 && p.code <= 108);
573
+ if (validPairs.length === 0)
163
574
  return null;
164
- const witnessSourceTypes = {
165
- 100: "Order", 101: "Order", 102: "Order",
166
- 103: "Progress",
167
- 104: "Arb", 105: "Arb", 106: "Arb", 107: "Arb", 108: "Arb",
168
- };
169
- const rootStr = JSON.stringify(ctx.root);
170
- const mismatches = [];
171
- for (const code of codes) {
172
- const expectedSource = witnessSourceTypes[code];
173
- if (!expectedSource)
575
+ const hardMismatches = [];
576
+ const missingAnnotations = [];
577
+ for (const { code, identifier } of validPairs) {
578
+ const chain = WITNESS_CHAIN[code];
579
+ if (!chain)
174
580
  continue;
175
- const pattern1 = new RegExp(`"identifier"\\s*:\\s*(\\d+)\\s*,\\s*"convert_witness"\\s*:\\s*${code}`, "g");
176
- const pattern2 = new RegExp(`"convert_witness"\\s*:\\s*${code}\\s*,\\s*"identifier"\\s*:\\s*(\\d+)`, "g");
177
- const foundIds = new Set();
178
- let m;
179
- while ((m = pattern1.exec(rootStr)) !== null) {
180
- foundIds.add(parseInt(m[1], 10));
581
+ const expectedSource = chain[0];
582
+ const entry = ctx.table.find((e) => e.identifier === identifier);
583
+ if (!entry)
584
+ continue;
585
+ const chainStr = `${code} (${chain.join("→")})`;
586
+ if (entry.object_type) {
587
+ if (entry.object_type !== expectedSource) {
588
+ hardMismatches.push(`witness ${chainStr} references #${identifier} ` +
589
+ `but object_type=${entry.object_type} (expected ${expectedSource})`);
590
+ }
181
591
  }
182
- while ((m = pattern2.exec(rootStr)) !== null) {
183
- foundIds.add(parseInt(m[1], 10));
592
+ else {
593
+ missingAnnotations.push(`witness ${chainStr} references #${identifier} ` +
594
+ `but object_type is not set (expected ${expectedSource})`);
184
595
  }
185
- for (const id of foundIds) {
186
- const entry = ctx.table.find((e) => e.identifier === id);
187
- if (entry?.object_type && entry.object_type !== expectedSource) {
188
- mismatches.push(`witness ${code}(${expectedSource}→?) references #${id} object_type=${entry.object_type}`);
596
+ if (chain.length === 3) {
597
+ const intermediateType = chain[1];
598
+ const hasIntermediate = ctx.table.some((e) => e.object_type === intermediateType);
599
+ if (!hasIntermediate) {
600
+ missingAnnotations.push(`witness ${chainStr} multi-hop chain expects intermediate ` +
601
+ `${intermediateType} in table, but no entry with object_type=${intermediateType} found`);
189
602
  }
190
603
  }
191
604
  }
192
- if (mismatches.length === 0)
605
+ const totalIssues = hardMismatches.length + missingAnnotations.length;
606
+ if (totalIssues === 0)
193
607
  return null;
608
+ const level = hardMismatches.length > 0 ? "medium" : "low";
609
+ const evidenceParts = [
610
+ ...hardMismatches,
611
+ ...missingAnnotations,
612
+ ];
194
613
  return {
195
614
  id: "R-C2-02",
196
615
  data_source_class: "type2_witness_derived",
197
- level: "low",
616
+ level,
198
617
  category: "data_source_trust",
199
- title: "witness source object object_type may not match the witness source type",
200
- description: "Guard uses witness derivation, but the source object's object_type in table is inconsistent with the witness expected source type.\n" +
201
- mismatches.map((mm) => ` - ${mm}`).join("\n") +
202
- "\nNote: object_type is an auxiliary annotation field on table entries; it is not strictly validated at on-chain creation." +
203
- "However, wrong annotations cause semantic misunderstanding and maintenance difficulty.",
618
+ title: hardMismatches.length > 0
619
+ ? "witness source object_type explicitly contradicts the witness derivation chain"
620
+ : "witness source object_type annotation missing or multi-hop chain incomplete",
621
+ description: "Guard uses witness derivation, but the source object's object_type in table is either " +
622
+ "inconsistent with the witness expected source type, or the annotation is missing " +
623
+ "(preventing validation), or the multi-hop chain lacks an intermediate object.\n" +
624
+ evidenceParts.map((e) => ` - ${e}`).join("\n") +
625
+ "\nNote: object_type is an auxiliary annotation field on table entries; it is not strictly " +
626
+ "validated at on-chain creation. However, wrong or missing annotations cause semantic " +
627
+ "misunderstanding and maintenance difficulty, and may indicate a deeper structural mismatch.",
204
628
  affected_stakeholders: ["provider"],
205
- scenario: "Type annotation mismatch: witness source type annotation is wrong, causing semantic confusion",
206
- mitigation: "1) Fix the table entry's object_type field to match the witness source type;" +
207
- "2) witness 100-102 source type is Order; 103 is Progress; 104-108 is Arb;" +
208
- "3) Use wowok_buildin_info(info='guard instructions') to query the full witness definition",
209
- evidence: mismatches.join("; "),
629
+ scenario: hardMismatches.length > 0
630
+ ? "Type annotation mismatch: witness source type annotation is wrong, causing semantic confusion"
631
+ : "Type annotation missing: witness source type cannot be validated, raising maintenance risk",
632
+ mitigation: "1) Set or fix the table entry's object_type field to match the witness source type;\n" +
633
+ "2) witness 100-102 source type is Order; 103 is Progress; 104-108 is Arb;\n" +
634
+ "3) For multi-hop witnesses (106-108), ensure the intermediate Order object is also in the table;\n" +
635
+ "4) Use wowok_buildin_info(info='guard instructions') to query the full witness definition",
636
+ evidence: evidenceParts.join("; "),
210
637
  };
211
638
  },
212
639
  },
@@ -290,6 +717,12 @@ const RISK_RULES = [
290
717
  return null;
291
718
  if (ctx.scene?.binding_field === "voting_guard")
292
719
  return null;
720
+ if (ctx.scene?.id === "service_order_allocators_guard" &&
721
+ ctx.sharing_recipients &&
722
+ ctx.sharing_recipients.length > 0 &&
723
+ !ctx.sharing_recipients.includes("Signer")) {
724
+ return null;
725
+ }
293
726
  if (!hasSignerCheck(ctx)) {
294
727
  if (ctx.scene?.id === "machine_forward_guard") {
295
728
  return {
@@ -436,6 +869,170 @@ const RISK_RULES = [
436
869
  };
437
870
  },
438
871
  },
872
+ {
873
+ id: "R-C3-05",
874
+ data_source_class: "type3_submitted_object",
875
+ trigger: "Guard accepts a runtime-submitted Order/Progress/Arb object (Type3) but does NOT verify the submitted object belongs to the current project (Machine or Service). " +
876
+ "The WoWok runtime does NOT enforce any binding between the submitted order_id and the actual Progress being forwarded — verify_guard receives only the caller-provided submission bytes, never the Progress object under advancement (passport.move:27,251-253; progress.move:458-483). " +
877
+ "A caller can therefore submit an unrelated order_id from a DIFFERENT project while forwarding a different Progress, causing witness=100 (Order->Progress) to evaluate against foreign data.",
878
+ check: (ctx) => {
879
+ const submittedAddrs = ctx.table.filter((e) => e.b_submission && e.value_type === "Address");
880
+ if (submittedAddrs.length === 0)
881
+ return null;
882
+ const witnessCodes = extractWitnessCodes(ctx.root);
883
+ const hasOrderWitness = witnessCodes.some((c) => [100, 101, 102].includes(c));
884
+ const hasProgressWitness = witnessCodes.some((c) => [103].includes(c));
885
+ const hasArbWitness = witnessCodes.some((c) => [104, 105, 106, 107, 108].includes(c));
886
+ const submittedOrders = submittedAddrs.filter((e) => e.object_type === "Order" || (hasOrderWitness && !e.object_type));
887
+ const submittedProgress = submittedAddrs.filter((e) => e.object_type === "Progress" || (hasProgressWitness && !e.object_type));
888
+ const submittedArbs = submittedAddrs.filter((e) => e.object_type === "Arb" || (hasArbWitness && !e.object_type));
889
+ const hasProjectRelevantSubmission = submittedOrders.length > 0 ||
890
+ submittedProgress.length > 0 ||
891
+ submittedArbs.length > 0;
892
+ if (!hasProjectRelevantSubmission)
893
+ return null;
894
+ const affectedSceneIds = [
895
+ "machine_forward_guard",
896
+ "service_order_allocators_guard",
897
+ "arbitration_usage_guard",
898
+ "arbitration_voting_guard",
899
+ "reward_guard",
900
+ "progress_submission_guard",
901
+ ];
902
+ const sceneId = ctx.scene?.id;
903
+ const isAffectedScene = !sceneId || affectedSceneIds.includes(sceneId);
904
+ if (!isAffectedScene)
905
+ return null;
906
+ const PROJECT_BINDING_QUERY_IDS = new Set([1563, 1560, 1250, 1402, 1401]);
907
+ const rootQueryIds = extractQueryIds(ctx.root);
908
+ const hasProjectBinding = rootQueryIds.some((id) => PROJECT_BINDING_QUERY_IDS.has(id));
909
+ if (hasProjectBinding)
910
+ return null;
911
+ const criticalScenes = new Set([
912
+ "machine_forward_guard",
913
+ "service_order_allocators_guard",
914
+ ]);
915
+ const level = sceneId && criticalScenes.has(sceneId)
916
+ ? "critical"
917
+ : "high";
918
+ const unboundEntries = [];
919
+ for (const e of submittedOrders) {
920
+ unboundEntries.push(`#${e.identifier}${e.object_type ? "(Order)" : "(inferred Order via witness 100-102)"}`);
921
+ }
922
+ for (const e of submittedProgress) {
923
+ unboundEntries.push(`#${e.identifier}${e.object_type ? "(Progress)" : "(inferred Progress via witness 103)"}`);
924
+ }
925
+ for (const e of submittedArbs) {
926
+ unboundEntries.push(`#${e.identifier}${e.object_type ? "(Arb)" : "(inferred Arb via witness 104-108)"}`);
927
+ }
928
+ const sceneLabel = sceneId ?? "unknown scene (conservative flag)";
929
+ return {
930
+ id: "R-C3-05",
931
+ data_source_class: "type3_submitted_object",
932
+ level,
933
+ category: "submission_forgery",
934
+ title: "Submitted Order/Progress/Arb is NOT verified to belong to the current project (cross-project bypass — CRITICAL trust-boundary gap)",
935
+ description: `Guard accepts runtime-submitted Order/Progress/Arb object(s) [${unboundEntries.join(", ")}] but the root does NOT verify the submitted object belongs to the current project (no query of order.service/order.machine/progress.machine/arb.service compared to a project constant).\n\n` +
936
+ "TRUST-BOUNDARY GAP (verified via Move source):\n" +
937
+ " - The Passport submission is built by the CALLER, not auto-injected by the runtime (passport.move:120,149,157)\n" +
938
+ " - verify_guard receives ONLY the caller-provided submission bytes — it never sees the Progress/Order actually being forwarded (passport.move:27,251-253)\n" +
939
+ " - next_with_passport performs NO linkage check between the passport submission and the Progress under advancement (progress.move:458-483)\n" +
940
+ " - Even order_next_with_passport's E_ORDER_NOT_MATCH only checks the separate `order:&Order` parameter, NOT the order_id embedded in the passport submission (progress.move:452)\n\n" +
941
+ `EXPLOIT (scene=${sceneLabel}):\n` +
942
+ " 1. Attacker holds Order Y from project B (where progress.current_time is old enough to pass the time-lock)\n" +
943
+ " 2. Attacker calls forward on Order X's Progress (project A, freshly entered the node)\n" +
944
+ " 3. Attacker submits Order Y's ID in the passport (lying about which order is being forwarded)\n" +
945
+ " 4. Guard's witness=100 reads Order Y's Progress -> time-lock passes (Order Y is old)\n" +
946
+ " 5. forward actually executes on Order X's Progress -> TIME-LOCK BYPASSED\n\n" +
947
+ "This is distinct from R-C3-01 (Signer binding — WHO can submit) and R-C3-03 (type matching — WHAT type is submitted). " +
948
+ "R-C3-05 addresses WHETHER the submitted object belongs to the current project — a third orthogonal axis of submission trust. " +
949
+ "Even with R-C3-01 satisfied (authorized Signer) and R-C3-03 satisfied (correct type), the Signer can still submit an unrelated project's object.",
950
+ affected_stakeholders: ["customer", "provider", "arbitrator"],
951
+ scenario: "Cross-project bypass: authorized signer submits an unrelated project's order/progress/arb to bypass time-lock, status, or allocation conditions",
952
+ mitigation: "Add a project-binding check to the root (wrap existing root in logic_and):\n" +
953
+ " - For submitted Order: add logic_equal[query(1563: order.service), identifier[N](project_service_address)] — RECOMMENDED (service is the canonical project anchor)\n" +
954
+ " - For submitted Order: add logic_equal[query(1560: order.machine), identifier[N](project_machine_address)] — alternative (machine anchor)\n" +
955
+ " - For submitted Progress: add logic_equal[query(1250: progress.machine), identifier[N](project_machine_address)]\n" +
956
+ " - For submitted Arb: add logic_equal[query(1402: arb.service), identifier[N](project_service_address)]\n" +
957
+ " - Use witness=102 (TypeOrderService) on the submitted Order to derive the Service, then compare to the project service constant\n\n" +
958
+ "Intentional cross-project exception: if the Guard is DESIGNED to accept cross-project submissions (rare), document this explicitly in the description and add a binding_hint annotation 'cross_project_intentional:true' to suppress this rule.",
959
+ evidence: `Unbound submitted project-relevant entries: ${unboundEntries.join(", ")}. ` +
960
+ `Scene: ${sceneLabel}. ` +
961
+ `Missing project-binding queries: none of [1563(order.service), 1560(order.machine), 1250(progress.machine), 1402(arb.service)] present in root.`,
962
+ };
963
+ },
964
+ },
965
+ {
966
+ id: "R-C3-06",
967
+ data_source_class: "type3_submitted_object",
968
+ trigger: "Guard is bound to Service order_allocators (service_order_allocators_guard scene). " +
969
+ "In this scene the Guard decides IF allocation happens, while sharing.who decides WHERE funds go. " +
970
+ "If sharing.who=Signer, the transaction caller receives the funds — so a Guard that does NOT bind " +
971
+ "context(Signer) lets ANYONE who passes the Guard steal 100% of the order funds. " +
972
+ "This is a Guard+sharing coupling risk: neither piece alone reveals the theft — only their combination.",
973
+ check: (ctx) => {
974
+ if (ctx.scene?.id !== "service_order_allocators_guard")
975
+ return null;
976
+ if (hasSignerCheck(ctx))
977
+ return null;
978
+ const recipients = ctx.sharing_recipients;
979
+ const sharingProvided = recipients !== undefined && recipients.length > 0;
980
+ const hasSignerSharing = sharingProvided && recipients.includes("Signer");
981
+ if (sharingProvided && !hasSignerSharing)
982
+ return null;
983
+ const level = hasSignerSharing ? "critical" : "medium";
984
+ const recipientsLabel = sharingProvided
985
+ ? `[${recipients.join(", ")}]`
986
+ : "(not provided — conservative flag)";
987
+ const titleSuffix = hasSignerSharing
988
+ ? "sharing.who=Signer confirmed — anyone can steal 100% of funds"
989
+ : "if sharing.who=Signer, anyone can steal 100% of funds";
990
+ return {
991
+ id: "R-C3-06",
992
+ data_source_class: "type3_submitted_object",
993
+ level,
994
+ category: "binding_risk",
995
+ title: `allocators Guard without Signer binding — ${titleSuffix}`,
996
+ description: `Guard is bound to Service order_allocators but does NOT verify context(Signer). ` +
997
+ `In the allocators scene, sharing.who determines the fund recipient:\n` +
998
+ ` - sharing.who=Signer → funds flow to the CALLER (transaction signer)\n` +
999
+ ` - sharing.who=Entity → funds flow to a FIXED address (Treasury/personal)\n` +
1000
+ ` - sharing.who=GuardIdentifier → funds flow to a Guard-table-derived address\n\n` +
1001
+ `When sharing.who=Signer and the Guard does not bind Signer, ANY caller who passes the Guard ` +
1002
+ `receives the order funds — a direct theft. Even an authorized caller is unnecessary: the attacker ` +
1003
+ `only needs to pass the Guard's condition (e.g. submit any completed order_id), then the funds go to ` +
1004
+ `their own address.\n\n` +
1005
+ `This is distinct from R-C3-01 (generic Signer binding) and R-C3-05 (cross-project bypass). ` +
1006
+ `R-C3-06 addresses the allocators-specific Guard+sharing coupling: the SAFE design uses ` +
1007
+ `sharing.who=Entity so the recipient is fixed, making Guard Signer binding unnecessary.\n\n` +
1008
+ `sharing_recipients hint: ${recipientsLabel}`,
1009
+ affected_stakeholders: ["provider", "customer"],
1010
+ scenario: "Fund theft: attacker passes the allocators Guard (e.g. submits a completed order_id) and " +
1011
+ "sharing.who=Signer routes 100% of the order funds to the attacker's address",
1012
+ mitigation: "TWO mitigation options:\n" +
1013
+ " (A) Traditional: add context(Signer) binding to the Guard root " +
1014
+ "(logic_equal[context(Signer), identifier[N](authorized_address)]) so only the authorized " +
1015
+ "recipient can trigger allocation.\n" +
1016
+ " (B) PREFERRED: use sharing.who=Entity (Treasury object or fixed personal address) in the " +
1017
+ "Allocator's sharing config. Funds then always flow to the designated recipient regardless of " +
1018
+ "who triggers allocation — the Guard does NOT need to bind Signer. This is more robust because " +
1019
+ "it does not depend on Guard correctness.\n\n" +
1020
+ "Design checklist when using sharing.who=Entity:\n" +
1021
+ " - If Entity points to a Treasury object, verify the Treasury's permission is consistent with " +
1022
+ "the Service's permission (different permissions = different permission organizations = minor risk).\n" +
1023
+ " - allocators require unique guard addresses: create separate Guard objects (identical root/table, " +
1024
+ "different descriptions) for each allocator.\n" +
1025
+ " - First-match-wins: only the first Allocator whose Guard passes executes; multiple allocators " +
1026
+ "represent alternative strategies, not a simultaneous split.\n" +
1027
+ " - Still add order.service project binding (query 1563) to prevent cross-project bypass (R-C3-05) " +
1028
+ "triggering premature allocation.",
1029
+ evidence: `Scene: service_order_allocators_guard. ` +
1030
+ `Guard binds Signer: no. ` +
1031
+ `sharing_recipients: ${recipientsLabel}. ` +
1032
+ `Level rationale: ${hasSignerSharing ? "Signer sharing confirmed → critical" : "sharing hint absent → conservative medium"}.`,
1033
+ };
1034
+ },
1035
+ },
439
1036
  {
440
1037
  id: "R-C4-01",
441
1038
  data_source_class: "type4_system_context",
@@ -488,7 +1085,7 @@ const RISK_RULES = [
488
1085
  category: "system_context_risk",
489
1086
  title: "Clock time-lock should have tolerance to avoid boundary invalidation from validator block-time variance",
490
1087
  description: "Guard uses context(Clock) for time-lock comparison. Clock comes from the timestamp_ms field (U64 millisecond timestamp) of system shared object 0x6.\n" +
491
- "The Sui on-chain timestamp is produced by validator consensus and has a slight deviation from real time (usually < 1 second), " +
1088
+ "The Wow on-chain timestamp is produced by validator consensus and has a slight deviation from real time (usually < 1 second), " +
492
1089
  "and block-time variance may cause timestamp jumps. Using precise boundaries (>= or ==) may cause:\n" +
493
1090
  " - The time-lock expiration transaction is packaged exactly at the moment but fails due to slight timestamp deviation\n" +
494
1091
  " - Or conversely, slight deviation causes the time-lock to pass early\n" +
@@ -530,10 +1127,63 @@ const RISK_RULES = [
530
1127
  };
531
1128
  },
532
1129
  },
1130
+ {
1131
+ id: "R-C4-04",
1132
+ data_source_class: "type4_system_context",
1133
+ trigger: "Guard uses logic_equal[context(Signer), identifier[N](fixed_address)] — Level 1 strict single-identity binding. " +
1134
+ "Only ONE address can pass the Guard, and because Guards are immutable, changing the authorized address requires " +
1135
+ "rebuilding the Guard and migrating all references. This creates a convenience trade-off: maximum security but " +
1136
+ "maximum inflexibility (key loss or personnel change permanently blocks the operation).",
1137
+ check: (ctx) => {
1138
+ if (!hasSignerCheck(ctx))
1139
+ return null;
1140
+ if (isSignerCheckInLogicOr(ctx.root))
1141
+ return null;
1142
+ const detection = detectLevel1StrictBinding(ctx.root, ctx.table);
1143
+ if (!detection.detected || !detection.entry)
1144
+ return null;
1145
+ const entry = detection.entry;
1146
+ return {
1147
+ id: "R-C4-04",
1148
+ data_source_class: "type4_system_context",
1149
+ level: "info",
1150
+ category: "system_context_risk",
1151
+ title: "Level 1 strict single-identity Signer binding — convenience trade-off (consider Level 2/3 alternatives)",
1152
+ description: `Guard uses logic_equal[context(Signer), identifier[${entry.identifier}](fixed address)] — only the address "${entry.value}" can pass.\n` +
1153
+ "This is Level 1 strict single-identity binding: maximally secure but maximally inconvenient.\n" +
1154
+ "Guards are immutable on-chain. If the authorized address becomes unavailable (key loss, personnel change, " +
1155
+ "role rotation), the Guard permanently blocks the operation — the only recovery is to build a new Guard " +
1156
+ "and migrate ALL references (Machine forward guards, Service buy_guard, allocators, etc.).\n\n" +
1157
+ "Three verifier constraint levels trade off security vs convenience:\n" +
1158
+ " - Level 1 (current): strict single-identity — one address, no flexibility (AVOID unless justified)\n" +
1159
+ " - Level 2: identity-set — logic_or of multiple valid identities (e.g. order.owner OR order.agent; " +
1160
+ "permission.owner OR permission.admin). Recommended for role-based access.\n" +
1161
+ " - Level 3: scene-combined — verify whether Signer binding is even needed. Many scenes (allocators with " +
1162
+ "sharing.who=Entity, machine forward with permissionIndex) already ensure safety without Signer binding.\n\n" +
1163
+ "Use Level 1 only when: (a) the role is permanently tied to one address, AND (b) the designer explicitly " +
1164
+ "accepts the immutability lock-in risk. Otherwise prefer Level 2 or Level 3.",
1165
+ affected_stakeholders: ["provider"],
1166
+ scenario: "Immutability lock-in: authorized address becomes unavailable, Guard permanently blocks the operation, " +
1167
+ "requiring full Guard rebuild and reference migration",
1168
+ mitigation: "Consider these alternatives before settling on Level 1 strict binding:\n" +
1169
+ " (A) Level 2 identity-set (RECOMMENDED for role-based access): use logic_or to allow multiple valid identities.\n" +
1170
+ " - Order holder: logic_or[logic_equal[query(1562: order.owner), context(Signer)], query(1567: order.agent has, parameters: [context(Signer)])]\n" +
1171
+ " - Service provider: logic_or[logic_equal[query(1002: permission.owner), context(Signer)], query(1004: permission.admin has, parameters: [context(Signer)])]\n" +
1172
+ " - Dynamic permission (survives rotation): submit permission address, verify query(1488: service.permission) == submitted, then check 1002/1004\n" +
1173
+ " (B) Level 3 scene-combined: evaluate whether Signer binding is needed at all.\n" +
1174
+ " - If using allocators with sharing.who=Entity (Treasury): funds flow to a fixed recipient regardless of caller — no Signer binding needed (R-C3-06 safe)\n" +
1175
+ " - If using machine forward: permissionIndex already verifies operator identity — Signer binding may be redundant\n" +
1176
+ " (C) If Level 1 is truly required: document the justification in the Guard description and ensure a key-recovery / address-rotation plan exists.",
1177
+ evidence: `Level 1 strict binding detected: logic_equal[context(Signer), identifier[${entry.identifier}]] ` +
1178
+ `where identifier[${entry.identifier}] is a fixed Address constant (b_submission=false, value="${entry.value}"${entry.name ? `, name="${entry.name}"` : ""}). ` +
1179
+ `Not wrapped in logic_or (not Level 2 identity-set).`,
1180
+ };
1181
+ },
1182
+ },
533
1183
  {
534
1184
  id: "R-X1-01",
535
1185
  data_source_class: "cross_type",
536
- trigger: "Time-lock query returns 0 causing logic_as_u256_greater_or_equal comparison to always be true",
1186
+ trigger: "Time-lock uses logic_as_u256_greater_or_equal with a baseline query that has NO non-zero invariant (i.e. the query MAY legitimately return 0 when it succeeds), causing Clock >= 0 + timeout to always be true",
537
1187
  check: (ctx) => {
538
1188
  const str = rootStr(ctx);
539
1189
  const hasGreaterOrEqual = str.includes("logic_as_u256_greater_or_equal");
@@ -542,46 +1192,79 @@ const RISK_RULES = [
542
1192
  const hasQuery = str.includes('"query"');
543
1193
  if (!(hasGreaterOrEqual && hasClock && hasCalcAdd && hasQuery))
544
1194
  return null;
1195
+ if (rootAllQueriesNonZero(ctx.root))
1196
+ return null;
1197
+ const queryIds = extractQueryIds(ctx.root);
1198
+ const riskyQueries = queryIds.filter((id) => !QUERY_INVARIANT_MAP.has(id));
1199
+ const evidence = riskyQueries.length > 0
1200
+ ? `Queries without non-zero invariant: ${riskyQueries
1201
+ .map((id) => {
1202
+ const q = GUARDQUERY.find((x) => x.id === id);
1203
+ return q ? `${q.id} (${q.name})` : `${id}`;
1204
+ })
1205
+ .join(", ")}`
1206
+ : "Baseline query may return 0";
545
1207
  return {
546
1208
  id: "R-X1-01",
547
1209
  data_source_class: "cross_type",
548
1210
  level: "medium",
549
1211
  category: "logic_gap",
550
- title: "Time-lock query may return 0 causing >= comparison to always be true",
551
- description: "Guard uses logic_as_u256_greater_or_equal for time-lock comparison, and the compared baseline value comes from a query (Type1/2/3 data sources)." +
552
- "If the query returns 0 (e.g. querying a timestamp of an operation that hasn't happened, or unset node entry time), " +
553
- "then Clock >= 0 + timeout is always true, the time-lock is effectively void, and the disadvantaged party can bypass the time constraint immediately.",
1212
+ title: "Time-lock baseline query may return 0 causing >= comparison to always be true",
1213
+ description: "Guard uses logic_as_u256_greater_or_equal for time-lock comparison, and the compared baseline value comes from a query (Type1/2/3 data sources). " +
1214
+ "If the query SUCCEEDS but its stored value is 0 (e.g. a count that is 0 in the empty initial state, a user-submitted value of 0, or a Some(0) from an Option field), " +
1215
+ "then Clock >= 0 + timeout is always true, the time-lock is effectively void, and the disadvantaged party can bypass the time constraint immediately.\n\n" +
1216
+ "IMPORTANT — 'VM error = Guard failure' baseline: " +
1217
+ "if a query targets an absent object, unset Option, or missing collection entry, the WoWok native runtime aborts with a VM error (e.g. W_FIELD_NOT_FOUND) " +
1218
+ "and the Guard fails as a whole (does NOT silently return 0). Therefore the null-bypass risk ONLY applies to queries whose stored value can legitimately be 0 " +
1219
+ "while the query itself succeeds. Such queries are exactly those WITHOUT an `invariant` annotation in the SDK GUARDQUERY catalog.",
554
1220
  affected_stakeholders: ["customer", "provider"],
555
- scenario: "Time-lock invalidation: query returns 0, making the >= condition always pass",
556
- mitigation: "1) Use a query that returns the current node entry time (e.g. progress.current_time, id=1272) " +
557
- "instead of the operation timestamp (e.g. progress.session.forward.time, id=1271);" +
558
- "2) Add a precondition check query > 0 before the time-lock;" +
559
- "3) Confirm the query must have a value when the Guard executes (query current node attributes rather than historical operations)",
1221
+ scenario: "Time-lock invalidation: baseline query returns 0 (not VM error), making the >= condition always pass",
1222
+ mitigation: "1) Use a query whose return is guaranteed non-zero (look for `invariant` in guard-ins.ts — e.g. progress.current_time id=1272 with invariant='clock_derived'); " +
1223
+ "2) Add a precondition check query > 0 before the time-lock (for queries without an invariant, e.g. user-submitted baselines); " +
1224
+ "3) Avoid using count/index/find/user-submitted values as time-lock baselines these can all legitimately return 0; " +
1225
+ "4) Distinguish 'VM error' (safe Guard fails) from 'query returns 0' (unsafe Guard bypassed). Only the latter is a null-bypass risk.",
1226
+ evidence,
560
1227
  };
561
1228
  },
562
1229
  },
563
1230
  {
564
1231
  id: "R-X1-02",
565
1232
  data_source_class: "cross_type",
566
- trigger: "Guard uses logic_as_u256_greater but does not handle null values",
1233
+ trigger: "Guard uses logic_as_u256_greater with a query operand that has NO non-zero invariant (i.e. the query MAY legitimately return 0 when it succeeds), and the comparison does not distinguish 'query succeeded with 0' from 'query failed'",
567
1234
  check: (ctx) => {
568
1235
  const str = rootStr(ctx);
569
1236
  if (!(str.includes("logic_as_u256_greater") && str.includes('"query"')))
570
1237
  return null;
1238
+ if (rootAllQueriesNonZero(ctx.root))
1239
+ return null;
1240
+ const queryIds = extractQueryIds(ctx.root);
1241
+ const riskyQueries = queryIds.filter((id) => !QUERY_INVARIANT_MAP.has(id));
1242
+ const evidence = riskyQueries.length > 0
1243
+ ? `Queries without non-zero invariant: ${riskyQueries
1244
+ .map((id) => {
1245
+ const q = GUARDQUERY.find((x) => x.id === id);
1246
+ return q ? `${q.id} (${q.name})` : `${id}`;
1247
+ })
1248
+ .join(", ")}`
1249
+ : "Query may return 0";
571
1250
  return {
572
1251
  id: "R-X1-02",
573
1252
  data_source_class: "cross_type",
574
1253
  level: "medium",
575
1254
  category: "logic_gap",
576
- title: "Numeric comparison does not handle the case of query returning null",
577
- description: "Guard uses logic_as_u256_greater for numeric comparison, and operands come from a query (Type1/2/3 data sources)." +
578
- "If the query returns null (e.g. object does not exist, field not set), the comparison result may be undefined." +
579
- "In the WoWok protocol null is usually treated as 0, which may cause unexpected pass.",
1255
+ title: "Numeric comparison with a query that may legitimately return 0",
1256
+ description: "Guard uses logic_as_u256_greater for numeric comparison, and one of the operands comes from a query (Type1/2/3 data sources) that has NO non-zero invariant annotation. " +
1257
+ "If the query SUCCEEDS but its stored value is 0 (e.g. a count that is 0 in the empty initial state, a user-submitted value of 0, or a Some(0) from an Option field), " +
1258
+ "the comparison may produce an unexpected result (e.g. `query > 0` fails, or `query > some_threshold` is silently bypassed).\n\n" +
1259
+ "IMPORTANT — 'VM error = Guard failure' baseline: " +
1260
+ "if a query targets absent data (e.g. object does not exist, Option is None, collection entry is missing), the WoWok native runtime aborts with a VM error " +
1261
+ "(not 0). The Guard fails as a whole (does NOT silently return 0). Therefore this rule only flags queries whose stored value can be 0 while the query succeeds.",
580
1262
  affected_stakeholders: ["customer", "provider"],
581
- scenario: "Null misjudgment: query returns 0, causing > 0 condition to unexpectedly pass or fail",
582
- mitigation: "1) Add a query result existence check before comparison;" +
583
- "2) Use query_reward_record_exists or query_progress_history_find to verify first;" +
584
- "3) Consider adding a logic_and(object exists, numeric comparison) combination",
1263
+ scenario: "Null misjudgment: query succeeds with value 0, causing > 0 condition to unexpectedly fail (or > threshold to be bypassed)",
1264
+ mitigation: "1) Prefer queries with an `invariant` annotation (e.g. clock_derived / positive_lower_bound); " +
1265
+ "2) Add a precondition check (query > 0) before the main comparison when using count/index/find/user-submitted baselines; " +
1266
+ "3) Distinguish 'VM error' (safe Guard fails) from 'query returns 0' (potentially unsafe). Only the latter is a null-bypass risk.",
1267
+ evidence,
585
1268
  };
586
1269
  },
587
1270
  },
@@ -918,7 +1601,93 @@ const RISK_RULES = [
918
1601
  };
919
1602
  },
920
1603
  },
1604
+ {
1605
+ id: "R-X1-14",
1606
+ data_source_class: "cross_type",
1607
+ trigger: "Scene-specific reentrancy: Guard pass triggers a state-changing side effect (fund distribution, vote count, Arb creation) that does NOT naturally invalidate the Guard condition. " +
1608
+ "Without an anti-reentrancy primitive, the same Signer can repeatedly pass the Guard. " +
1609
+ "Additionally detects pseudo-protection: primitive present but not properly wrapped in logic_not (for Bool queries) or logic_equal(0) (for count queries).",
1610
+ check: (ctx) => {
1611
+ const spec = REENTRANCY_SPECS.find((s) => s.sceneId === ctx.scene?.id);
1612
+ if (!spec)
1613
+ return null;
1614
+ const protection = detectReentrancyProtection(ctx.root, ctx.table, spec);
1615
+ if (protection.state === "protected")
1616
+ return null;
1617
+ if (protection.state === "pseudo") {
1618
+ return {
1619
+ id: "R-X1-14",
1620
+ data_source_class: "cross_type",
1621
+ level: "medium",
1622
+ category: "logic_gap",
1623
+ title: `Reentrancy pseudo-protection: anti-reentrancy primitive present but not properly wrapped (${spec.sceneId})`,
1624
+ description: `Scene: ${spec.sceneId}\n` +
1625
+ `The Guard root contains an anti-reentrancy primitive (${protection.foundPrimitiveName}), but it is NOT properly wrapped.\n` +
1626
+ "Proper wrapping rules:\n" +
1627
+ " - Bool-returning primitives (exists-style): must be wrapped in logic_not\n" +
1628
+ " - U64-returning primitives (count-style): must be compared to 0 via logic_equal\n" +
1629
+ " - Address-returning primitives: must be wrapped in logic_not (None check)\n\n" +
1630
+ "WHY THIS IS DANGEROUS:\n" +
1631
+ " The developer likely intended to prevent reentrancy but the wrapping is incorrect, " +
1632
+ "so the primitive does NOT actually block repeated Guard passes. This is arguably MORE " +
1633
+ "dangerous than absence — code reviewers see the primitive and assume protection is in place.\n\n" +
1634
+ `Scene rationale: ${spec.rationale}`,
1635
+ affected_stakeholders: deriveStakeholders(spec.sceneId),
1636
+ scenario: `Developer added anti-reentrancy primitive but used wrong wrapping; reentrancy exploit still possible in ${spec.sceneId}`,
1637
+ mitigation: "Fix the wrapping:\n" +
1638
+ " - For exists-style (Bool) primitives: wrap in logic_not — { type: 'logic_not', node: { type: 'query', query: <primitive>, ... } }\n" +
1639
+ " - For count-style (U64) primitives: compare to 0 — { type: 'logic_equal', nodes: [ { type: 'query', query: <primitive>, ... }, identifier(N) ] } where table[N].value = 0\n" +
1640
+ " - For address-style primitives: wrap in logic_not (None check)\n" +
1641
+ `Then wrap the protection in logic_and with the existing root conditions.\n\n` +
1642
+ `Full mitigation for ${spec.sceneId}:\n${spec.mitigation}`,
1643
+ evidence: protection.evidence,
1644
+ };
1645
+ }
1646
+ const level = spec.level;
1647
+ const stakeholders = deriveStakeholders(spec.sceneId);
1648
+ return {
1649
+ id: "R-X1-14",
1650
+ data_source_class: "cross_type",
1651
+ level,
1652
+ category: "logic_gap",
1653
+ title: level === "info"
1654
+ ? `Reentrancy reminder: ${spec.sceneId} has no anti-reentrancy primitive (LOW risk)`
1655
+ : `Reentrancy risk: ${spec.sceneId} has NO anti-reentrancy protection (${level.toUpperCase()})`,
1656
+ description: `Scene: ${spec.sceneId}\n` +
1657
+ `Reentrancy risk level: ${level.toUpperCase()}\n\n` +
1658
+ `RATIONALE:\n${spec.rationale}\n\n` +
1659
+ (protection.evidence
1660
+ ? `DETECTION:\n${protection.evidence}\n\n`
1661
+ : "") +
1662
+ (level === "critical"
1663
+ ? "IMPACT: Without protection, the same Signer can repeatedly trigger the side effect (fund distribution / vote counting), causing direct fund loss or result manipulation. THIS IS A CRITICAL VULNERABILITY — the Guard MUST be rebuilt with an anti-reentrancy primitive before deployment.\n\n"
1664
+ : level === "high"
1665
+ ? "IMPACT: Without protection, the system can be abused (multiple dispute cases, arbitration harassment). Strongly recommended to add protection.\n\n"
1666
+ : "IMPACT: Low by default; only relevant if business intent requires single-action enforcement. Review the rationale above.\n\n"),
1667
+ affected_stakeholders: stakeholders,
1668
+ scenario: level === "info"
1669
+ ? `${spec.sceneId} lacks anti-reentrancy primitive; may or may not be needed depending on business intent`
1670
+ : `${spec.sceneId} lacks anti-reentrancy primitive; same Signer can repeatedly trigger the side effect`,
1671
+ mitigation: spec.mitigation,
1672
+ evidence: protection.evidence,
1673
+ };
1674
+ },
1675
+ },
921
1676
  ];
1677
+ function deriveStakeholders(sceneId) {
1678
+ switch (sceneId) {
1679
+ case "reward_guard":
1680
+ return ["claimant", "provider"];
1681
+ case "arbitration_voting_guard":
1682
+ return ["customer", "provider", "arbitrator"];
1683
+ case "arbitration_usage_guard":
1684
+ return ["customer", "provider"];
1685
+ case "service_order_allocators_guard":
1686
+ return ["customer", "provider"];
1687
+ default:
1688
+ return ["provider"];
1689
+ }
1690
+ }
922
1691
  export function assessGuardRisks(params) {
923
1692
  const ctx = {
924
1693
  table: params.table,
@@ -926,6 +1695,7 @@ export function assessGuardRisks(params) {
926
1695
  rely: params.rely,
927
1696
  scene: params.scene,
928
1697
  operation_type: params.operation_type,
1698
+ sharing_recipients: params.sharing_recipients,
929
1699
  };
930
1700
  const risks = [];
931
1701
  for (const rule of RISK_RULES) {