feat: add b-route send adapter shell
This commit is contained in:
@@ -58,7 +58,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
|
||||
|
||||
## 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`.
|
||||
|
||||
@@ -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`.
|
||||
- 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.
|
||||
- Last completed round: R6h central production gate policy for send message.
|
||||
- Next active round: R6i B-route connector adapter shell.
|
||||
- Last completed 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:
|
||||
|
||||
@@ -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.
|
||||
- Standard verification still reports 9 tools and `production_send_enabled=false`.
|
||||
- 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.
|
||||
|
||||
@@ -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**.
|
||||
|
||||
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.
|
||||
|
||||
@@ -296,7 +296,7 @@ git push gitea main
|
||||
- Produces: `func NewBRouteSendMessageConnector(config BRouteSendAdapterConfig) SendMessageConnector`.
|
||||
- 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
|
||||
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`.
|
||||
|
||||
- [ ] **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.
|
||||
|
||||
- [ ] **Step 4: Verify and commit**
|
||||
- [x] **Step 4: Verify and commit**
|
||||
|
||||
```powershell
|
||||
git diff --check
|
||||
@@ -332,6 +332,14 @@ git push gitea main
|
||||
|
||||
**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
|
||||
|
||||
81
internal/tools/send_message_broute_adapter.go
Normal file
81
internal/tools/send_message_broute_adapter.go
Normal 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
|
||||
}
|
||||
55
internal/tools/send_message_broute_adapter_test.go
Normal file
55
internal/tools/send_message_broute_adapter_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user