@whyshu/argent-engine 0.0.0-stage → 0.6.0
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/MANIFEST.json +307 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +575 -0
- package/engine/scripts/billing-mode-daily.sh +12 -0
- package/engine/scripts/build-engine-package.mjs +123 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +377 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +134 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +46 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +260 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +720 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +86 -0
- package/engine/scripts/flow-timers.sh +182 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1024 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2275 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +194 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent_datadir.py +262 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +293 -0
- package/engine/scripts/poll-invoice.sh +74 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +70 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/run-tests.sh +256 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +136 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +73 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +34 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +216 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
- package/engine/scripts/ziniao-windows.sh +153 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +706 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +21 -4
- package/README.md +0 -3
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* argent-mcp(0.3 P2 第一步)—— **最小可用 MCP server(stdio 传输)**
|
|
4
|
+
*
|
|
5
|
+
* 目的:把 Argent 的能力以 MCP 契约暴露给任意宿主(dsh / 自建 client / 未来 agent),
|
|
6
|
+
* 宿主不再依赖具体脚本名 —— 这是"抽离基座"的对外接口层。
|
|
7
|
+
*
|
|
8
|
+
* 实现取舍:**不引第三方依赖**(MCP stdio 传输 = 每行一个 JSON-RPC 2.0 消息),
|
|
9
|
+
* 因此可在客户机内网/离线环境直接跑;协议子集:initialize / notifications/initialized /
|
|
10
|
+
* tools/list / tools/call / ping。
|
|
11
|
+
*
|
|
12
|
+
* 暴露面(2026-10-03 P1 第二批更新):只读 tools 为主(version / license / ledger / sop / doctor /
|
|
13
|
+
* preflight / report / failures / writeback_audit)+ **写类 tools 全部带门禁**:
|
|
14
|
+
* · `argent_claim_shadow` / `argent_writeback_shadow`:隔离沙箱里跑**同一脚本 + --dry-run**(真台账零改动);
|
|
15
|
+
* · `argent_claim`(confirm=true)/ `argent_writeback`(confirm=true):真写,**先过影子比对 + 许可闸门**
|
|
16
|
+
* (`core/lib/license-guard.mjs` ⇒ 与 CLI 同一份判据/证据;默认 observe 只留证据不拦)。
|
|
17
|
+
*
|
|
18
|
+
* 安全注解(2026-10-06 加固):`tools/list` 每个 tool 带 `annotations`(只读 `readOnlyHint:true` /
|
|
19
|
+
* 写类 `readOnlyHint:false + destructiveHint:true`),让宿主能在**协议层**分辨只读与写类、
|
|
20
|
+
* 从而收窄授权面;分类与理由见下方 {@link annotationsFor} 上方的注释块。
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'node:child_process'
|
|
23
|
+
import { cpSync, existsSync as fExists, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs'
|
|
24
|
+
import { tmpdir } from 'node:os'
|
|
25
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
26
|
+
import { dirname, join, resolve } from 'node:path'
|
|
27
|
+
import { fileURLToPath } from 'node:url'
|
|
28
|
+
import { resolveSop } from '../lib/sop.mjs'
|
|
29
|
+
import { queryLedger, resolveLedgerFlow } from '../lib/ledger.mjs'
|
|
30
|
+
import { pickPython } from '../lib/python.mjs'
|
|
31
|
+
import { resolveDataDir } from '../lib/argent-datadir.mjs'
|
|
32
|
+
import { loadRegistry, resolveFlow } from '../lib/flow-registry.mjs'
|
|
33
|
+
// 许可闸门(写类 tool 的真写路径;**同一个模块**给 CLI 用 ⇒ 判据与证据只有一份)
|
|
34
|
+
import { gateWrite } from '../lib/license-guard.mjs'
|
|
35
|
+
// `license status` 的 verb 适配层(判定点唯一在 CLI 进程内;MCP 只做搬运,不自己算三态)
|
|
36
|
+
import { runLicenseVerb } from '../lib/license.mjs'
|
|
37
|
+
|
|
38
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
39
|
+
const REPO = resolve(HERE, '..', '..')
|
|
40
|
+
// 数据目录:内核三级链(唯一判据)—— MCP 与 CLI/插件必须同口径,否则影子跑读错了"生产"目录
|
|
41
|
+
const DATA_DIR = resolveDataDir({ env: process.env }).dir
|
|
42
|
+
// 解释器(D3,2026-10-03):**与 CLI 同源**的多候选探测(`core/lib/python.mjs`)。
|
|
43
|
+
// 此前这里是单候选(`ARGENT_PY || <数据目录>/venv/bin/python3`)——候选不存在时 exec 失败,
|
|
44
|
+
// 各 tool 的 catch 把它当成"没结果"⇒ 宿主看到的是"这台机器上没有流程"。现在:
|
|
45
|
+
// · 多候选(ARGENT_PY → venv → PATH),全不可用 ⇒ tool 返回明确的诊断(不再只有 ENOENT);
|
|
46
|
+
// · 探测在模块加载时做一次并记忆(MCP server 是常驻进程,不像 CLI 需要"无解释器也能 version")。
|
|
47
|
+
const PY_PICK = pickPython({ env: process.env, dataDir: DATA_DIR, repo: REPO })
|
|
48
|
+
const PY = PY_PICK.path
|
|
49
|
+
const PY_FAILURE = PY_PICK.ok ? null : PY_PICK.diagnostics
|
|
50
|
+
const SERVER_INFO = { name: 'argent', version: (readFileSync(join(REPO, 'VERSION'), 'utf8') || '').trim() || '0.0.0' }
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 需要 Python 的 tool 的**唯一出口**(对齐 CLI 的 `runPy`,D3 收口 2026-10-03)。
|
|
54
|
+
*
|
|
55
|
+
* 此前 python 调用散在 7 处 inline `execFileSync(PY, …)`:解释器不可用时 exec 一个空路径 ⇒ 各
|
|
56
|
+
* `catch` 把 ENOENT 当成"没结果"⇒ 宿主看到的是"这台机器上没有流程/没有失败",**假绿**。
|
|
57
|
+
* 现在统一成一条路径:**解释器不可用 ⇒ `ok:false, code:2, reason:'python_unavailable'` + 诊断**
|
|
58
|
+
* (与 `core/cli/argent.mjs:79`、`core/lib/sop.mjs:40` 逐字同口径,宿主只需认一个 reason)。
|
|
59
|
+
*
|
|
60
|
+
* **成功路径行为不变**:成功 = `{ok:true, code:0, output:stdout}`(cwd/编码与 inline 版一致)。
|
|
61
|
+
* 失败 = 保留 stdout + stderr(inline 版只看 stdout)—— 这是**加强**,不是行为变更:
|
|
62
|
+
* 所有消费点原先就在 `catch` 里读 `e.stdout`,现在读到的只会更多(stderr 是守卫拒绝原因所在)。
|
|
63
|
+
*
|
|
64
|
+
* @param {string[]} argv 脚本路径 + 参数(调用方拼)
|
|
65
|
+
* @param {{env?: object}} [opts] 额外环境(影子跑要覆盖 `ARGENT_DATA_DIR`)
|
|
66
|
+
* @returns {{ok:boolean, code:number, output:string, error:string, reason:string, argv:string[]}}
|
|
67
|
+
*/
|
|
68
|
+
function runPy(argv, { env = null } = {}) {
|
|
69
|
+
if (PY_FAILURE) return { ok: false, code: 2, output: '', reason: 'python_unavailable', error: PY_FAILURE, argv }
|
|
70
|
+
try {
|
|
71
|
+
const out = execFileSync(PY, argv, { encoding: 'utf8', cwd: REPO, ...(env ? { env } : {}) })
|
|
72
|
+
return { ok: true, code: 0, output: out, reason: '', error: '', argv }
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
code: e.status ?? 1,
|
|
77
|
+
output: String(e.stdout || ''),
|
|
78
|
+
reason: '',
|
|
79
|
+
error: String(e.stderr || e.message || ''),
|
|
80
|
+
argv,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `--json` 输出的解析(失败不抛:返回 `{ok:false, error}`,由调用方按既有失败形状回)。 */
|
|
86
|
+
function parseJsonOut(text) {
|
|
87
|
+
try { return { ok: true, value: JSON.parse(text) } } catch (e) { return { ok: false, error: `JSON 解析失败:${e.message}` } }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 建一个与真实环境同构的影子沙箱(排除重目录),返回路径。 */
|
|
91
|
+
function makeShadow() {
|
|
92
|
+
const dataDir = DATA_DIR
|
|
93
|
+
const shadow = mkdtempSync(join(tmpdir(), 'argent-shadow-'))
|
|
94
|
+
const SKIP = new Set(['logs', 'backup', 'packs-cache', 'kanban-backup', 'invoice-writeback'])
|
|
95
|
+
for (const entry of readdirSync(dataDir, { withFileTypes: true })) {
|
|
96
|
+
if (SKIP.has(entry.name)) continue
|
|
97
|
+
cpSync(join(dataDir, entry.name), join(shadow, entry.name), { recursive: true })
|
|
98
|
+
}
|
|
99
|
+
return { shadow, dataDir }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 在影子沙箱里跑一条流程,返回 {code, out, events(影子内新增的台账事件)}。 */
|
|
103
|
+
function shadowRun(flow, shadow, extraArgs = []) {
|
|
104
|
+
const before = countEvents(shadow)
|
|
105
|
+
// ⚠️ 必须同时设 **ARGENT_DATA_HOME**:测试夹具(daily-report-flow.test.mjs)就是两者都设,
|
|
106
|
+
// 只设 DATA_DIR 时清单路径按 HOME 解析 ⇒ 读到真实空清单、夹具被忽略(实测 0 票据)。
|
|
107
|
+
const r = runPy([join(REPO, 'scripts', 'flow_triage.py'), '--flow', String(flow), '--json', ...extraArgs],
|
|
108
|
+
{ env: { ...process.env, ARGENT_DATA_DIR: shadow, ARGENT_DATA_HOME: shadow } })
|
|
109
|
+
return { code: r.code, out: r.output, events: readEvents(shadow).slice(before) }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function countEvents(dir) {
|
|
113
|
+
const f = join(dir, 'flow-ledger.jsonl')
|
|
114
|
+
return fExists(f) ? readFileSync(f, 'utf8').trim().split('\n').filter(Boolean).length : 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readEvents(dir) {
|
|
118
|
+
const f = join(dir, 'flow-ledger.jsonl')
|
|
119
|
+
if (!fExists(f)) return []
|
|
120
|
+
return readFileSync(f, 'utf8').trim().split('\n').filter(Boolean)
|
|
121
|
+
.map((l) => { try { return JSON.parse(l) } catch { return { raw: l } } })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 归一化:剔除"必然不同"的易变字段,只留可比对的语义字段。 */
|
|
125
|
+
const VOLATILE = new Set(['ts', 'createdAt', 'updatedAt', 'leaseUntil', 'sessionId', 'at', 'instanceId', 'id'])
|
|
126
|
+
function normalize(ev) {
|
|
127
|
+
const out = {}
|
|
128
|
+
for (const k of Object.keys(ev || {}).sort()) {
|
|
129
|
+
if (VOLATILE.has(k)) continue
|
|
130
|
+
out[k] = ev[k]
|
|
131
|
+
}
|
|
132
|
+
return out
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 夹具注入(2026-09-29):让影子跑**真正产生台账事件**,否则"两边都 0 事件"毫无比对价值。
|
|
137
|
+
* schema 取自 `scripts/flow_sources/common.py#read_cases_batch`(接受 `{"cases":[{ticketKey,fields}]}`),
|
|
138
|
+
* 语义对齐 `scripts/tests/daily-report-flow.test.mjs`:写 `flow-inbox/<flow>.json` + 跑 `--no-regenerate`
|
|
139
|
+
* (夹具语义:不重算清单,否则生成器会覆盖夹具)。
|
|
140
|
+
*/
|
|
141
|
+
function injectFixture(flow, shadow, ticketKey = 'MLM-2026-09-29-1234567890') {
|
|
142
|
+
// ⚠️ 夹具必须写成**生成器的形状**(含 generatedAt/reportTime/skipped):实测只写 {cases:[…]} 时,
|
|
143
|
+
// 新鲜度判据看不出"何时生成" ⇒ 视为过期 ⇒ 生成器重新生成并**覆盖夹具**(沙箱里最终是 cases: [])。
|
|
144
|
+
const dir = join(shadow, 'flow-inbox')
|
|
145
|
+
mkdirSync(dir, { recursive: true })
|
|
146
|
+
const body = {
|
|
147
|
+
flow,
|
|
148
|
+
generatedAt: new Date().toISOString(),
|
|
149
|
+
reportTime: '00:05',
|
|
150
|
+
// 票号必须合流程的形态(真实票号如 MLM-2026-09-28-2475636269);字段名按 lip 表口径给足常见列
|
|
151
|
+
cases: [{ ticketKey, fields: { 店铺: '测试店铺', 站点: 'MLM', 店铺ID: '2216458475', 日期: '2026-09-29', 推送状态: '未推送' } }],
|
|
152
|
+
skipped: [],
|
|
153
|
+
}
|
|
154
|
+
writeFileSync(join(dir, `${flow}.json`), JSON.stringify(body, null, 2))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 认领步骤的"工作量"信号(2026-09-29 修正):
|
|
159
|
+
* `flow_triage` **不写台账**(台账事件由后续派发/角色会话写)⇒ 用台账 delta 当工作量信号会永远 0
|
|
160
|
+
* ⇒ 假绿。正确信号是**分诊输出里的票据集合**(--json 的 tickets,或 stdout 的 [to_dispatch] 行)。
|
|
161
|
+
*/
|
|
162
|
+
function parseTickets(out) {
|
|
163
|
+
const t = String(out || '')
|
|
164
|
+
try {
|
|
165
|
+
const j = JSON.parse(t)
|
|
166
|
+
// 实测形状:{ plan: { to_dispatch: [{caseId, ticket, …}], retry: [], needs_human: [], … }, stats: {...} }
|
|
167
|
+
// ⇒ 必须下钻 plan 的各桶取 ticket(此前只看顶层 tickets/planned/cases ⇒ 明明有票却判 0 = 假阴)
|
|
168
|
+
const buckets = []
|
|
169
|
+
const push = (v) => { if (Array.isArray(v)) buckets.push(...v) }
|
|
170
|
+
push(j.tickets); push(j.planned); push(j.cases)
|
|
171
|
+
if (j.plan && typeof j.plan === 'object') for (const v of Object.values(j.plan)) push(v)
|
|
172
|
+
if (buckets.length) return [...new Set(buckets.map((x) => String((x && (x.ticket || x.ticketKey || x)) || '')).filter(Boolean))].sort()
|
|
173
|
+
} catch { /* 非 JSON,走 stdout 解析 */ }
|
|
174
|
+
const m = t.match(/\[to_dispatch\]\s+(\S+)/g) || []
|
|
175
|
+
return [...new Set(m.map((x) => x.replace(/\[to_dispatch\]\s+/, '')))].sort()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 能力 id 解析(MCP 侧;与 CLI 的 `gateContext` / flow-trigger.sh 的 `flow_config.py --get capability`
|
|
180
|
+
* 同口径):注册表 `flows.<canonical>.capability` 优先;读不到 ⇒ 退化为流程名(**宁可不拦,不可误停**)。
|
|
181
|
+
* 只用于闸门的 `--cap` 文案/能力门,**不复制**任何判定。
|
|
182
|
+
*/
|
|
183
|
+
function capOfFlow(flow) {
|
|
184
|
+
if (PY_FAILURE || !PY) return flow
|
|
185
|
+
try {
|
|
186
|
+
const reg = loadRegistry({ repo: REPO, py: PY })
|
|
187
|
+
if (!reg.ok) return flow
|
|
188
|
+
const r = resolveFlow({ input: flow, doc: reg.doc })
|
|
189
|
+
const f = (reg.doc.flows || {})[r.canonical] || {}
|
|
190
|
+
return String(f.capability || flow)
|
|
191
|
+
} catch { return flow }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* `flow_claim.py --json` 的 `claimed` 数组(**真认领**才有的字段)。
|
|
196
|
+
* 与 `parseTickets` 分开:后者解析的是**分诊计划**里的票(只读路径也会用),两者语义不同 ——
|
|
197
|
+
* 混用正是"影子调 triage、真写也调 triage"那个 bug 的温床(真写要的是 claimed,不是 plan)。
|
|
198
|
+
*/
|
|
199
|
+
function parseClaimed(out) {
|
|
200
|
+
try {
|
|
201
|
+
const j = JSON.parse(String(out || ''))
|
|
202
|
+
return Array.isArray(j.claimed) ? j.claimed.map(String).filter(Boolean) : []
|
|
203
|
+
} catch { return [] }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 自证信息(2026-09-29):夹具是否存活、分诊到底看到了什么 —— 不再靠外部猜沙箱。 */
|
|
207
|
+
function inboxInfo(shadow, flow) {
|
|
208
|
+
const f = join(shadow, 'flow-inbox', `${flow}.json`)
|
|
209
|
+
if (!fExists(f)) return { exists: false }
|
|
210
|
+
try {
|
|
211
|
+
const j = JSON.parse(readFileSync(f, 'utf8'))
|
|
212
|
+
const cs = j.cases || []
|
|
213
|
+
return { exists: true, cases: cs.length, firstKey: cs[0] ? String(cs[0].ticketKey || '') : '', generatedAt: j.generatedAt || '', skipped: (j.skipped || []).length }
|
|
214
|
+
} catch (e) { return { exists: true, parseError: String(e.message).slice(0, 120) } }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 影子比对(抽成函数,供 argent_shadow_compare 与**真写门禁**共用)。 */
|
|
218
|
+
function compareShadow(flow, fixture = false) {
|
|
219
|
+
const a = makeShadow(); const b = makeShadow()
|
|
220
|
+
const extra = fixture ? ['--no-regenerate'] : []
|
|
221
|
+
if (fixture) { injectFixture(flow, a.shadow); injectFixture(flow, b.shadow) }
|
|
222
|
+
const ra = shadowRun(flow, a.shadow, extra); const rb = shadowRun(flow, b.shadow, extra)
|
|
223
|
+
const na = ra.events.map(normalize); const nb = rb.events.map(normalize)
|
|
224
|
+
const ta = parseTickets(ra.out); const tb = parseTickets(rb.out)
|
|
225
|
+
const ticketsEqual = ta.length === tb.length && ta.every((x, k) => x === tb[k])
|
|
226
|
+
const diffs = []
|
|
227
|
+
for (let k = 0; k < Math.max(na.length, nb.length); k += 1) {
|
|
228
|
+
if (JSON.stringify(na[k] ?? null) !== JSON.stringify(nb[k] ?? null)) diffs.push({ index: k, runA: na[k] ?? null, runB: nb[k] ?? null })
|
|
229
|
+
}
|
|
230
|
+
const verified = ta.length > 0 && ticketsEqual && diffs.length === 0 && ra.code === 0 && rb.code === 0
|
|
231
|
+
return { a, b, ra, rb, na, nb, ta, tb, ticketsEqual, diffs, verified }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 作用域表(**运行时读** `core/contracts/verbs.schema.json#scope`;P1 第二批收口,缺陷同 `http.mjs`):
|
|
236
|
+
* local=只有本机有意义(写台账/回写生产表);account=账号级只读或门禁写。
|
|
237
|
+
*
|
|
238
|
+
* 此前这里是**硬编码副本**(`TOOL_SCOPE` 常量):契约一改,云端的本地/账号门禁就悄悄判错 ——
|
|
239
|
+
* 正是"第二份判据"的典型形态(`core/mcp/http.mjs:28-43` 已先改成运行时读,本次把 stdio 这半边对齐)。
|
|
240
|
+
* 读不到 ⇒ **不静默**:`toolScope()` 退化为 `account` 并往 stderr 打印一行(云端门禁的失效必须可见)。
|
|
241
|
+
*/
|
|
242
|
+
export const SCOPE = (() => {
|
|
243
|
+
try {
|
|
244
|
+
const doc = JSON.parse(readFileSync(new URL('../contracts/verbs.schema.json', import.meta.url), 'utf8'))
|
|
245
|
+
const sc = doc.scope || {}
|
|
246
|
+
const m = {}
|
|
247
|
+
for (const t of sc.local || []) m[t] = 'local'
|
|
248
|
+
for (const t of sc.account || []) m[t] = 'account'
|
|
249
|
+
if (!Object.keys(m).length) throw new Error('scope 表为空')
|
|
250
|
+
return m
|
|
251
|
+
} catch (e) {
|
|
252
|
+
console.error(`[argent-mcp] ⚠️ 作用域表加载失败(门禁退化为"全部放行"):${String(e.message)}`)
|
|
253
|
+
return {}
|
|
254
|
+
}
|
|
255
|
+
})()
|
|
256
|
+
export const TOOL_SCOPE = SCOPE // 兼容旧读法(`server.mjs` 的导出曾叫这个名字)
|
|
257
|
+
export function toolScope(name) {
|
|
258
|
+
const bare = String(name || '').replace(/^argent_/, '')
|
|
259
|
+
return SCOPE[bare] || 'account'
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 工具注解(MCP 规范 `ToolAnnotations`)—— **纯元数据,零行为改动**(2026-10-06 安全加固)。
|
|
264
|
+
*
|
|
265
|
+
* ## 为什么加(实测根因,见 `docs/H1-第三方宿主采信-2026-10-06.md` §六)
|
|
266
|
+
* 宿主(实测 **OpenCode 1.18.14**)默认 `permission * allow` ⇒ 它在**协议层**拿不到"这个是只读、
|
|
267
|
+
* 那个会写"的信号 ⇒ 会在**不问人**的情况下调 `argent_claim` / `argent_writeback`。内核侧虽有
|
|
268
|
+
* **双重门禁**兜底(写类默认 `confirm=false` 只回影子;真写要显式 `confirm:true` **且**过
|
|
269
|
+
* `gateWrite` 许可闸),但那是**最后一道**;注解是**第一道** —— 宿主据此就能**收窄授权面**
|
|
270
|
+
* (只放行只读子集 / 对写类要求人工确认),不必等到工具被调起来才拦。
|
|
271
|
+
*
|
|
272
|
+
* ## 分类(逐工具;白名单断言见 `scripts/tests/mcp-tool-annotations.test.mjs`)
|
|
273
|
+
* · **只读 9**:`version` / `license` / `ledger` / `report` / `failures` / `sop` / `doctor` /
|
|
274
|
+
* `preflight` / `writeback_audit` ⇒ `{ readOnlyHint: true }`
|
|
275
|
+
* (规范:`destructiveHint` **仅在** `readOnlyHint:false` 时有意义 ⇒ 只读类不给它)
|
|
276
|
+
* · **写类 5**:`claim` / `writeback` / `shadow_compare` / `writeback_shadow` / `claim_shadow`
|
|
277
|
+
* ⇒ `{ readOnlyHint: false, destructiveHint: true }`
|
|
278
|
+
* —— ⚠️ **`*_shadow` 三个语义介于两者之间**(它们在**隔离沙箱**里跑:真台账/生产表零改动),
|
|
279
|
+
* 仍按**保守缺省**标注,理由三条:① 它们**确实不是只读**(`cpSync` 整个数据目录、建临时目录、
|
|
280
|
+
* 起子进程);② `destructiveHint` 的**规范缺省值本来就是 `true`** ⇒ 标 `true` 是"不谎报更安全";
|
|
281
|
+
* ③ 宿主据此多问一句"要不要执行",正是我们想要的默认姿态(⛔ 不为了让宿主更宽松而低估风险)。
|
|
282
|
+
* · **未登记的名字 ⇒ 一律按写类回**(fail-safe:宁可多要一次确认,不可少要)。
|
|
283
|
+
*
|
|
284
|
+
* ⛔ 纪律:本段只被 `tools/list` 读;**不改**任何 tool 的 handler / `inputSchema` / 返回形状,
|
|
285
|
+
* 也**不增删**工具(仍 14 个)。
|
|
286
|
+
*/
|
|
287
|
+
export const READ_ONLY_TOOLS = Object.freeze([
|
|
288
|
+
'argent_version', 'argent_license', 'argent_ledger', 'argent_report', 'argent_failures',
|
|
289
|
+
'argent_sop', 'argent_doctor', 'argent_preflight', 'argent_writeback_audit',
|
|
290
|
+
])
|
|
291
|
+
export const WRITE_TOOLS = Object.freeze([
|
|
292
|
+
'argent_claim', 'argent_writeback', 'argent_shadow_compare', 'argent_writeback_shadow', 'argent_claim_shadow',
|
|
293
|
+
])
|
|
294
|
+
const READ_ONLY_SET = new Set(READ_ONLY_TOOLS)
|
|
295
|
+
const WRITE_SET = new Set(WRITE_TOOLS)
|
|
296
|
+
|
|
297
|
+
/** 某个 tool 的 `annotations`(**唯一出口**;未登记 ⇒ 按写类,fail-safe)。 */
|
|
298
|
+
export function annotationsFor(name) {
|
|
299
|
+
if (READ_ONLY_SET.has(name)) return { readOnlyHint: true }
|
|
300
|
+
if (WRITE_SET.has(name)) return { readOnlyHint: false, destructiveHint: true }
|
|
301
|
+
return { readOnlyHint: false, destructiveHint: true }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const TOOLS = {
|
|
305
|
+
argent_version: {
|
|
306
|
+
description: 'Argent 与宿主 dsh 的版本(诊断用)',
|
|
307
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
308
|
+
handler: () => {
|
|
309
|
+
let dsh = ''
|
|
310
|
+
try { dsh = execFileSync('dsh', ['--version'], { encoding: 'utf8' }).trim() } catch { /* 不在 PATH */ }
|
|
311
|
+
return { argent: SERVER_INFO.version, dsh }
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
/**
|
|
315
|
+
* 许可状态(只读)—— **与 CLI 同源**(§3.5① 纪律 1:判定点唯一在 CLI 进程内,宿主不得自己算权益)。
|
|
316
|
+
* 此前这里回的是"计费/provider 巡检文本"(恒 ok、与许可无关)⇒ §2.2④ 的假绿形态之一。
|
|
317
|
+
* 现在直接调 `core/lib/license.mjs` 的 `status` 子命令(它再透传 `scripts/check-license.mjs`)。
|
|
318
|
+
*/
|
|
319
|
+
argent_license: {
|
|
320
|
+
description: '离线许可状态(三态 valid/grace/denied + 能力/业务线 + 设备绑定);只读,判定与 CLI 同源',
|
|
321
|
+
inputSchema: { type: 'object', properties: { capability: { type: 'string' }, vertical: { type: 'string' } }, additionalProperties: false },
|
|
322
|
+
handler: ({ capability = '', vertical = '' } = {}) => {
|
|
323
|
+
const argv = ['status', '--json']
|
|
324
|
+
if (capability) argv.push('--cap', String(capability))
|
|
325
|
+
if (vertical) argv.push('--vertical', String(vertical))
|
|
326
|
+
const res = runLicenseVerb(argv, { repo: REPO, env: process.env })
|
|
327
|
+
const j = res.json || {}
|
|
328
|
+
return { ok: j.ok === true, state: j.state, reason: j.reason, code: j.code,
|
|
329
|
+
licenseFile: j.licenseFile, expiresAt: j.expiresAt, graceDays: j.graceDays, graceUntil: j.graceUntil,
|
|
330
|
+
inGrace: j.inGrace, graceSource: j.graceSource, capabilities: j.capabilities, verticals: j.verticals,
|
|
331
|
+
// 签名版本 + device 是否进签名(v1 ⇒ false / v2 ∧ 验签通过 ⇒ true):宿主按它决定要不要说"未进签名"
|
|
332
|
+
signatureVersion: j.signatureVersion ?? null, deviceSigned: j.device && j.device.signed === true,
|
|
333
|
+
device: j.device, gateReason: j.gateReason, conclusion: j.conclusion, warnings: j.warnings,
|
|
334
|
+
judgedBy: j.judgedBy }
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
argent_ledger: {
|
|
338
|
+
description: '台账查询(唯一真相;只读)',
|
|
339
|
+
inputSchema: { type: 'object', properties: { flow: { type: 'string' }, tail: { type: 'number' } }, additionalProperties: false },
|
|
340
|
+
// D1(2026-10-03):**与 CLI 同一个实现**(`core/lib/ledger.mjs`)——此前这里是自带的一份,
|
|
341
|
+
// `slice(-tail)` 在 `filter` **之前** ⇒ 给了 --flow 也只在该尾部窗口内筛(假空);且没有生效条件回显。
|
|
342
|
+
handler: ({ flow = '', tail = 5 } = {}) => queryLedger({
|
|
343
|
+
file: join(DATA_DIR, 'flow-ledger.jsonl'),
|
|
344
|
+
flow,
|
|
345
|
+
tailInput: tail,
|
|
346
|
+
resolveFilter: (f) => resolveLedgerFlow(f, { repo: REPO, py: PY, pyFailure: PY_FAILURE }),
|
|
347
|
+
}),
|
|
348
|
+
},
|
|
349
|
+
/**
|
|
350
|
+
* 影子认领(写类 tool 的安全形态,2026-09-29):
|
|
351
|
+
* 用**隔离的 ARGENT_DATA_DIR** 跑真实 CLI 路径 ⇒ 台账/案件只写进影子沙箱,
|
|
352
|
+
* 并回报"真台账确实未改动"。这就是"敢让 MCP 碰生产"的前提:
|
|
353
|
+
* 先影子跑、与 CLI 路径逐字段比对,一致后才允许真写。
|
|
354
|
+
*/
|
|
355
|
+
argent_claim_shadow: {
|
|
356
|
+
description: '影子认领:在隔离沙箱里跑真实分诊/认领,真台账零改动(返回影子新增事件 + 未改动证明)',
|
|
357
|
+
inputSchema: { type: 'object', properties: { flow: { type: 'string' } }, required: ['flow'], additionalProperties: false },
|
|
358
|
+
handler: ({ flow }) => {
|
|
359
|
+
const dataDir = DATA_DIR
|
|
360
|
+
const shadow = mkdtempSync(join(tmpdir(), 'argent-shadow-'))
|
|
361
|
+
mkdirSync(join(shadow, 'verticals', 'mercadolibre'), { recursive: true })
|
|
362
|
+
// 影子沙箱必须是**完整环境**:只拷台账会漏掉流程所需配置(实测:daily-report 的生成器
|
|
363
|
+
// 需要 ml-api-stores.json,缺了就"源再生成失败")。⇒ 拷贝整个数据目录,排除重目录。
|
|
364
|
+
const SKIP = new Set(['logs', 'backup', 'packs-cache', 'kanban-backup', 'invoice-writeback'])
|
|
365
|
+
for (const entry of readdirSync(dataDir, { withFileTypes: true })) {
|
|
366
|
+
if (SKIP.has(entry.name)) continue
|
|
367
|
+
cpSync(join(dataDir, entry.name), join(shadow, entry.name), { recursive: true })
|
|
368
|
+
}
|
|
369
|
+
const count = (dir) => {
|
|
370
|
+
const f = join(dir, 'flow-ledger.jsonl')
|
|
371
|
+
return fExists(f) ? readFileSync(f, 'utf8').trim().split('\n').filter(Boolean).length : 0
|
|
372
|
+
}
|
|
373
|
+
const realBefore = count(dataDir)
|
|
374
|
+
// P1 第二批收口:影子与真写**只差一个 `--dry-run`**(同一个脚本 `flow_claim.py`),
|
|
375
|
+
// 不再"影子调 triage、真写调 claim" —— 那样两者根本不是同一条判据路径,比对结论无意义。
|
|
376
|
+
const r = runPy([join(REPO, 'scripts', 'flow_claim.py'), '--flow', String(flow), '--json', '--dry-run'],
|
|
377
|
+
{ env: { ...process.env, ARGENT_DATA_DIR: shadow, ARGENT_DATA_HOME: shadow } })
|
|
378
|
+
const out = r.output
|
|
379
|
+
const code = r.code
|
|
380
|
+
const realAfter = count(dataDir)
|
|
381
|
+
const shadowRows = (() => {
|
|
382
|
+
const f = join(shadow, 'flow-ledger.jsonl')
|
|
383
|
+
if (!fExists(f)) return []
|
|
384
|
+
return readFileSync(f, 'utf8').trim().split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l) } catch { return { raw: l } } })
|
|
385
|
+
})()
|
|
386
|
+
const copied = fExists(join(dataDir, 'flow-ledger.jsonl')) ? readFileSync(join(dataDir, 'flow-ledger.jsonl'), 'utf8').trim().split('\n').filter(Boolean).length : 0
|
|
387
|
+
return {
|
|
388
|
+
ok: code === 0,
|
|
389
|
+
code,
|
|
390
|
+
realUnchanged: realBefore === realAfter,
|
|
391
|
+
realEventsBefore: realBefore,
|
|
392
|
+
shadowNewEvents: shadowRows.slice(copied),
|
|
393
|
+
stdoutTail: out.slice(-1500),
|
|
394
|
+
shadowDir: shadow,
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
/**
|
|
399
|
+
* 影子比对(放行真写前的最后一道证据,2026-09-29):
|
|
400
|
+
* **同输入两次独立影子跑**(各自隔离沙箱)→ 事件序列逐字段比对(归一化 ts/lease 等易变字段)。
|
|
401
|
+
* 一致 ⇒ 该流程在"隔离环境 + 真实 CLI 路径"下**确定/幂等**,MCP 真写才可以照此路径放行。
|
|
402
|
+
*/
|
|
403
|
+
|
|
404
|
+
argent_shadow_compare: {
|
|
405
|
+
description: '影子比对:同输入两次独立影子跑,事件序列逐字段比对(归一化易变字段),输出一致性报告',
|
|
406
|
+
inputSchema: { type: 'object', properties: { flow: { type: 'string' }, fixture: { type: 'boolean' } }, required: ['flow'], additionalProperties: false },
|
|
407
|
+
handler: ({ flow, fixture = false }) => {
|
|
408
|
+
const { a, b, ra, rb, na, nb, ta, tb, ticketsEqual, diffs, verified } = compareShadow(flow, fixture)
|
|
409
|
+
return {
|
|
410
|
+
ok: verified,
|
|
411
|
+
verified,
|
|
412
|
+
okReason: verified ? 'verified'
|
|
413
|
+
: (ta.length === 0 ? 'no_workload(分诊未产出票据 ⇒ 未验证,需夹具注入)'
|
|
414
|
+
: (!ticketsEqual ? 'tickets_differ(两次影子跑的票据集合不一致 ⇒ 非确定性)' : 'diff_or_error')),
|
|
415
|
+
flow,
|
|
416
|
+
ticketsA: ta.length, ticketsB: tb.length, ticketsEqual,
|
|
417
|
+
sandboxA: a.shadow, sandboxB: b.shadow,
|
|
418
|
+
inboxA: inboxInfo(a.shadow, flow), inboxB: inboxInfo(b.shadow, flow),
|
|
419
|
+
outA: String(ra.out || '').slice(-600), outB: String(rb.out || '').slice(-600),
|
|
420
|
+
eventsA: na.length, eventsB: nb.length, diffs: diffs.slice(0, 5), diffCount: diffs.length,
|
|
421
|
+
realLedgerUnchanged: countEvents(a.dataDir) === countEvents(a.dataDir), // 沙箱外未写入(由 ARGENT_DATA_DIR 保证)
|
|
422
|
+
codes: { a: ra.code, b: rb.code },
|
|
423
|
+
note: !verified
|
|
424
|
+
? (ta.length === 0 ? '⚠️ 未验证:分诊未产出票据(本流程当前无待办)⇒ 需夹具注入才有比对价值'
|
|
425
|
+
: '两次影子跑事件序列不一致 ⇒ 该流程当前不适合走 MCP 真写(先查非确定性来源)')
|
|
426
|
+
: '两次独立影子跑事件序列一致(已归一化 ts/lease 等易变字段)⇒ 可放行 MCP 真写路径',
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
/**
|
|
431
|
+
* 真写:认领(2026-09-29 P2 放行)
|
|
432
|
+
* **门禁**:先跑夹具影子比对,未 verified 直接拒绝 ⇒ 保证"只有确定/幂等的流程才允许真写"。
|
|
433
|
+
* 默认 `confirm=false` ⇒ 只回影子结果(不碰真台账);真写必须显式 `confirm:true`
|
|
434
|
+
* (生产正由调度器运行,手动真写需明确意图)。
|
|
435
|
+
*/
|
|
436
|
+
argent_claim: {
|
|
437
|
+
description: '认领(写):门禁=影子 verified;默认只回影子结果,confirm=true 才真写台账',
|
|
438
|
+
inputSchema: { type: 'object', properties: { flow: { type: 'string' }, confirm: { type: 'boolean' } }, required: ['flow'], additionalProperties: false },
|
|
439
|
+
handler: ({ flow, confirm = false }) => {
|
|
440
|
+
// ① **许可闸门**在最前(§3.6① 第 4 步):授权不通过就没必要跑影子/碰台账 —— 也最省时间。
|
|
441
|
+
// 与 CLI 同一个模块 `core/lib/license-guard.mjs`(⇒ 判据/证据一份,不是第二份实现):
|
|
442
|
+
// observe(默认)⇒ 只留证据不拦;enforce 且不可用 ⇒ 拒绝真写(零副作用:不写台账/不落盘)
|
|
443
|
+
let lic = null
|
|
444
|
+
if (confirm) {
|
|
445
|
+
lic = gateWrite({ repo: REPO, flow: String(flow), cap: capOfFlow(String(flow)), env: process.env })
|
|
446
|
+
if (lic.blocked) {
|
|
447
|
+
return { ok: false, mode: 'real', gated: true, reason: 'license_denied',
|
|
448
|
+
license: { mode: lic.mode, state: lic.licenseState, allowed: lic.allowed, evidence: lic.evidence },
|
|
449
|
+
note: `许可闸门[enforce]:${lic.human || lic.reason} ⇒ 拒绝真写(零副作用)` }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// ② 影子比对门禁:**只许可确定/幂等的流程真写**。
|
|
453
|
+
// ⚠️ 它比的是"输入 → 分诊计划"的确定性(`flow_triage.py`),与下面真写调 `flow_claim.py`
|
|
454
|
+
// 不是同一件事:前者证明流程稳定,后者才是真正落台账的那一步。两者都要,别混为一谈。
|
|
455
|
+
const gate = compareShadow(flow, true)
|
|
456
|
+
if (!gate.verified) {
|
|
457
|
+
return { ok: false, gated: true, reason: `shadow_not_verified:${gate.ta.length === 0 ? 'no_workload' : 'inconsistent'}`, note: '影子未验证 ⇒ 拒绝真写(先修流程确定性或用例)' }
|
|
458
|
+
}
|
|
459
|
+
if (!confirm) {
|
|
460
|
+
return { ok: true, mode: 'shadow_only', verified: true, tickets: gate.ta, note: '门禁通过;未真写(confirm=false)。真写请显式 confirm=true' }
|
|
461
|
+
}
|
|
462
|
+
// P1 第二批收口(**名不副实**):真写路径此前调的是 `flow_triage.py`(**只出计划、不写台账**)⇒
|
|
463
|
+
// 宿主以为"已经认领了",实际台账没有任何 planned 事件(同一票会被重复派发)。
|
|
464
|
+
// 现在与 CLI 一致:走 `scripts/flow_claim.py`(原子认领 + 租约)。
|
|
465
|
+
const r = runPy([join(REPO, 'scripts', 'flow_claim.py'), '--flow', String(flow), '--json'])
|
|
466
|
+
// 真写工具**不得**在解释器不可用时报 ok:true(那正是 D3 的假绿形态)
|
|
467
|
+
if (r.reason === 'python_unavailable') return { ok: false, mode: 'real', reason: 'python_unavailable', error: r.error }
|
|
468
|
+
const out = r.ok ? r.output : (r.output || r.error)
|
|
469
|
+
const claimed = parseClaimed(out)
|
|
470
|
+
return {
|
|
471
|
+
ok: r.ok === true, mode: 'real', verified: true,
|
|
472
|
+
claimed, tickets: claimed.length ? claimed : parseTickets(out),
|
|
473
|
+
// 脚本的 3 = 无票(**不是**许可宽限)⇒ 这里如实回报,不冒充成功也不冒充失败
|
|
474
|
+
scriptStatus: r.code,
|
|
475
|
+
empty: r.ok === true && claimed.length === 0,
|
|
476
|
+
ledgerLines: countEvents(DATA_DIR),
|
|
477
|
+
license: lic ? { mode: lic.mode, state: lic.licenseState, allowed: lic.allowed, decision: lic.decision, evidence: lic.evidence } : null,
|
|
478
|
+
outTail: out.slice(-400),
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
/**
|
|
483
|
+
* 真写:回写(走既有守卫脚本,**默认 dry-run**)。
|
|
484
|
+
* 守卫在脚本里(字段白名单/记录白名单/写前备份/写后读回/审计 JSONL)—— 这里只做透传与"必须有记录白名单"的护栏。
|
|
485
|
+
*/
|
|
486
|
+
argent_writeback: {
|
|
487
|
+
description: '回写(写,默认 dry-run):透传既有守卫回写器;必须给 allowRecord,真写需 confirm=true',
|
|
488
|
+
inputSchema: { type: 'object', properties: { recordId: { type: 'string' }, allowRecord: { type: 'string' }, file: { type: 'string' }, status: { type: 'string' }, confirm: { type: 'boolean' } }, required: ['recordId', 'allowRecord'], additionalProperties: false },
|
|
489
|
+
handler: ({ recordId, allowRecord, file, status, confirm = false }) => {
|
|
490
|
+
if (!recordId || !allowRecord) return { ok: false, reason: 'missing_record_or_allow' }
|
|
491
|
+
// 许可闸门:只在**真写**(confirm=true)时查;dry-run 不查(不打扰只读路径、也不污染证据)
|
|
492
|
+
const lic = confirm ? gateWrite({ repo: REPO, flow: 'wenyao-invoice', cap: 'invoice', env: process.env }) : null
|
|
493
|
+
if (lic && lic.blocked) {
|
|
494
|
+
return { ok: false, dryRun: false, gated: true, reason: 'license_denied',
|
|
495
|
+
license: { mode: lic.mode, state: lic.licenseState, allowed: lic.allowed, evidence: lic.evidence },
|
|
496
|
+
note: `许可闸门[enforce]:${lic.human || lic.reason} ⇒ 拒绝真写(零副作用)` }
|
|
497
|
+
}
|
|
498
|
+
const argv = [join(REPO, 'scripts', 'wenyao-invoice-writeback.mjs'), '--record-id', String(recordId), '--allow-record', String(allowRecord)]
|
|
499
|
+
if (file) argv.push('--file', String(file))
|
|
500
|
+
if (status) argv.push('--status', String(status))
|
|
501
|
+
if (confirm) argv.push('--yes')
|
|
502
|
+
const licEcho = lic ? { mode: lic.mode, state: lic.licenseState, allowed: lic.allowed, decision: lic.decision, evidence: lic.evidence } : null
|
|
503
|
+
try {
|
|
504
|
+
const out = execFileSync(process.execPath, argv, { encoding: 'utf8', cwd: REPO })
|
|
505
|
+
return { ok: true, dryRun: !confirm, outTail: out.slice(-800), ...(licEcho ? { license: licEcho } : {}) }
|
|
506
|
+
} catch (e) {
|
|
507
|
+
return { ok: false, dryRun: !confirm, code: e.status ?? 1, outTail: String(e.stdout || '').slice(-800),
|
|
508
|
+
error: String(e.stderr || e.message).slice(0, 300), ...(licEcho ? { license: licEcho } : {}) }
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
/** SOP 取用(只读,共享实现 core/lib/sop.mjs):提示词 + 技能目录 + 踩坑清单。 */
|
|
513
|
+
argent_sop: {
|
|
514
|
+
description: '取流程 SOP(只读):分诊提示词 + 角色技能目录 + 踩坑清单路径',
|
|
515
|
+
inputSchema: { type: 'object', properties: { flow: { type: 'string' } }, required: ['flow'], additionalProperties: false },
|
|
516
|
+
// pyFailure 必须传(D3):否则"解释器不可用"会被伪装成"注册表坏了",宿主拿不到修复指引。
|
|
517
|
+
handler: ({ flow }) => resolveSop(flow, { repo: REPO, py: PY, pyFailure: PY_FAILURE }),
|
|
518
|
+
},
|
|
519
|
+
/** 环境自检(只读)= 门禁;命名对齐 CLI 的 doctor verb。 */
|
|
520
|
+
argent_doctor: {
|
|
521
|
+
description: '环境自检(只读):跑流程门禁,列出 FAIL/WARN(不写表、不认领)',
|
|
522
|
+
inputSchema: { type: 'object', properties: { flow: { type: 'string' }, online: { type: 'boolean' } }, required: ['flow'], additionalProperties: false },
|
|
523
|
+
handler: ({ flow, online = false }) => {
|
|
524
|
+
const argv = [join(REPO, 'scripts', 'flow_preflight.py'), '--flow', String(flow)]
|
|
525
|
+
if (online) argv.push('--online')
|
|
526
|
+
const r = runPy(argv)
|
|
527
|
+
if (r.reason === 'python_unavailable') return { ok: false, code: 2, reason: 'python_unavailable', output: '', error: r.error }
|
|
528
|
+
return r.ok
|
|
529
|
+
? { ok: true, output: r.output.slice(-4000) }
|
|
530
|
+
: { ok: false, code: r.code, output: r.output.slice(-4000), error: r.error.slice(0, 300) }
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
/** 失败分类(只读):把一段失败文本映射到引擎错误词表(决定 retryable / needs_human / needs_agent)。 */
|
|
534
|
+
argent_report: {
|
|
535
|
+
description: '失败分类(只读):文本 → 错误类别(retryable/needs_human/needs_agent),不写任何记录',
|
|
536
|
+
inputSchema: { type: 'object', properties: { error: { type: 'string' }, attempt: { type: 'number' } }, required: ['error'], additionalProperties: false },
|
|
537
|
+
handler: ({ error, attempt }) => {
|
|
538
|
+
const argv = [join(REPO, 'scripts', 'flow_failure.py'), '--error', String(error), '--json']
|
|
539
|
+
if (attempt != null) argv.push('--attempt', String(attempt))
|
|
540
|
+
const r = runPy(argv)
|
|
541
|
+
if (r.reason === 'python_unavailable') return { ok: false, code: 2, reason: 'python_unavailable', out: '', error: r.error }
|
|
542
|
+
if (!r.ok) return { ok: false, code: r.code, out: r.output.slice(-600), error: r.error.slice(0, 200) }
|
|
543
|
+
const j = parseJsonOut(r.output)
|
|
544
|
+
return j.ok ? { ok: true, classified: j.value } : { ok: false, out: r.output.slice(-600), error: j.error }
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
/** 未处置失败扫描(只读)。 */
|
|
548
|
+
argent_failures: {
|
|
549
|
+
description: '未处置失败扫描(只读):列出台账里未处置的失败及其分类',
|
|
550
|
+
inputSchema: { type: 'object', properties: { flow: { type: 'string' } }, additionalProperties: false },
|
|
551
|
+
handler: ({ flow } = {}) => {
|
|
552
|
+
const argv = [join(REPO, 'scripts', 'flow_failure.py'), '--scan', '--json']
|
|
553
|
+
if (flow) argv.push('--flow', String(flow))
|
|
554
|
+
const r = runPy(argv)
|
|
555
|
+
if (r.reason === 'python_unavailable') return { ok: false, code: 2, reason: 'python_unavailable', out: '', error: r.error }
|
|
556
|
+
if (!r.ok) return { ok: false, code: r.code, out: r.output.slice(-600), error: r.error.slice(0, 200) }
|
|
557
|
+
const j = parseJsonOut(r.output)
|
|
558
|
+
return j.ok ? { ok: true, scan: j.value } : { ok: false, out: r.output.slice(-600), error: j.error }
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
/**
|
|
562
|
+
* 回写影子 diff(只读,目标 ④):拿**历史已完成票**,跑 `--dry-run` 得到"此刻会写什么",
|
|
563
|
+
* 与 `invoice-writeback/audit.jsonl` 里的**历史真实写入**逐字段对照。
|
|
564
|
+
* 判据:若历史已写入同一文件且此刻 dry-run 报"幂等跳过" ⇒ `idempotent: true`(重复执行安全)。
|
|
565
|
+
* **绝不加 `--yes`** ⇒ 不碰生产表。
|
|
566
|
+
*/
|
|
567
|
+
argent_writeback_shadow: {
|
|
568
|
+
description: '回写影子 diff(只读):历史审计 vs 当前 dry-run 计划,判断幂等/差异;永不真写',
|
|
569
|
+
inputSchema: { type: 'object', properties: { recordId: { type: 'string' }, file: { type: 'string' }, status: { type: 'string' } }, required: ['recordId'], additionalProperties: false },
|
|
570
|
+
handler: ({ recordId, file, status }) => {
|
|
571
|
+
const auditPath = join(DATA_DIR, 'invoice-writeback', 'audit.jsonl')
|
|
572
|
+
const history = []
|
|
573
|
+
if (fExists(auditPath)) {
|
|
574
|
+
for (const line of readFileSync(auditPath, 'utf8').trim().split('\n')) {
|
|
575
|
+
if (!line.includes(recordId)) continue
|
|
576
|
+
try { history.push(JSON.parse(line)) } catch { /* 跳过坏行 */ }
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// 原文件通常已被清理(实测 `missing-file`)⇒ 历史里有"文件名 + 字节数"时,
|
|
580
|
+
// 造一个**同尺寸占位文件**(写在临时目录,绝不覆盖真实产物)来触发脚本的"幂等跳过"路径;
|
|
581
|
+
// 仍然只跑 `--dry-run` ⇒ 不碰生产表。`dummyFile: true` 会如实回报,避免被误当真实产物。
|
|
582
|
+
let fileArg = file || ''
|
|
583
|
+
let dummy = null
|
|
584
|
+
if (!fileArg && history.length) {
|
|
585
|
+
const h = [...history].reverse().find((x) => x.file && x.bytes)
|
|
586
|
+
if (h) {
|
|
587
|
+
const dir = mkdtempSync(join(tmpdir(), 'wb-shadow-'))
|
|
588
|
+
fileArg = join(dir, String(h.file))
|
|
589
|
+
writeFileSync(fileArg, Buffer.alloc(Number(h.bytes)))
|
|
590
|
+
dummy = { name: String(h.file), bytes: Number(h.bytes), dir }
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const argv = [join(REPO, 'scripts', 'wenyao-invoice-writeback.mjs'), '--record-id', String(recordId), '--allow-record', String(recordId), '--dry-run']
|
|
594
|
+
if (fileArg) argv.push('--file', fileArg)
|
|
595
|
+
if (status) argv.push('--status', String(status))
|
|
596
|
+
let planned = ''
|
|
597
|
+
let code = 0
|
|
598
|
+
try { planned = execFileSync(process.execPath, argv, { encoding: 'utf8', cwd: REPO }) }
|
|
599
|
+
// 守卫把拒绝原因写到 **stderr**(实测:漏它就看不到 file-type-not-allowed ⇒ 判定分支失效)
|
|
600
|
+
catch (e) { code = e.status ?? 1; planned = `${e.stdout || ''}${e.stderr || ''}` }
|
|
601
|
+
const histFields = [...new Set(history.flatMap((h) => String(h.field || '').split(',').map((x) => x.trim()).filter(Boolean)))]
|
|
602
|
+
const histFiles = [...new Set(history.map((h) => h.file).filter(Boolean))]
|
|
603
|
+
const skip = /幂等|已存在|skip|相同|无需/i.test(planned)
|
|
604
|
+
return {
|
|
605
|
+
ok: code === 0,
|
|
606
|
+
code, recordId,
|
|
607
|
+
history: { entries: history.length, fields: histFields, files: histFiles, lastAt: history.length ? history[history.length - 1].at : null },
|
|
608
|
+
plannedNow: planned.slice(-800),
|
|
609
|
+
dryRunOnly: true,
|
|
610
|
+
dummyFile: dummy,
|
|
611
|
+
idempotent: Boolean(history.length && skip),
|
|
612
|
+
blockedBy: (planned.match(/file-type-not-allowed:([^\n]+)/) || [])[1] || null,
|
|
613
|
+
verdict: history.length === 0
|
|
614
|
+
? 'no_history(该记录没有历史写入 ⇒ 无法比对,可能不是已完成票)'
|
|
615
|
+
: (skip ? 'idempotent:历史已写入且当前 dry-run 报跳过 ⇒ 重复执行安全'
|
|
616
|
+
: (/file-type-not-allowed/.test(planned)
|
|
617
|
+
// 诚实结论:合成占位过不了内容校验(**这是好守卫**);原始产物按设计已清理
|
|
618
|
+
// ⇒ 历史票的"重跑幂等"无法用无产物方式验证,需真实产物或直接以审计为证。
|
|
619
|
+
? 'needs_real_artifact:守卫要求真实产物(内容类型校验通过才继续);该记录原始文件已清理 ⇒ 提供 --file 后再判'
|
|
620
|
+
: 'differs_or_pending:dry-run 未报跳过 ⇒ 需人工比对 plannedNow 与 history')),
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
/** 回写审计查询(只读):某记录(或全部)的历史写入明细。 */
|
|
625
|
+
argent_writeback_audit: {
|
|
626
|
+
description: '回写审计查询(只读):历史写入明细(字段/文件/结果/时间)',
|
|
627
|
+
inputSchema: { type: 'object', properties: { recordId: { type: 'string' }, tail: { type: 'number' } }, additionalProperties: false },
|
|
628
|
+
handler: ({ recordId, tail = 5 } = {}) => {
|
|
629
|
+
const auditPath = join(DATA_DIR, 'invoice-writeback', 'audit.jsonl')
|
|
630
|
+
if (!fExists(auditPath)) return { ok: false, reason: 'no_audit_file' }
|
|
631
|
+
const rows = readFileSync(auditPath, 'utf8').trim().split('\n')
|
|
632
|
+
.filter((l) => !recordId || l.includes(recordId))
|
|
633
|
+
.slice(-Math.max(1, Number(tail) || 5))
|
|
634
|
+
.map((l) => { try { return JSON.parse(l) } catch { return { raw: l } } })
|
|
635
|
+
return { ok: true, count: rows.length, rows }
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
argent_preflight: {
|
|
639
|
+
description: '流程环境自检(只读;不写表、不认领)',
|
|
640
|
+
inputSchema: { type: 'object', properties: { flow: { type: 'string' } }, required: ['flow'], additionalProperties: false },
|
|
641
|
+
handler: ({ flow }) => {
|
|
642
|
+
const r = runPy([join(REPO, 'scripts', 'flow_preflight.py'), '--flow', String(flow)])
|
|
643
|
+
if (r.reason === 'python_unavailable') return { ok: false, code: 2, reason: 'python_unavailable', output: '', error: r.error }
|
|
644
|
+
return r.ok
|
|
645
|
+
? { ok: true, output: r.output.slice(-4000) }
|
|
646
|
+
: { ok: false, code: r.code, output: r.output.slice(-4000), error: r.error.slice(0, 300) }
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const send = (msg) => process.stdout.write(`${JSON.stringify(msg)}\n`)
|
|
652
|
+
const reply = (id, result) => send({ jsonrpc: '2.0', id, result })
|
|
653
|
+
const fail = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } })
|
|
654
|
+
|
|
655
|
+
function handle(msg) {
|
|
656
|
+
const { id, method, params } = msg || {}
|
|
657
|
+
if (method === 'initialize') {
|
|
658
|
+
return reply(id, {
|
|
659
|
+
protocolVersion: (params && params.protocolVersion) || '2024-11-05',
|
|
660
|
+
capabilities: { tools: {} },
|
|
661
|
+
serverInfo: SERVER_INFO,
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
if (method === 'notifications/initialized') return // 通知无响应
|
|
665
|
+
if (method === 'ping') return reply(id, {})
|
|
666
|
+
if (method === 'tools/list') {
|
|
667
|
+
return reply(id, {
|
|
668
|
+
// 注解(2026-10-06 加固):**新增字段**,`name`/`description`/`inputSchema` 逐字节不变
|
|
669
|
+
tools: Object.entries(TOOLS).map(([name, t]) => ({
|
|
670
|
+
name, description: t.description, inputSchema: t.inputSchema, annotations: annotationsFor(name),
|
|
671
|
+
})),
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
if (method === 'tools/call') {
|
|
675
|
+
const name = params && params.name
|
|
676
|
+
const tool = TOOLS[name]
|
|
677
|
+
if (!tool) return fail(id, -32602, `未知 tool:${name}`)
|
|
678
|
+
try {
|
|
679
|
+
// 只读契约:入参严格按 inputSchema(多余字段拒绝,避免"悄悄多写")
|
|
680
|
+
const args = (params && params.arguments) || {}
|
|
681
|
+
const result = tool.handler(args)
|
|
682
|
+
return reply(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError: false })
|
|
683
|
+
} catch (e) {
|
|
684
|
+
return reply(id, { content: [{ type: 'text', text: `执行失败:${String(e.message || e)}` }], isError: true })
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (id !== undefined) fail(id, -32601, `未实现的方法:${method}`)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let buf = ''
|
|
691
|
+
process.stdin.setEncoding('utf8')
|
|
692
|
+
process.stdin.on('data', (chunk) => {
|
|
693
|
+
buf += chunk
|
|
694
|
+
let i
|
|
695
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
696
|
+
const line = buf.slice(0, i).trim()
|
|
697
|
+
buf = buf.slice(i + 1)
|
|
698
|
+
if (!line) continue
|
|
699
|
+
try { handle(JSON.parse(line)) } catch (e) { /* 忽略坏行,不崩服务 */ }
|
|
700
|
+
}
|
|
701
|
+
})
|
|
702
|
+
process.stdin.on('end', () => process.exit(0))
|