From 161f90d09d6ad1368973b1a85d51059059495223 Mon Sep 17 00:00:00 2001 From: inman Date: Mon, 31 Aug 2026 10:36:57 +0800 Subject: [PATCH] fix: support internal AgentBus attachments --- .project-docs/10-decisions/decision-index.md | 1 + .project-docs/20-architecture/data-flow.md | 5 +- .../20-architecture/system-overview.md | 9 +++- .project-docs/30-worklog/current-state.md | 21 +++++--- ...1-integrate-server-diagnostics-8b42c6d1.md | 33 +++++++++--- .project-docs/40-domain/business-rules.md | 7 ++- .project-docs/50-evidence/evidence-index.md | 4 ++ .project-docs/80-commitments/commitments.md | 2 +- agent设计规范/agentbus-reply-contract.md | 4 +- control-plane/README.md | 4 +- control-plane/src/input-attachment.ts | 53 +++---------------- control-plane/test/input-attachment.test.ts | 39 ++++++++------ 12 files changed, 96 insertions(+), 86 deletions(-) diff --git a/.project-docs/10-decisions/decision-index.md b/.project-docs/10-decisions/decision-index.md index 9d00d63..0d8bff3 100644 --- a/.project-docs/10-decisions/decision-index.md +++ b/.project-docs/10-decisions/decision-index.md @@ -9,6 +9,7 @@ | ROUTE-001 | The machine registry is the authority for 18 parser routes; the two passenger-list routes are Program-only. | Active | 2026-08-28 | Manual and AgentBus intake | [Machine registry](../../control-plane/src/business-routes.ts) | | RELEASE-001 | Current artifacts, filenames, versions, and SHA-256 values are defined only by `dist/release-manifest.json`. | Active | 2026-08-28 | Release and delivery | [Release manifest](../../dist/release-manifest.json) | | SAFETY-001 | Real ERP access/write, task mutation, extension reload, service restart, deployment, and external delivery require explicit task-scoped authorization. | Active | 2026-08-28 | Operations and maintenance | [Governance](../../AGENTS.md) | +| NETWORK-001 | In the trusted internal deployment, AgentBus roster attachment URLs may resolve to internal/private addresses; HTTPS, credential rejection, DNS pinning, redirect validation, bounds, and digest checks remain. | Active | 2026-08-31 | AgentBus attachment ingress | [Reply contract](../../agent设计规范/agentbus-reply-contract.md) | ## Superseded Decisions diff --git a/.project-docs/20-architecture/data-flow.md b/.project-docs/20-architecture/data-flow.md index b9da539..a14bfa9 100644 --- a/.project-docs/20-architecture/data-flow.md +++ b/.project-docs/20-architecture/data-flow.md @@ -10,6 +10,9 @@ | ERP execution | Confirmed task | Chrome extension and logged-in ERP page | Requires unique object, page identity, ownership, and write preflight | | Completion evidence | ERP response/requery | Control-plane receipt and business reply | Evidence is action-specific; uncertain writes fail closed | | Passenger workbook | Single `.xls/.xlsx` attachment | Deterministic encrypted canonical TSV | First row ignored, second row fixed header, exact leader-contact rules | +| WeChat roster attachment | Strict transport envelope plus one structured `payload.attachments[]` entry | Existing `awaiting_attachment` task | Explicit conversation ID wins; otherwise strict `Conversation:` supplies the fallback. Placeholder text alone never creates a task. | +| Internal attachment download | Credential-free HTTPS URL | Bounded in-memory workbook bytes | Internal/private DNS answers are allowed; the selected address is pinned, every redirect is revalidated, and URL/host/IP/bytes are omitted from logs. | +| Operational diagnostics | Service, request, task, parser, AgentBus, attachment, database, and cleanup stages | Structured stdout/stderr and bounded Docker logs | Correlation identifiers, codes, outcomes, and durations only; no secrets or business payloads. | | Confirmation export | ERP source file | Archived source plus mobile delivery artifact | Visitor XLS becomes real XLSX; other types prefer PDF | | Release | Editable source | `dist/release-manifest.json` and versioned artifacts | Manifest owns current hashes and filenames | @@ -29,4 +32,4 @@ ## Last Updated -2026-08-28 +2026-08-31 diff --git a/.project-docs/20-architecture/system-overview.md b/.project-docs/20-architecture/system-overview.md index a8e6e60..508a596 100644 --- a/.project-docs/20-architecture/system-overview.md +++ b/.project-docs/20-architecture/system-overview.md @@ -10,7 +10,7 @@ Manual or AgentBus input is routed through task-scoped AI/Shadow/Auto/Program or |---|---|---| | `agent设计规范/` | Agent Prompt, five parsing Skills, business templates, business registry, and stable fixtures | Editable source for business semantics; not runtime evidence | | `schemas/` and `mappings/` | Parse-state, execution-state, ERP form, field, and lifecycle contracts | Current contracts only | -| `control-plane/` | Task/session persistence, parser orchestration, confirmation, audit, AgentBus, attachments, and receipts | TypeScript source; build output goes to `.build/` | +| `control-plane/` | Task/session persistence, parser orchestration, confirmation, audit, AgentBus, attachments, receipts, and structured diagnostics | TypeScript source; build output goes to `.build/` | | `LianSyn-platform/` | Operator workbench and external parser adapter | Source and UI, not local task output | | `chrome-extension/ltjt-order-assistant/` | Logged-in ERP resolution, preflight, native execution, response handling, and requery | Any code change requires synchronized versioned release updates | | `dist/` | Versioned current deliverables and machine-readable release manifest | Not a compilation directory | @@ -22,6 +22,10 @@ Manual or AgentBus input is routed through task-scoped AI/Shadow/Auto/Program or - AI/Program parsing and ERP resolution/execution share the final operation contract but do not share authority. - Platform envelope fields such as task ID, session, parser decision, confirmation, transport, and audit never enter the business operation. - Unknown, ambiguous, unverified, or post-write-uncertain states fail closed; automatic retries must not create duplicate writes. +- PostgreSQL is the sole required durable database/state middleware, and the production artifact provider is OSS. Redis, message queues, MongoDB, and search services are not runtime dependencies. +- Migrations must complete before the application starts. The current ACK topology starts with one application replica because AgentBus listeners and SSE emission are process-local; horizontal scale requires explicit coordination first. +- Operational diagnostics are privacy-safe structured JSON on stdout/stderr. Docker owns bounded rotation; repository files and a second mutable log database are not log sinks. +- In the trusted internal deployment, AgentBus roster attachment downloads may resolve to private/reserved addresses. Credential-free HTTPS, DNS resolution/pinning, redirect revalidation, size, timeout, and digest checks remain mandatory, and trusted channels/bridges own the network-input boundary. - Canonical project memory is updated only under Integration Gate; feature tasks write only their task-scoped records. ## Related Decisions @@ -31,7 +35,8 @@ Manual or AgentBus input is routed through task-scoped AI/Shadow/Auto/Program or - ROUTE-001 - RELEASE-001 - SAFETY-001 +- NETWORK-001 ## Last Updated -2026-08-28 +2026-08-31 diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index 424c1d1..8a2c05a 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -4,42 +4,47 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through -- Commit `7b5d855b093af39bf834fab4f41f41b37be1170d` as the inspected repository baseline. -- Integration task `20260828-migrate-project-docs-6f1a9c2d` for the project-documentation migration. +- Commit `c4c469f4441d744627af2d34abe693b6783e833c` for the independently advanced remote deployment/extension line. +- Commit `cd45ce17d0fcd25f7fa89ab9f8a391d3e904ecdf` for WeChat attachment correlation and privacy-safe server diagnostics. +- Integration task `20260831-integrate-server-diagnostics-8b42c6d1` for mainline reconciliation and trusted-intranet attachment compatibility. ## Current Focus -Operate the current `0.5.157` release baseline safely, keep Program/AI routing and ERP execution boundaries synchronized, and close the remaining authorization-dependent validation gaps. +Operate the current `0.5.163` extension release baseline safely, keep Program/AI routing and ERP execution boundaries synchronized, and prepare the integrated control-plane revision for a separately authorized deployment. ## Recently Completed - 2026-08-28: Initialized `.project-docs/`, migrated durable project memory, and retired the root Planning with Files system into date-scoped history. -- 2026-08-28: Released Chrome extension `0.5.157`, Program parser `v1.0.6`, input contract/DOCX `0.5.123`, and five business Skills `0.5.125`. +- 2026-08-30: Advanced the synchronized Chrome extension/runtime release to `0.5.163`; Program parser remains `v1.0.6`, input contract/DOCX `0.5.123`, and five business Skills `0.5.125`. - 2026-08-28: Added narrow shared-mother-plan whole-visitor export using `shared_plan + visitor-list + tid-only` while preserving child/independent `did+tid` behavior. - 2026-08-28: Restarted the standard 8786 control plane under authorization and observed AgentBus 4/4 channels ready across repeated samples. +- 2026-08-31: Integrated strict WeChat envelope conversation fallback, placeholder-only attachment rejection before task ingestion, and safe attachment error summaries while preserving the original `awaiting_attachment` task. +- 2026-08-31: Integrated structured privacy-safe diagnostics across service, HTTP, task/audit, parser, AgentBus, attachment, database, and cleanup stages, with bounded Docker stdout retention and a read-only server diagnostic command. +- 2026-08-31: Confirmed from the supplied production log that attachment correlation succeeded and the failure was private/reserved DNS rejection; removed that rejection for the trusted internal deployment while retaining credential-free HTTPS, DNS pinning, redirect validation, size, timeout, and SHA-256 controls. ## In Progress -- No separate repository feature task is recorded at this integration snapshot. +- No separate repository feature task is recorded at this integration snapshot; deployment and restart remain unperformed. ## Next Recommended Steps 1. With explicit authorization, run a live read-only ERP verification of the shared-mother-plan `tid-only` whole-visitor export path. 2. With explicit authorization, perform ERP write verification for independent-order SGL/TWN and adult/child/leader headcount mappings. -3. Design a controlled public-DNS or host-allowlist fallback for AgentBus OSS attachments without weakening private-network SSRF blocking. +3. Under separate deployment authorization, publish the integrated control-plane image with `DEPLOYMENT_REVISION` set and verify one real internal AgentBus roster attachment through the new diagnostic stages. ## Open Questions / Blockers - Shared-mother-plan whole-visitor export has historical read evidence and static coverage but lacks a fresh authorized runtime ERP read verification. - Independent-order SGL/TWN and four headcount categories lack authorized current-version ERP write evidence. -- Some AgentBus OSS attachment URLs can be rejected when local DNS resolves them to private or reserved addresses. +- The repository fix for internal attachment URLs is not active on the server until a separately authorized image build/deployment/restart occurs. ## Risky Areas - Any ERP write, uncertain post-write state, automatic retry, or scope widening. - Passenger workbook normalization, encrypted attachment persistence, leader-contact projection, and native ERP row capacity. +- AgentBus channels and their upstream bridge are now a trusted network boundary because attachment URLs may target internal HTTPS hosts. - Release synchronization across extension source, minimum platform version, mapping, ZIP, Skills, DOCX, and `dist/release-manifest.json`. ## Last Updated -2026-08-28 +2026-08-31 diff --git a/.project-docs/30-worklog/tasks/20260831-integrate-server-diagnostics-8b42c6d1.md b/.project-docs/30-worklog/tasks/20260831-integrate-server-diagnostics-8b42c6d1.md index 0cbb2f4..220b9ef 100644 --- a/.project-docs/30-worklog/tasks/20260831-integrate-server-diagnostics-8b42c6d1.md +++ b/.project-docs/30-worklog/tasks/20260831-integrate-server-diagnostics-8b42c6d1.md @@ -14,31 +14,48 @@ - Merge the independently advanced `origin/main`, local project-governance/diagnosis commits, and the published WeChat attachment-correlation/diagnostics branch into `main`. - Preserve the remote deployment adaptations and Chrome extension `0.5.163` release while retaining the accepted `.project-docs`-only governance boundary. -- Add production support for attachment URLs that intentionally resolve inside the server environment through an exact, configuration-driven private-host allowlist. -- Preserve HTTPS, credential rejection, redirect revalidation, DNS pinning, byte limits, declared size, and SHA-256 verification; do not globally permit private/reserved networks. +- Remove the private/reserved-network rejection from the AgentBus roster attachment downloader because the service and attachment source intentionally share a trusted internal network. +- Preserve HTTPS, credential rejection, redirect revalidation, DNS resolution/pinning, byte limits, declared size, and SHA-256 verification while allowing internal hostnames, private IPv4/IPv6, and localhost. - Reconcile accepted promotion candidates into canonical data-flow, business-rule, architecture/current-state, evidence, and deployment documentation where supported. - Run all repository gates, commit the integrated result on `main`, and push it to `origin/main`. ## Intent And Constraints - The user explicitly authorized merging all current work into `main` and pushing a new revision, and explicitly confirmed that the attachment source is expected to use a private address in the server environment. -- Private-network compatibility must be opt-in per exact hostname or IP. Empty configuration preserves the existing fail-closed SSRF behavior; wildcards, URL prefixes, paths, credentials, and CIDR-wide bypasses are not accepted. -- Every initial URL and redirect target must be evaluated independently against the same exact allowlist; logging may record only allowlist presence/count and a boolean match, never the configured host values or attachment URL. +- The user subsequently clarified that private-network blocking is not required in this deployment. Do not add an allowlist or retain private/reserved address filtering for inbound AgentBus attachments. +- Every initial URL and redirect target must still require credential-free HTTPS, resolve successfully, and use DNS pinning for the selected address. Logging must never record the attachment URL, hostname, IP, file bytes, or roster values. +- Treat enabled AgentBus channels and their upstream bridge as the trusted input boundary for internal attachment URLs. - Resolve the root planning-file merge according to accepted decision `DOC-001`: remote implementation/release facts and immutable archives are retained, but retired root planning files are not restored as active sources. - Do not deploy, restart services, mutate Kubernetes, read secrets, access ERP, retry live tasks, or send external messages. ## Outcome -- Not completed. +- Reconciled the independently advanced `origin/main` line with the local `.project-docs` governance history in merge commit `c4c469f`; retained Chrome extension/runtime `0.5.163`, the synchronized release manifest and archives, and remote deployment facts without restoring retired root planning files. +- Integrated WeChat attachment correlation and privacy-safe diagnostics in merge commit `cd45ce1`. Strict envelope conversation fallback and placeholder-only rejection preserve the original `awaiting_attachment` task and prevent an attachment card from becoming a second business task. +- Inspected the user-supplied server log and confirmed that the actual attachment frame already carried structured metadata in the same conversation. It failed before task ingestion because its hostname resolved to a private/reserved address; correlation was not the failing boundary in that run. +- Removed the private/reserved-network rejection from AgentBus roster attachment URL validation and DNS resolution for this trusted internal deployment. Internal DNS names, private IPv4/IPv6 literals, and localhost now pass; credential-free HTTPS, successful DNS resolution, selected-address pinning, redirect revalidation, timeout, byte limits, declared size, optional SHA-256, and privacy-safe diagnostics remain. +- Updated the active AgentBus contract, control-plane operator documentation, canonical architecture/data flow/business rules/current state/evidence/decision/commitment memory, and regression coverage. No deployment, restart, Kubernetes mutation, secret read, ERP access, live-task retry, or external message occurred. +- Local integration and all required gates are complete; the authorized non-force `origin/main` push remains to be performed and verified. ## Verification -- Not run. +- Focused attachment test: 7/7 passed, including internal DNS, private IPv4/IPv6, localhost, credential rejection, and diagnostic redaction. +- Focused AgentBus/diagnostics/attachment regression before the final resolver assertion: 26/26 passed. +- `node --run check:repo`: 9/9 passed. +- `node --run check`: passed. +- `node --run test:control-plane`: 135/135 passed. +- `node --run test:legacy`: 255/255 passed. +- `node --run build`: passed. +- `sh -n infra/diagnose-server.sh infra/predeploy-check.sh`: passed. +- `check_project_docs.py`: passed. +- `git diff --check`: passed. +- Docker CLI is unavailable on this workstation, so Compose runtime expansion was not repeated; repository hygiene covers the checked-in Compose rotation structure. ## Follow-ups -- None recorded. +- Under separate authorization, build/deploy the integrated control-plane image with `DEPLOYMENT_REVISION` set, restart it, and verify one real internal WeChat roster attachment through the new diagnostic stages. +- ERP reads/writes, extension loading, deployment, and live-task retry remain outside this integration task. ## Promotion Candidates -- None recorded. +- None. Accepted attachment, diagnostics, deployment-boundary, and internal-network facts were promoted to canonical project memory during this Integration task. diff --git a/.project-docs/40-domain/business-rules.md b/.project-docs/40-domain/business-rules.md index b5b4a37..e3a32b7 100644 --- a/.project-docs/40-domain/business-rules.md +++ b/.project-docs/40-domain/business-rules.md @@ -5,6 +5,9 @@ - `agent设计规范/business-adaptation-registry.md` is the cross-session business entry; each business maps user input, Skill/action, ERP flow, contracts, implementation, fixtures, and verification status. - Manual and AgentBus tasks share the same 18 machine routes, task-scoped parser mode snapshot, and organization automation rules. - The two passenger-list import routes are Program-only and wait for exactly one `.xls` or `.xlsx` attachment before deterministic normalization. +- A WeChat attachment card is transport placeholder text, not file content. Only a structured `payload.attachments[]` entry can resume a roster task; missing metadata fails before ingestion and leaves the original task in `awaiting_attachment` instead of creating a new task. +- The trusted internal deployment accepts credential-free HTTPS roster attachment URLs whose host is internal, private/reserved IPv4/IPv6, or localhost. DNS pinning, redirect revalidation, download timeout, byte limits, declared-size checks, and optional SHA-256 verification remain mandatory. +- AgentBus attachment diagnostics may record stage, address count/family, status, byte count, code, outcome, and duration, but never URL, hostname, IP, file name, bytes, message text, or roster values. - Passenger overwrite requires confirmation when target ERP rows are occupied; after `full_replace + confirmed=true`, every attachment-specified sequence is written even when values are unchanged. - A single strict `领队` row supplies leader contact; ambiguous, incomplete, duplicate, or structurally inconsistent leader data fails closed. - Shared-mother-plan `整团游客信息` export is only `shared_plan + visitor-list + tid-only`; independent and concrete shared-child visitor lists remain `did+tid`. @@ -15,8 +18,8 @@ - Fresh authorized runtime read verification remains for the shared-mother-plan whole-visitor export branch. - Authorized current-version ERP write verification remains for SGL/TWN and four independent-order headcount categories. -- AgentBus OSS DNS fallback must preserve private/reserved-network SSRF blocking. +- Deployment/restart and one live internal attachment verification still require separate authorization. ## Last Reviewed -2026-08-28 +2026-08-31 diff --git a/.project-docs/50-evidence/evidence-index.md b/.project-docs/50-evidence/evidence-index.md index 61c122b..fd3be57 100644 --- a/.project-docs/50-evidence/evidence-index.md +++ b/.project-docs/50-evidence/evidence-index.md @@ -8,6 +8,10 @@ Use this index for searchable, traceable evidence records. | 2026-08-28 | Current release capability and real-validation boundary | Current integrated evidence | [Lifecycle release gate](../../agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md) | Defines active conclusions and links immutable evidence. | | 2026-08-28 | Release artifact hashes and sources | Machine-verified | [Release manifest](../../dist/release-manifest.json) | Seven current artifacts and their source/hash metadata. | | 2026-08-28 | AgentBus reconnect and runtime switch | Verified at observation time; time-sensitive | [Archived legacy progress](../../archive/project-history/2026-08-28/legacy-planning-with-files-progress-final.md) | Repeated 4/4-ready samples after authorized restart; live status must be rechecked when needed. | +| 2026-08-30 | Chrome extension `0.5.163` and ACK deployment constraints | Source/release verified; not deployed by this task | [ACK and release task](../30-worklog/tasks/20260828-ack-deploy-guide-8c1d.md) | Records synchronized versions, package/source comparison, PostgreSQL/OSS requirements, migration ordering, and current single-replica constraint. | +| 2026-08-31 | WeChat attachment correlation | Repository verified; not deployed by this task | [Attachment-correlation task](../30-worklog/tasks/20260830-wechat-attachment-correlation-9f3a2c.md) | Strict conversation fallback and placeholder-only failure preserve the waiting task and prevent accidental task creation. | +| 2026-08-31 | Privacy-safe server diagnostics | Repository verified; not deployed by this task | [Diagnostics task](../30-worklog/tasks/20260830-server-diagnostics-c4d8a1f2.md) | Structured lifecycle diagnostics, redaction, bounded Docker retention, and read-only server inspection command. | +| 2026-08-31 | Production attachment failure | Root cause verified from supplied log | [Log-inspection task](../30-worklog/tasks/20260831-inspect-server-log-5d1e8a7c.md) | The original task remained waiting; the later structured attachment failed because DNS returned a private/reserved address. | ## When To Add Evidence diff --git a/.project-docs/80-commitments/commitments.md b/.project-docs/80-commitments/commitments.md index f0c27e6..1731717 100644 --- a/.project-docs/80-commitments/commitments.md +++ b/.project-docs/80-commitments/commitments.md @@ -6,7 +6,7 @@ Track future-facing memory: promised follow-ups, unfinished loops, timed checks, |---|---|---|---|---|---| | 2026-08-28 | Verify shared-mother-plan `tid-only` whole-visitor export against the current runtime ERP path. | Explicit user authorization for ERP read access | Future authorized task | Pending authorization | Run read-only source and artifact checks without external delivery. | | 2026-08-28 | Verify independent-order SGL/TWN and adult/child/leader headcount mappings with real ERP writes. | Explicit user authorization for controlled ERP writes | Future authorized task | Pending authorization | Use reversible values and action-specific requery evidence. | -| 2026-08-28 | Resolve AgentBus OSS attachment rejection caused by private/reserved local DNS answers. | User schedules the networking task | Future feature task | Pending | Design controlled public-DNS or host-allowlist fallback; keep private-network blocking. | +| 2026-08-28 | Resolve AgentBus OSS attachment rejection caused by private/reserved local DNS answers. | User scheduled integration and confirmed the environment is trusted internal networking | Integration task `20260831-integrate-server-diagnostics-8b42c6d1` | Completed in repository | Deploy/restart and run one live internal attachment verification only under separate authorization. | ## Use diff --git a/agent设计规范/agentbus-reply-contract.md b/agent设计规范/agentbus-reply-contract.md index cce32e5..abaef74 100644 --- a/agent设计规范/agentbus-reply-contract.md +++ b/agent设计规范/agentbus-reply-contract.md @@ -15,11 +15,11 @@ 两项名单业务固定走 Program Parser。首次文字指令到达但没有名单文件时,任务进入 `awaiting_attachment`,并返回一条要求发送 `.xls/.xlsx` 的重要消息;该状态不是最终失败,也不会触发 ERP。后续可以在同一 `conversation_id`,或明确携带 `payload.task_id`,发送一个附件而不带正文。 -名单入站 `payload.attachments` 最多一个,使用 `name`、`content_type`、`size`、可选 `sha256` 和不含凭据的 HTTPS `url`。控制面拒绝 HTTP、URL 用户名密码、私网/保留地址、DNS 解析到私网、超过大小限制、大小或 SHA-256 不一致以及超过两次的重定向;下载时固定到已校验的公网 IP。附件 URL 和原始字节不写入任务 JSONB 或日志。 +名单入站 `payload.attachments` 最多一个,使用 `name`、`content_type`、`size`、可选 `sha256` 和不含凭据的 HTTPS `url`。当前服务运行在受信内网,控制面允许该 URL 直接使用内网域名、私网 IPv4/IPv6 或 localhost;仍拒绝 HTTP、URL 用户名密码、无法解析的地址、超过大小限制、大小或 SHA-256 不一致以及超过两次的重定向。每次 DNS 解析后固定到选定地址发起 HTTPS 请求,重定向目标重新执行同一 URL 与 DNS 流程。附件 URL 和原始字节不写入任务 JSONB 或日志;允许内网目标意味着 AgentBus 渠道及其上游桥接器必须是受信输入源。 微信传输信封的 `Conversation:` 在帧未显式提供 `conversation_id` 时作为会话键,显式字段优先。`[WeChat attachment: 文件名]` 只是桥接器的文字占位符:没有同时提供 `payload.attachments[]` 时,控制面不得把它作为新业务消息创建任务,也不得假装已收到文件;应保留原任务的 `awaiting_attachment` 状态并提示附件内容尚未传到平台。附件元数据或安全下载校验失败时只返回预定义的安全摘要,不回显 URL、文件内容或名单值。 -服务端诊断以 `diagnostic_event=agentbus.` 串联 frame、conversation、channel、任务和最终回执。附件入口必须分别记录元数据、DNS、公网地址数量/IP family、HTTPS 状态、重定向、字节校验、结果和耗时,但不能记录 URL、hostname、IP、文件名、文件内容或名单值。异常使用错误码、稳定指纹和清理后的代码栈定位;生产环境不得启用正文 payload 日志。 +服务端诊断以 `diagnostic_event=agentbus.` 串联 frame、conversation、channel、任务和最终回执。附件入口必须分别记录元数据、DNS、解析地址数量/IP family、HTTPS 状态、重定向、字节校验、结果和耗时,但不能记录 URL、hostname、IP、文件名、文件内容或名单值。异常使用错误码、稳定指纹和清理后的代码栈定位;生产环境不得启用正文 payload 日志。 附件通过程序模板校验后,任务从 `awaiting_attachment` 转为 `parse_queued`;校验失败则保留等待状态并只返回安全错误码和行列位置,不回显名单值。原始工作簿不持久化,只暂存加密 canonical TSV 供 Program Parser 使用。 diff --git a/control-plane/README.md b/control-plane/README.md index b7a1f03..0d51919 100644 --- a/control-plane/README.md +++ b/control-plane/README.md @@ -21,7 +21,7 @@ - Chrome 插件最低兼容版本由正式操作台与根目录发布清单共同门禁。插件包含分段前门禁、ERP 只读唯一解析、严格写前门禁、当前窄生命周期适配、写入前 `write_started` 持久化和写后回查;扩展后台重启后也不会重跑同一任务。本次双轨解析不修改插件执行契约或扩展版本。 - 渠道 Adapter 由 AgentBus 负责;控制平面只作为 AgentBus Bot 连接到文档中的 WebSocket,不实现微信、个人微信或其他渠道协议。 - 微信桥接器把正文放在严格的 `New WeChat message` / `Conversation:` / `Text:` 三行传输信封中;AgentBus listener 会在任务快照前只解开这一已知信封,把 `Text:` 同行值及后续行作为业务正文,并在帧没有显式 `conversation_id` 时使用信封中的 `Conversation:` 值。显式字段仍优先;近似、缺失字段或空正文的包装保持原文,不能通过忽略任意未知标签来绕过 Program parser 的失败关闭。 -- 微信侧的 `[WeChat attachment: 文件名]` 只是一段传输占位文字,不代表控制面已经收到文件。若同一帧没有符合契约的 `payload.attachments[]`,listener 会在进入任务服务前失败关闭、保留原名单任务的等待状态,并返回“附件内容未传到平台”;不会把占位文字创建成新业务任务。附件元数据、HTTPS URL、DNS、大小或摘要校验失败时返回对应的安全摘要,仍不回显 URL、文件字节或名单内容。 +- 微信侧的 `[WeChat attachment: 文件名]` 只是一段传输占位文字,不代表控制面已经收到文件。若同一帧没有符合契约的 `payload.attachments[]`,listener 会在进入任务服务前失败关闭、保留原名单任务的等待状态,并返回“附件内容未传到平台”;不会把占位文字创建成新业务任务。附件元数据、HTTPS URL、DNS、大小或摘要校验失败时返回对应的安全摘要,仍不回显 URL、文件字节或名单内容。当前生产部署位于受信内网,入站附件 URL 可以使用内网域名、私网 IPv4/IPv6 或 localhost;因此 AgentBus 渠道和上游桥接器必须被视为受信输入边界。 - AgentBus 入站消息会复用 `TaskService` 的任务/会话/解析队列,解析完成后通过同一 WebSocket 返回一次 `task.result`。组织级“全自动化”关闭时,手工与 AgentBus 新任务都要求管理员确认;开启后,两种来源的合法解析结果都自动进入 ERP 队列,不再按来源或创建、名单、安排、修改、取消/恢复、导出等业务类型保留人工例外。操作台在 EventSource 建连/重连、30 秒后台刷新以及页面重新可见或聚焦时重新读取数据库权威开关,避免后台变更后按钮仍显示旧值。缺资料、解析失败、歧义、插件校验失败或 ERP 回查不确定时仍会停止,不会绕过校验或重试不确定写入。 - 一个组织可以维护多个“用户渠道”。渠道代表外部 AgentBus 用户身份,不等同于平台管理员账号;管理员在独立根路径 `/channels` 的“AgentBus 渠道”目录中创建、停用、启用、轮换或删除渠道。删除会停止对应 listener、移除服务端保存的 key 和该渠道尚存的持久化回执;历史任务本体保留,其 `channel_id` 按数据库契约置空。每个渠道独立保存加密后的 AgentBus key,并建立独立 WebSocket listener;列表和日志都不会回显 key。`AGENTBUS_WS_URL`、重连策略和客户端类型仍是全局连接配置,`AGENTBUS_BOT_ADDRESS` 可作为渠道 bot address 的默认值。仍由完整旧环境变量托管的兼容渠道会自动重建,必须先移除环境配置并重启服务,才允许删除其数据库记录。 - `/history` 支持逐条彻底删除,以及勾选当前页后批量删除。单条使用 `DELETE /api/tasks/:taskId`,批量使用 `POST /api/tasks/bulk-delete`(一次 1–100 个且不能重复);两者都要求管理员 mutation 会话、same-origin 与 CSRF 门禁。批量删除会在同一数据库事务中按组织锁定并核对全部目标,任一任务不存在或不属于当前组织时整批回滚;成功后任务、生命周期、尝试、会话、附件元数据和 AgentBus 回执按外键级联删除,task-scoped audit/outbox 行显式删除。OSS 附件对象在事务提交后使用已冻结的 storage key 逐一清理,清理异常写入服务日志。对已确认或正在插件流程中的任务,页面会明确警告:停止插件只是尽力而为,已经发生的 ERP 写入及已投递到外部渠道的副本不会因删除平台历史而撤回。 @@ -97,7 +97,7 @@ Auto 一旦发生 AI fallback,任务会永久绑定原 AI 会话。每次解 对微信来源,listener 在调用 `TaskService.ingestMessage()` 前执行上述严格信封解包,因此手工正文与 AgentBus 正文进入同一个业务 route resolver、任务级 mode snapshot 和 parser orchestrator;`Conversation` 只属于传输路由,不会再污染业务字段签名。 -AgentBus 全链路日志使用控制平面 stdout/Pino 输出,同时带 `diagnostic_event=agentbus.` 与原有 `agentbus_event`。日志覆盖连接尝试、socket 生命周期、session.ready、每个收发帧、帧忽略原因、任务入队、解析队列、持久化回执出队、最终回复和发送错误。名单附件另外记录元数据存在性、DNS 开始/通过、公网地址数量与 IP family、HTTPS 状态、重定向次数、接收字节数、大小/摘要校验和各阶段耗时;不记录 URL、hostname、IP、文件名、附件字节或名单值。开发/测试环境可临时设置 `AGENTBUS_LOG_PAYLOADS=true` 记录最多 2,000 个字符的正文预览,生产环境会拒绝以该值启动。渠道 key、WebSocket Token、Invoke Token 和 Authorization header 永不写入日志。 +AgentBus 全链路日志使用控制平面 stdout/Pino 输出,同时带 `diagnostic_event=agentbus.` 与原有 `agentbus_event`。日志覆盖连接尝试、socket 生命周期、session.ready、每个收发帧、帧忽略原因、任务入队、解析队列、持久化回执出队、最终回复和发送错误。名单附件另外记录元数据存在性、DNS 开始/通过、解析地址数量与 IP family、HTTPS 状态、重定向次数、接收字节数、大小/摘要校验和各阶段耗时;不记录 URL、hostname、IP、文件名、附件字节或名单值。开发/测试环境可临时设置 `AGENTBUS_LOG_PAYLOADS=true` 记录最多 2,000 个字符的正文预览,生产环境会拒绝以该值启动。渠道 key、WebSocket Token、Invoke Token 和 Authorization header 永不写入日志。 监听器只使用每个渠道的 WebSocket key;文档中的 Invoke Token 仅用于另一服务通过 Function Call API 主动向 Bot 投递任务,本服务的监听链路不会使用它。 diff --git a/control-plane/src/input-attachment.ts b/control-plane/src/input-attachment.ts index 05042f5..892eb2d 100644 --- a/control-plane/src/input-attachment.ts +++ b/control-plane/src/input-attachment.ts @@ -158,41 +158,6 @@ export function decodeInlineInputAttachment( }; } -function ipv4Number(address: string): number | null { - const parts = address.split('.').map(Number); - if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null; - return (((parts[0] * 256 + parts[1]) * 256 + parts[2]) * 256 + parts[3]) >>> 0; -} - -function ipv4InCidr(address: string, network: string, prefix: number): boolean { - const value = ipv4Number(address); - const base = ipv4Number(network); - if (value === null || base === null) return false; - const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0; - return (value & mask) === (base & mask); -} - -export function isPrivateOrReservedIp(address: string): boolean { - const family = isIP(address); - if (family === 4) { - return [ - ['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], - ['169.254.0.0', 16], ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24], - ['192.168.0.0', 16], ['198.18.0.0', 15], ['198.51.100.0', 24], ['203.0.113.0', 24], - ['224.0.0.0', 4], ['240.0.0.0', 4] - ].some(([network, prefix]) => ipv4InCidr(address, String(network), Number(prefix))); - } - if (family !== 6) return true; - const normalized = address.toLowerCase().split('%')[0]; - if (normalized === '::' || normalized === '::1') return true; - const mapped = /^(?:::ffff:)?(\d+\.\d+\.\d+\.\d+)$/.exec(normalized); - if (mapped) return isPrivateOrReservedIp(mapped[1]); - return /^(?:fc|fd)/.test(normalized) - || /^fe[89ab]/.test(normalized) - || /^ff/.test(normalized) - || normalized.startsWith('2001:db8:'); -} - export function validateAgentBusAttachmentUrl(value: unknown): URL { let url: URL; try { @@ -203,12 +168,8 @@ export function validateAgentBusAttachmentUrl(value: unknown): URL { if (url.protocol !== 'https:' || url.username || url.password) { throw new InputAttachmentError('roster_attachment_url_unsafe', '名单附件必须使用不含用户名密码的 HTTPS URL。'); } - const hostname = url.hostname.replace(/^\[|\]$/g, ''); - if (!hostname || hostname.toLowerCase() === 'localhost') { - throw new InputAttachmentError('roster_attachment_url_unsafe', '名单附件 URL 指向了不允许的地址。'); - } - if (isIP(hostname) && isPrivateOrReservedIp(hostname)) { - throw new InputAttachmentError('roster_attachment_url_unsafe', '名单附件 URL 指向了不允许的地址。'); + if (!url.hostname.replace(/^\[|\]$/g, '')) { + throw new InputAttachmentError('roster_attachment_url_invalid', '名单附件 URL 无效。'); } return url; } @@ -230,7 +191,9 @@ export function parseAgentBusInputAttachment( }; } -async function publicAddresses(hostname: string): Promise> { +export async function resolveAgentBusAttachmentAddresses( + hostname: string +): Promise> { if (isIP(hostname)) return [{ address: hostname, family: isIP(hostname) }]; let addresses: Array<{ address: string; family: number }>; try { @@ -238,8 +201,8 @@ async function publicAddresses(hostname: string): Promise isPrivateOrReservedIp(item.address))) { - throw new InputAttachmentError('roster_attachment_url_unsafe', '名单附件地址解析到了不允许的网络。'); + if (!addresses.length) { + throw new InputAttachmentError('roster_attachment_host_unresolved', '名单附件地址无法解析。'); } return addresses; } @@ -319,7 +282,7 @@ export async function downloadAgentBusInputAttachment( redirectCount = redirects; const dnsStartedAt = process.hrtime.bigint(); emitAttachmentDiagnostic(diagnostic, 'dns_started', { redirect_count: redirects }); - const addresses = await publicAddresses(url.hostname.replace(/^\[|\]$/g, '')); + const addresses = await resolveAgentBusAttachmentAddresses(url.hostname.replace(/^\[|\]$/g, '')); emitAttachmentDiagnostic(diagnostic, 'dns_validated', { redirect_count: redirects, address_count: addresses.length, diff --git a/control-plane/test/input-attachment.test.ts b/control-plane/test/input-attachment.test.ts index 2c71b97..c7459f0 100644 --- a/control-plane/test/input-attachment.test.ts +++ b/control-plane/test/input-attachment.test.ts @@ -6,8 +6,8 @@ import { InputAttachmentError, decodeInlineInputAttachment, downloadAgentBusInputAttachment, - isPrivateOrReservedIp, parseAgentBusInputAttachment, + resolveAgentBusAttachmentAddresses, validateAgentBusAttachmentUrl } from '../src/input-attachment.js'; @@ -46,26 +46,23 @@ test('manual roster attachment rejects unsupported names, invalid base64 and mis }, 1_000)), 'roster_file_sha256_mismatch'); }); -test('AgentBus attachment URLs require public HTTPS endpoints', () => { +test('AgentBus attachment URLs allow private HTTPS endpoints but reject unsafe URL syntax', () => { for (const value of [ 'http://files.example.test/list.xlsx', - 'https://localhost/list.xlsx', - 'https://127.0.0.1/list.xlsx', - 'https://10.0.0.8/list.xlsx', - 'https://[::1]/list.xlsx', + 'ftp://files.example.test/list.xlsx', 'https://user:secret@files.example.test/list.xlsx' ]) { assert.equal(codeOf(() => validateAgentBusAttachmentUrl(value)), 'roster_attachment_url_unsafe'); } - assert.equal(validateAgentBusAttachmentUrl('https://files.example.com/list.xlsx').protocol, 'https:'); -}); - -test('private and documentation address ranges are rejected', () => { - for (const address of ['0.0.0.1', '10.1.2.3', '100.64.0.1', '127.0.0.1', '169.254.1.1', '172.31.0.1', '192.168.1.1', '198.51.100.1', '203.0.113.8', '::1', 'fc00::1', 'fe80::1', '2001:db8::1']) { - assert.equal(isPrivateOrReservedIp(address), true, address); + for (const value of [ + 'https://files.example.com/list.xlsx', + 'https://localhost/list.xlsx', + 'https://127.0.0.1/list.xlsx', + 'https://10.0.0.8/list.xlsx', + 'https://[::1]/list.xlsx' + ]) { + assert.equal(validateAgentBusAttachmentUrl(value).protocol, 'https:', value); } - assert.equal(isPrivateOrReservedIp('8.8.8.8'), false); - assert.equal(isPrivateOrReservedIp('2606:4700:4700::1111'), false); }); test('AgentBus attachment metadata is normalized without exposing URL credentials', () => { @@ -81,6 +78,18 @@ test('AgentBus attachment metadata is normalized without exposing URL credential assert.equal(reference.sha256, 'a'.repeat(64)); }); +test('AgentBus attachment DNS accepts internal names and private literal addresses', async () => { + assert.deepEqual(await resolveAgentBusAttachmentAddresses('10.0.0.8'), [ + { address: '10.0.0.8', family: 4 } + ]); + assert.deepEqual(await resolveAgentBusAttachmentAddresses('fd00::8'), [ + { address: 'fd00::8', family: 6 } + ]); + const localhost = await resolveAgentBusAttachmentAddresses('localhost'); + assert.ok(localhost.length > 0); + assert.ok(localhost.every((entry) => entry.family === 4 || entry.family === 6)); +}); + test('AgentBus attachment diagnostics expose stages and codes without URL data', async () => { const events: Array<{ event: string; metadata: Record }> = []; await assert.rejects( @@ -88,7 +97,7 @@ test('AgentBus attachment diagnostics expose stages and codes without URL data', name: 'synthetic.xls', contentType: 'application/vnd.ms-excel', size: 128, - url: 'https://127.0.0.1/private-roster.xls?token=secret' + url: 'https://user:secret@127.0.0.1/private-roster.xls?token=secret' }, 1_000, (event, metadata) => events.push({ event, metadata })), (error: unknown) => error instanceof InputAttachmentError && error.code === 'roster_attachment_url_unsafe'