diff --git a/docs/current-status-card.md b/docs/current-status-card.md index ac07331..04c6d66 100644 --- a/docs/current-status-card.md +++ b/docs/current-status-card.md @@ -28,6 +28,7 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor - `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 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`. - `isphere_search_contacts` and `isphere_search_groups` accept and validate the safe search-contract args for the current local-readonly path. +- `isphere_receive_files` list mode accepts and validates the safe file-list contract args for the current local-readonly path while keeping download blocked. - 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. @@ -54,7 +55,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme ## Current loop -Current loop: `Stage C / Loop C22 - file receive remaining contract argument validation`. +Current loop: `Stage C / Loop C23 - contact/group display-name source precheck`. Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`. @@ -88,10 +89,11 @@ Current loop output so far: 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. 21. C21: `isphere_search_contacts` and `isphere_search_groups` now accept and validate safe search-contract args, while rejecting unsupported source/cursor cases. +22. C22: `isphere_receive_files` list mode now accepts and validates safe contract args, rejects unsupported source/cursor/output/download cases, and keeps download blocked. ## Next business mainline -1. Run Stage C Loop C22: apply the same safe-argument validation pattern to `isphere_receive_files` list mode while keeping download blocked. +1. Run Stage C Loop C23: inspect whether current evidence can enrich contact/group display names beyond JID-only identities. 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 d3112e0..f616957 100644 --- a/docs/go-mcp-runbook.md +++ b/docs/go-mcp-runbook.md @@ -187,7 +187,7 @@ Allowed parameters: - `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`, `cursor`, `source_preference`, `include_inactive`, `limit` only. `source_preference` currently supports empty/`auto`/`local_readonly`; `cursor` must be empty until pagination is implemented. - `isphere_search_groups`: `query`, `cursor`, `source_preference`, `include_archived`, `limit` only. `source_preference` currently supports empty/`auto`/`local_readonly`; `cursor` must be empty until pagination is implemented. -- `isphere_receive_files`: `conversation_id`, `message_id`, `file_id`, `name_contains`, `mode`, `limit` only. C14 supports `mode` empty or `list`; download is deferred. +- `isphere_receive_files`: `conversation_id`, `message_id`, `file_id`, `name_contains`, `mode`, `output_dir`, `cursor`, `source_preference`, `preview`, `limit` only. C22 supports `mode` empty or `list`; `source_preference` currently supports empty/`auto`/`local_readonly`; `cursor` and `output_dir` must be empty in current list-only mode; download is deferred. ## 9. Current tool surface and next-stage route @@ -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, supports `conversation_id`, keyword `query`, and RFC3339 `since` filtering, and validates the remaining safe contract args without pretending unsupported connectors/pagination exist. Contact and group search also accept the safe local-readonly contract args. `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. Contact and group search also accept the safe local-readonly contract args. `isphere_receive_files` supports list mode with safe contract arg validation; download/cache mapping remains 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 7f4f835..e0fd6ac 100644 --- a/docs/source-discovery/capability-source-matrix.md +++ b/docs/source-discovery/capability-source-matrix.md @@ -20,7 +20,7 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md` | `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` | C21 verifies safe search-contract args plus default/configured contact smokes; 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 | C21 verifies safe search-contract args plus default/configured group smokes; 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 | +| `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` | C22 verifies safe file-list contract args; no log-to-cache/download mapping yet | defer download mapping; precheck contact/group display-name evidence | | `isphere_send_message` | none validated yet | existing bridge/API/local service preferred; UIA write connector only after internal-sandbox action evidence; network/protocol connector only after protocol discovery | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | C15 blocked: contract exists, but no committed bridge/API/local service, helper write op, UIA action chain, or protocol connector is validated | do not implement write behavior; harden existing read source configuration | | `isphere_send_file` | none validated yet | depends on send-message connector plus outbound file/upload evidence | `docs/source-discovery/2026-07-10-send-message-source-precheck.md` | blocked behind `isphere_send_message` connector selection and file upload/source policy | defer until send-message connector is validated | 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 895218f..808f58b 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 @@ -1816,19 +1816,19 @@ Implemented and verified in this loop: - Accept empty `output_dir` in list mode; reject non-empty `output_dir` unless/until download mode is implemented to avoid implying files were written. - Keep mode empty/`list` only; continue rejecting `download`. -- [ ] **Step 1: Write failing file MCP arg tests** +- [x] **Step 1: Write failing file MCP arg tests** Extend `internal/tools/isphere_files_test.go` with accepted safe args and rejected unsupported source/cursor/output/download cases. -- [ ] **Step 2: Implement file arg validation** +- [x] **Step 2: Implement file arg validation** Reuse local-readonly source/cursor validation and add output/download list-mode checks. -- [ ] **Step 3: Update verification and docs** +- [x] **Step 3: Update verification and docs** Update `scripts/verify-go-mcp.ps1`, runbook, status card, matrix, and this plan. -- [ ] **Step 4: Run full verification and commit** +- [x] **Step 4: Run full verification and commit** Run: @@ -1843,6 +1843,58 @@ Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. C --- +## Loop C22 Result + +Implemented and verified in this loop: + +- `isphere_receive_files` list mode now accepts safe contract args `cursor`, `source_preference`, `preview`, and `output_dir`. +- It accepts empty/`auto`/`local_readonly` source preference and rejects unsupported connector selections until those sources exist. +- It accepts empty cursor and rejects non-empty cursor until pagination exists. +- It accepts empty `output_dir` in list mode and rejects non-empty `output_dir` to avoid implying a download/write happened. +- It continues to reject `download` mode until message-to-cache/download mapping is validated. +- `scripts/verify-go-mcp.ps1` now exercises file-list safe args in default and configured source smokes. + +--- + +## Loop C23: Contact/group display-name source precheck + +**Goal:** Decide whether the current N12-pre evidence contains a safe source for human-readable contact/group display names, because current contact/group tools expose JID-derived identities only. + +**Planned files:** +- Modify: `docs/source-discovery/capability-source-matrix.md` +- Modify: `docs/current-status-card.md` +- Modify: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md` +- Optionally create a focused source-discovery note under `docs/source-discovery/` if evidence is non-trivial. +- Optionally create ignored metadata under `runs/`; do not commit raw logs or user content. + +**Planned behavior:** +- Inspect committed schema notes and ignored metadata/log summaries for roster/contact display names, group display names, aliases, departments, or conversation-title fields. +- If evidence is enough, write C24 as a source implementation loop for display-name enrichment. +- If evidence is not enough, keep JID-only behavior explicit and choose the next business-read hardening node. +- Do not add write/send/file-download behavior. + +- [ ] **Step 1: Inspect current evidence** + +Use committed source-discovery docs first; only inspect ignored local summaries/counts if needed. Do not commit raw evidence. + +- [ ] **Step 2: Update matrix/status and next loop** + +Record whether display-name enrichment can proceed and rewrite C24 accordingly. + +- [ ] **Step 3: Verify and commit docs** + +Run: + +```powershell +git diff --check +go test ./... +powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1 +``` + +Expected: all exit 0. Commit exact C23 paths only and rewrite the next loop from actual results. + +--- + ## Self-Review - Spec coverage: the plan follows the user's cyclic requirement: plan first, implement one loop at a time, update the next loop after each result, and ask only on blockers. diff --git a/internal/tools/isphere_files.go b/internal/tools/isphere_files.go index efdc77c..7e377ad 100644 --- a/internal/tools/isphere_files.go +++ b/internal/tools/isphere_files.go @@ -14,12 +14,16 @@ import ( const ToolNameReceiveFiles = "isphere_receive_files" type ReceiveFilesArgs struct { - ConversationID string `json:"conversation_id,omitempty" jsonschema:"conversation id filter for file listing"` - MessageID string `json:"message_id,omitempty" jsonschema:"message id filter for file listing"` - FileID string `json:"file_id,omitempty" jsonschema:"file id filter for file listing"` - NameContains string `json:"name_contains,omitempty" jsonschema:"case-insensitive filename substring filter"` - Mode string `json:"mode,omitempty" jsonschema:"file receive mode; C14 supports list only"` - Limit int `json:"limit,omitempty" jsonschema:"maximum number of files to return; zero or negative returns all matches"` + ConversationID string `json:"conversation_id,omitempty" jsonschema:"conversation id filter for file listing"` + MessageID string `json:"message_id,omitempty" jsonschema:"message id filter for file listing"` + FileID string `json:"file_id,omitempty" jsonschema:"file id filter for file listing"` + NameContains string `json:"name_contains,omitempty" jsonschema:"case-insensitive filename substring filter"` + Mode string `json:"mode,omitempty" jsonschema:"file receive mode; C14 supports list only"` + OutputDir string `json:"output_dir,omitempty" jsonschema:"download output directory; not accepted in current list-only mode"` + 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 in list mode"` + Limit int `json:"limit,omitempty" jsonschema:"maximum number of files to return; zero or negative returns all matches"` } func RegisterISphereFileTools(server *mcp.Server, source ReceiveMessagesSource) { @@ -39,6 +43,12 @@ func RegisterISphereFileTools(server *mcp.Server, source ReceiveMessagesSource) if mode != "list" { return nil, nil, fmt.Errorf("isphere_receive_files mode %q is not available; only list is supported", input.Mode) } + if err := validateLocalReadonlySourceAndCursor(ToolNameReceiveFiles, input.SourcePreference, input.Cursor); err != nil { + return nil, nil, err + } + if strings.TrimSpace(input.OutputDir) != "" { + return nil, nil, fmt.Errorf("isphere_receive_files output_dir is only available for download mode, which is not implemented yet") + } messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0}) if err != nil { return nil, nil, err diff --git a/internal/tools/isphere_files_test.go b/internal/tools/isphere_files_test.go index 9cbb24c..736a4d7 100644 --- a/internal/tools/isphere_files_test.go +++ b/internal/tools/isphere_files_test.go @@ -88,3 +88,70 @@ func TestISphereReceiveFilesToolReturnsLogBackedFiles(t *testing.T) { t.Fatalf("source queries = %+v, want one unbounded receive query", fake.queries) } } + +func TestISphereReceiveFilesToolValidatesContractArgs(t *testing.T) { + fake := &fakeReceiveMessagesSource{ + result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{ + ID: "msg-file-1", + ConversationID: "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou", + Text: "redacted-report.docx", + Timestamp: "1783423807000", + Subject: "FILE_TRANSFER_CANCEL", + }}}, + } + session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) { + RegisterISphereFileTools(server, fake) + }) + defer cleanup() + + callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameReceiveFiles, + Arguments: map[string]any{ + "mode": "list", + "name_contains": "report", + "source_preference": "local_readonly", + "preview": true, + "cursor": "", + "output_dir": "", + "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) + } + + bridgeResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameReceiveFiles, + Arguments: map[string]any{"mode": "list", "source_preference": "bridge"}, + }) + if err == nil && !bridgeResult.IsError { + t.Fatalf("bridge source_preference was accepted before connector exists") + } + + cursorResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameReceiveFiles, + Arguments: map[string]any{"mode": "list", "cursor": "next-page"}, + }) + if err == nil && !cursorResult.IsError { + t.Fatalf("non-empty cursor was accepted before pagination exists") + } + + outputResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameReceiveFiles, + Arguments: map[string]any{"mode": "list", "output_dir": "C:\\tmp\\isphere"}, + }) + if err == nil && !outputResult.IsError { + t.Fatalf("output_dir was accepted in list mode before download exists") + } + + downloadResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: ToolNameReceiveFiles, + Arguments: map[string]any{"mode": "download", "file_id": "msg-file-1:redacted-report.docx"}, + }) + if err == nil && !downloadResult.IsError { + t.Fatalf("download mode was accepted before cache mapping exists") + } +} diff --git a/scripts/verify-go-mcp.ps1 b/scripts/verify-go-mcp.ps1 index 801ce9f..919b90c 100644 --- a/scripts/verify-go-mcp.ps1 +++ b/scripts/verify-go-mcp.ps1 @@ -567,7 +567,15 @@ func verifySearchGroups(ctx context.Context, session *mcp.ClientSession, query s func verifyReceiveFiles(ctx context.Context, session *mcp.ClientSession, nameContains string, step string, wantCount int, wantFileID string, wantFileName string) int { - callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_files", Arguments: map[string]any{"mode": "list", "name_contains": nameContains, "limit": 5}}) + callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_files", Arguments: map[string]any{ + "mode": "list", + "name_contains": nameContains, + "source_preference": "local_readonly", + "preview": true, + "cursor": "", + "output_dir": "", + "limit": 5, + }}) if err != nil { fail(step, err) }