diff --git a/cmd/isphere-capability-smoke/main.go b/cmd/isphere-capability-smoke/main.go new file mode 100644 index 0000000..9d9e105 --- /dev/null +++ b/cmd/isphere-capability-smoke/main.go @@ -0,0 +1,585 @@ +package main + +import ( + "context" + "crypto/cipher" + "crypto/des" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "sort" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "isphere-ai-bridge/internal/mcpserver" +) + +var expectedTools = []string{ + "win_helper_version", + "win_helper_self_check", + "win_helper_scan_windows", + "win_helper_dump_uia", + "isphere_receive_messages", + "isphere_search_contacts", + "isphere_search_groups", + "isphere_receive_files", + "isphere_send_message", +} + +type smokeResult struct { + OK bool `json:"ok"` + Package string `json:"package"` + Transport string `json:"transport"` + Initialized bool `json:"initialized"` + ToolCount int `json:"tool_count"` + Tools []string `json:"tools"` + HelperName string `json:"helper_name"` + ReceiveMessageCount int `json:"receive_message_count"` + ContactCount int `json:"contact_count"` + GroupCount int `json:"group_count"` + FileCount int `json:"file_count"` + FixtureReceiveMessageCount int `json:"fixture_receive_message_count"` + FixtureContactCount int `json:"fixture_contact_count"` + FixtureGroupCount int `json:"fixture_group_count"` + FixtureFileCount int `json:"fixture_file_count"` + FixtureDirReceiveMessageCount int `json:"fixture_dir_receive_message_count"` + FixtureDirContactCount int `json:"fixture_dir_contact_count"` + FixtureDirGroupCount int `json:"fixture_dir_group_count"` + FixtureDirFileCount int `json:"fixture_dir_file_count"` + SendPreviewToolPresent bool `json:"send_preview_tool_present"` + ProductionSendEnabled bool `json:"production_send_enabled"` + LocalLoginRequired bool `json:"local_login_required"` + RealISphereLoginRequired bool `json:"real_isphere_login_required"` + PythonRuntimeRequired bool `json:"python_runtime_required"` + NoRealSend bool `json:"no_real_send"` + ActionToolsPresent bool `json:"action_tools_present"` + PacketLogFileConfiguredAfterRun bool `json:"packet_log_file_configured_after_run"` + PacketLogDirConfiguredAfterRun bool `json:"packet_log_dir_configured_after_run"` + MsgLibConfigured bool `json:"msglib_configured"` + SendAuditPath string `json:"send_audit_path"` +} + +func main() { + if err := run(); err != nil { + fail("capability_smoke", err) + } +} + +func run() error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + clearRuntimeEnv() + auditPath := filepath.Join("runs", "capability-smoke", "send-preview.jsonl") + if err := os.Setenv("ISPHERE_SEND_AUDIT_PATH", auditPath); err != nil { + return err + } + + session, cleanup, err := newSession(ctx, "isphere-capability-smoke") + if err != nil { + return err + } + defer cleanup() + + toolNames, err := listToolNames(ctx, session) + if err != nil { + return err + } + if err := requireExpectedTools(toolNames); err != nil { + return err + } + + helperName, err := verifyHelperVersion(ctx, session) + if err != nil { + return err + } + receiveCount, err := verifyReceiveMessages(ctx, session, 0) + if err != nil { + return err + } + contactCount, err := verifySearchContacts(ctx, session, "sender", 0) + if err != nil { + return err + } + groupCount, err := verifySearchGroups(ctx, session, "project", 0) + if err != nil { + return err + } + fileCount, err := verifyReceiveFiles(ctx, session, "report", 0, "", "") + if err != nil { + return err + } + sendPreviewOK, productionSendEnabled, err := verifySendMessage(ctx, session) + if err != nil { + return err + } + + fixtureReceiveCount, fixtureContactCount, fixtureGroupCount, fixtureFileCount, err := verifyPacketLogFileFixture(ctx) + if err != nil { + return err + } + fixtureDirReceiveCount, fixtureDirContactCount, fixtureDirGroupCount, fixtureDirFileCount, err := verifyPacketLogDirFixture(ctx) + if err != nil { + return err + } + + result := smokeResult{ + OK: true, + Package: "send-capability-test", + Transport: "sdk_in_memory", + Initialized: true, + ToolCount: len(toolNames), + Tools: toolNames, + HelperName: helperName, + ReceiveMessageCount: receiveCount, + ContactCount: contactCount, + GroupCount: groupCount, + FileCount: fileCount, + FixtureReceiveMessageCount: fixtureReceiveCount, + FixtureContactCount: fixtureContactCount, + FixtureGroupCount: fixtureGroupCount, + FixtureFileCount: fixtureFileCount, + FixtureDirReceiveMessageCount: fixtureDirReceiveCount, + FixtureDirContactCount: fixtureDirContactCount, + FixtureDirGroupCount: fixtureDirGroupCount, + FixtureDirFileCount: fixtureDirFileCount, + SendPreviewToolPresent: sendPreviewOK, + ProductionSendEnabled: productionSendEnabled, + LocalLoginRequired: false, + RealISphereLoginRequired: false, + PythonRuntimeRequired: false, + NoRealSend: true, + ActionToolsPresent: false, + PacketLogFileConfiguredAfterRun: os.Getenv("ISPHERE_PACKET_LOG_FILE") != "", + PacketLogDirConfiguredAfterRun: os.Getenv("ISPHERE_PACKET_LOG_DIR") != "", + MsgLibConfigured: false, + SendAuditPath: auditPath, + } + encoded, err := json.Marshal(result) + if err != nil { + return err + } + fmt.Println(string(encoded)) + return nil +} + +func clearRuntimeEnv() { + for _, key := range []string{ + "ISPHERE_PACKET_LOG_FILE", + "ISPHERE_PACKET_LOG_DIR", + "ISPHERE_MSGLIB_SIDECAR_EXE", + "ISPHERE_MSGLIB_SQLITE_DLL", + "ISPHERE_MSGLIB_DB", + "ISPHERE_MSGLIB_PASSWORD", + } { + _ = os.Unsetenv(key) + } +} + +func newSession(ctx context.Context, name string) (*mcp.ClientSession, func(), error) { + serverTransport, clientTransport := mcp.NewInMemoryTransports() + server, err := mcpserver.NewServerFromEnv() + if err != nil { + return nil, nil, fmt.Errorf("server config: %w", err) + } + serverCtx, cancel := context.WithCancel(ctx) + serverErr := make(chan error, 1) + go func() { + serverErr <- server.Run(serverCtx, serverTransport) + }() + + client := mcp.NewClient(&mcp.Implementation{Name: name, Version: "0.1.0"}, nil) + session, err := client.Connect(serverCtx, clientTransport, nil) + if err != nil { + cancel() + return nil, nil, fmt.Errorf("connect: %w", err) + } + cleanup := func() { + _ = session.Close() + cancel() + select { + case <-serverErr: + case <-time.After(2 * time.Second): + fmt.Fprintln(os.Stderr, `{"ok":false,"step":"server_shutdown","error":"server did not stop before timeout"}`) + } + } + return session, cleanup, nil +} + +func listToolNames(ctx context.Context, session *mcp.ClientSession) ([]string, error) { + result, err := session.ListTools(ctx, &mcp.ListToolsParams{}) + if err != nil { + return nil, fmt.Errorf("list tools: %w", err) + } + names := make([]string, 0, len(result.Tools)) + for _, tool := range result.Tools { + names = append(names, tool.Name) + } + sort.Strings(names) + return names, nil +} + +func requireExpectedTools(got []string) error { + want := append([]string(nil), expectedTools...) + sort.Strings(want) + if !reflect.DeepEqual(got, want) { + return fmt.Errorf("tool names = %#v, want %#v", got, want) + } + return nil +} + +func verifyHelperVersion(ctx context.Context, session *mcp.ClientSession) (string, error) { + payload, err := callToolObject(ctx, session, "win_helper_version", map[string]any{}) + if err != nil { + return "", err + } + if payload["ok"] != true { + return "", fmt.Errorf("win_helper_version ok = %#v, want true", payload["ok"]) + } + data, ok := payload["data"].(map[string]any) + if !ok { + return "", fmt.Errorf("win_helper_version data missing: %#v", payload["data"]) + } + helperName, _ := data["helper_name"].(string) + if helperName != "ISphereWinHelper" { + return "", fmt.Errorf("helper_name = %q, want ISphereWinHelper", helperName) + } + return helperName, nil +} + +func verifyReceiveMessages(ctx context.Context, session *mcp.ClientSession, wantCount int) (int, error) { + payload, err := callToolObject(ctx, session, "isphere_receive_messages", map[string]any{ + "source_preference": "local_readonly", + "preview": true, + "cursor": "", + "include_attachment_metadata": true, + "limit": 5, + }) + if err != nil { + return 0, err + } + if payload["ok"] != true { + return 0, fmt.Errorf("isphere_receive_messages ok = %#v, want true", payload["ok"]) + } + messages, ok := payload["messages"].([]any) + if !ok { + return 0, fmt.Errorf("messages missing or not array: %#v", payload["messages"]) + } + if len(messages) != wantCount { + return 0, fmt.Errorf("message count = %d, want %d", len(messages), wantCount) + } + return len(messages), nil +} + +func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, wantCount int) (int, error) { + payload, err := callToolObject(ctx, session, "isphere_search_contacts", map[string]any{ + "query": query, + "source_preference": "local_readonly", + "include_inactive": true, + "cursor": "", + "limit": 5, + }) + if err != nil { + return 0, err + } + if payload["ok"] != true { + return 0, fmt.Errorf("isphere_search_contacts ok = %#v, want true", payload["ok"]) + } + contacts, ok := payload["contacts"].([]any) + if !ok { + return 0, fmt.Errorf("contacts missing or not array: %#v", payload["contacts"]) + } + if len(contacts) != wantCount { + return 0, fmt.Errorf("contact count = %d, want %d", len(contacts), wantCount) + } + return len(contacts), nil +} + +func verifySearchGroups(ctx context.Context, session *mcp.ClientSession, query string, wantCount int) (int, error) { + payload, err := callToolObject(ctx, session, "isphere_search_groups", map[string]any{ + "query": query, + "source_preference": "local_readonly", + "include_archived": true, + "cursor": "", + "limit": 5, + }) + if err != nil { + return 0, err + } + if payload["ok"] != true { + return 0, fmt.Errorf("isphere_search_groups ok = %#v, want true", payload["ok"]) + } + groups, ok := payload["groups"].([]any) + if !ok { + return 0, fmt.Errorf("groups missing or not array: %#v", payload["groups"]) + } + if len(groups) != wantCount { + return 0, fmt.Errorf("group count = %d, want %d", len(groups), wantCount) + } + return len(groups), nil +} + +func verifyReceiveFiles(ctx context.Context, session *mcp.ClientSession, nameContains string, wantCount int, wantFileID string, wantFileName string) (int, error) { + payload, err := callToolObject(ctx, session, "isphere_receive_files", map[string]any{ + "mode": "list", + "name_contains": nameContains, + "source_preference": "local_readonly", + "preview": true, + "cursor": "", + "output_dir": "", + "limit": 5, + }) + if err != nil { + return 0, err + } + if payload["ok"] != true { + return 0, fmt.Errorf("isphere_receive_files ok = %#v, want true", payload["ok"]) + } + files, ok := payload["files"].([]any) + if !ok { + return 0, fmt.Errorf("files missing or not array: %#v", payload["files"]) + } + if len(files) != wantCount { + return 0, fmt.Errorf("file count = %d, want %d", len(files), wantCount) + } + if wantCount > 0 { + first, ok := files[0].(map[string]any) + if !ok { + return 0, fmt.Errorf("first file not object: %#v", files[0]) + } + if first["file_id"] != wantFileID || first["file_name"] != wantFileName { + return 0, fmt.Errorf("unexpected file: %#v", first) + } + if first["download_ref"] != nil || first["saved_path"] != nil || first["sha256"] != nil { + return 0, fmt.Errorf("file list should not download or save: %#v", first) + } + } + return len(files), nil +} + +func verifySendMessage(ctx context.Context, session *mcp.ClientSession) (bool, bool, error) { + content := "redacted preview body" + contentHash := sha256Hex(content) + preview, err := callToolObject(ctx, session, "isphere_send_message", map[string]any{ + "target_type": "direct", + "target_id": "sender@imopenfire1-lanzhou", + "content_text": content, + "content_sha256": contentHash, + "idempotency_key": "capability-preview-idempotency-key", + "execution_mode": "preview", + }) + if err != nil { + return false, false, err + } + if preview["ok"] != true || preview["send_status"] != "planned" || preview["connector_stage"] != "preview" { + return false, false, fmt.Errorf("unexpected send preview payload: %#v", preview) + } + sideEffects, ok := preview["side_effects"].(map[string]any) + if !ok || sideEffects["sent_message"] != false || sideEffects["clicked_ui"] != false || sideEffects["typed_text"] != false { + return false, false, fmt.Errorf("send preview side effects mismatch: %#v", preview["side_effects"]) + } + + production, err := callToolObject(ctx, session, "isphere_send_message", map[string]any{ + "target_type": "direct", + "target_id": "sender@imopenfire1-lanzhou", + "content_text": content, + "content_sha256": contentHash, + "idempotency_key": "capability-production-idempotency-key", + "execution_mode": "production", + }) + if err != nil { + return false, false, err + } + if production["ok"] != false || production["send_status"] != "blocked" || production["connector_stage"] != "blocked" { + return false, false, fmt.Errorf("production send was not blocked: %#v", production) + } + productionSideEffects, ok := production["side_effects"].(map[string]any) + if !ok || productionSideEffects["sent_message"] != false || productionSideEffects["uploaded_file"] != false || productionSideEffects["captured_network"] != false { + return false, false, fmt.Errorf("production side effects mismatch: %#v", production["side_effects"]) + } + return true, false, nil +} + +func verifyPacketLogFileFixture(ctx context.Context) (int, int, int, int, error) { + plaintext := fixturePacketLogPlaintext("msg-fixture-1", "redacted-report.docx") + line, err := encryptPacketLogLine(plaintext) + if err != nil { + return 0, 0, 0, 0, err + } + file, err := os.CreateTemp("", "isphere-capability-packet-*.log") + if err != nil { + return 0, 0, 0, 0, err + } + fixturePath := file.Name() + defer os.Remove(fixturePath) + if _, err := file.WriteString("\n" + line + "\n"); err != nil { + _ = file.Close() + return 0, 0, 0, 0, err + } + if err := file.Close(); err != nil { + return 0, 0, 0, 0, err + } + + restore := replaceEnv("ISPHERE_PACKET_LOG_FILE", fixturePath) + defer restore() + _ = os.Unsetenv("ISPHERE_PACKET_LOG_DIR") + + session, cleanup, err := newSession(ctx, "isphere-capability-smoke-file-fixture") + if err != nil { + return 0, 0, 0, 0, err + } + defer cleanup() + receiveCount, err := verifyReceiveMessages(ctx, session, 1) + if err != nil { + return 0, 0, 0, 0, err + } + contactCount, err := verifySearchContacts(ctx, session, "sender", 1) + if err != nil { + return 0, 0, 0, 0, err + } + groupCount, err := verifySearchGroups(ctx, session, "project", 1) + if err != nil { + return 0, 0, 0, 0, err + } + fileCount, err := verifyReceiveFiles(ctx, session, "report", 1, "msg-fixture-1:redacted-report.docx", "redacted-report.docx") + if err != nil { + return 0, 0, 0, 0, err + } + return receiveCount, contactCount, groupCount, fileCount, nil +} + +func verifyPacketLogDirFixture(ctx context.Context) (int, int, int, int, error) { + plaintext := fixturePacketLogPlaintext("msg-dir-fixture-1", "redacted-dir-report.docx") + line, err := encryptPacketLogLine(plaintext) + if err != nil { + return 0, 0, 0, 0, err + } + dir, err := os.MkdirTemp("", "isphere-capability-packet-dir-*") + if err != nil { + return 0, 0, 0, 0, err + } + defer os.RemoveAll(dir) + if err := os.WriteFile(filepath.Join(dir, "a-packet-reader.log"), []byte("\n"+line+"\n"), 0o600); err != nil { + return 0, 0, 0, 0, err + } + if err := os.WriteFile(filepath.Join(dir, "ignored.tmp"), []byte("ignored\n"), 0o600); err != nil { + return 0, 0, 0, 0, err + } + + restoreFile := replaceEnv("ISPHERE_PACKET_LOG_FILE", "") + restoreDir := replaceEnv("ISPHERE_PACKET_LOG_DIR", dir) + defer restoreFile() + defer restoreDir() + + session, cleanup, err := newSession(ctx, "isphere-capability-smoke-dir-fixture") + if err != nil { + return 0, 0, 0, 0, err + } + defer cleanup() + receiveCount, err := verifyReceiveMessages(ctx, session, 1) + if err != nil { + return 0, 0, 0, 0, err + } + contactCount, err := verifySearchContacts(ctx, session, "sender", 1) + if err != nil { + return 0, 0, 0, 0, err + } + groupCount, err := verifySearchGroups(ctx, session, "project", 1) + if err != nil { + return 0, 0, 0, 0, err + } + fileCount, err := verifyReceiveFiles(ctx, session, "dir-report", 1, "msg-dir-fixture-1:redacted-dir-report.docx", "redacted-dir-report.docx") + if err != nil { + return 0, 0, 0, 0, err + } + return receiveCount, contactCount, groupCount, fileCount, nil +} + +func callToolObject(ctx context.Context, session *mcp.ClientSession, name string, args map[string]any) (map[string]any, error) { + result, err := session.CallTool(ctx, &mcp.CallToolParams{Name: name, Arguments: args}) + if err != nil { + return nil, fmt.Errorf("%s: %w", name, err) + } + if result.IsError { + return nil, fmt.Errorf("%s returned IsError=true: %#v", name, result.Content) + } + payload, err := json.Marshal(result.StructuredContent) + if err != nil { + return nil, fmt.Errorf("%s marshal structuredContent: %w", name, err) + } + var object map[string]any + if err := json.Unmarshal(payload, &object); err != nil { + return nil, fmt.Errorf("%s decode structuredContent %s: %w", name, payload, err) + } + return object, nil +} + +func fixturePacketLogPlaintext(messageID string, body string) string { + return "--------------------------------------------------------------------------------------------------------------------------------------------\n" + + "2026/7/7 15:30:07\n" + + "\n" + + " " + body + "\n" + + " \n" + + " FILE_TRANSFER_CANCEL\n" + + " \n" + + " \n" + + "" +} + +func encryptPacketLogLine(plaintext string) (string, error) { + block, err := des.NewCipher([]byte("hyhccdtm")) + if err != nil { + return "", err + } + plain := padPKCS7([]byte(plaintext), des.BlockSize) + ciphertext := make([]byte, len(plain)) + iv := []byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF} + cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, plain) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +func padPKCS7(data []byte, blockSize int) []byte { + pad := blockSize - len(data)%blockSize + out := append([]byte(nil), data...) + for i := 0; i < pad; i++ { + out = append(out, byte(pad)) + } + return out +} + +func sha256Hex(value string) string { + sum := sha256.Sum256([]byte(value)) + return hex.EncodeToString(sum[:]) +} + +func replaceEnv(key string, value string) func() { + oldValue, hadOldValue := os.LookupEnv(key) + if value == "" { + _ = os.Unsetenv(key) + } else { + _ = os.Setenv(key, value) + } + return func() { + if hadOldValue { + _ = os.Setenv(key, oldValue) + } else { + _ = os.Unsetenv(key) + } + } +} + +func fail(step string, err error) { + encoded, _ := json.Marshal(map[string]any{ + "ok": false, + "step": step, + "error": err.Error(), + }) + fmt.Fprintln(os.Stderr, string(encoded)) + os.Exit(1) +} diff --git a/docs/current-status-card.md b/docs/current-status-card.md index 2f1be08..f2373cb 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: `R6c local online-sandbox evidence package prepared; next R6d returned sandbox evidence intake after package is run and returned`. +Current loop: `R6c local online-sandbox evidence package consolidated plus offline capability-test package prepared; next R6d returned sandbox evidence intake after package is run and returned`. Plan file: `docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md`. @@ -116,6 +116,7 @@ Current loop output so far: 43. R6a/C31: added WinHelper `0.4.0` read-only `probe_send_entrypoints` and `probe_send_uia_controls`, wired the live recorder to emit `send_entrypoints_preflight.json` and `send_uia_controls_preflight.json`, and verified that copied install metadata reports all 5 required B-route entrypoints available while the synthetic UIA classifier reports `A_ROUTE_OFFLINE_BLOCKED`. Production send/file upload remains blocked pending preview/dry-run, dynamic observation, idempotency/audit, and one approved sandbox send. 44. R6b: added `isphere_send_message` preview/dry-run only. The tool validates `target_type`, `target_id`, `content_sha256`, and `idempotency_key`, returns connector metadata `implatform-sidecar`/`preview`, appends redacted JSONL audit events without raw message body or raw idempotency key, and returns `send_status="blocked"` for production with all side-effect flags false. `verify-go-mcp.ps1` now verifies the 9-tool surface and proves `send_preview_tool_present=true` and `production_send_enabled=false`. 45. R6c: because the local machine cannot log in, prepared an online/internal sandbox evidence package instead of attempting local send. Added `scripts/package-send-sandbox-gate.ps1`, `scripts/verify-send-sandbox-gate-package.ps1`, and `docs/source-discovery/2026-07-10-send-sandbox-gate.md`; local verification generates `runs/send-sandbox-gate-package.zip` with the read-only recorder, before/after scripts, sandbox input template, SHA256 helper, expected-return checklist, and return-zip helper. +46. R6c package consolidation: upgraded `runs/send-sandbox-gate-package.zip` into a one-pass operator recording suite with `RUN-RECORDING-SUITE.bat`, numbered `01/02/03` scripts, and `RECORDING-PACKAGE-MANIFEST.json`; added a second offline-safe `runs/send-capability-test-package.zip` with `isphere-capability-smoke.exe` plus packaged `ISphereWinHelper.exe` to verify the 9-tool MCP surface, receive/search/file-list fixture behavior, send preview, and `production_send_enabled=false`. ## Recompiled business roadmap @@ -194,6 +195,9 @@ R6c local preparation for online sandbox-send evidence is complete: - Added package script `scripts/package-send-sandbox-gate.ps1`. - Added verification script `scripts/verify-send-sandbox-gate-package.ps1`. - Verified package output: `runs/send-sandbox-gate-package.zip`. -- Package requires an online/internal sandbox operator to run before/after read-only recorder captures around exactly one manual sandbox send. +- Package now includes an all-in-one `RUN-RECORDING-SUITE.bat`, numbered `01/02/03` scripts, and a manifest so the online/internal sandbox operator can execute the full recording/return flow from one extracted folder. +- Added `scripts/package-send-capability-test.ps1`, `scripts/verify-send-capability-test-package.ps1`, and `cmd/isphere-capability-smoke`. +- Verified second package output: `runs/send-capability-test-package.zip`. +- The capability-test package is offline-safe and confirms 9 MCP tools, fixture-backed receive/search/file-list behavior, send preview, no real send, and `production_send_enabled=false`. - Production `isphere_send_message` and `isphere_send_file` remain blocked until the returned package proves success/ack or sent-record evidence and no second send. - Next node: R6d returned sandbox evidence intake after the package is run and returned. diff --git a/docs/go-mcp-runbook.md b/docs/go-mcp-runbook.md index 8d29966..7bbec0e 100644 --- a/docs/go-mcp-runbook.md +++ b/docs/go-mcp-runbook.md @@ -390,16 +390,57 @@ runs\send-sandbox-gate-package.zip Send that zip to the online/internal sandbox operator. The operator must follow `OPERATOR-STEPS.md` inside the package: 1. fill `SANDBOX-SEND-INPUTS.json`; -2. run `run-before-recorder.bat`; +2. run `RUN-RECORDING-SUITE.bat` for the guided full flow, or run `01-run-before-recorder.bat`; 3. manually send exactly one sandbox message in the official client; 4. fill `success_ack_or_sent_record`; -5. run `run-after-recorder.bat`; -6. run `make-return-zip.ps1`; +5. continue the guided flow, or run `02-run-after-recorder.bat`; +6. continue the guided flow, or run `03-make-return-zip.bat`; 7. return the generated zip. +The package also contains `RECORDING-PACKAGE-MANIFEST.json`, `run-before-recorder.bat`, `run-after-recorder.bat`, `compute-content-hash.ps1`, `make-return-zip.ps1`, and the read-only recorder under `recorder\`. + Production `isphere_send_message` remains blocked until the returned package is analyzed. -## 14. Troubleshooting +## 14. Offline capability-test package + +Use this second package when you want to test the current capability surface without logging in and without any real send. + +Build and verify it locally: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\package-send-capability-test.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-send-capability-test-package.ps1 +``` + +Generated package: + +```text +runs\send-capability-test-package.zip +``` + +Inside the extracted package, run: + +```text +run-capability-test.bat +``` + +Expected final JSON includes: + +```json +{ + "ok": true, + "tool_count": 9, + "helper_name": "ISphereWinHelper", + "send_preview_tool_present": true, + "production_send_enabled": false, + "local_login_required": false, + "no_real_send": true +} +``` + +This package includes `isphere-capability-smoke.exe` and `runs\win-helper\ISphereWinHelper.exe`. It verifies the nine-tool MCP surface, fixture-backed receive/search/file-list behavior, send preview, and the production-send block. It does not log in, click, type, upload, hook, inject, or send. + +## 15. Troubleshooting - If C# helper build fails, run `scripts\build-win-helper.ps1` directly and check for missing .NET Framework reference assemblies. - If `win_helper_version` fails, rerun `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-win-helper.ps1` first. diff --git a/docs/source-discovery/2026-07-10-send-sandbox-gate.md b/docs/source-discovery/2026-07-10-send-sandbox-gate.md index f386732..5f8454c 100644 --- a/docs/source-discovery/2026-07-10-send-sandbox-gate.md +++ b/docs/source-discovery/2026-07-10-send-sandbox-gate.md @@ -30,6 +30,20 @@ runs/send-sandbox-gate-package/ runs/send-sandbox-gate-package.zip ``` +Companion capability-test package: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\package-send-capability-test.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-send-capability-test-package.ps1 +``` + +```text +runs/send-capability-test-package/ +runs/send-capability-test-package.zip +``` + +The capability-test package is local/offline safe. It runs `isphere-capability-smoke.exe`, confirms the nine-tool MCP surface, exercises receive/search/file-list against synthetic fixtures, verifies send preview, and confirms `production_send_enabled=false`. + The package embeds the existing read-only live probe recorder under: ```text @@ -45,6 +59,11 @@ recorder/variants/ISphereLiveProbeRecorder-x86.exe | `OPERATOR-STEPS.md` | Step-by-step operator procedure. | | `SANDBOX-SEND-INPUTS.template.json` | Template for sandbox target, content hash, idempotency key, and observed result. | | `EXPECTED-RETURN-FILES.txt` | Return package acceptance checklist. | +| `RUN-RECORDING-SUITE.bat` | Top-level guided before/manual-send/after/return-zip flow. | +| `01-run-before-recorder.bat` | Numbered wrapper for before recording. | +| `02-run-after-recorder.bat` | Numbered wrapper for after recording. | +| `03-make-return-zip.bat` | Numbered wrapper for return zip creation. | +| `RECORDING-PACKAGE-MANIFEST.json` | Machine-readable package manifest and one-send markers. | | `run-before-recorder.bat` | Runs read-only recorder before the manual sandbox send. | | `run-after-recorder.bat` | Runs read-only recorder after the manual sandbox send. | | `compute-content-hash.ps1` | Computes SHA256 for the exact test message body. | @@ -68,12 +87,12 @@ Required online procedure: 1. Log in normally on an online/internal sandbox machine. 2. Fill `SANDBOX-SEND-INPUTS.json` from the template. -3. Run `run-before-recorder.bat`. +3. Run `RUN-RECORDING-SUITE.bat` for the guided all-in-one flow, or run `01-run-before-recorder.bat`. 4. Manually send exactly one message to the declared sandbox target. 5. Record `success_ack_or_sent_record`. 6. Do not send a second copy. -7. Run `run-after-recorder.bat`. -8. Run `make-return-zip.ps1`. +7. Continue the guided flow, or run `02-run-after-recorder.bat`. +8. Continue the guided flow, or run `03-make-return-zip.bat`. 9. Return the generated zip. ## Safety boundary @@ -102,7 +121,7 @@ R6c can pass only after a returned package proves all of the following: ## Current decision -R6c local-preparation slice is complete, but the full online evidence gate is not passed yet. +R6c local-preparation slice is complete, including the consolidated recording suite and the separate offline capability-test package. The full online evidence gate is not passed yet. Production `isphere_send_message` remains blocked. diff --git a/docs/source-discovery/capability-source-matrix.md b/docs/source-discovery/capability-source-matrix.md index cd7c2f9..9df5045 100644 --- a/docs/source-discovery/capability-source-matrix.md +++ b/docs/source-discovery/capability-source-matrix.md @@ -25,7 +25,7 @@ Send sandbox gate: `docs/source-discovery/2026-07-10-send-sandbox-gate.md` | `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `TD_Roster`, `TD_CustomEffigy`, `tblRecent`, `tblChatLevel`, `tblPersonMsg`; decrypted roster/contact stanzas from `Smark.SendReceive`; UIA helper source if display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; `internal/isphere/contacts_test.go`; `internal/tools/isphere_contacts_test.go` | C34 documents optional MsgLib contact display enrichment; R2 adds deterministic exact-match-first ranking, case-insensitive de-duplication across log/MsgLib candidates, and preserves `source`/`raw_ref` | core contact search complete; optional richer fields later | | `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `tblMsgGroupPersonMsg`, `TD_WorkGroupAuth`, `tblRecent`; UIA helper source if group display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; `internal/isphere/groups_test.go`; `internal/tools/isphere_groups_test.go`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C34 documents optional MsgLib group display enrichment; R2 adds deterministic exact-match-first ranking, case-insensitive de-duplication across log/MsgLib candidates, and preserves `source`/`raw_ref` | core group search complete; optional member/owner hierarchy later | | `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\` cache entries remain unlinked | MsgLib `TD_ReceiveFileRecord` safe metadata through explicit copied-DB path; decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck`; `docs/source-discovery/2026-07-10-file-download-mapping-precheck.md`; `docs/source-discovery/2026-07-10-file-cache-mapping-diagnostic.md` | C22 verifies safe file-list contract args; R3 confirms list metadata is available; R3b diagnostic finds 0 current cache/archive matches, so R4 remains blocked | blocked pending better cache evidence or UI/client download connector | -| `isphere_send_message` | B-route selected: running-client sidecar / in-process connector, preferring `AppContextManager.SendTxtMessageByJid(...)` or `MessageScheduling.SendChatMessage(...)` over the unimplemented `SendP2PMessage(...)` interface path; current MCP tool exposes preview/dry-run only | A-route fallback: constrained UI/RPA wrapper only if B cannot pass runtime probe, entrypoint availability, dynamic observation, and idempotent dry-run gates; network/protocol replay remains deferred | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md`; `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md`; `docs/source-discovery/2026-07-10-returned-live-probe-analysis.md`; `docs/source-discovery/2026-07-10-send-connector-preflight.md`; `docs/source-discovery/2026-07-10-send-sandbox-gate.md`; `internal/tools/isphere_send_message_test.go` | R6c prepares the online sandbox evidence package because local login is unavailable. `runs/send-sandbox-gate-package.zip` can collect before/after read-only recorder outputs around one manual sandbox send, but no returned evidence has been analyzed yet. Production remains blocked. | next: R6d returned sandbox evidence intake after user returns the package | +| `isphere_send_message` | B-route selected: running-client sidecar / in-process connector, preferring `AppContextManager.SendTxtMessageByJid(...)` or `MessageScheduling.SendChatMessage(...)` over the unimplemented `SendP2PMessage(...)` interface path; current MCP tool exposes preview/dry-run only | A-route fallback: constrained UI/RPA wrapper only if B cannot pass runtime probe, entrypoint availability, dynamic observation, and idempotent dry-run gates; network/protocol replay remains deferred | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md`; `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md`; `docs/source-discovery/2026-07-10-returned-live-probe-analysis.md`; `docs/source-discovery/2026-07-10-send-connector-preflight.md`; `docs/source-discovery/2026-07-10-send-sandbox-gate.md`; `internal/tools/isphere_send_message_test.go`; `cmd/isphere-capability-smoke/main.go` | R6c prepares the online sandbox evidence package because local login is unavailable. `runs/send-sandbox-gate-package.zip` now contains a consolidated recording suite for one manual sandbox send; `runs/send-capability-test-package.zip` separately verifies the offline-safe 9-tool surface, fixture-backed receive/search/file-list behavior, send preview, and `production_send_enabled=false`. No returned online send evidence has been analyzed yet. Production remains blocked. | next: R6d returned sandbox evidence intake after user returns the package | | `isphere_send_file` | B-route selected after send-message probe: managed offline-file path through `OffLineFileSend.sendFile()`, `IMPP.Service*.dll` `IFileTransfer.FileUpload(...)`, and `Chat.sendFileMessage(...)` | A-route fallback: constrained UI/RPA upload/send-file flow; native `TcpFileTransfer.dll` is a later candidate only after managed path is rejected | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md`; `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md`; `docs/source-discovery/2026-07-10-returned-live-probe-analysis.md`; `docs/source-discovery/2026-07-10-send-connector-preflight.md`; `docs/source-discovery/2026-07-10-send-sandbox-gate.md` | R6a/C31 metadata confirms `FileTransfer.FileUpload`, `FileTransfer.AsynFileUpload`, `OffLineFileSend.sendFile`, and `Chat.sendFileMessage` candidates. File-send remains blocked behind returned message-send sandbox evidence plus later upload/file-message dry-run evidence. | next: wait for R6d, then file-send upload preflight | ## Stage C entry rule @@ -43,4 +43,4 @@ Start Stage C with a narrow log-backed `isphere_receive_messages` source abstrac ## Deferred source work -`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, C31 proves real copied-DB MCP enrichment with sanitized output, C32 reuses it for receive-message display fields, C33 proves that receive display path through a real copied-DB MCP smoke without printing entity values, C34 documents the operator setup/verification path, C35 maps MsgLib message tables to receive-message contract fields without reading row values, C36 adds metadata-only `message_sources` readiness, C37 defines the bounded DB-backed receive-source design, C38 implements sidecar/Go-wrapper `list_messages` with sanitized verification, C39 adds a Go adapter from MsgLib list results to the receive-message domain model, C40 adds explicit tool-level `msglib_readonly` source selection, C41 wires env-configured explicit MsgLib receive with optional sanitized smoke, R1 documents source reconciliation keys, and R2 hardens contact/group search ranking and de-duplication. The active roadmap is `docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md`; next step is R6d returned sandbox evidence intake after the online package is returned. +`MsgLib.db` has a validated copied read-only open path through the bundled 32-bit `System.Data.SQLite.dll` and password `123`. C27 adds `MsgLibReadSidecar` as the bounded x86 .NET reader boundary, C28 adds the Go `internal/msglib` process client, C29 adds bounded `display_entities` extraction, C30 wires it as optional contact/group MCP enrichment, C31 proves real copied-DB MCP enrichment with sanitized output, C32 reuses it for receive-message display fields, C33 proves that receive display path through a real copied-DB MCP smoke without printing entity values, C34 documents the operator setup/verification path, C35 maps MsgLib message tables to receive-message contract fields without reading row values, C36 adds metadata-only `message_sources` readiness, C37 defines the bounded DB-backed receive-source design, C38 implements sidecar/Go-wrapper `list_messages` with sanitized verification, C39 adds a Go adapter from MsgLib list results to the receive-message domain model, C40 adds explicit tool-level `msglib_readonly` source selection, C41 wires env-configured explicit MsgLib receive with optional sanitized smoke, R1 documents source reconciliation keys, and R2 hardens contact/group search ranking and de-duplication. R6c now has both the consolidated online recording package and the offline capability-test package. The active roadmap is `docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md`; next step is R6d returned sandbox evidence intake after the online package is returned. diff --git a/docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md b/docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md index a640e5c..a74681b 100644 --- a/docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md +++ b/docs/superpowers/plans/2026-07-10-core-business-capabilities-roadmap.md @@ -28,7 +28,7 @@ | Search contacts | Registered MCP tool; deterministic exact-match ranking and case-insensitive de-duplication across log/JID candidates and optional copied-DB MsgLib display enrichment. | Optional richer fields such as department/title remain future enrichment, not a core search blocker. | complete for core search; optional enrichment later | | Search groups | Registered MCP tool; deterministic exact-match ranking and case-insensitive de-duplication across groupchat/conference candidates and optional copied-DB MsgLib display enrichment. | Optional member count, owner refs, and nested/group hierarchy remain future enrichment, not a core search blocker. | complete for core search; optional enrichment later | | Receive messages | PacketReader source works; copied `MsgLib.db` explicit receive works with `source_preference="msglib_readonly"`; R1 precheck says default should remain PacketReader until a future reconciliation helper is implemented. | Future reconciliation helper, source confidence scoring, pagination policy. | future receive helper after R2/R3 | -| Send messages | `isphere_send_message` preview/dry-run is registered; R6a confirms B-route entrypoint metadata; R6c local package for online sandbox evidence is prepared. | Need returned online sandbox evidence proving success/ack and no duplicate send before production. | next R6d returned sandbox evidence intake | +| Send messages | `isphere_send_message` preview/dry-run is registered; R6a confirms B-route entrypoint metadata; R6c has a consolidated online sandbox recording package plus a separate offline capability-test package. | Need returned online sandbox evidence proving success/ack and no duplicate send before production. | next R6d returned sandbox evidence intake | | Receive files | List mode exists from message-derived file metadata; R3/R3b confirm DB file metadata exists but current cache/archive evidence does not map to local files. | R4 download is blocked until better cache evidence or a UI/client download connector is available. | blocked; continue R5 | | Send files | Contract exists; R6a/C31 metadata confirms file-upload/offline-file/file-message candidates and A-route file menu classifier support. | Blocked behind returned send-message sandbox evidence plus upload/file-transfer dry-run. | wait for R6d, then file-send upload preflight | @@ -480,7 +480,9 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-go-mcp.ps1 - Added `scripts/package-send-sandbox-gate.ps1`. - Added `scripts/verify-send-sandbox-gate-package.ps1`. - Generated and locally verified `runs/send-sandbox-gate-package.zip`. -- The package includes the read-only live probe recorder plus before/after recorder batch files, sandbox input template, SHA256 helper, expected-return checklist, and return-zip script. +- The package includes the read-only live probe recorder plus before/after recorder batch files, sandbox input template, SHA256 helper, expected-return checklist, return-zip script, `RUN-RECORDING-SUITE.bat`, numbered `01/02/03` step scripts, and `RECORDING-PACKAGE-MANIFEST.json`. +- Added `cmd/isphere-capability-smoke`, `scripts/package-send-capability-test.ps1`, and `scripts/verify-send-capability-test-package.ps1`. +- Generated and locally verified the companion offline-safe package `runs/send-capability-test-package.zip`; it embeds `isphere-capability-smoke.exe` and `runs/win-helper/ISphereWinHelper.exe`, verifies the nine-tool MCP surface, fixture-backed receive/search/file-list behavior, send preview, and `production_send_enabled=false`. - This local slice does not pass the online evidence gate because the local machine cannot log in. It only prepares the exact package needed for an online/internal sandbox operator. - Decision: next node is R6d returned sandbox evidence intake after the package is run and returned. Production `isphere_send_message` and `isphere_send_file` remain blocked. diff --git a/scripts/package-send-capability-test.ps1 b/scripts/package-send-capability-test.ps1 new file mode 100644 index 0000000..2f1394e --- /dev/null +++ b/scripts/package-send-capability-test.ps1 @@ -0,0 +1,147 @@ +param( + [string]$OutputDir = "runs/send-capability-test-package", + [switch]$NoZip +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path +$packageDir = Join-Path $repo $OutputDir +$zipPath = "$packageDir.zip" +$helperRelative = Join-Path $OutputDir "runs\win-helper" +$helperDir = Join-Path $packageDir "runs\win-helper" +$helperExe = Join-Path $helperDir "ISphereWinHelper.exe" +$smokeExe = Join-Path $packageDir "isphere-capability-smoke.exe" + +if (Test-Path -LiteralPath $packageDir) { + Remove-Item -LiteralPath $packageDir -Recurse -Force +} +if (Test-Path -LiteralPath $zipPath) { + Remove-Item -LiteralPath $zipPath -Force +} +New-Item -ItemType Directory -Force -Path $packageDir | Out-Null + +& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repo "scripts\build-win-helper.ps1") -OutputDir $helperRelative -Platform anycpu +if ($LASTEXITCODE -ne 0) { + throw "build-win-helper.ps1 failed with exit code $LASTEXITCODE" +} +if (-not (Test-Path -LiteralPath $helperExe)) { + throw "helper executable was not created: $helperExe" +} + +Push-Location $repo +try { + & go build -o $smokeExe ./cmd/isphere-capability-smoke + if ($LASTEXITCODE -ne 0) { + throw "go build ./cmd/isphere-capability-smoke failed with exit code $LASTEXITCODE" + } +} +finally { + Pop-Location +} +if (-not (Test-Path -LiteralPath $smokeExe)) { + throw "capability smoke executable was not created: $smokeExe" +} + +@' +iSphere Send Capability Test Package + +LOCAL_LOGIN_UNAVAILABLE: this package does not require local iSphere login. +NO_REAL_SEND: this package does not send real messages or files. + +What to run: +1. Extract the zip to a normal folder. +2. Double-click run-capability-test.bat, or run: + .\isphere-capability-smoke.exe + +What it tests: +- the MCP server initializes with the expected 9 tools; +- ISphereWinHelper can be called through the packaged helper path; +- receive messages / receive files / search contacts / search groups work against offline fixtures; +- isphere_send_message preview returns planned with no side effects; +- production send returns blocked and production_send_enabled remains false. + +What it does not do: +- it does not log in to iSphere; +- it does not click, type, upload, or send; +- it does not hook or inject into the client; +- it does not modify iSphere client data. + +Expected final JSON fields: +- ok=true +- tool_count=9 +- helper_name=ISphereWinHelper +- send_preview_tool_present=true +- production_send_enabled=false +- local_login_required=false +- no_real_send=true +'@ | Set-Content -LiteralPath (Join-Path $packageDir "README.txt") -Encoding ASCII + +@' +Expected capability smoke result: + +Required: +- ok = true +- tool_count = 9 +- helper_name = ISphereWinHelper +- receive_message_count = 0 +- contact_count = 0 +- group_count = 0 +- file_count = 0 +- fixture_receive_message_count = 1 +- fixture_contact_count = 1 +- fixture_group_count = 1 +- fixture_file_count = 1 +- fixture_dir_receive_message_count = 1 +- fixture_dir_contact_count = 1 +- fixture_dir_group_count = 1 +- fixture_dir_file_count = 1 +- send_preview_tool_present = true +- production_send_enabled = false +- local_login_required = false +- real_isphere_login_required = false +- no_real_send = true + +If production_send_enabled is true, treat the package as failed. +If no_real_send is false, treat the package as failed. +'@ | Set-Content -LiteralPath (Join-Path $packageDir "EXPECTED-RESULTS.txt") -Encoding ASCII + +@' +@echo off +setlocal +cd /d "%~dp0" +echo Running iSphere capability smoke test... +isphere-capability-smoke.exe +echo exit code: %ERRORLEVEL% +pause +exit /b %ERRORLEVEL% +'@ | Set-Content -LiteralPath (Join-Path $packageDir "run-capability-test.bat") -Encoding ASCII + +@' +{ + "package": "send-capability-test", + "LOCAL_LOGIN_UNAVAILABLE": true, + "NO_REAL_SEND": true, + "entrypoint": "run-capability-test.bat", + "smoke_exe": "isphere-capability-smoke.exe", + "helper_exe": "runs/win-helper/ISphereWinHelper.exe", + "expected_tool_count": 9, + "production_send_enabled": false +} +'@ | Set-Content -LiteralPath (Join-Path $packageDir "CAPABILITY-TEST-MANIFEST.json") -Encoding ASCII + +if (-not $NoZip) { + Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -Force +} + +[ordered]@{ + ok = $true + package_dir = (Resolve-Path -LiteralPath $packageDir).Path + zip = if (Test-Path -LiteralPath $zipPath) { (Resolve-Path -LiteralPath $zipPath).Path } else { $null } + helper_exe = (Resolve-Path -LiteralPath $helperExe).Path + smoke_exe = (Resolve-Path -LiteralPath $smokeExe).Path + local_login_required = $false + no_real_send = $true + production_send_enabled = $false +} | ConvertTo-Json -Depth 4 -Compress diff --git a/scripts/package-send-sandbox-gate.ps1 b/scripts/package-send-sandbox-gate.ps1 index 896e656..d302bbc 100644 --- a/scripts/package-send-sandbox-gate.ps1 +++ b/scripts/package-send-sandbox-gate.ps1 @@ -44,6 +44,7 @@ What this package does: - asks the operator to manually send exactly one sandbox message in the official client; - runs the same read-only recorder after the manual send; - zips the returned evidence folder. +- provides RUN-RECORDING-SUITE.bat plus numbered 01/02/03 batch files so the operator can run the whole recording flow from this package. What this package does not do: - it does not send messages automatically; @@ -60,6 +61,7 @@ Required rule: Return: - run make-return-zip.ps1 and send back the generated zip. +- preferred one-file flow: run RUN-RECORDING-SUITE.bat and follow the pauses. '@ | Set-Content -LiteralPath (Join-Path $packageDir "README.txt") -Encoding UTF8 @' @@ -96,6 +98,10 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\compute-content-hash.ps1 - 3. Do not send anything yet. 4. Run `run-before-recorder.bat`. +Alternative one-file flow: + +Run `RUN-RECORDING-SUITE.bat`. It will run the before recorder, pause for the manual one-send action, run the after recorder, and then call the return-zip script. + ## Step 3 - Manual one-send action 1. In the official iSphere client, manually send exactly the `content_text_to_send`. @@ -122,6 +128,8 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\make-return-zip.ps1 Send back the generated zip file. +You can also run `03-make-return-zip.bat`. + ## Required returned evidence The returned evidence should contain: @@ -205,6 +213,21 @@ echo Done. Check return-evidence\after. pause '@ | Set-Content -LiteralPath (Join-Path $packageDir "run-after-recorder.bat") -Encoding ASCII +@' +@echo off +setlocal +cd /d "%~dp0" +call run-before-recorder.bat +'@ | Set-Content -LiteralPath (Join-Path $packageDir "01-run-before-recorder.bat") -Encoding ASCII + +@' +@echo off +setlocal +cd /d "%~dp0" +echo Manual one-send action must already be complete before this step. +call run-after-recorder.bat +'@ | Set-Content -LiteralPath (Join-Path $packageDir "02-run-after-recorder.bat") -Encoding ASCII + @' param( [Parameter(Mandatory=$true)] @@ -256,6 +279,59 @@ Compress-Archive -Path $items -DestinationPath $zip -Force } | ConvertTo-Json -Depth 4 -Compress '@ | Set-Content -LiteralPath (Join-Path $packageDir "make-return-zip.ps1") -Encoding ASCII +@' +@echo off +setlocal +cd /d "%~dp0" +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0make-return-zip.ps1" +echo exit code: %ERRORLEVEL% +pause +exit /b %ERRORLEVEL% +'@ | Set-Content -LiteralPath (Join-Path $packageDir "03-make-return-zip.bat") -Encoding ASCII + +@' +@echo off +setlocal +cd /d "%~dp0" +echo R6c online sandbox-send recording suite +echo. +echo Step 1: BEFORE read-only recorder. +call "%~dp0run-before-recorder.bat" +echo. +echo Step 2: manual one-send action. +echo In the official iSphere client, manually send exactly one sandbox message. +echo Fill SANDBOX-SEND-INPUTS.json before continuing. +pause +echo. +echo Step 3: AFTER read-only recorder. +call "%~dp0run-after-recorder.bat" +echo. +echo Step 4: make return zip. +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0make-return-zip.ps1" +echo exit code: %ERRORLEVEL% +pause +exit /b %ERRORLEVEL% +'@ | Set-Content -LiteralPath (Join-Path $packageDir "RUN-RECORDING-SUITE.bat") -Encoding ASCII + +@' +{ + "package": "send-sandbox-gate", + "LOCAL_LOGIN_UNAVAILABLE": true, + "ONLINE_SANDBOX_REQUIRED": true, + "MANUAL_ONE_SEND_ONLY": true, + "DO_NOT_SEND_SECOND_TIME": true, + "entrypoint": "RUN-RECORDING-SUITE.bat", + "numbered_steps": [ + "01-run-before-recorder.bat", + "02-run-after-recorder.bat", + "03-make-return-zip.bat" + ], + "recorder": "recorder/ISphereLiveProbeRecorder.exe", + "return_inputs": "SANDBOX-SEND-INPUTS.json", + "production_send_enabled": false +} +'@ | Set-Content -LiteralPath (Join-Path $packageDir "RECORDING-PACKAGE-MANIFEST.json") -Encoding ASCII + if (-not $NoZip) { Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -Force } diff --git a/scripts/verify-send-capability-test-package.ps1 b/scripts/verify-send-capability-test-package.ps1 new file mode 100644 index 0000000..4f7b454 --- /dev/null +++ b/scripts/verify-send-capability-test-package.ps1 @@ -0,0 +1,106 @@ +param( + [string]$OutputDir = "runs/send-capability-test-package" +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path +$packageScript = Join-Path $repo "scripts\package-send-capability-test.ps1" + +function Assert-True([bool]$Condition, [string]$Message) { + if (-not $Condition) { + throw $Message + } +} + +if (-not (Test-Path -LiteralPath $packageScript)) { + throw "missing package script: $packageScript" +} + +$packageRoot = Join-Path $repo $OutputDir +$zipPath = "$packageRoot.zip" +if (Test-Path -LiteralPath $packageRoot) { + Remove-Item -LiteralPath $packageRoot -Recurse -Force +} +if (Test-Path -LiteralPath $zipPath) { + Remove-Item -LiteralPath $zipPath -Force +} + +& powershell -NoProfile -ExecutionPolicy Bypass -File $packageScript -OutputDir $OutputDir +if ($LASTEXITCODE -ne 0) { + throw "package-send-capability-test.ps1 failed with exit code $LASTEXITCODE" +} + +$requiredFiles = @( + "README.txt", + "EXPECTED-RESULTS.txt", + "run-capability-test.bat", + "isphere-capability-smoke.exe", + "runs\win-helper\ISphereWinHelper.exe" +) + +foreach ($relative in $requiredFiles) { + $path = Join-Path $packageRoot $relative + if (-not (Test-Path -LiteralPath $path)) { + throw "missing package file: $relative" + } +} + +$readme = Get-Content -LiteralPath (Join-Path $packageRoot "README.txt") -Raw +$expected = Get-Content -LiteralPath (Join-Path $packageRoot "EXPECTED-RESULTS.txt") -Raw +$runner = Get-Content -LiteralPath (Join-Path $packageRoot "run-capability-test.bat") -Raw +foreach ($needle in @( + "LOCAL_LOGIN_UNAVAILABLE", + "NO_REAL_SEND", + "isphere-capability-smoke.exe", + "tool_count", + "production_send_enabled" +)) { + if ($readme -notmatch [regex]::Escape($needle) -and $expected -notmatch [regex]::Escape($needle) -and $runner -notmatch [regex]::Escape($needle)) { + throw "package instructions missing required phrase: $needle" + } +} + +if (-not (Test-Path -LiteralPath $zipPath)) { + throw "missing package zip: $zipPath" +} + +$smokeOutput = $null +Push-Location $packageRoot +try { + $smokeOutput = & ".\isphere-capability-smoke.exe" + if ($LASTEXITCODE -ne 0) { + throw "isphere-capability-smoke.exe failed with exit code $LASTEXITCODE" + } +} +finally { + Pop-Location +} + +$smokeOutput | ForEach-Object { Write-Host $_ } +$lastLine = @($smokeOutput | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) | Select-Object -Last 1 +Assert-True (-not [string]::IsNullOrWhiteSpace($lastLine)) "smoke output was empty" +$smokeJson = $lastLine | ConvertFrom-Json + +Assert-True ($smokeJson.ok -eq $true) "smoke ok mismatch: $($smokeJson.ok)" +Assert-True ($smokeJson.helper_name -eq "ISphereWinHelper") "smoke helper_name mismatch: $($smokeJson.helper_name)" +Assert-True ($smokeJson.tool_count -eq 9) "smoke tool_count mismatch: $($smokeJson.tool_count)" +Assert-True ($smokeJson.send_preview_tool_present -eq $true) "smoke send_preview_tool_present mismatch: $($smokeJson.send_preview_tool_present)" +Assert-True ($smokeJson.production_send_enabled -eq $false) "smoke production_send_enabled mismatch: $($smokeJson.production_send_enabled)" +Assert-True ($smokeJson.local_login_required -eq $false) "smoke local_login_required mismatch: $($smokeJson.local_login_required)" +Assert-True ($smokeJson.real_isphere_login_required -eq $false) "smoke real_isphere_login_required mismatch: $($smokeJson.real_isphere_login_required)" +Assert-True ($smokeJson.no_real_send -eq $true) "smoke no_real_send mismatch: $($smokeJson.no_real_send)" + +[ordered]@{ + ok = $true + package_dir = (Resolve-Path -LiteralPath $packageRoot).Path + zip = (Resolve-Path -LiteralPath $zipPath).Path + required_file_count = $requiredFiles.Count + smoke_tool_count = $smokeJson.tool_count + helper_name = $smokeJson.helper_name + send_preview_tool_present = $smokeJson.send_preview_tool_present + production_send_enabled = $smokeJson.production_send_enabled + local_login_required = $false + no_real_send = $true +} | ConvertTo-Json -Depth 6 -Compress diff --git a/scripts/verify-send-sandbox-gate-package.ps1 b/scripts/verify-send-sandbox-gate-package.ps1 index e037e8c..87d3255 100644 --- a/scripts/verify-send-sandbox-gate-package.ps1 +++ b/scripts/verify-send-sandbox-gate-package.ps1 @@ -31,6 +31,11 @@ $requiredFiles = @( "OPERATOR-STEPS.md", "SANDBOX-SEND-INPUTS.template.json", "EXPECTED-RETURN-FILES.txt", + "RUN-RECORDING-SUITE.bat", + "01-run-before-recorder.bat", + "02-run-after-recorder.bat", + "03-make-return-zip.bat", + "RECORDING-PACKAGE-MANIFEST.json", "run-before-recorder.bat", "run-after-recorder.bat", "make-return-zip.ps1", @@ -54,6 +59,7 @@ foreach ($needle in @( "ONLINE_SANDBOX_REQUIRED", "MANUAL_ONE_SEND_ONLY", "DO_NOT_SEND_SECOND_TIME", + "RUN-RECORDING-SUITE.bat", "content_sha256", "idempotency_key", "success_ack_or_sent_record"