@seasonkoh/webaz 0.1.26 → 0.1.27
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/server.js +36 -28
- 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.js +679 -679
- package/dist/pwa/public/i18n.js +15 -28
- package/dist/pwa/public/index.html +1 -1
- package/dist/pwa/public/openapi.json +4760 -2769
- 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/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 +1 -1
- package/dist/pwa/server.js +156 -469
- package/dist/settlement-math.js +3 -3
- package/dist/version.js +6 -4
- package/package.json +33 -7
- 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
|
@@ -521,7 +521,7 @@ Options:
|
|
|
521
521
|
},
|
|
522
522
|
promoter_api_key: {
|
|
523
523
|
type: 'string',
|
|
524
|
-
description: "Referrer api_key (optional). ⚠️ Only L1 recorded (direct referrer, 70% commission); L2/L3 can't be inferred via MCP,
|
|
524
|
+
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
525
|
},
|
|
526
526
|
// B2 隐私购物
|
|
527
527
|
anonymous_recipient: {
|
|
@@ -1444,7 +1444,8 @@ Discovery + suggesting need NO api_key (anyone / any agent can browse and propos
|
|
|
1444
1444
|
Actions:
|
|
1445
1445
|
- 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
1446
|
- 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.
|
|
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 (but pass your key to LINK it to your account so you can track it via my_suggestions).
|
|
1448
|
+
- 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
1449
|
- 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
1450
|
- 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
1451
|
- status: tasks you hold (api_key).
|
|
@@ -1454,12 +1455,12 @@ Coordinates + records only — NO merge/reward; acceptance (done) = human mainta
|
|
|
1454
1455
|
inputSchema: {
|
|
1455
1456
|
type: 'object',
|
|
1456
1457
|
properties: {
|
|
1457
|
-
action: { type: 'string', enum: ['list_open', 'detail', 'suggest', 'claim', 'submit', 'status', 'profile'], description: 'list_open (default) | detail | suggest | claim | submit | status | profile' },
|
|
1458
|
+
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
1459
|
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
1460
|
task_id: { type: 'string', description: 'detail / claim / submit: the task id' },
|
|
1460
1461
|
area: { type: 'string', description: 'list_open: area filter / suggest: suggested area (e.g. search / docs / mcp)' },
|
|
1461
1462
|
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 —
|
|
1463
|
+
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
1464
|
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
1465
|
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
1466
|
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 +1519,22 @@ async function handleFeedback(args) {
|
|
|
1518
1519
|
}
|
|
1519
1520
|
// RFC-006 断点1(b)交接:从【可信】canonical 目标(API 响应里,绝不硬编码/不取自 task metadata)构造"怎么真正
|
|
1520
1521
|
// 动手"。人的编码 agent 做 git/PR;Passkey 真人担责。sandbox 运行 / 本地草稿不算正式参与。
|
|
1521
|
-
function buildContributeHandoff(cct, taskId) {
|
|
1522
|
+
function buildContributeHandoff(cct, taskId, caseId) {
|
|
1522
1523
|
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 || '
|
|
1524
|
+
const repoUrl = c.canonical_github_url || 'https://github.com/webaz-protocol/webaz';
|
|
1525
|
+
const baseRepo = c.expected_pr_base_repo || c.canonical_repository_full_name || 'webaz-protocol/webaz';
|
|
1525
1526
|
const baseBranch = c.base_branch || 'main';
|
|
1527
|
+
// case_id threads proposal → task → PR. = the source proposal id when this task came from a proposal,
|
|
1528
|
+
// else the task id itself. Quote it in the PR so the whole case stays traceable end to end.
|
|
1529
|
+
const cid = caseId || taskId;
|
|
1526
1530
|
return {
|
|
1531
|
+
case_id: cid,
|
|
1527
1532
|
canonical_repo: baseRepo,
|
|
1528
1533
|
repo: repoUrl,
|
|
1529
1534
|
base_branch: baseBranch,
|
|
1530
1535
|
start_here: 'Read AGENTS.md (project map + before-you-code + PR flow), then CONTRIBUTING.md.',
|
|
1531
1536
|
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.`,
|
|
1537
|
+
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
1538
|
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
1539
|
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
1540
|
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 +1585,7 @@ export async function handleContribute(args) {
|
|
|
1580
1585
|
return { error: 'task_id required for action=detail' };
|
|
1581
1586
|
const r = await apiCall('/api/public/build-tasks/' + encodeURIComponent(tid));
|
|
1582
1587
|
if (!r.error && r.task)
|
|
1583
|
-
r.agent_handoff = buildContributeHandoff(r.canonical_contribution_target, tid);
|
|
1588
|
+
r.agent_handoff = buildContributeHandoff(r.canonical_contribution_target, tid, r.task.case_id);
|
|
1584
1589
|
return r;
|
|
1585
1590
|
}
|
|
1586
1591
|
if (action === 'suggest') {
|
|
@@ -1592,6 +1597,7 @@ export async function handleContribute(args) {
|
|
|
1592
1597
|
return { error: 'summary (the reason) required for action=suggest' };
|
|
1593
1598
|
const r = await apiCall('/api/public/task-proposals', {
|
|
1594
1599
|
method: 'POST',
|
|
1600
|
+
apiKey, // optional — when present, links the proposal to the submitter so it shows up in action=my_suggestions (still works anonymously)
|
|
1595
1601
|
body: {
|
|
1596
1602
|
title, summary,
|
|
1597
1603
|
suggested_area: args.area ?? args.suggested_area,
|
|
@@ -1600,6 +1606,8 @@ export async function handleContribute(args) {
|
|
|
1600
1606
|
proposer_github_login: args.proposer_github_login,
|
|
1601
1607
|
},
|
|
1602
1608
|
});
|
|
1609
|
+
if (!r.error && r.linked_to_account)
|
|
1610
|
+
r._next = 'Track this proposal\'s review status + reply: webaz_contribute action=my_suggestions api_key=<key>.';
|
|
1603
1611
|
// typed errors (RATE_LIMITED / DUPLICATE_PROPOSAL / validation) are already mapped by apiCall; the
|
|
1604
1612
|
// success response already carries the route-level `proposal_notice` (suggestion ≠ contribution/reward).
|
|
1605
1613
|
return r;
|
|
@@ -1612,6 +1620,13 @@ export async function handleContribute(args) {
|
|
|
1612
1620
|
};
|
|
1613
1621
|
if (action === 'status')
|
|
1614
1622
|
return apiCall('/api/build-tasks?mine=1', { apiKey });
|
|
1623
|
+
if (action === 'my_suggestions') {
|
|
1624
|
+
// your OWN past proposals + review status/public_reply/next_action (agent-readable 回执). Own rows only (server-enforced).
|
|
1625
|
+
const r = await apiCall('/api/me/task-proposals', { apiKey });
|
|
1626
|
+
if (!r.error)
|
|
1627
|
+
r._next = 'Each item carries status + public_reply + next_action. needs_info → resubmit via action=suggest referencing the id; converted → see converted_ref.';
|
|
1628
|
+
return r;
|
|
1629
|
+
}
|
|
1615
1630
|
if (action === 'profile')
|
|
1616
1631
|
return apiCall('/api/build-reputation/me', { apiKey });
|
|
1617
1632
|
if (action === 'claim') {
|
|
@@ -1730,8 +1745,11 @@ async function handleInfo() {
|
|
|
1730
1745
|
// 连接两个场景:用协议(本工具) ↔ 改协议(开发协作)。想改 WebAZ 本身的 agent 从这里进。
|
|
1731
1746
|
for_contributors: {
|
|
1732
1747
|
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/
|
|
1748
|
+
repo: 'https://github.com/webaz-protocol/webaz',
|
|
1734
1749
|
start_here: 'AGENTS.md (project map + before-you-code + PR flow) → CONTRIBUTING.md (full guide)',
|
|
1750
|
+
// 低门槛路径:无需 api_key、无需 clone 仓库,直接经协议发现任务 / 提建议(对外 well-known 入口也有,见 agent_quickstart)。
|
|
1751
|
+
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 提建议。',
|
|
1752
|
+
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
1753
|
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
1754
|
},
|
|
1737
1755
|
// NETWORK 模式:真网络 live 状态(best-effort 拉自 webaz.xyz);SANDBOX 模式为 null。
|
|
@@ -1741,10 +1759,10 @@ async function handleInfo() {
|
|
|
1741
1759
|
// 佣金机制 —— 纯功能性描述(怎么运作),不做"自证清白"式辩护。
|
|
1742
1760
|
commission_model: {
|
|
1743
1761
|
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 →
|
|
1762
|
+
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
1763
|
attribution: 'EXPLICIT per-order — commission goes to the promoter attributed at purchase time, not derived from the buyer\'s sponsor chain.',
|
|
1746
1764
|
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
|
|
1765
|
+
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
1766
|
l1_gate: 'the promoter must be a verified buyer (≥1 completed order) to receive commission, otherwise that share redirects.',
|
|
1749
1767
|
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
1768
|
},
|
|
@@ -3438,7 +3456,7 @@ function handleRotateKey(args) {
|
|
|
3438
3456
|
},
|
|
3439
3457
|
};
|
|
3440
3458
|
}
|
|
3441
|
-
// ─── 推广 /
|
|
3459
|
+
// ─── 推广 / 推荐网络 (Tokenomics) ───────────────────────────────────
|
|
3442
3460
|
async function handleReferral(args) {
|
|
3443
3461
|
// RFC-003 Batch 2:NETWORK 模式 → webaz.xyz 真网络聚合(Bearer api_key);SANDBOX 走本地。
|
|
3444
3462
|
if (toolBackend('webaz_referral') === 'network') {
|
|
@@ -3465,16 +3483,9 @@ async function handleReferral(args) {
|
|
|
3465
3483
|
const completed = db.prepare("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
|
|
3466
3484
|
const override = db.prepare("SELECT l1_share_override FROM users WHERE id = ?").get(userId)?.l1_share_override ?? 0;
|
|
3467
3485
|
const canL1 = override === 1 || (override === 0 && completed > 0);
|
|
3468
|
-
//
|
|
3486
|
+
// Neutral participation record only — placement position + per-leg PV. Matching-rewards engine excised (#401):
|
|
3487
|
+
// no Score / tier / pair-volume / payout is read or exposed.
|
|
3469
3488
|
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
3489
|
// invite / share links use permanent_code ONLY — never usr_xxx. (sandbox users have one from register.)
|
|
3479
3490
|
const permaCode = db.prepare("SELECT permanent_code FROM users WHERE id = ?").get(userId)?.permanent_code || null;
|
|
3480
3491
|
return {
|
|
@@ -3496,15 +3507,12 @@ async function handleReferral(args) {
|
|
|
3496
3507
|
l1: byLevel[1], l2: byLevel[2], l3: byLevel[3],
|
|
3497
3508
|
grand_total: byLevel[1].total + byLevel[2].total + byLevel[3].total,
|
|
3498
3509
|
},
|
|
3499
|
-
|
|
3500
|
-
//
|
|
3510
|
+
placement: {
|
|
3511
|
+
// Neutral participation/attribution record: a single referral code + per-leg PV. No matching rewards.
|
|
3501
3512
|
referral_link: permaCode ? `/i/${permaCode}` : null,
|
|
3502
3513
|
total_left_pv: Number(me?.total_left_pv ?? 0),
|
|
3503
3514
|
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,
|
|
3515
|
+
note: 'total_left_pv / total_right_pv are a participation / attribution record only — not income, not redeemable, no entitlement.',
|
|
3508
3516
|
},
|
|
3509
3517
|
rewards_status: (() => {
|
|
3510
3518
|
// RFC-002 §3.5 — 4 states + pending escrow visibility (PR-4)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
|
|
2
|
+
import { coordinationActionSpec, LIVE_ADMIN_COORDINATION_AUDIT_ACTIONS } from './admin-coordination-store.js';
|
|
3
|
+
import { resolveOperatorClaimAsOf, resolveAgentMandateAsOf } from './admin-coordination-resolver.js';
|
|
4
|
+
import { readAdminActionContext } from '../../pwa/admin-audit.js';
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
|
+
function deriveProvenance(ctx) {
|
|
7
|
+
const p = ctx.provenance;
|
|
8
|
+
if (p === 'human' || p === 'ai_assisted' || p === 'ai_authored' || p === 'unknown')
|
|
9
|
+
return p;
|
|
10
|
+
if (ctx.actor_type === 'agent')
|
|
11
|
+
return ctx.agent_mode === 'agent_assisted' ? 'ai_assisted' : 'ai_authored';
|
|
12
|
+
if (ctx.actor_type === 'human' || ctx.actor_type === 'admin_account')
|
|
13
|
+
return ctx.agent_mode === 'human_direct' ? 'human' : 'unknown';
|
|
14
|
+
return 'unknown';
|
|
15
|
+
}
|
|
16
|
+
export function ingestAdminCoordinationFact(db, input) {
|
|
17
|
+
const { auditId, redactionSummary } = input;
|
|
18
|
+
if (!auditId)
|
|
19
|
+
return { ok: false, reason: 'invalid_input', detail: 'auditId is required' };
|
|
20
|
+
// ── anchor: the audit row is the evidence truth; read EVERYTHING from it (incl. the coordinated
|
|
21
|
+
// object target_id) so the fact can NEVER point at a different object than the audit row records ──
|
|
22
|
+
const row = db.prepare('SELECT id, admin_id, action, detail, created_at, target_type, target_id FROM admin_audit_log WHERE id = ?').get(auditId);
|
|
23
|
+
if (!row)
|
|
24
|
+
return { ok: false, reason: 'audit_row_not_found', detail: auditId };
|
|
25
|
+
const action = row.action;
|
|
26
|
+
const spec = coordinationActionSpec(action);
|
|
27
|
+
if (!spec)
|
|
28
|
+
return { ok: false, reason: 'unknown_action', detail: action }; // allowlist fail-closed
|
|
29
|
+
const ctx = readAdminActionContext(row.detail);
|
|
30
|
+
const actorType = ctx.actor_type;
|
|
31
|
+
const adminAccountId = row.admin_id;
|
|
32
|
+
const occurredAt = row.created_at;
|
|
33
|
+
// legacy / context-less / system rows are NOT contribution-eligible.
|
|
34
|
+
if (actorType !== 'human' && actorType !== 'admin_account' && actorType !== 'agent') {
|
|
35
|
+
return { ok: false, reason: 'not_eligible_context', detail: `actor_type=${String(actorType)}` };
|
|
36
|
+
}
|
|
37
|
+
// ── attribution gate (as-of) — NOT written onto the fact; only proves the work is attributable ──
|
|
38
|
+
let contributorAccountId;
|
|
39
|
+
let via;
|
|
40
|
+
let executorRef;
|
|
41
|
+
if (actorType === 'agent') {
|
|
42
|
+
const actorRef = String(ctx.actor_ref || '');
|
|
43
|
+
const agentRef = actorRef.startsWith('agent:') ? actorRef.slice('agent:'.length) : actorRef;
|
|
44
|
+
const mandateId = ctx.mandate_id ? String(ctx.mandate_id) : '';
|
|
45
|
+
// The audit row's mandate_id is REQUIRED and DECIDES attribution — resolve by (agent_ref, mandate_id),
|
|
46
|
+
// never by agent_ref alone (one agent_ref may hold several mandates → wrong-account错账 otherwise).
|
|
47
|
+
if (!agentRef || !mandateId)
|
|
48
|
+
return { ok: false, reason: 'not_eligible_context', detail: 'agent action requires _ctx.actor_ref + _ctx.mandate_id' };
|
|
49
|
+
const m = resolveAgentMandateAsOf(db, agentRef, mandateId, occurredAt);
|
|
50
|
+
if (!m)
|
|
51
|
+
return { ok: false, reason: 'no_attribution', detail: `no valid mandate (agent_ref=${agentRef}, mandate_id=${mandateId}) as-of occurred_at` };
|
|
52
|
+
if (!m.allowed_actions.includes(action))
|
|
53
|
+
return { ok: false, reason: 'agent_action_not_in_mandate', detail: action };
|
|
54
|
+
contributorAccountId = m.owner_contributor_account_id;
|
|
55
|
+
via = 'agent_mandate';
|
|
56
|
+
executorRef = `agent:${agentRef}#${mandateId}`; // mandate encoded → deterministic read-time resolution
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const c = resolveOperatorClaimAsOf(db, adminAccountId, occurredAt);
|
|
60
|
+
if (!c)
|
|
61
|
+
return { ok: false, reason: 'no_attribution', detail: 'no approved operator claim as-of occurred_at' };
|
|
62
|
+
// A self/related (root/founder bootstrap) approval that is NOT honestly disclosed must NOT enter
|
|
63
|
+
// production evidence — fail closed until an append-only marking correction discloses self_or_related.
|
|
64
|
+
if (c.self_related && !c.honestly_disclosed) {
|
|
65
|
+
return { ok: false, reason: 'self_related_not_disclosed', detail: `claim ${c.claim_event_id} is self/related but marked ${c.approval_kind}/${c.conflict_disclosure}; needs a governance-marking correction` };
|
|
66
|
+
}
|
|
67
|
+
contributorAccountId = c.contributor_account_id;
|
|
68
|
+
via = 'operator_claim';
|
|
69
|
+
executorRef = `admin:${adminAccountId}`;
|
|
70
|
+
}
|
|
71
|
+
const provenance = deriveProvenance(ctx);
|
|
72
|
+
const visibility = input.visibility ?? 'governance_only';
|
|
73
|
+
const sourceEventKey = `admin:${auditId}:coordination`;
|
|
74
|
+
// The coordinated object is the audit row's target_id (NOT a caller-supplied value). artifact_ref is
|
|
75
|
+
// NOT NULL → fall back to the action when the row has no target_id; source_id mirrors it (or NULL).
|
|
76
|
+
const targetId = row.target_id ?? null;
|
|
77
|
+
const artifactRef = targetId || action;
|
|
78
|
+
// dry-run: all gates passed; report what WOULD happen without writing (read-only — no insert, no tx).
|
|
79
|
+
if (input.dryRun) {
|
|
80
|
+
const existing = db.prepare('SELECT fact_id FROM contribution_facts WHERE source_event_key = ?').get(sourceEventKey);
|
|
81
|
+
return { ok: true, status: existing ? 'already_present' : 'would_ingest', factId: existing?.fact_id ?? '(dry-run)', sourceEventKey, executorRef, contributorAccountId, via };
|
|
82
|
+
}
|
|
83
|
+
const run = db.transaction(() => {
|
|
84
|
+
const existing = db.prepare('SELECT fact_id FROM contribution_facts WHERE source_event_key = ?').get(sourceEventKey);
|
|
85
|
+
if (existing)
|
|
86
|
+
return { factId: existing.fact_id, status: 'already_present' };
|
|
87
|
+
const factId = generateId('cfact');
|
|
88
|
+
db.prepare(`INSERT INTO contribution_facts (fact_id, source_event_key, source, type, artifact_ref, occurred_at, executor_ref, accountable_ref, provenance, status, immutable)
|
|
89
|
+
VALUES (?,?,?,?,?,?,?, NULL, ?, 'active', 1)`).run(factId, sourceEventKey, spec.factSource, spec.factType, artifactRef, occurredAt, executorRef, provenance);
|
|
90
|
+
db.prepare(`INSERT INTO admin_coordination_fact_sources (fact_id, admin_audit_log_id, source_type, source_id, visibility, redaction_summary)
|
|
91
|
+
VALUES (?,?,?,?,?,?)`).run(factId, auditId, action, targetId, visibility, redactionSummary ?? null);
|
|
92
|
+
return { factId, status: 'ingested' };
|
|
93
|
+
});
|
|
94
|
+
const out = run.immediate();
|
|
95
|
+
return { ok: true, status: out.status, factId: out.factId, sourceEventKey, executorRef, contributorAccountId, via };
|
|
96
|
+
}
|
|
97
|
+
// ───────────────────────────── batch / operator entry ─────────────────────────────
|
|
98
|
+
// Small, bounded, manual-run batch over ALLOWLISTED audit rows. NOT a historical backfill: the operator
|
|
99
|
+
// scopes it with sinceTime / sinceId + a hard limit, and it is DRY-RUN unless `commit` is set. Each row
|
|
100
|
+
// goes through the SAME single-row engine above (same fail-closed gates, same idempotency), so the batch
|
|
101
|
+
// adds no new attribution logic — it only selects candidates and aggregates the per-row outcomes.
|
|
102
|
+
export const DEFAULT_INGEST_LIMIT = 50;
|
|
103
|
+
export const MAX_INGEST_LIMIT = 500;
|
|
104
|
+
/**
|
|
105
|
+
* Parse the `--commit` switch at the production write entry. Accept ONLY a bare flag (`raw === ''`) or
|
|
106
|
+
* `--commit=true`. Anything else (`false`, `0`, `no`, …) THROWS — an explicit dry-run intent must never
|
|
107
|
+
* be misread as a write. `undefined` (flag absent) → false (dry-run).
|
|
108
|
+
*/
|
|
109
|
+
export function parseCommitSwitch(raw) {
|
|
110
|
+
if (raw === undefined)
|
|
111
|
+
return false;
|
|
112
|
+
if (raw === '' || raw === 'true')
|
|
113
|
+
return true;
|
|
114
|
+
throw new Error(`invalid_commit_flag: --commit takes no value (or =true); got ${JSON.stringify(raw)}`);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Select rows whose action is in the LIVE production set (only the real `operator_claim.*` actions —
|
|
118
|
+
* NOT the reserved concept names), run each through the single-row engine, and aggregate. Dry-run by
|
|
119
|
+
* default (NOTHING written unless `commit: true`). The candidate query filters to the live set so
|
|
120
|
+
* non-coordination AND reserved-concept rows are never even scanned; unknown/uneligible/no-claim rows
|
|
121
|
+
* that DO match still fail closed per-row and are reported as `skipped` with a reason.
|
|
122
|
+
*
|
|
123
|
+
* THROWS `invalid_cursor` when `sinceId` is supplied but matches no audit row — a fail-closed guard so a
|
|
124
|
+
* typo'd cursor can NEVER silently degrade into a from-the-beginning (backfill) scan.
|
|
125
|
+
*
|
|
126
|
+
* THROWS `commit_requires_cursor` when `commit` is true but NEITHER `sinceTime` nor `sinceId` is given —
|
|
127
|
+
* a no-cursor commit would write from the earliest live row, i.e. a small historical backfill. This
|
|
128
|
+
* pipeline is "from the present, cursor + limit scoped", so a write MUST be cursor-bounded. A no-cursor
|
|
129
|
+
* DRY-RUN is still allowed (preview from the earliest row writes nothing).
|
|
130
|
+
*/
|
|
131
|
+
export function ingestAdminCoordinationSince(db, options = {}) {
|
|
132
|
+
const commit = options.commit === true;
|
|
133
|
+
if (commit && !options.sinceTime && !options.sinceId) {
|
|
134
|
+
throw new Error('commit_requires_cursor: --commit requires --since-time or --since-id (no-cursor commit would backfill history); run a dry-run first to find a cursor');
|
|
135
|
+
}
|
|
136
|
+
const limit = Math.min(Math.max(1, options.limit ?? DEFAULT_INGEST_LIMIT), MAX_INGEST_LIMIT);
|
|
137
|
+
const actions = [...LIVE_ADMIN_COORDINATION_AUDIT_ACTIONS];
|
|
138
|
+
const placeholders = actions.map(() => '?').join(',');
|
|
139
|
+
const where = [`action IN (${placeholders})`];
|
|
140
|
+
const params = [...actions];
|
|
141
|
+
// resume cursor: rows strictly after (cursorTime, cursorId) in (created_at ASC, id ASC) order.
|
|
142
|
+
let cursorTime = options.sinceTime;
|
|
143
|
+
let cursorId;
|
|
144
|
+
if (options.sinceId) {
|
|
145
|
+
const c = db.prepare('SELECT id, created_at FROM admin_audit_log WHERE id = ?').get(options.sinceId);
|
|
146
|
+
// fail-closed: an explicit-but-unknown cursor must NOT degrade into a full from-earliest scan.
|
|
147
|
+
if (!c)
|
|
148
|
+
throw new Error(`invalid_cursor: no admin_audit_log row with id=${options.sinceId}`);
|
|
149
|
+
cursorTime = c.created_at;
|
|
150
|
+
cursorId = c.id;
|
|
151
|
+
}
|
|
152
|
+
if (cursorTime && cursorId) {
|
|
153
|
+
where.push('(created_at > ? OR (created_at = ? AND id > ?))');
|
|
154
|
+
params.push(cursorTime, cursorTime, cursorId);
|
|
155
|
+
}
|
|
156
|
+
else if (cursorTime) {
|
|
157
|
+
where.push('created_at > ?');
|
|
158
|
+
params.push(cursorTime);
|
|
159
|
+
}
|
|
160
|
+
params.push(limit);
|
|
161
|
+
const candidates = db.prepare(`SELECT id, action, admin_id, created_at FROM admin_audit_log WHERE ${where.join(' AND ')} ORDER BY created_at ASC, id ASC LIMIT ?`).all(...params);
|
|
162
|
+
const rows = [];
|
|
163
|
+
let ingested = 0, wouldIngest = 0, alreadyPresent = 0, skipped = 0;
|
|
164
|
+
for (const cand of candidates) {
|
|
165
|
+
const r = ingestAdminCoordinationFact(db, { auditId: cand.id, visibility: options.visibility, dryRun: !commit });
|
|
166
|
+
const base = { auditId: cand.id, action: cand.action, adminId: cand.admin_id, occurredAt: cand.created_at };
|
|
167
|
+
if (!r.ok) {
|
|
168
|
+
skipped++;
|
|
169
|
+
rows.push({ ...base, outcome: 'skipped', reason: r.reason + (r.detail ? `: ${r.detail}` : '') });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (r.status === 'ingested')
|
|
173
|
+
ingested++;
|
|
174
|
+
else if (r.status === 'would_ingest')
|
|
175
|
+
wouldIngest++;
|
|
176
|
+
else
|
|
177
|
+
alreadyPresent++;
|
|
178
|
+
rows.push({ ...base, outcome: r.status, contributorAccountId: r.contributorAccountId, via: r.via, factId: r.factId });
|
|
179
|
+
}
|
|
180
|
+
return { committed: commit, scanned: candidates.length, ingested, wouldIngest, alreadyPresent, skipped, limit, rows };
|
|
181
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/** As-of resolution of which contributor a non-root admin SEAT was attributed to at `asOf`. */
|
|
3
|
+
export function resolveOperatorClaimAsOf(db, adminAccountId, asOf) {
|
|
4
|
+
const events = db.prepare(`SELECT event_type, contributor_account_id, approval_kind, approved_by, conflict_disclosure, event_id, effective_from, supersedes_event_id, created_at
|
|
5
|
+
FROM admin_operator_claim_events WHERE admin_account_id = ? ORDER BY effective_from ASC, created_at ASC, rowid ASC`).all(adminAccountId);
|
|
6
|
+
// An approval is terminated as-of `asOf` iff a revoked/superseded event that is EFFECTIVE by then
|
|
7
|
+
// LINKS to it (supersedes_event_id). Link-based (not a timestamp window) so a same-second
|
|
8
|
+
// approve→revoke and a same-instant rotation both resolve deterministically.
|
|
9
|
+
const terminatedIds = new Set(events
|
|
10
|
+
.filter(e => (e.event_type === 'revoked' || e.event_type === 'superseded') && e.effective_from && e.effective_from <= asOf && e.supersedes_event_id)
|
|
11
|
+
.map(e => e.supersedes_event_id));
|
|
12
|
+
const active = events.filter(e => e.event_type === 'approved' && e.effective_from && e.effective_from <= asOf && !terminatedIds.has(e.event_id));
|
|
13
|
+
if (active.length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
const latest = active[active.length - 1]; // latest effective approval still active as-of asOf
|
|
16
|
+
// Overlay the LATEST append-only marking correction (if any). The correction never changes the
|
|
17
|
+
// contributor or the effective interval — only the disclosure marking — so it applies regardless of
|
|
18
|
+
// asOf (the approval was always self/related; we are only recording that honestly now).
|
|
19
|
+
const correction = db.prepare(`SELECT approval_kind, conflict_disclosure FROM admin_operator_claim_marking_corrections
|
|
20
|
+
WHERE approved_event_id = ? ORDER BY corrected_at DESC, rowid DESC LIMIT 1`).get(latest.event_id);
|
|
21
|
+
const approvalKind = (correction?.approval_kind ?? latest.approval_kind) ?? null;
|
|
22
|
+
const conflictDisclosure = correction?.conflict_disclosure ?? latest.conflict_disclosure;
|
|
23
|
+
const approvedBy = latest.approved_by ?? null;
|
|
24
|
+
// self/related = the approver was itself a party to the claim (admin seat or contributor) — a
|
|
25
|
+
// root/founder bootstrap self-approval. Such an approval MUST disclose self_or_related + a
|
|
26
|
+
// non-independent_governance kind; otherwise it is dishonestly marked (gated downstream).
|
|
27
|
+
const selfRelated = !!approvedBy && (approvedBy === adminAccountId || approvedBy === latest.contributor_account_id);
|
|
28
|
+
const honestlyDisclosed = !selfRelated || (conflictDisclosure === 'self_or_related' && approvalKind !== 'independent_governance');
|
|
29
|
+
return {
|
|
30
|
+
contributor_account_id: latest.contributor_account_id,
|
|
31
|
+
approval_kind: approvalKind,
|
|
32
|
+
conflict_disclosure: conflictDisclosure,
|
|
33
|
+
claim_event_id: latest.event_id,
|
|
34
|
+
approved_by: approvedBy,
|
|
35
|
+
self_related: selfRelated,
|
|
36
|
+
corrected: !!correction,
|
|
37
|
+
honestly_disclosed: honestlyDisclosed,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* As-of resolution of a SPECIFIC agent mandate (agent_ref + mandate_id) effective at `asOf` (→ owner
|
|
42
|
+
* contributor). Keyed on BOTH agent_ref AND mandate_id: when one agent_ref has several mandates, the
|
|
43
|
+
* audit row's mandate_id decides attribution — never "whichever mandate is latest" (which would
|
|
44
|
+
* mis-credit). A mandate_id belonging to a different agent_ref does not match.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveAgentMandateAsOf(db, agentRef, mandateId, asOf) {
|
|
47
|
+
if (!agentRef || !mandateId)
|
|
48
|
+
return null;
|
|
49
|
+
const events = db.prepare(`SELECT event_type, mandate_id, owner_contributor_account_id, allowed_actions, effective_from, expires_at, revoked_at, event_id, created_at
|
|
50
|
+
FROM agent_execution_mandate_events WHERE agent_ref = ? AND mandate_id = ? ORDER BY effective_from ASC, created_at ASC`).all(agentRef, mandateId);
|
|
51
|
+
const grants = events.filter(e => e.event_type === 'granted' &&
|
|
52
|
+
e.effective_from && e.effective_from <= asOf &&
|
|
53
|
+
(!e.expires_at || e.expires_at >= asOf) &&
|
|
54
|
+
(!e.revoked_at || e.revoked_at > asOf));
|
|
55
|
+
if (grants.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
const latest = grants[grants.length - 1];
|
|
58
|
+
const terminated = events.some(e => (e.event_type === 'revoked' || e.event_type === 'superseded') &&
|
|
59
|
+
e.effective_from && e.effective_from > latest.effective_from && e.effective_from <= asOf);
|
|
60
|
+
if (terminated)
|
|
61
|
+
return null;
|
|
62
|
+
let allowed = [];
|
|
63
|
+
try {
|
|
64
|
+
const p = JSON.parse(latest.allowed_actions);
|
|
65
|
+
if (Array.isArray(p))
|
|
66
|
+
allowed = p.map(String);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
allowed = [];
|
|
70
|
+
}
|
|
71
|
+
return { owner_contributor_account_id: latest.owner_contributor_account_id, mandate_id: latest.mandate_id, allowed_actions: allowed, grant_event_id: latest.event_id };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Unified resolver. `github:<id>` → current identity binding; `admin:<id>` / `agent:<ref>` → as-of
|
|
75
|
+
* claim/mandate at `occurredAt`. Returns null when no valid attribution exists (the fact then has no
|
|
76
|
+
* resolvable contributor — it stays evidence only).
|
|
77
|
+
*/
|
|
78
|
+
export function resolveCoordinationContributor(db, executorRef, occurredAt) {
|
|
79
|
+
if (executorRef.startsWith('github:')) {
|
|
80
|
+
const githubActorId = executorRef.slice('github:'.length);
|
|
81
|
+
if (!githubActorId)
|
|
82
|
+
return null;
|
|
83
|
+
const row = db.prepare('SELECT account_id FROM identity_bindings_active WHERE github_actor_id = ?').get(githubActorId);
|
|
84
|
+
if (!row)
|
|
85
|
+
return null;
|
|
86
|
+
return { contributor_account_id: row.account_id, via: 'github_binding', detail: {} };
|
|
87
|
+
}
|
|
88
|
+
if (executorRef.startsWith('admin:')) {
|
|
89
|
+
const adminAccountId = executorRef.slice('admin:'.length);
|
|
90
|
+
if (!adminAccountId)
|
|
91
|
+
return null;
|
|
92
|
+
const r = resolveOperatorClaimAsOf(db, adminAccountId, occurredAt);
|
|
93
|
+
if (!r)
|
|
94
|
+
return null;
|
|
95
|
+
return { contributor_account_id: r.contributor_account_id, via: 'operator_claim', detail: { approval_kind: r.approval_kind, conflict_disclosure: r.conflict_disclosure, claim_event_id: r.claim_event_id } };
|
|
96
|
+
}
|
|
97
|
+
if (executorRef.startsWith('agent:')) {
|
|
98
|
+
// executor_ref for agents is `agent:<agent_ref>#<mandate_id>` — the mandate is encoded so read-time
|
|
99
|
+
// resolution is as deterministic as ingest-time (no "latest mandate wins" ambiguity).
|
|
100
|
+
const rest = executorRef.slice('agent:'.length);
|
|
101
|
+
const hash = rest.indexOf('#');
|
|
102
|
+
if (hash < 0)
|
|
103
|
+
return null; // no mandate encoded → not resolvable (refuse rather than guess)
|
|
104
|
+
const agentRef = rest.slice(0, hash);
|
|
105
|
+
const mandateId = rest.slice(hash + 1);
|
|
106
|
+
if (!agentRef || !mandateId)
|
|
107
|
+
return null;
|
|
108
|
+
const r = resolveAgentMandateAsOf(db, agentRef, mandateId, occurredAt);
|
|
109
|
+
if (!r)
|
|
110
|
+
return null;
|
|
111
|
+
return { contributor_account_id: r.owner_contributor_account_id, via: 'agent_mandate', detail: { mandate_id: r.mandate_id } };
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|