diff --git a/docs/current-status-card.md b/docs/current-status-card.md index c3b39a1..c203875 100644 --- a/docs/current-status-card.md +++ b/docs/current-status-card.md @@ -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. diff --git a/docs/source-discovery/2026-07-10-send-connector-preflight.md b/docs/source-discovery/2026-07-10-send-connector-preflight.md index d1138d5..d50db18 100644 --- a/docs/source-discovery/2026-07-10-send-connector-preflight.md +++ b/docs/source-discovery/2026-07-10-send-connector-preflight.md @@ -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. diff --git a/docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md b/docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md index 81484ac..2e18cbb 100644 --- a/docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md +++ b/docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md @@ -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 diff --git a/internal/tools/send_message_broute_adapter.go b/internal/tools/send_message_broute_adapter.go new file mode 100644 index 0000000..c03cedb --- /dev/null +++ b/internal/tools/send_message_broute_adapter.go @@ -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 +} diff --git a/internal/tools/send_message_broute_adapter_test.go b/internal/tools/send_message_broute_adapter_test.go new file mode 100644 index 0000000..7ea619b --- /dev/null +++ b/internal/tools/send_message_broute_adapter_test.go @@ -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) + } +}