diff --git a/docs/current-status-card.md b/docs/current-status-card.md index 5541e74..77999ee 100644 --- a/docs/current-status-card.md +++ b/docs/current-status-card.md @@ -26,7 +26,7 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor - C# `ISphereWinHelper` exists under `native/ISphereWinHelper` and uses the `isphere.helper.v1` stdin/stdout JSON contract. - `scripts/verify-win-helper.ps1` and `scripts/verify-go-mcp.ps1` provide repeatable local checks for the helper, eight-tool MCP surface, default empty-source receive/contact/group/file calls, synthetic configured-file receive/contact/group/file smoke, and synthetic configured-directory receive/contact/group/file smoke. - `isphere_receive_messages` now returns the contract-facing `ok`, `conversation`, `messages`, `next_cursor`, and `audit` envelope while retaining legacy parser-native message fields for compatibility. -- `isphere_receive_messages` accepts `conversation_id`, `query`, `since`, and `limit` for local readonly PacketReader evidence filtering. +- `isphere_receive_messages` accepts the read-contract argument set for the current local-readonly path: `conversation_id`, `query`, `since`, `limit`, `include_attachment_metadata`, `source_preference`, `preview`, and empty `cursor`. - N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures. - N13/N14/N15 produced selector catalog/report validation infrastructure, but those are supporting validation assets only. @@ -53,7 +53,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme ## Current loop -Current loop: `Stage C / Loop C20 - receive-message remaining contract argument validation`. +Current loop: `Stage C / Loop C21 - contact/group search remaining contract argument validation`. Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`. @@ -85,10 +85,11 @@ Current loop output so far: 17. C17: `isphere_receive_messages` output was aligned to the core read contract envelope and message fields while retaining legacy aliases; verify-go-mcp now asserts contract fields for configured file and directory source smokes. 18. C18: `isphere_receive_messages` gained `conversation_id` and `query` filters across source, MCP args, tests, and configured file/directory smoke verification. 19. C19: `isphere_receive_messages` gained RFC3339 `since` filtering against PacketReader epoch-millisecond timestamps plus invalid-since error handling. +20. C20: `isphere_receive_messages` now accepts and validates the remaining safe read-contract args, rejects unsupported source/cursor cases, and can suppress inline attachment metadata. ## Next business mainline -1. Run Stage C Loop C20: accept and validate the remaining safe `isphere_receive_messages` contract arguments (`include_attachment_metadata`, `source_preference`, `preview`, and empty `cursor`) without pretending unsupported connectors/pagination exist. +1. Run Stage C Loop C21: apply the same safe-argument validation pattern to `isphere_search_contacts` and `isphere_search_groups`. 2. Leave `isphere_send_message` blocked until bridge/API, UIA action-chain, or protocol connector evidence is validated. 3. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated. 4. Keep UIA selector/report work as fallback support, not as the primary roadmap. diff --git a/docs/go-mcp-runbook.md b/docs/go-mcp-runbook.md index 69b2bc2..dd4cae2 100644 --- a/docs/go-mcp-runbook.md +++ b/docs/go-mcp-runbook.md @@ -184,7 +184,7 @@ Allowed parameters: - `win_helper_self_check`: zero business parameters. - `win_helper_scan_windows`: `include_all_visible` only. - `win_helper_dump_uia`: `hwnd`, `max_depth`, `include_text`, `max_children` only. -- `isphere_receive_messages`: `conversation_id`, `query`, `since`, `limit` only. `since` must be RFC3339/RFC3339Nano when supplied. +- `isphere_receive_messages`: `conversation_id`, `query`, `since`, `include_attachment_metadata`, `source_preference`, `preview`, `cursor`, `limit` only. `since` must be RFC3339/RFC3339Nano when supplied; `source_preference` currently supports empty/`auto`/`local_readonly`; `cursor` must be empty until pagination is implemented. - `isphere_search_contacts`: `query`, `limit` only. - `isphere_search_groups`: `query`, `limit` only. - `isphere_receive_files`: `conversation_id`, `message_id`, `file_id`, `name_contains`, `mode`, `limit` only. C14 supports `mode` empty or `list`; download is deferred. @@ -202,7 +202,7 @@ The current runbook verifies eight tools: - `isphere_search_groups` - `isphere_receive_files` -The current log-backed business read tools have parser/source/tool registration and an empty default server source. Raw N12-pre evidence remains under ignored `runs/` paths and is not committed. The current command entry point can read an operator-local encrypted PacketReader log-line file via `ISPHERE_PACKET_LOG_FILE` or a directory of `.log`/`.txt` files via `ISPHERE_PACKET_LOG_DIR`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `isphere_receive_messages` now exposes contract-facing fields for digital-employee consumption while preserving older aliases for compatibility, and supports `conversation_id`, keyword `query`, and RFC3339 `since` filtering. `isphere_receive_files` currently supports list mode only; download/cache mapping is deferred. +The current log-backed business read tools have parser/source/tool registration and an empty default server source. Raw N12-pre evidence remains under ignored `runs/` paths and is not committed. The current command entry point can read an operator-local encrypted PacketReader log-line file via `ISPHERE_PACKET_LOG_FILE` or a directory of `.log`/`.txt` files via `ISPHERE_PACKET_LOG_DIR`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `isphere_receive_messages` now exposes contract-facing fields for digital-employee consumption while preserving older aliases for compatibility, supports `conversation_id`, keyword `query`, and RFC3339 `since` filtering, and validates the remaining safe contract args without pretending unsupported connectors/pagination exist. `isphere_receive_files` currently supports list mode only; download/cache mapping is deferred. Core business tools for contacts, groups, messages, and files are defined in `docs/mcp-core-tools-contract.md`. Send capabilities enter the roadmap through that contract after source discovery, connector selection, preview metadata, execution metadata, and audit records are in place. diff --git a/docs/source-discovery/capability-source-matrix.md b/docs/source-discovery/capability-source-matrix.md index 8813a1a..74931f7 100644 --- a/docs/source-discovery/capability-source-matrix.md +++ b/docs/source-discovery/capability-source-matrix.md @@ -17,7 +17,7 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md` | MCP tool | Primary source | Fallback source | Evidence file | Decision status | Next implementation slice | | --- | --- | --- | --- | --- | --- | -| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; wrapped `MsgLib.db` after DB wrapper is solved | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | C19 supports contract-shaped output plus `conversation_id`, `query`, `since`, and `limit` filters; default source remains empty when unset | validate remaining safe contract args | +| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `` stanzas via `internal/isphere.EncryptedPacketLogSource`; configured by `ISPHERE_PACKET_LOG_FILE` or `ISPHERE_PACKET_LOG_DIR` | decrypted `Smark.SendReceive` transport stanzas; decrypted `SaveToDB` `Chat_OnMessageArrived` traces; wrapped `MsgLib.db` after DB wrapper is solved | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#field-mapping-evidence` | C20 supports and validates the current safe receive-message contract args; default source remains empty when unset | apply safe contract args to contact/group search | | `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | decrypted roster/contact stanzas from `Smark.SendReceive`; `MsgLib.db` contact/conversation tables after DB wrapper is solved; UIA helper source if display names are missing | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#log-decryption-result` | C16 verification covers default, configured-file, and configured-directory contact smoke; JID-only display/account fallback | enrich display names from roster/db/uia evidence | | `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | `MsgLib.db` group/conversation tables after DB wrapper is solved; UIA helper source if group display names are missing | C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C16 verification covers default, configured-file, and configured-directory group smoke; JID-only display fallback | enrich group display/member data from roster/db/uia evidence | | `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 | 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` | C16 verification covers default, configured-file, and configured-directory file-list smoke; no log-to-cache/download mapping yet | defer download mapping; align read-tool contract output | diff --git a/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md b/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md index 25cc7e2..9a21b84 100644 --- a/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md +++ b/docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md @@ -1693,14 +1693,76 @@ Implemented and verified in this loop: - Treat `include_attachment_metadata=false` as a request to suppress inline receive-message attachments while leaving `isphere_receive_files` available for explicit file listing. - Do not add write/send/file-download behavior. -- [ ] **Step 1: Write failing MCP argument validation tests** +- [x] **Step 1: Write failing MCP argument validation tests** Extend `internal/tools/isphere_read_test.go` with accepted safe args and rejected unsupported source/cursor cases. -- [ ] **Step 2: Implement argument validation and attachment toggle** +- [x] **Step 2: Implement argument validation and attachment toggle** Add minimal validation/helpers in `internal/tools/isphere_read.go`. +- [x] **Step 3: Update verification and docs** + +Update `scripts/verify-go-mcp.ps1`, runbook, status card, and this plan. + +- [x] **Step 4: Run full verification and commit** + +Run: + +```powershell +git diff --check +go test ./... +go build ./cmd/isphere-mcp +powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1 +``` + +Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. Commit exact C20 paths only and rewrite the next loop from actual results. + +--- + +## Loop C20 Result + +Implemented and verified in this loop: + +- `isphere_receive_messages` now accepts the remaining safe read-contract arguments: `include_attachment_metadata`, `source_preference`, `preview`, and `cursor`. +- `source_preference` accepts empty, `auto`, or `local_readonly`; bridge/uia/network are rejected until those connectors exist. +- Empty `cursor` is accepted for contract compatibility; non-empty cursor is rejected until pagination exists. +- `preview` is accepted without mutation; audit execution remains `read`. +- `include_attachment_metadata=false` suppresses inline receive-message attachments while keeping explicit file listing available through `isphere_receive_files`. +- `scripts/verify-go-mcp.ps1` now exercises the accepted contract arguments in default, configured-file, and configured-directory receive-message smokes. + +--- + +## Loop C21: Contact/group search remaining contract argument validation + +**Goal:** Apply the same contract-compatibility pattern to `isphere_search_contacts` and `isphere_search_groups` so digital employees can pass safe search-contract fields without tripping schema validation. + +**Planned files:** +- Modify: `internal/tools/isphere_contacts.go` +- Modify: `internal/tools/isphere_contacts_test.go` +- Modify: `internal/tools/isphere_groups.go` +- Modify: `internal/tools/isphere_groups_test.go` +- Modify: `scripts/verify-go-mcp.ps1` +- Modify: `docs/go-mcp-runbook.md` +- Modify: `docs/current-status-card.md` +- Modify: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md` + +**Planned behavior:** +- For contacts: accept `cursor`, `source_preference`, and `include_inactive`. +- For groups: accept `cursor`, `source_preference`, and `include_archived`. +- Accept empty cursor only; reject non-empty cursor until pagination exists. +- Accept source preference empty, `auto`, or `local_readonly`; reject bridge/uia/network until those connectors exist. +- Treat include flags as accepted no-op on local readonly JID-derived candidates because active/archive state is not available in current source. +- Do not add write/send/file-download behavior. + +- [ ] **Step 1: Write failing contact/group MCP arg tests** + +Extend contact/group tool tests with accepted safe args plus rejected unsupported source/cursor cases. + +- [ ] **Step 2: Implement validation helpers** + +Add minimal validation to contact and group tool handlers. + - [ ] **Step 3: Update verification and docs** Update `scripts/verify-go-mcp.ps1`, runbook, status card, and this plan. @@ -1716,7 +1778,7 @@ go build ./cmd/isphere-mcp powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1 ``` -Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. Commit exact C20 paths only and rewrite the next loop from actual results. +Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. Commit exact C21 paths only and rewrite the next loop from actual results. --- diff --git a/internal/tools/isphere_read.go b/internal/tools/isphere_read.go index b417db2..95b51d7 100644 --- a/internal/tools/isphere_read.go +++ b/internal/tools/isphere_read.go @@ -2,6 +2,7 @@ package tools import ( "context" + "fmt" "strconv" "strings" "time" @@ -18,10 +19,14 @@ type ReceiveMessagesSource interface { } type ReceiveMessagesArgs struct { - ConversationID string `json:"conversation_id,omitempty" jsonschema:"exact conversation id filter for local readonly message logs"` - Query string `json:"query,omitempty" jsonschema:"case-insensitive keyword filter over message text, subject, ids, and participants"` - Since string `json:"since,omitempty" jsonschema:"RFC3339 timestamp; return messages at or after this time when packet timestamps are available"` - Limit int `json:"limit,omitempty" jsonschema:"maximum number of messages to return; zero or negative returns all available messages"` + ConversationID string `json:"conversation_id,omitempty" jsonschema:"exact conversation id filter for local readonly message logs"` + Query string `json:"query,omitempty" jsonschema:"case-insensitive keyword filter over message text, subject, ids, and participants"` + Since string `json:"since,omitempty" jsonschema:"RFC3339 timestamp; return messages at or after this time when packet timestamps are available"` + Cursor string `json:"cursor,omitempty" jsonschema:"pagination cursor; currently only empty cursor is supported"` + SourcePreference string `json:"source_preference,omitempty" jsonschema:"source selector; currently supports auto or local_readonly only"` + Preview bool `json:"preview,omitempty" jsonschema:"accepted for contract compatibility; read-only execution remains read mode"` + IncludeAttachmentMetadata *bool `json:"include_attachment_metadata,omitempty" jsonschema:"include inline attachment metadata in receive message results; default true"` + Limit int `json:"limit,omitempty" jsonschema:"maximum number of messages to return; zero or negative returns all available messages"` } func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource) { @@ -33,6 +38,9 @@ func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource) Name: ToolNameReceiveMessages, Description: "Return iSphere messages from the configured read-only log-backed source.", }, func(ctx context.Context, _ *mcp.CallToolRequest, input ReceiveMessagesArgs) (*mcp.CallToolResult, map[string]any, error) { + if err := validateReceiveMessagesArgs(input); err != nil { + return nil, nil, err + } started := time.Now().UTC() result, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{ ConversationID: input.ConversationID, @@ -43,14 +51,43 @@ func RegisterISphereReadTools(server *mcp.Server, source ReceiveMessagesSource) if err != nil { return nil, nil, err } - return nil, receiveMessagesResultToMap(result, started, time.Now().UTC()), nil + return nil, receiveMessagesResultToMap(result, started, time.Now().UTC(), receiveMessagesMapOptions{ + IncludeAttachmentMetadata: includeReceiveMessageAttachmentMetadata(input), + }), nil }) } -func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started time.Time, finished time.Time) map[string]any { +func validateReceiveMessagesArgs(input ReceiveMessagesArgs) error { + sourcePreference := strings.ToLower(strings.TrimSpace(input.SourcePreference)) + switch sourcePreference { + case "", "auto", "local_readonly": + default: + return fmt.Errorf("isphere_receive_messages source_preference %q is not available; only auto and local_readonly are supported", input.SourcePreference) + } + if strings.TrimSpace(input.Cursor) != "" { + return fmt.Errorf("isphere_receive_messages cursor pagination is not available yet") + } + return nil +} + +func includeReceiveMessageAttachmentMetadata(input ReceiveMessagesArgs) bool { + if input.IncludeAttachmentMetadata == nil { + return true + } + return *input.IncludeAttachmentMetadata +} + +type receiveMessagesMapOptions struct { + IncludeAttachmentMetadata bool +} + +func receiveMessagesResultToMap(result isphere.ReceiveMessagesResult, started time.Time, finished time.Time, options receiveMessagesMapOptions) map[string]any { messages := make([]map[string]any, 0, len(result.Messages)) for _, message := range result.Messages { - attachments := receiveMessageAttachments(message) + attachments := []map[string]any{} + if options.IncludeAttachmentMetadata { + attachments = receiveMessageAttachments(message) + } messages = append(messages, map[string]any{ "id": message.ID, "message_id": message.ID, diff --git a/internal/tools/isphere_read_test.go b/internal/tools/isphere_read_test.go index d9ed497..0b4c0aa 100644 --- a/internal/tools/isphere_read_test.go +++ b/internal/tools/isphere_read_test.go @@ -142,6 +142,75 @@ func TestISphereReceiveMessagesToolReturnsSourceMessages(t *testing.T) { } } +func TestISphereReceiveMessagesToolValidatesRemainingContractArgs(t *testing.T) { + fake := &fakeReceiveMessagesSource{ + result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ + ID: "msg-1", + ConversationID: "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou", + ConversationType: "chat", + SenderID: "sender@imopenfire1-lanzhou", + ReceiverID: "receiver@imopenfire1-lanzhou", + Text: "redacted-report.docx", + Timestamp: "1783423807000", + Subject: "FILE_TRANSFER_CANCEL", + }}}, + } + session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { + RegisterISphereReadTools(server, fake) + }) + defer cleanup() + + callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameReceiveMessages, + Arguments: map[string]any{ + "include_attachment_metadata": false, + "source_preference": "local_readonly", + "preview": true, + "cursor": "", + "limit": 5, + }, + }) + if err != nil { + t.Fatalf("call with safe contract args: %v", err) + } + if callResult.IsError { + t.Fatalf("call result is error: %+v", callResult) + } + var decoded struct { + Messages []struct { + Attachments []struct { + FileID string `json:"file_id"` + } `json:"attachments"` + } `json:"messages"` + } + payload, err := json.Marshal(callResult.StructuredContent) + if err != nil { + t.Fatalf("marshal structured content: %v", err) + } + if err := json.Unmarshal(payload, &decoded); err != nil { + t.Fatalf("decode structured content %s: %v", payload, err) + } + if len(decoded.Messages) != 1 || len(decoded.Messages[0].Attachments) != 0 { + t.Fatalf("attachments = %+v, want suppressed", decoded.Messages) + } + + bridgeResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameReceiveMessages, + Arguments: map[string]any{"source_preference": "bridge", "limit": 5}, + }) + if err == nil && !bridgeResult.IsError { + t.Fatalf("bridge source_preference was accepted before connector exists") + } + + cursorResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameReceiveMessages, + Arguments: map[string]any{"cursor": "next-page", "limit": 5}, + }) + if err == nil && !cursorResult.IsError { + t.Fatalf("non-empty cursor was accepted before pagination exists") + } +} + type fakeReceiveMessagesSource struct { queries []isphere.ReceiveMessagesQuery result isphere.ReceiveMessagesResult diff --git a/scripts/verify-go-mcp.ps1 b/scripts/verify-go-mcp.ps1 index 7652320..b5fdd61 100644 --- a/scripts/verify-go-mcp.ps1 +++ b/scripts/verify-go-mcp.ps1 @@ -172,7 +172,13 @@ func main() { fail("tools/call win_helper_version", fmt.Errorf("helper_name = %q, want ISphereWinHelper", helperName)) } - receiveResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{"limit": 5}}) + receiveResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{ + "source_preference": "local_readonly", + "preview": true, + "cursor": "", + "include_attachment_metadata": true, + "limit": 5, + }}) if err != nil { fail("tools/call isphere_receive_messages", err) } @@ -302,6 +308,10 @@ func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int, int) { "conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou", "query": "report", "since": "2026-07-07T11:30:00Z", + "source_preference": "auto", + "preview": true, + "cursor": "", + "include_attachment_metadata": true, "limit": 5, }}) if err != nil { @@ -427,6 +437,10 @@ func verifyConfiguredDirectorySource(ctx context.Context) (int, int, int, int) { "conversation_id": "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou", "query": "dir-report", "since": "2026-07-07T11:30:00Z", + "source_preference": "auto", + "preview": true, + "cursor": "", + "include_attachment_metadata": true, "limit": 5, }}) if err != nil {