feat: add b-route send adapter shell

This commit is contained in:
zhaoyilun
2026-07-10 20:40:18 +08:00
parent b08b0e01bd
commit 713917d929
5 changed files with 178 additions and 7 deletions

View File

@@ -58,7 +58,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
## Current loop ## Current loop
Current loop: `R6h central production gate policy for send message complete; next R6i B-route connector adapter shell`. Current loop: `R6i B-route connector adapter shell complete; next R6j online sandbox-send recording package v2`.
Active continuous execution plan: `docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md`. Active continuous execution plan: `docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md`.
@@ -239,8 +239,8 @@ Continuous execution plan R6f-R14 is approved and execution has started:
- Plan file: `docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md`. - Plan file: `docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md`.
- Scope: 15 ordered rounds covering send-message connector/gate hardening, send-file preview/idempotency/package, receive-file download preview, receive-message reconciliation, end-to-end business smoke, and release-candidate report. - Scope: 15 ordered rounds covering send-message connector/gate hardening, send-file preview/idempotency/package, receive-file download preview, receive-message reconciliation, end-to-end business smoke, and release-candidate report.
- Execution rule after approval: one round at a time, status-card update, verification, commit, push, then continue automatically until an evidence-only blocker requires user action. - Execution rule after approval: one round at a time, status-card update, verification, commit, push, then continue automatically until an evidence-only blocker requires user action.
- Last completed round: R6h central production gate policy for send message. - Last completed round: R6i B-route connector adapter shell.
- Next active round: R6i B-route connector adapter shell. - Next active round: R6j online sandbox-send recording package v2.
R6g send audit and idempotency replay diagnostics is complete: R6g send audit and idempotency replay diagnostics is complete:
@@ -259,3 +259,12 @@ R6h central production gate policy for send message is complete:
- `ISPHERE_SEND_PRODUCTION_ENABLED=1` is recognized by policy tests, but it is not enough by itself; evidence and connector gates must also pass. - `ISPHERE_SEND_PRODUCTION_ENABLED=1` is recognized by policy tests, but it is not enough by itself; evidence and connector gates must also pass.
- Standard verification still reports 9 tools and `production_send_enabled=false`. - Standard verification still reports 9 tools and `production_send_enabled=false`.
- Next node: R6i B-route connector adapter shell. - Next node: R6i B-route connector adapter shell.
R6i B-route connector adapter shell is complete:
- Added `BRouteSendAdapterConfig` and `NewBRouteSendMessageConnector`.
- `disabled` mode returns `broute_mode_blocked`.
- `dry_run_contract` validates request shape and returns `broute_dry_run_only`.
- The shell does not start sidecars, load DLLs, click/type UI, replay network traffic, or upload files.
- Standard verification still reports 9 tools and `production_send_enabled=false`.
- Next node: R6j online sandbox-send recording package v2.

View File

@@ -119,3 +119,21 @@ R6b B-route preview/dry-run contract is complete. `isphere_send_message` now pla
Next node: **R6c online sandbox-send evidence gate**. Next node: **R6c online sandbox-send evidence gate**.
R6c should collect or prepare the one approved sandbox-send evidence package needed before production can be exposed. It must still prove success/ack or sent-record evidence and duplicate idempotency protection before any production send path is enabled. R6c should collect or prepare the one approved sandbox-send evidence package needed before production can be exposed. It must still prove success/ack or sent-record evidence and duplicate idempotency protection before any production send path is enabled.
## R6i B-route adapter shell update
R6i adds a Go-side connector adapter shell only:
- `BRouteSendAdapterConfig`
- `NewBRouteSendMessageConnector`
- `Mode="disabled"`
- `Mode="dry_run_contract"`
The shell does not start a sidecar process, does not load DLLs, does not click/type UI, does not send network traffic, and does not upload files.
Current behavior:
- `disabled` returns `ErrorCode="broute_mode_blocked"` and a blocked connector result.
- `dry_run_contract` validates the request shape and returns `ErrorCode="broute_dry_run_only"` with `ConnectorMode="broute-dry-run-contract"`.
Decision: B-route now has a stable Go connector boundary for later sidecar work, but production `isphere_send_message` remains blocked until the strict online sandbox-send evidence gate passes and a validated sidecar exists.

