chore: package send recording and capability tests
This commit is contained in:
585
cmd/isphere-capability-smoke/main.go
Normal file
585
cmd/isphere-capability-smoke/main.go
Normal file
@@ -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" +
|
||||
"<message id=\"" + messageID + "\" from=\"project-room@conference.imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"sender@imopenfire1-lanzhou\" type=\"groupchat\">\n" +
|
||||
" <body>" + body + "</body>\n" +
|
||||
" <received xmlns=\"urn:xmpp:receipts\" id=\"receipt-" + messageID + "\" type=\"1\" stamp=\"\" />\n" +
|
||||
" <subject>FILE_TRANSFER_CANCEL</subject>\n" +
|
||||
" <isphere xmlns=\"isphere.im\" type=\"1001\" sendtime=\"1783423807000\" version=\"1\" />\n" +
|
||||
" <readed />\n" +
|
||||
"</message>"
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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\<hash>` 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
147
scripts/package-send-capability-test.ps1
Normal file
147
scripts/package-send-capability-test.ps1
Normal file
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
106
scripts/verify-send-capability-test-package.ps1
Normal file
106
scripts/verify-send-capability-test-package.ps1
Normal file
@@ -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
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user