From dd24fd9a1cdddefd1ecf50b6127b548751867f2f Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 23 Sep 2026 08:43:09 +0800 Subject: [PATCH] refactor: simplify production deployment layout --- deploys/README.md | 173 ++++-------------- deploys/build-package.sh | 9 +- deploys/config/agent-endpoints.example.json | 6 - deploys/env/agent.env.example | 15 -- deploys/install.sh | 17 +- deploys/packages/README.md | 29 +-- ....1.0-p1.20260919-linux-amd64.tar.gz.sha256 | 1 - deploys/physical-deployment.md | 41 +++-- deploys/test/README.md | 27 +++ .../{config => test}/ai-dental-meiba-v1.json | 0 .../dispatcher.offline-oss.env.example | 0 .../{cell => test}/nonprod-call-evidence.sh | 0 .../20260919-single-node-ai-closure.md | 2 +- ...20260920-nonprod-capture-preflight-v6.json | 2 +- .../20260920-nonprod-capture-preflight.json | 2 +- docs/evidence/20260920-offline-oss-profile.md | 2 +- ...20-real-provider-second-3turn-attempt.json | 2 +- ...real-provider-second-capture-first-v4.json | 2 +- docs/evidence/20260922-mq-only-local-final.md | 2 +- docs/plan-0918.md | 2 +- internal/config/deployment_layout_test.go | 2 +- internal/dispatcher/mq_integration_test.go | 46 ++++- 22 files changed, 160 insertions(+), 222 deletions(-) delete mode 100644 deploys/packages/sip-go-agent-0.1.0-p1.20260919-linux-amd64.tar.gz.sha256 create mode 100644 deploys/test/README.md rename deploys/{config => test}/ai-dental-meiba-v1.json (100%) rename deploys/{env => test}/dispatcher.offline-oss.env.example (100%) rename deploys/{cell => test}/nonprod-call-evidence.sh (100%) diff --git a/deploys/README.md b/deploys/README.md index 6bf998c..8048ce8 100644 --- a/deploys/README.md +++ b/deploys/README.md @@ -1,156 +1,57 @@ -# Physical-host deployment +# Deployment -Production services run directly on Debian 13 (Trixie) physical/virtual host -processes managed by systemd. Docker is permitted only for disposable local or -ECS smoke validation; it is not a production runtime dependency. +## Production -The pinned baseline is [`versions.lock.json`](versions.lock.json): Go 1.27.1, -`0.1.0-p1.20260919`, Debian 13 amd64 and Asterisk 22.10.1. The Go release -package contains only our SIP Agent/Dispatcher business binaries and their -systemd units. SaaS-provided MQ, OSS, AI and other infrastructure are endpoints, -not packages deployed by this project; local chain validation may use isolated -fixtures/mocks only. Secrets, certificates, SIP credentials, broker URLs and -phone-log keys are injected separately. +Production runs directly on Debian 13 with systemd. The only services deployed +by this project are: -Before every real outbound attempt, obtain a fresh user confirmation in the -current conversation and display the exact SIP channel, raw target number and -packet-capture plan. Real SIP outbound calls are permitted only from 09:00 -(inclusive) through 20:00 (exclusive), Asia/Shanghai time; outside that window -the Agent/Dispatcher must fail closed rather than wait, retry, delay or switch -trunks. A prior confirmation does not authorize retries or another target; stop -after a failed attempt until a new confirmation is received. +- `sip-go-agent-dispatcher.service` +- `sip-go-agent-agent.service` +- the separately managed native `asterisk.service` -## One deployment directory +RabbitMQ, OSS, AI providers and SaaS are external endpoints. They are not +installed by the production package and are not started by systemd or Docker. -`deploys/` is the only deployment directory for local checks, physical-host -packages, Asterisk installation, configuration examples and systemd services. -There is no separate draft service set or compatibility deployment directory. - -## Local and isolated checks - -From the project root: - -```sh -make check -make release -./dist/sip-go-agent agent --help -./dist/sip-go-agent dispatcher --help -``` - -`make release` creates a local-development binary, module copies, SHA-256 -checksums and a manifest. A dirty-source marker is preserved; a local build is -not a signed production candidate or proof of external service acceptance. - -For an isolated Dispatcher-to-Agent startup check, prepare the strict Dispatcher -JSON configuration from `config/dispatcher.json.example`, inject its referenced -credentials outside the repository, and supply the deployment-owned endpoint -inventory and mTLS files. The local acceptance script also requires a loopback -RabbitMQ URL because `dispatcher --once` must prove it can publish through the -configured broker; it fails closed when the URL is absent: - -```sh -GO_SIP_LOCAL_MQ_URL=amqp://guest:guest@127.0.0.1:33252/ \ - ./scripts/acceptance-local.sh -``` - -The command below is the long-running endpoint check: - -```sh -SIP_GO_AGENT_MODE=mock \ -DISPATCHER_DB=./dispatcher.db \ -DISPATCHER_AGENT_ENDPOINTS_FILE=./deploys/config/agent-endpoints.example.json \ -MTLS_CA_FILE=/path/to/ca.pem \ -MTLS_CERT_FILE=/path/to/dispatcher.pem \ -MTLS_KEY_FILE=/path/to/dispatcher.key \ -./dist/sip-go-agent dispatcher --config /path/to/dispatcher.json --once -``` - -The Dispatcher probes the configured Agent, verifies its boot identity and -activates a session before use. Tenant commands cannot select an endpoint or -certificate. Agent RPC listening uses `AGENT_GRPC_LISTEN` and deployment-provided -mTLS files; an Agent-side peer allowlist can be supplied with -`MTLS_PEER_CERT_FINGERPRINTS`. Never place credentials or private keys in this -repository. - -Dispatcher owns its SQLite database; Agent owns a separate `AGENT_SPOOL` and has -no business database. These are single-node, single-Agent/Cell/tenant checks, -not multi-Cell or production acceptance. Local protocol tests do not replace -the mandatory deployment/capture diagnostics below. Mock is not authorization -for real calls or paid provider requests. - -## Build an uploadable package - -From the project root: +Build a release candidate from the project root: ```sh deploys/build-package.sh -deploys/packages/sip-go-agent-0.1.0-p1.20260919-linux-amd64.tar.gz +# output: dist/packages/sip-go-agent--linux-amd64.tar.gz ``` -The package includes its SHA-256 manifest, a non-root systemd deployment -layout, and the fail-closed non-production capture-first entrypoint. The -installer installs that entrypoint as `/usr/local/sbin/agent-call-nonprod-evidence` -and adds a dedicated validated sudoers rule for `rogee`; it does not install -`tcpdump` or silently weaken production gates. A dirty/unapproved source -manifest is intentionally rejected by the installer unless -`--allow-nonproduction` is supplied for smoke work. - -## Install on Debian 13 - -Upload and extract the archive on the target host, then run as root: +The local builder intentionally marks the manifest as not production-approved. +Only an externally approved, clean release (`source_dirty=false` and +`production_approval=true`) may be installed on a Debian 13 amd64 host as +root: ```sh -tar -xzf sip-go-agent-0.1.0-p1.20260919-linux-amd64.tar.gz +tar -xzf sip-go-agent--linux-amd64.tar.gz ./install.sh ``` -The installer verifies Debian 13 amd64, package checksums and the release -manifest; creates `rogee`, `/opt/sip-go-agent`, `/etc/sip-go-agent` and -`/var/lib/sip-go-agent`, installs both systemd units, and does not overwrite -existing environment or PKI files. Configure the injected values and approved -static Cell artifact, then start explicitly: +The package contains only the two Go services, their systemd units, production +environment templates, the endpoint inventory, the version lock and the +installer. Credentials, certificates, broker URLs and the approved static Cell +artifact are injected separately. Asterisk is built or installed with the +scripts under [`cell/`](cell/), and its management-owned configuration is never +overwritten. -```sh -systemctl enable sip-go-agent-agent.service sip-go-agent-dispatcher.service -systemctl start sip-go-agent-dispatcher.service sip-go-agent-agent.service -``` +The production install does not install test scripts, test fixtures, Docker or +sudo rules for test tooling. -Use `./install.sh --start` only after the environment, mTLS identity, broker -ACL and static Cell artifact have been reviewed. Production mode never silently -falls back to Mock. +## Test-only tooling -## Non-production capture-first gate +Test dependencies must run in disposable Docker containers. The existing +`make mq-integration-local` target starts a temporary RabbitMQ container, +runs the integration tests, and removes the container. Do not install RabbitMQ +or other test infrastructure as a host service. -Development, `mock`, `mixed` and non-production `real` validation must use -[`cell/nonprod-call-evidence.sh`](cell/nonprod-call-evidence.sh) as the single -capture-first entrypoint. It refuses `production`, validates the approved trunk -and whitelist target, verifies `tcpdump` raw-capture capability, records the -pre-call Debian/systemd/ECS-facing facts plus Asterisk/PJSIP/channel/media -state, enables the PJSIP logger, captures SIP UDP 5060 and RTP UDP -10000-10800 before the call command starts, and always stops capture/logger and -writes redacted status plus SHA-256 facts on success or failure. Real calls reserve a daily attempt in `/var/lib/sip-go-agent/state/real-call-attempts.tsv` per `trunk + original target`; the fourth attempt is rejected fail-closed, while `--preflight-only` does not consume quota. Existing real evidence is counted when seeding the ledger. +[`test/nonprod-call-evidence.sh`](test/nonprod-call-evidence.sh) is a host-side +capture-first gate for non-production mixed/real calls against native Asterisk. +It is deliberately outside the production package and is not a business +service. It still fails closed when root, `tcpdump`, Asterisk or the required +system diagnostics are unavailable. -Run it only after a fresh current-conversation confirmation naming the exact -trunk, raw target and capture plan; the command after `--` must execute as the -non-root `rogee` user. Do not invoke the Agent directly for a non-production -real/mixed call, do not retry inside the wrapper, and do not treat a missing -PCAP or state snapshot as a pass. The private call output and raw capture stay -under the mode-0700 evidence directory and must not be copied into repository -long-term evidence without redaction. - -## Cell boundary - -Asterisk remains the SIP owner and is installed as the separately approved -physical Asterisk 22.10.1 Cell service. Asterisk and the SIP Agent are the only -business-code services in this repository. The Go package does not rewrite -`pjsip.conf`, embed SIP credentials, or run Asterisk in Docker. Management owns -the immutable static Cell artifact and its systemd/maintenance release; the Go -Agent consumes the approved artifact and reports the applied revision. - -RabbitMQ, OSS, AI providers and SaaS APIs are external infrastructure. They are -not installed by `deploys/`; their production ACLs/endpoints are supplied by -SaaS, while local validation uses explicitly isolated test infrastructure. For -non-ECS/offline Alibaba OSS validation, use -`deploys/env/dispatcher.offline-oss.env.example` (public -`oss-cn-beijing.aliyuncs.com`); keep `dispatcher.env.example`'s internal -endpoint for the separately managed production ECS profile. +The test directory also contains the offline OSS environment example and the +AI fixture. They are test inputs only; they do not authorize external access +and are never copied into a production release. diff --git a/deploys/build-package.sh b/deploys/build-package.sh index d8c941a..7a426f7 100755 --- a/deploys/build-package.sh +++ b/deploys/build-package.sh @@ -8,19 +8,18 @@ VERSION=${1:-$(awk -F'"' '/"version"[[:space:]]*:/ {print $4; exit}' "$LOCK")} RELEASE_DIR="$ROOT/dist/release-$VERSION" STAGE="$ROOT/dist/package-$VERSION" -ARCHIVE="$ROOT/deploys/packages/sip-go-agent-$VERSION-linux-amd64.tar.gz" +ARCHIVE="$ROOT/dist/packages/sip-go-agent-$VERSION-linux-amd64.tar.gz" rm -rf -- "$RELEASE_DIR" "$STAGE" "$ARCHIVE" "$ARCHIVE.sha256" RELEASE_VERSION="$VERSION" "$ROOT/scripts/build-release.sh" "$RELEASE_DIR" mkdir -p "$STAGE" "$(dirname -- "$ARCHIVE")" cp -a "$RELEASE_DIR/." "$STAGE/" -mkdir -p "$STAGE/systemd" "$STAGE/env" "$STAGE/config" "$STAGE/cell" +mkdir -p "$STAGE/systemd" "$STAGE/env" "$STAGE/config" cp "$ROOT/deploys/install.sh" "$STAGE/install.sh" -cp "$ROOT/deploys/cell/nonprod-call-evidence.sh" "$STAGE/cell/nonprod-call-evidence.sh" cp "$ROOT/deploys/systemd/"*.service "$STAGE/systemd/" -cp "$ROOT/deploys/env/"*.env.example "$STAGE/env/" +cp "$ROOT/deploys/env/agent.env.example" "$ROOT/deploys/env/dispatcher.env.example" "$STAGE/env/" cp "$ROOT/deploys/config/agent-endpoints.example.json" "$STAGE/config/" cp "$LOCK" "$STAGE/versions.lock.json" -chmod 0755 "$STAGE/install.sh" "$STAGE/cell/nonprod-call-evidence.sh" +chmod 0755 "$STAGE/install.sh" chmod 0644 "$STAGE/systemd/"*.service "$STAGE/env/"*.env.example "$STAGE/config/agent-endpoints.example.json" "$STAGE/versions.lock.json" ( cd "$STAGE" diff --git a/deploys/config/agent-endpoints.example.json b/deploys/config/agent-endpoints.example.json index 371879a..d3987c7 100644 --- a/deploys/config/agent-endpoints.example.json +++ b/deploys/config/agent-endpoints.example.json @@ -4,11 +4,5 @@ "cell_id": "cell-a", "address": "agent-cell-a.internal:19090", "server_name": "agent-cell-a.internal" - }, - { - "agent_id": "agent-cell-b", - "cell_id": "cell-b", - "address": "agent-cell-b.internal:19090", - "server_name": "agent-cell-b.internal" } ] diff --git a/deploys/env/agent.env.example b/deploys/env/agent.env.example index ac311bc..ce6f7b7 100644 --- a/deploys/env/agent.env.example +++ b/deploys/env/agent.env.example @@ -16,18 +16,3 @@ DISPATCHER_AGENT_ENDPOINTS_FILE=/etc/sip-go-agent/agent-endpoints.json AGENT_STATIC_ARTIFACT=/etc/sip-go-agent/artifacts/cell-a.json AGENT_CALL_BUSINESS_LOG=/var/lib/sip-go-agent/agent/call-business.jsonl # AGENT_CALL_PHONE_LOG_KEY= -# Optional one-shot flow inputs; mode selects adapters, not a separate business path. -# AGENT_CALL_TARGET= -# AGENT_CALL_TRUNK_ID= -# AGENT_CALL_CALLER_ID= -# AGENT_CALL_MEDIA_BIND=127.0.0.1 -# AGENT_CALL_MEDIA_PORT=12000 -# AGENT_CALL_RECORDING_DIR=/var/lib/sip-go-agent/recordings -# AGENT_CALL_AI_SNAPSHOT=/etc/sip-go-agent/ai/full-ai-v1.json -# Required only when DISPATCHER_GRPC_ENDPOINT is enabled for --call-once evidence: -# AGENT_CALL_TENANT_ID= -# AGENT_CALL_TENANT_KEY= -# AGENT_CALL_TASK_ID= -# AGENT_CALL_TASK_ITEM_ID= -RABBITMQ_EXCHANGE=agent-call.commands.v1 -# RABBITMQ_URL= diff --git a/deploys/install.sh b/deploys/install.sh index cf25483..93db540 100755 --- a/deploys/install.sh +++ b/deploys/install.sh @@ -6,11 +6,9 @@ if [[ ${EUID} -ne 0 ]]; then exit 1 fi -ALLOW_NONPRODUCTION=false START=false for arg in "$@"; do case "$arg" in - --allow-nonproduction) ALLOW_NONPRODUCTION=true ;; --start) START=true ;; *) echo "unknown option: $arg" >&2; exit 2 ;; esac @@ -30,10 +28,8 @@ sha256sum -c package.SHA256SUMS if ! grep -q '"source_dirty": false' manifest.json || ! grep -q '"production_approval": true' manifest.json; then - if [[ "$ALLOW_NONPRODUCTION" != true ]]; then - echo 'release is dirty or not production-approved; use an approved package or --allow-nonproduction for smoke only' >&2 - exit 1 - fi + echo 'release is dirty or not production-approved' >&2 + exit 1 fi VERSION=$(awk -F'"' '/"version"[[:space:]]*:/ {print $4; exit}' manifest.json) @@ -47,14 +43,6 @@ install -d -m 0755 /opt/sip-go-agent/releases \ install -d -m 0700 -o rogee -g rogee \ /var/lib/sip-go-agent/agent /var/lib/sip-go-agent/agent/spool \ /var/lib/sip-go-agent/dispatcher -install -m 0755 cell/nonprod-call-evidence.sh /usr/local/sbin/agent-call-nonprod-evidence -command -v visudo >/dev/null || { echo 'visudo is required for the non-production capture gate' >&2; exit 1; } -cat >/etc/sudoers.d/agent-call-nonprod-evidence <<'EOF' -rogee ALL=(root) NOPASSWD: /usr/local/sbin/agent-call-nonprod-evidence -EOF -chmod 0440 /etc/sudoers.d/agent-call-nonprod-evidence -visudo -cf /etc/sudoers.d/agent-call-nonprod-evidence >/dev/null - RELEASE_DIR=/opt/sip-go-agent/releases/$VERSION install -d -m 0755 "$RELEASE_DIR" install -m 0755 sip-go-agent "$RELEASE_DIR/sip-go-agent" @@ -74,6 +62,7 @@ if [[ ! -e /etc/sip-go-agent/agent-endpoints.json ]]; then install -m 0644 config/agent-endpoints.example.json /etc/sip-go-agent/agent-endpoints.json fi install -m 0644 versions.lock.json /etc/sip-go-agent/versions.lock.json +rm -f /usr/local/sbin/agent-call-nonprod-evidence /etc/sudoers.d/agent-call-nonprod-evidence systemctl daemon-reload systemctl enable sip-go-agent-agent.service sip-go-agent-dispatcher.service diff --git a/deploys/packages/README.md b/deploys/packages/README.md index 1504b11..291852f 100644 --- a/deploys/packages/README.md +++ b/deploys/packages/README.md @@ -1,23 +1,12 @@ -# Release packages +# Asterisk production inputs -`../build-package.sh` writes the self-contained Linux package and checksum here: +This directory contains only the locked Asterisk source/dependency archives and +native stage used by the physical Cell deployment: -```text -sip-go-agent--linux-amd64.tar.gz -sip-go-agent--linux-amd64.tar.gz.sha256 -``` +- `asterisk-22.10.1-source.tar.gz` and its checksum +- `asterisk-22.10.1-deps/` +- `asterisk-22.10.1-native/` -The package contains only the SIP Agent/Dispatcher business binary, module -checksums, manifest, systemd units, safe environment templates, endpoint -template, version lock and `install.sh`. It contains no credentials, -certificates, phone log key, audio, provider configuration, MQ/OSS/AI runtime or -SaaS infrastructure. - -Asterisk 22.10.1 source, its third-party dependency cache and a validated -native stage are staged separately under `asterisk-22.10.1-*` and -`asterisk-22.10.1-native/`, with SHA-256 files and a locked Git commit. A management-approved physical Cell release can -use `deploys/cell/build-asterisk-native.sh` or the native stage, then must -own its systemd unit/config; -this Go package does not rewrite Asterisk. RabbitMQ, OSS and AI are SaaS -infrastructure endpoints rather than packages here; this directory does not -turn any of them into Docker services. +Build or install them with the scripts in `../cell/`. The Go Agent/Dispatcher +release is written to `../../dist/packages/`; RabbitMQ, OSS and AI are not +installed here. \ No newline at end of file diff --git a/deploys/packages/sip-go-agent-0.1.0-p1.20260919-linux-amd64.tar.gz.sha256 b/deploys/packages/sip-go-agent-0.1.0-p1.20260919-linux-amd64.tar.gz.sha256 deleted file mode 100644 index a6b7d31..0000000 --- a/deploys/packages/sip-go-agent-0.1.0-p1.20260919-linux-amd64.tar.gz.sha256 +++ /dev/null @@ -1 +0,0 @@ -fd38deece346f1f8ef820a465d41046ccf04ffa2a67c0791dba452a6234b00e5 deploys/packages/sip-go-agent-0.1.0-p1.20260919-linux-amd64.tar.gz diff --git a/deploys/physical-deployment.md b/deploys/physical-deployment.md index 8e5d218..841eaa9 100644 --- a/deploys/physical-deployment.md +++ b/deploys/physical-deployment.md @@ -1,16 +1,31 @@ -# Production physical deployment contract +# Physical production deployment -This project installs the Go Agent and single-active Dispatcher as ordinary -Debian 13 systemd services. The production path is not a Docker Compose stack. +Production is a small systemd installation on Debian 13 amd64: -- Version and host pins: `versions.lock.json`. -- Uploadable package: `packages/` after `build-package.sh`. -- Service units: `systemd/`. -- Safe configuration templates: `env/` and `config/`. -- Secrets/PKI/static Cell artifacts: injected by deployment, never packaged. -- Asterisk 22.10.1: separate approved physical Cell installation; this project - does not rewrite or containerize it. +1. native Asterisk Cell (`asterisk.service`), owned by the approved SIP + management release; +2. `sip-go-agent-agent.service`; +3. `sip-go-agent-dispatcher.service`. -The package installer deliberately refuses dirty or non-approved manifests for -production. `--allow-nonproduction` exists only for an explicitly labelled -smoke installation and does not change the manifest or claim P1 acceptance. +This project does not run production services in Docker and does not install +RabbitMQ, OSS, AI or SaaS infrastructure. Those systems are supplied through +injected endpoints and credentials. + +The pinned versions are in [`versions.lock.json`](versions.lock.json). Build +a release candidate with `build-package.sh`; it writes the archive to +`dist/packages/` and is intentionally not production-approved. Production +installation requires a clean, externally approved manifest. Build/install +Asterisk separately with the scripts in [`cell/`](cell/). The Asterisk +installer preserves `/etc/asterisk`; the management-approved static Cell +configuration is installed separately. + +The Go installer creates `/opt/sip-go-agent`, `/etc/sip-go-agent` and +`/var/lib/sip-go-agent`, installs the two Go units, and enables them. It does +not install test tooling, Docker, a broker, provider SDK credentials or AI +snapshots. Start services only after the injected environment, mTLS identity, +broker ACL and static Cell artifact have been reviewed. + +For local or isolated testing, use the Docker-backed RabbitMQ target +`make mq-integration-local`. Use [`test/nonprod-call-evidence.sh`](test/nonprod-call-evidence.sh) +for the required capture-first gate when testing against a native non-production +Asterisk host. Neither path is part of the production package. diff --git a/deploys/test/README.md b/deploys/test/README.md new file mode 100644 index 0000000..98b9821 --- /dev/null +++ b/deploys/test/README.md @@ -0,0 +1,27 @@ +# Test-only deployment helpers + +Nothing in this directory is installed by the production package. + +## Dependency infrastructure + +Use the existing Docker-backed target for RabbitMQ integration tests: + +```sh +make mq-integration-local +``` + +It starts a disposable RabbitMQ container, waits for readiness, runs the +integration tests and removes the container. Do not install RabbitMQ as a +systemd service or add it to a production host. + +## Native Asterisk validation + +`nonprod-call-evidence.sh` is the mandatory capture-first wrapper for +non-production `mock`, `mixed` and `real` call checks. It runs on a validation +host with native Asterisk and required diagnostics; it is not an Asterisk or +Agent replacement and is not containerized. Run it explicitly with the current +call authorization and the approved target/trunk. It refuses production mode +and fails closed when its prerequisites are missing. + +The offline OSS environment file and `ai-dental-meiba-v1.json` are fixtures for +isolated tests only. They contain no real credentials or production approval. diff --git a/deploys/config/ai-dental-meiba-v1.json b/deploys/test/ai-dental-meiba-v1.json similarity index 100% rename from deploys/config/ai-dental-meiba-v1.json rename to deploys/test/ai-dental-meiba-v1.json diff --git a/deploys/env/dispatcher.offline-oss.env.example b/deploys/test/dispatcher.offline-oss.env.example similarity index 100% rename from deploys/env/dispatcher.offline-oss.env.example rename to deploys/test/dispatcher.offline-oss.env.example diff --git a/deploys/cell/nonprod-call-evidence.sh b/deploys/test/nonprod-call-evidence.sh similarity index 100% rename from deploys/cell/nonprod-call-evidence.sh rename to deploys/test/nonprod-call-evidence.sh diff --git a/docs/evidence/20260919-single-node-ai-closure.md b/docs/evidence/20260919-single-node-ai-closure.md index e4ca794..8b658b7 100644 --- a/docs/evidence/20260919-single-node-ai-closure.md +++ b/docs/evidence/20260919-single-node-ai-closure.md @@ -54,7 +54,7 @@ The second Cell is intentionally out of this iteration. - The three earlier bounded real-provider CallFlow attempts and their no-`StasisStart` causes remain recorded in `docs/evidence/20260919-real-provider-callflow-attempts.json`. -- The new 美吧口腔 policy is in `deploys/config/ai-dental-meiba-v1.json`: it +- The new 美吧口腔 policy is in `deploys/test/ai-dental-meiba-v1.json`: it identifies 美吧口腔, asks which dental project the user wants, limits the call to three effective turns/120 seconds, and requires the `[INVALID_CALL]` early-stop marker for invalid calls. diff --git a/docs/evidence/20260920-nonprod-capture-preflight-v6.json b/docs/evidence/20260920-nonprod-capture-preflight-v6.json index 7499a66..ca0037b 100644 --- a/docs/evidence/20260920-nonprod-capture-preflight-v6.json +++ b/docs/evidence/20260920-nonprod-capture-preflight-v6.json @@ -10,7 +10,7 @@ "tcpdump_filter": "udp port 5060 or udp portrange 10000-10800" }, "entrypoint": { - "path": "deploys/cell/nonprod-call-evidence.sh", + "path": "deploys/test/nonprod-call-evidence.sh", "remote_path": "/usr/local/sbin/agent-call-nonprod-evidence", "call_id": "preflight-20260920-v6", "mode": "--preflight-only", diff --git a/docs/evidence/20260920-nonprod-capture-preflight.json b/docs/evidence/20260920-nonprod-capture-preflight.json index 24ee21b..5797e46 100644 --- a/docs/evidence/20260920-nonprod-capture-preflight.json +++ b/docs/evidence/20260920-nonprod-capture-preflight.json @@ -13,7 +13,7 @@ "pcap_bytes": 24 }, "preflight": { - "entrypoint": "deploys/cell/nonprod-call-evidence.sh", + "entrypoint": "deploys/test/nonprod-call-evidence.sh", "call_id": "preflight-20260920-v2", "run_command": "/bin/true", "entrypoint_exit": 0, diff --git a/docs/evidence/20260920-offline-oss-profile.md b/docs/evidence/20260920-offline-oss-profile.md index 1362975..055109f 100644 --- a/docs/evidence/20260920-offline-oss-profile.md +++ b/docs/evidence/20260920-offline-oss-profile.md @@ -1,7 +1,7 @@ # 2026-09-20 非 ECS OSS 测试配置 - 离线/非 ECS OSS 测试使用 `cn-beijing` 的公网 Endpoint:`oss-cn-beijing.aliyuncs.com`。 -- 可复用示例:`deploys/env/dispatcher.offline-oss.env.example`。 +- 可复用示例:`deploys/test/dispatcher.offline-oss.env.example`。 - 生产 ECS 示例 `deploys/env/dispatcher.env.example` 继续使用受控内网 Endpoint,不与离线配置混用。 - AK/SK 仅通过受控运行时文件注入;本证据不保存凭据。 - 普通离线 OSS 测试不创建、释放或清理 ECS;ECS 仅在另行授权的真实外呼阶段使用。 diff --git a/docs/evidence/20260920-real-provider-second-3turn-attempt.json b/docs/evidence/20260920-real-provider-second-3turn-attempt.json index 3d0f147..d9243c5 100644 --- a/docs/evidence/20260920-real-provider-second-3turn-attempt.json +++ b/docs/evidence/20260920-real-provider-second-3turn-attempt.json @@ -9,7 +9,7 @@ "fixed_eip": "123.56.71.98", "package": "deploys/packages/sip-go-agent-0.1.0-p1.20260919-linux-amd64.tar.gz", "package_sha256": "b189e2cf2d51417fcb5b56a190b1f93ad8fa5e33b6e3f2eabdafb9ac66eaa10a", - "ai_snapshot": "deploys/config/ai-dental-meiba-v1.json", + "ai_snapshot": "deploys/test/ai-dental-meiba-v1.json", "ai_version_id": "agent_dental_meiba_v1" }, "route": { diff --git a/docs/evidence/20260920-real-provider-second-capture-first-v4.json b/docs/evidence/20260920-real-provider-second-capture-first-v4.json index f63d4f4..210b605 100644 --- a/docs/evidence/20260920-real-provider-second-capture-first-v4.json +++ b/docs/evidence/20260920-real-provider-second-capture-first-v4.json @@ -20,7 +20,7 @@ "capture_filter": "udp port 5060 or udp portrange 10000-10800" }, "entrypoint": { - "path": "deploys/cell/nonprod-call-evidence.sh", + "path": "deploys/test/nonprod-call-evidence.sh", "remote_path": "/usr/local/sbin/agent-call-nonprod-evidence", "capture_first": true, "pjsip_logger_enabled_before_call": true, diff --git a/docs/evidence/20260922-mq-only-local-final.md b/docs/evidence/20260922-mq-only-local-final.md index 35e45e8..ae74c1e 100644 --- a/docs/evidence/20260922-mq-only-local-final.md +++ b/docs/evidence/20260922-mq-only-local-final.md @@ -57,6 +57,6 @@ Dispatcher 配置文件是 OSS 配置唯一来源;grant 固定 15 分钟。Age ## 未执行与边界 -已按 `deploys/cell/nonprod-call-evidence.sh --preflight-only` 尝试诊断;当前开发主机以非root运行,入口立即以 `must run as root for tcpdump and Asterisk diagnostics` fail-closed(日志 `/tmp/go-sip-nonprod-preflight-missing.log`)。同时核验主机缺少 Asterisk、tcpdump,systemd 为 degraded。因此没有伪造 mixed/real 抓包、真实外呼或部署诊断通过。项目的本地 mock/协议回归通过不替代该诊断,也不替代第二阶段真实供应商/SaaS/生产验收。 +已按 `deploys/test/nonprod-call-evidence.sh --preflight-only` 尝试诊断;当前开发主机以非root运行,入口立即以 `must run as root for tcpdump and Asterisk diagnostics` fail-closed(日志 `/tmp/go-sip-nonprod-preflight-missing.log`)。同时核验主机缺少 Asterisk、tcpdump,systemd 为 degraded。因此没有伪造 mixed/real 抓包、真实外呼或部署诊断通过。项目的本地 mock/协议回归通过不替代该诊断,也不替代第二阶段真实供应商/SaaS/生产验收。 本目标明确要求保留工作树自有改动且不自动提交、暂存或清理;因此当前改动保持未提交/未暂存。验收依据是上述可复现命令、日志和源码检查,不是任务树声明或提交状态。最终工作树摘要另保存于 `/tmp/go-sip-working-tree-final.txt`。 diff --git a/docs/plan-0918.md b/docs/plan-0918.md index 46ff374..f2e874c 100644 --- a/docs/plan-0918.md +++ b/docs/plan-0918.md @@ -267,7 +267,7 @@ go build ./... | W05/W08/W12 / 完成 | 旧SaaS HTTP业务入口已删除;MQ查询/补传、控制worker、重复/丢回复/SQLite重启、断连和迟到revision有本地往返证据,见`mq-control-recovery.md`、查询/补传证据及`20260922-mq-only-local-final.md` | 外部SaaS receipt不在本轮;accepted不等于applied,不重发执行当补传 | | W07 / 完成 | 实际本地RabbitMQ配置请求/内嵌授权响应、原请求关联、重复、范围和SQLite恢复通过,见`mq-ai-local-roundtrip.md` | 不发明独立授权消息;Agent动态交付边界仍按现有Unary合同 | | W11 / 完成 | D配置文件、固定15分钟授权、原请求/显式新请求、单次PUT及recording.uploaded恢复已有本地证据,见`20260921-mq-upload-progress.md` | 不等待SaaS verified/OSS ID;不宣称SaaS消费 | -| W13/W14 / 完成 | 本地/隔离联合MQ、D1/D2隔离、断连/不可路由/confirm/DLQ/重启/覆盖率和acceptance已通过;`deploys/cell/nonprod-call-evidence.sh --preflight-only`已实际执行并因当前主机缺Asterisk/tcpdump且非root而fail-closed,未将其记为mixed/real通过 | 本地门禁已解除;物理部署诊断具备相应主机条件后另行执行,不得用本地Mock代替mixed/real诊断 | +| W13/W14 / 完成 | 本地/隔离联合MQ、D1/D2隔离、断连/不可路由/confirm/DLQ/重启/覆盖率和acceptance已通过;`deploys/test/nonprod-call-evidence.sh --preflight-only`已实际执行并因当前主机缺Asterisk/tcpdump且非root而fail-closed,未将其记为mixed/real通过 | 本地门禁已解除;物理部署诊断具备相应主机条件后另行执行,不得用本地Mock代替mixed/real诊断 | 当前负责人为本会话Agent,用户明确要求不启动子Agent。实现前基线已完成,见[基线证据](archive/evidence/20260921-mq-only-adjustment-baseline.md);v2方向及AI JCS摘要已获确认,本地v3契约和主要MQ实现已有新增证据。W01/W02/W05/W07/W08/W11/W12的项目内门禁已按§8.2关闭;W04项目内门禁和W13/W14本地/隔离门禁已关闭;外部权威签收及物理部署诊断需具备相应外部条件后另行执行,不属于本地MQ-only目标的完成条件。旧通过数和`docs/archive/evidence/20260920-local-p1-acceptance.md`不覆盖新修订。 diff --git a/internal/config/deployment_layout_test.go b/internal/config/deployment_layout_test.go index e3ce724..133815e 100644 --- a/internal/config/deployment_layout_test.go +++ b/internal/config/deployment_layout_test.go @@ -12,7 +12,7 @@ func TestDeploymentHasOneCanonicalDirectory(t *testing.T) { if _, err := os.Stat(filepath.Join(root, "deploy")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("obsolete deploy directory must not remain: %v", err) } - for _, path := range []string{"README.md", "build-package.sh", "install.sh", "versions.lock.json", "cell/install-asterisk-native.sh", "cell/nonprod-call-evidence.sh", "systemd/sip-go-agent-agent.service", "systemd/sip-go-agent-dispatcher.service", "config/dispatcher.json.example"} { + for _, path := range []string{"README.md", "build-package.sh", "install.sh", "versions.lock.json", "cell/install-asterisk-native.sh", "test/nonprod-call-evidence.sh", "test/README.md", "systemd/sip-go-agent-agent.service", "systemd/sip-go-agent-dispatcher.service", "config/dispatcher.json.example"} { info, err := os.Stat(filepath.Join(root, "deploys", path)) if err != nil { t.Errorf("canonical deployment entry %s: %v", path, err) diff --git a/internal/dispatcher/mq_integration_test.go b/internal/dispatcher/mq_integration_test.go index 69c0eed..4dea88d 100644 --- a/internal/dispatcher/mq_integration_test.go +++ b/internal/dispatcher/mq_integration_test.go @@ -11,7 +11,9 @@ import ( "git.ipao.vip/rogee/go-sip/internal/mq" "git.ipao.vip/rogee/go-sip/internal/store" + "git.ipao.vip/rogee/go-sip/internal/tenant" "git.ipao.vip/rogee/go-sip/internal/testfixture" + amqp "github.com/rabbitmq/amqp091-go" ) func TestLocalDispatcherConsumesCommandIntoSQLiteAndPublishesOutbox(t *testing.T) { @@ -19,7 +21,7 @@ func TestLocalDispatcherConsumesCommandIntoSQLiteAndPublishesOutbox(t *testing.T if url == "" { t.Skip("RABBITMQ_URL is not configured") } - broker, err := mq.OpenWithPrefetch(url, "", 1) + broker, err := mq.OpenWithPrefetch(url, testfixture.DispatcherID, 1) if err != nil { t.Fatal(err) } @@ -37,6 +39,9 @@ func TestLocalDispatcherConsumesCommandIntoSQLiteAndPublishesOutbox(t *testing.T t.Error(err) } }) + if err := st.BindDispatcherID(testfixture.DispatcherID); err != nil { + t.Fatal(err) + } d, err := New(st, broker, time.Now) if err != nil { t.Fatal(err) @@ -46,17 +51,52 @@ func TestLocalDispatcherConsumesCommandIntoSQLiteAndPublishesOutbox(t *testing.T t.Fatal(err) } const tenantKey = "tenant-demo-key" - routingKey := "agent-call.tenant." + tenantKey + ".call.execute" + route, err := tenant.NewDispatcherRoute(testfixture.DispatcherID, tenantKey) + if err != nil { + t.Fatal(err) + } if _, err := broker.DeclareTenantQueue(tenantKey); err != nil { t.Fatal(err) } + routingKey := route.InboundKey ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second) defer cancel() consumeDone := make(chan error, 1) go func() { consumeDone <- d.ConsumeTenant(ctx, broker, tenantKey) }() - if err := broker.Publish(ctx, mq.DefaultExchange, routingKey, raw); err != nil { + connection, err := amqp.Dial(url) + if err != nil { t.Fatal(err) } + defer connection.Close() + publishChannel, err := connection.Channel() + if err != nil { + t.Fatal(err) + } + defer publishChannel.Close() + if err := publishChannel.Confirm(false); err != nil { + t.Fatal(err) + } + returned := publishChannel.NotifyReturn(make(chan amqp.Return, 1)) + confirmation, err := publishChannel.PublishWithDeferredConfirmWithContext(ctx, mq.DefaultExchange, routingKey, true, false, amqp.Publishing{ + ContentType: "application/json", + DeliveryMode: amqp.Persistent, + Body: raw, + }) + if err != nil { + t.Fatal(err) + } + if confirmation == nil { + t.Fatal("command publication confirmation unavailable") + } + acked, err := confirmation.WaitContext(ctx) + if err != nil || !acked { + t.Fatalf("command publication was not confirmed: %v", err) + } + select { + case result := <-returned: + t.Fatalf("command publication returned: code=%d", result.ReplyCode) + default: + } deadline := time.Now().Add(10 * time.Second) for { var taskCount, outboxCount int