View File

@@ -296,7 +296,7 @@ git push gitea main
- Produces: `func NewBRouteSendMessageConnector(config BRouteSendAdapterConfig) SendMessageConnector`. - Produces: `func NewBRouteSendMessageConnector(config BRouteSendAdapterConfig) SendMessageConnector`.
- In this round, accepted modes are `disabled` and `dry_run_contract`; any other mode returns `broute_mode_blocked`. - In this round, accepted modes are `disabled` and `dry_run_contract`; any other mode returns `broute_mode_blocked`.
- [ ] **Step 1: Write disabled-mode test** - [x] **Step 1: Write disabled-mode test**
```go ```go
func TestBRouteSendMessageConnectorDisabledMode(t *testing.T) { func TestBRouteSendMessageConnectorDisabledMode(t *testing.T) {
@@ -309,15 +309,15 @@ func TestBRouteSendMessageConnectorDisabledMode(t *testing.T) {
} }
``` ```
- [ ] **Step 2: Write dry-run contract test** - [x] **Step 2: Write dry-run contract test**
Add `TestBRouteSendMessageConnectorDryRunContract` asserting no external process starts and result contains `ConnectorMode="broute-dry-run-contract"` and `Accepted=false`. Add `TestBRouteSendMessageConnectorDryRunContract` asserting no external process starts and result contains `ConnectorMode="broute-dry-run-contract"` and `Accepted=false`.
- [ ] **Step 3: Implement adapter shell** - [x] **Step 3: Implement adapter shell**
Implement a connector that validates request fields and returns contract-shaped blocked/dry-run results only. Do not call `Start-Process`, do not load DLLs, do not click/type. Implement a connector that validates request fields and returns contract-shaped blocked/dry-run results only. Do not call `Start-Process`, do not load DLLs, do not click/type.
- [ ] **Step 4: Verify and commit** - [x] **Step 4: Verify and commit**
```powershell ```powershell
git diff --check git diff --check
@@ -332,6 +332,14 @@ git push gitea main
**Acceptance gate:** B-route contract exists; no real send entrypoint is invoked. **Acceptance gate:** B-route contract exists; no real send entrypoint is invoked.
**R6i Result:**
- Added `internal/tools/send_message_broute_adapter.go` with `BRouteSendAdapterConfig` and `NewBRouteSendMessageConnector`.
- Added disabled-mode and dry-run-contract tests.
- `disabled` returns `broute_mode_blocked`; `dry_run_contract` validates request shape and returns `broute_dry_run_only`.
- The adapter shell does not start a sidecar, load DLLs, click/type UI, replay network traffic, or upload files.
- Production `isphere_send_message` remains blocked in the default MCP runtime.
--- ---
### R6j: Online sandbox-send recording package v2 ### R6j: Online sandbox-send recording package v2

View File

@@ -0,0 +1,81 @@
package tools
import (
"context"
"fmt"
"strings"
)
type BRouteSendAdapterConfig struct {
SidecarPath string
TimeoutSeconds int
Mode string
}
type bRouteSendMessageConnector struct {
config BRouteSendAdapterConfig
}
func NewBRouteSendMessageConnector(config BRouteSendAdapterConfig) SendMessageConnector {
config.Mode = strings.TrimSpace(strings.ToLower(config.Mode))
if config.Mode == "" {
config.Mode = "disabled"
}
return bRouteSendMessageConnector{config: config}
}
func (c bRouteSendMessageConnector) ExecuteSendMessage(_ context.Context, request SendMessageConnectorRequest) (SendMessageConnectorResult, error) {
switch c.config.Mode {
case "disabled":
result := SendMessageConnectorResult{
Accepted: false,
Status: "blocked",
ErrorCode: "broute_mode_blocked",
ErrorMessage: "B-route send adapter is disabled",
ConnectorMode: "broute-disabled",
}
return result, fmt.Errorf(result.ErrorMessage)
case "dry_run_contract":
if err := validateBRouteSendMessageRequest(request); err != nil {
return SendMessageConnectorResult{
Accepted: false,
Status: "failed",
ErrorCode: "broute_invalid_request",
ErrorMessage: err.Error(),
ConnectorMode: "broute-dry-run-contract",
}, nil
}
return SendMessageConnectorResult{
Accepted: false,
Status: "blocked",
ErrorCode: "broute_dry_run_only",
ErrorMessage: "B-route adapter dry_run_contract mode validates request shape only and does not start a sidecar",
ConnectorMode: "broute-dry-run-contract",
}, nil
default:
result := SendMessageConnectorResult{
Accepted: false,
Status: "blocked",
ErrorCode: "broute_mode_blocked",
ErrorMessage: "B-route send adapter mode is not enabled: " + c.config.Mode,
ConnectorMode: "broute-disabled",
}
return result, fmt.Errorf(result.ErrorMessage)
}
}
func validateBRouteSendMessageRequest(request SendMessageConnectorRequest) error {
if strings.TrimSpace(request.TargetRef) == "" {
return fmt.Errorf("target_ref is required")
}
if strings.TrimSpace(request.ContentSHA256) == "" {
return fmt.Errorf("content_sha256 is required")
}
if strings.TrimSpace(request.IdempotencyKeySHA256) == "" {
return fmt.Errorf("idempotency_key_sha256 is required")
}
if request.ExecutionMode != sendMessageProductionMode {
return fmt.Errorf("execution_mode must be production")
}
return nil
}

View File

@@ -0,0 +1,55 @@
package tools
import (
"context"
"strings"
"testing"
)
func TestBRouteSendMessageConnectorDisabledMode(t *testing.T) {
connector := NewBRouteSendMessageConnector(BRouteSendAdapterConfig{Mode: "disabled"})
result, err := connector.ExecuteSendMessage(context.Background(), SendMessageConnectorRequest{
TargetType: "direct",
TargetID: "u1",
TargetRef: "contact:u1",
ContentSHA256: "abc",
ContentLength: 3,
IdempotencyKeySHA256: "idem",
ExecutionMode: "production",
})
if err == nil {
t.Fatalf("expected disabled error")
}
if result.ErrorCode != "broute_mode_blocked" {
t.Fatalf("unexpected error code: %s", result.ErrorCode)
}
if result.ConnectorMode != "broute-disabled" {
t.Fatalf("connector mode = %q, want broute-disabled", result.ConnectorMode)
}
}
func TestBRouteSendMessageConnectorDryRunContract(t *testing.T) {
connector := NewBRouteSendMessageConnector(BRouteSendAdapterConfig{
SidecarPath: `Z:\path\that\must\not\be\started\SendSidecar.exe`,
TimeoutSeconds: 5,
Mode: "dry_run_contract",
})
result, err := connector.ExecuteSendMessage(context.Background(), SendMessageConnectorRequest{
TargetType: "direct",
TargetID: "u1",
TargetRef: "contact:u1",
ContentSHA256: strings.Repeat("a", 64),
ContentLength: 3,
IdempotencyKeySHA256: strings.Repeat("b", 64),
ExecutionMode: "production",
})
if err != nil {
t.Fatalf("dry-run contract should not try to start a sidecar or return process error: %v", err)
}
if result.Accepted {
t.Fatalf("dry-run contract must not accept a real send: %+v", result)
}
if result.ConnectorMode != "broute-dry-run-contract" || result.ErrorCode != "broute_dry_run_only" {
t.Fatalf("unexpected dry-run result: %+v", result)
}
}