feat: register isphere file receive list tool
This commit is contained in:
@@ -14,7 +14,7 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
|
|||||||
## Current completed state
|
## Current completed state
|
||||||
|
|
||||||
- Go MCP foundation exists: `cmd/isphere-mcp`, `internal/mcpserver`, `internal/helperclient`, and `internal/tools` build a stdio MCP server.
|
- Go MCP foundation exists: `cmd/isphere-mcp`, `internal/mcpserver`, `internal/helperclient`, and `internal/tools` build a stdio MCP server.
|
||||||
- The current MCP surface exposes seven read-oriented tools:
|
- The current MCP surface exposes eight read-oriented tools:
|
||||||
- `win_helper_version`
|
- `win_helper_version`
|
||||||
- `win_helper_self_check`
|
- `win_helper_self_check`
|
||||||
- `win_helper_scan_windows`
|
- `win_helper_scan_windows`
|
||||||
@@ -22,14 +22,15 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
|
|||||||
- `isphere_receive_messages`
|
- `isphere_receive_messages`
|
||||||
- `isphere_search_contacts`
|
- `isphere_search_contacts`
|
||||||
- `isphere_search_groups`
|
- `isphere_search_groups`
|
||||||
|
- `isphere_receive_files`
|
||||||
- C# `ISphereWinHelper` exists under `native/ISphereWinHelper` and uses the `isphere.helper.v1` stdin/stdout JSON contract.
|
- 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, seven-tool MCP surface, default empty-source receive/contact/group calls, and synthetic configured-source receive/contact/group smoke.
|
- `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, and synthetic configured-source receive/contact/group/file smoke.
|
||||||
- N12-pre and N12R documents define safe offline evidence intake and internal-sandbox live UIA capture procedures.
|
- 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.
|
- N13/N14/N15 produced selector catalog/report validation infrastructure, but those are supporting validation assets only.
|
||||||
|
|
||||||
## What cannot be claimed externally
|
## What cannot be claimed externally
|
||||||
|
|
||||||
- The project has not yet implemented business MCP tools for message sending, file receiving/download, or file sending; `isphere_receive_messages`, `isphere_search_contacts`, and `isphere_search_groups` have operator-local encrypted log-file support but not a production ingestion path.
|
- The project has not yet implemented business MCP tools for message sending, file download, or file sending; `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files` list mode have operator-local encrypted log-file support but not a production ingestion path.
|
||||||
- The project has not completed production iSphere integration.
|
- The project has not completed production iSphere integration.
|
||||||
- The project has not proven a stable real message source for the full business workflow beyond decrypted PacketReader log evidence and synthetic/redacted tests.
|
- The project has not proven a stable real message source for the full business workflow beyond decrypted PacketReader log evidence and synthetic/redacted tests.
|
||||||
- The project has not implemented real message sending or real file upload.
|
- The project has not implemented real message sending or real file upload.
|
||||||
@@ -41,8 +42,8 @@ Remote base before local roadmap commits: `b2d839e Merge branch 'codex/n15-repor
|
|||||||
- The repository has a Go MCP + C# WinHelper foundation for controlled Windows-side read-only observation.
|
- The repository has a Go MCP + C# WinHelper foundation for controlled Windows-side read-only observation.
|
||||||
- The helper protocol boundary is explicit and keeps Windows/UIA work separate from the Go MCP service layer.
|
- The helper protocol boundary is explicit and keeps Windows/UIA work separate from the Go MCP service layer.
|
||||||
- The current code can support future MCP business tools by routing low-level Windows/UIA fallback work through a bounded helper layer.
|
- The current code can support future MCP business tools by routing low-level Windows/UIA fallback work through a bounded helper layer.
|
||||||
- The project has repeatable verification scripts for the helper/MCP foundation and the current seven-tool surface.
|
- The project has repeatable verification scripts for the helper/MCP foundation and the current eight-tool surface.
|
||||||
- The project now has a corrected business contract and plan for core MCP communication tools, plus registered receive/contact/group read tools that can use `ISPHERE_PACKET_LOG_FILE` for operator-local encrypted PacketReader log lines.
|
- The project now has a corrected business contract and plan for core MCP communication tools, plus registered receive/contact/group/file-list read tools that can use `ISPHERE_PACKET_LOG_FILE` for operator-local encrypted PacketReader log lines.
|
||||||
|
|
||||||
## N13/N14/N15 positioning
|
## N13/N14/N15 positioning
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
|
|||||||
|
|
||||||
## Current loop
|
## Current loop
|
||||||
|
|
||||||
Current loop: `Stage C / Loop C14 - register isphere_receive_files list mode`.
|
Current loop: `Stage C / Loop C15 - send message source evidence precheck`.
|
||||||
|
|
||||||
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
|
Plan file: `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop.md`.
|
||||||
|
|
||||||
@@ -75,11 +76,12 @@ Current loop output so far:
|
|||||||
10. C10: log-backed group candidate extraction from groupchat/conference JIDs.
|
10. C10: log-backed group candidate extraction from groupchat/conference JIDs.
|
||||||
11. C11: `isphere_search_groups` MCP registration and seven-tool verification.
|
11. C11: `isphere_search_groups` MCP registration and seven-tool verification.
|
||||||
12. C12: file evidence precheck confirmed enough PacketReader/Smark evidence for file metadata listing, but not enough for real cache/download mapping.
|
12. C12: file evidence precheck confirmed enough PacketReader/Smark evidence for file metadata listing, but not enough for real cache/download mapping.
|
||||||
13. C13: `internal/isphere.ListFilesFromMessages` extracts synthetic/redacted file metadata candidates from normalized messages; MCP registration is next.
|
13. C13: `internal/isphere.ListFilesFromMessages` extracts synthetic/redacted file metadata candidates from normalized messages.
|
||||||
|
14. C14: `isphere_receive_files` MCP list mode registered and verify-go-mcp now covers eight tools plus default/configured file-list smoke.
|
||||||
|
|
||||||
## Next business mainline
|
## Next business mainline
|
||||||
|
|
||||||
1. Run Stage C Loop C14: register `isphere_receive_files` list mode on top of `ListFilesFromMessages`.
|
1. Run Stage C Loop C15: verify available evidence for `isphere_send_message` connector selection before implementing any write-capable path.
|
||||||
2. Leave actual download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated.
|
2. Leave actual file download/cache mapping as a later connector node until a message-to-cache hash or client download source is validated.
|
||||||
3. Add `isphere_send_message` and `isphere_send_file` after source discovery and connector selection.
|
3. Add `isphere_send_message` and `isphere_send_file` only after source discovery, preview semantics, execution metadata, and audit shape are defined.
|
||||||
4. Keep UIA selector/report work as fallback support, not as the primary roadmap.
|
4. Keep UIA selector/report work as fallback support, not as the primary roadmap.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This runbook is for the first Go MCP phase of `isphere-ai-bridge`. It lets an operator build and verify the Windows helper and the Go MCP server without reading the source code.
|
This runbook is for the first Go MCP phase of `isphere-ai-bridge`. It lets an operator build and verify the Windows helper and the Go MCP server without reading the source code.
|
||||||
|
|
||||||
Current scope: expose four WinHelper observation operations plus the first log-backed business read tool, `isphere_receive_messages`, through Go MCP. The receive tool is registered with an empty default source unless `ISPHERE_PACKET_LOG_FILE` points to an operator-local encrypted PacketReader log-line file.
|
Current scope: expose four WinHelper observation operations plus four log-backed business read tools through Go MCP: `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files` list mode. These business read tools use an empty default source unless `ISPHERE_PACKET_LOG_FILE` points to an operator-local encrypted PacketReader log-line file.
|
||||||
|
|
||||||
## 1. Prerequisites
|
## 1. Prerequisites
|
||||||
|
|
||||||
@@ -90,14 +90,15 @@ The verification confirms:
|
|||||||
- C# helper can be built.
|
- C# helper can be built.
|
||||||
- Go MCP binary can be built.
|
- Go MCP binary can be built.
|
||||||
- MCP initialize/list/call flow works through the SDK harness.
|
- MCP initialize/list/call flow works through the SDK harness.
|
||||||
- Exactly seven tools are exposed: four WinHelper observation tools plus `isphere_receive_messages`, `isphere_search_contacts`, and `isphere_search_groups`.
|
- Exactly eight tools are exposed: four WinHelper observation tools plus `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files`.
|
||||||
- `win_helper_version` returns `ISphereWinHelper`.
|
- `win_helper_version` returns `ISphereWinHelper`.
|
||||||
- `isphere_receive_messages` is callable and returns an empty `messages` array from the default empty source.
|
- `isphere_receive_messages` is callable and returns an empty `messages` array from the default empty source.
|
||||||
- A synthetic configured-source smoke check proves `ISPHERE_PACKET_LOG_FILE` can drive `isphere_receive_messages` end-to-end with one redacted fixture message.
|
- A synthetic configured-source smoke check proves `ISPHERE_PACKET_LOG_FILE` can drive `isphere_receive_messages` end-to-end with one redacted fixture message.
|
||||||
- `isphere_search_contacts` is callable and returns JID-derived contact candidates from the same configured synthetic message fixture.
|
- `isphere_search_contacts` is callable and returns JID-derived contact candidates from the same configured synthetic message fixture.
|
||||||
- `isphere_search_groups` is callable and returns JID-derived group candidates from the configured synthetic groupchat fixture.
|
- `isphere_search_groups` is callable and returns JID-derived group candidates from the configured synthetic groupchat fixture.
|
||||||
|
- `isphere_receive_files` is callable in list mode and returns one file metadata candidate from the configured synthetic file-transfer fixture.
|
||||||
- Verification uses synthetic/local helper checks and does not load raw N12-pre evidence.
|
- Verification uses synthetic/local helper checks and does not load raw N12-pre evidence.
|
||||||
- Contact/group/file/search/send business tools remain later-stage work.
|
- File download/cache mapping and send business tools remain later-stage work.
|
||||||
|
|
||||||
## 6. Configure optional message source
|
## 6. Configure optional message source
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ E:\coding\codex\isphere-ai-bridge\isphere-mcp.exe
|
|||||||
|
|
||||||
## 8. Allowed tools
|
## 8. Allowed tools
|
||||||
|
|
||||||
These seven tools are currently allowed:
|
These eight tools are currently allowed:
|
||||||
|
|
||||||
| Tool | Source/op | Purpose |
|
| Tool | Source/op | Purpose |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -165,6 +166,7 @@ These seven tools are currently allowed:
|
|||||||
| `isphere_receive_messages` | log-backed `EncryptedPacketLogSource` | Read normalized messages from the configured PacketReader log source. The default server source is empty until a loader is wired. |
|
| `isphere_receive_messages` | log-backed `EncryptedPacketLogSource` | Read normalized messages from the configured PacketReader log source. The default server source is empty until a loader is wired. |
|
||||||
| `isphere_search_contacts` | log-backed message JIDs | Search contact candidates extracted from sender/receiver bare JIDs in the configured message source. |
|
| `isphere_search_contacts` | log-backed message JIDs | Search contact candidates extracted from sender/receiver bare JIDs in the configured message source. |
|
||||||
| `isphere_search_groups` | log-backed groupchat JIDs | Search group candidates extracted from groupchat/conference/MUC bare JIDs in the configured message source. |
|
| `isphere_search_groups` | log-backed groupchat JIDs | Search group candidates extracted from groupchat/conference/MUC bare JIDs in the configured message source. |
|
||||||
|
| `isphere_receive_files` | log-backed file-transfer messages | List file metadata candidates extracted from file-transfer message bodies. Download/cache mapping is not implemented yet. |
|
||||||
|
|
||||||
Allowed parameters:
|
Allowed parameters:
|
||||||
|
|
||||||
@@ -175,10 +177,11 @@ Allowed parameters:
|
|||||||
- `isphere_receive_messages`: `limit` only.
|
- `isphere_receive_messages`: `limit` only.
|
||||||
- `isphere_search_contacts`: `query`, `limit` only.
|
- `isphere_search_contacts`: `query`, `limit` only.
|
||||||
- `isphere_search_groups`: `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.
|
||||||
|
|
||||||
## 9. Current tool surface and next-stage route
|
## 9. Current tool surface and next-stage route
|
||||||
|
|
||||||
The current runbook verifies seven tools:
|
The current runbook verifies eight tools:
|
||||||
|
|
||||||
- `win_helper_version`
|
- `win_helper_version`
|
||||||
- `win_helper_self_check`
|
- `win_helper_self_check`
|
||||||
@@ -186,10 +189,12 @@ The current runbook verifies seven tools:
|
|||||||
- `win_helper_dump_uia`
|
- `win_helper_dump_uia`
|
||||||
- `isphere_receive_messages`
|
- `isphere_receive_messages`
|
||||||
- `isphere_search_contacts`
|
- `isphere_search_contacts`
|
||||||
|
- `isphere_search_groups`
|
||||||
|
- `isphere_receive_files`
|
||||||
|
|
||||||
`isphere_receive_messages` currently has 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`; this is still not a production ingestion claim because raw evidence remains local and uncommitted.
|
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`; this is still not a production ingestion claim because raw evidence remains local and uncommitted. `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/file capabilities enter the roadmap through that contract after source discovery, connector selection, preview metadata, execution metadata, and audit records are in place.
|
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.
|
||||||
|
|
||||||
## 10. Real-login UIA capture procedure
|
## 10. Real-login UIA capture procedure
|
||||||
|
|
||||||
@@ -248,7 +253,7 @@ win_helper_scan_windows
|
|||||||
win_helper_dump_uia
|
win_helper_dump_uia
|
||||||
```
|
```
|
||||||
|
|
||||||
The MCP server also registers `isphere_receive_messages`, but its default source is empty and it should not be used as N12R UIA evidence collection.
|
The MCP server also registers the log-backed read tools (`isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files`), but their default source is empty and they should not be used as N12R UIA evidence collection.
|
||||||
|
|
||||||
After receiving an N12R package, validate it with `docs/n12r-return-package-validation-checklist.md` before using it as evidence for selector-design review. Prefer the scripted read-only validator:
|
After receiving an N12R package, validate it with `docs/n12r-return-package-validation-checklist.md` before using it as evidence for selector-design review. Prefer the scripted read-only validator:
|
||||||
|
|
||||||
|
|||||||
@@ -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 `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource` | 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` | C5 env loader wired through ISPHERE_PACKET_LOG_FILE; default source remains empty when unset | fixture-backed configured-source operator smoke |
|
| `isphere_receive_messages` | decrypted `PacketReader.ProcessPacket` XMPP `<message>` stanzas via `internal/isphere.EncryptedPacketLogSource` | 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` | C5 env loader wired through ISPHERE_PACKET_LOG_FILE; default source remains empty when unset | fixture-backed configured-source operator smoke |
|
||||||
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages | 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` | C8 MCP tool registered and verify-go-mcp covers default/configured contact smoke; JID-only display/account fallback | enrich display names from roster/db/uia evidence |
|
| `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages | 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` | C8 MCP tool registered and verify-go-mcp covers default/configured 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 | `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 | C11 MCP tool registered and verify-go-mcp covers default/configured group smoke; JID-only display fallback | enrich group display/member data from roster/db/uia evidence |
|
| `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs | `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 | C11 MCP tool registered and verify-go-mcp covers default/configured 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 for metadata listing via `internal/isphere.ListFilesFromMessages`; `zyl\importal\<hash>` 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` | C13 source behavior implemented for metadata listing; no log-to-cache/download mapping yet | register `isphere_receive_files` MCP list mode |
|
| `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; `zyl\importal\<hash>` 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` | C14 MCP list mode registered and verify-go-mcp covers default/configured file-list smoke; no log-to-cache/download mapping yet | defer download mapping; run send-message source evidence precheck |
|
||||||
|
|
||||||
## Stage C entry rule
|
## Stage C entry rule
|
||||||
|
|
||||||
|
|||||||
@@ -1341,7 +1341,7 @@ Implemented and verified in this loop:
|
|||||||
- Output shape: `ok`, `mode`, `files`, `next_cursor`, and `audit`, aligned with `docs/mcp-core-tools-contract.md`.
|
- Output shape: `ok`, `mode`, `files`, `next_cursor`, and `audit`, aligned with `docs/mcp-core-tools-contract.md`.
|
||||||
- Default empty source returns `ok: true`, `mode: "list"`, `files: []`.
|
- Default empty source returns `ok: true`, `mode: "list"`, `files: []`.
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing MCP tool test**
|
- [x] **Step 1: Write failing MCP tool test**
|
||||||
|
|
||||||
Create `internal/tools/isphere_files_test.go` and run:
|
Create `internal/tools/isphere_files_test.go` and run:
|
||||||
|
|
||||||
@@ -1351,15 +1351,15 @@ go test ./internal/tools -run TestISphereReceiveFilesToolReturnsLogBackedFiles -
|
|||||||
|
|
||||||
Expected initial failure: file receive tool registration does not exist.
|
Expected initial failure: file receive tool registration does not exist.
|
||||||
|
|
||||||
- [ ] **Step 2: Implement file tool registration**
|
- [x] **Step 2: Implement file tool registration**
|
||||||
|
|
||||||
Register `isphere_receive_files` with source injection for tests. Use `ListFilesFromMessages` and return a contract-shaped map with minimal audit metadata.
|
Register `isphere_receive_files` with source injection for tests. Use `ListFilesFromMessages` and return a contract-shaped map with minimal audit metadata.
|
||||||
|
|
||||||
- [ ] **Step 3: Wire server and verification**
|
- [x] **Step 3: Wire server and verification**
|
||||||
|
|
||||||
Register file receive in server construction. Update server tool-count tests and `scripts/verify-go-mcp.ps1` to expect eight tools and call `isphere_receive_files` against default empty source and synthetic configured file-transfer fixture.
|
Register file receive in server construction. Update server tool-count tests and `scripts/verify-go-mcp.ps1` to expect eight tools and call `isphere_receive_files` against default empty source and synthetic configured file-transfer fixture.
|
||||||
|
|
||||||
- [ ] **Step 4: Run full verification and commit**
|
- [x] **Step 4: Run full verification and commit**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -1374,6 +1374,56 @@ Expected: all exit 0. Remove any generated root `isphere-mcp.exe` after build. C
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Loop C14 Result
|
||||||
|
|
||||||
|
Implemented and verified in this loop:
|
||||||
|
|
||||||
|
- `internal/tools.RegisterISphereFileTools(server, source)` registers `isphere_receive_files` over the same receive-message source used by receive/contact/group tools.
|
||||||
|
- `internal/mcpserver.NewServerWithReceiveSource` now exposes eight tools: four WinHelper tools plus `isphere_receive_messages`, `isphere_search_contacts`, `isphere_search_groups`, and `isphere_receive_files`.
|
||||||
|
- `scripts/verify-go-mcp.ps1` now expects eight tools, allows the file-list read tool, and verifies default empty file results plus configured synthetic file-transfer extraction.
|
||||||
|
- `isphere_receive_files` currently supports list mode only; download/cache mapping remains deferred because C12 found no message-to-cache hash link.
|
||||||
|
- Verification completed for C14 with affected package tests and `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1`; full verification is run again before commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loop C15: Send message source evidence precheck
|
||||||
|
|
||||||
|
**Goal:** Before implementing `isphere_send_message`, identify the lowest-risk connector path and preview/audit shape from existing bridge/API, local data, UIA, or network evidence; do not implement write-capable behavior without a validated connector decision.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**Planned behavior:**
|
||||||
|
- Inspect existing docs/contracts and committed helper/UIA/source-discovery artifacts for a send-message source candidate.
|
||||||
|
- Rank candidate connector paths: existing bridge/API/local service, UIA draft/send path, network/protocol path, or blocked pending internal sandbox evidence.
|
||||||
|
- If a safe connector exists, write C16 around preview-first `isphere_send_message` design or implementation.
|
||||||
|
- If evidence is insufficient, mark send-message blocked on live/internal-sandbox connector evidence and choose the next read hardening node.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Inspect current source evidence for send-message connector candidates**
|
||||||
|
|
||||||
|
Use committed docs and generated selector/source-discovery reports only. Do not infer a write path from unrelated read evidence.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update matrix and next loop**
|
||||||
|
|
||||||
|
Record whether send-message implementation can proceed, and rewrite C16 accordingly.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify docs-only change and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git diff --check
|
||||||
|
go test ./...
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-go-mcp.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all exit 0. Commit exact C15 paths only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Self-Review
|
## 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.
|
- 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.
|
||||||
|
|||||||
@@ -46,5 +46,6 @@ func NewServerWithReceiveSource(source tools.ReceiveMessagesSource) *mcp.Server
|
|||||||
tools.RegisterISphereReadTools(server, source)
|
tools.RegisterISphereReadTools(server, source)
|
||||||
tools.RegisterISphereContactTools(server, source)
|
tools.RegisterISphereContactTools(server, source)
|
||||||
tools.RegisterISphereGroupTools(server, source)
|
tools.RegisterISphereGroupTools(server, source)
|
||||||
|
tools.RegisterISphereFileTools(server, source)
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func TestNewServerRegistersN8WinHelperToolsWithoutCallingHelper(t *testing.T) {
|
|||||||
gotNames = append(gotNames, tool.Name)
|
gotNames = append(gotNames, tool.Name)
|
||||||
}
|
}
|
||||||
sort.Strings(gotNames)
|
sort.Strings(gotNames)
|
||||||
wantNames := []string{"isphere_receive_messages", "isphere_search_contacts", "isphere_search_groups", "win_helper_dump_uia", "win_helper_scan_windows", "win_helper_self_check", "win_helper_version"}
|
wantNames := []string{"isphere_receive_files", "isphere_receive_messages", "isphere_search_contacts", "isphere_search_groups", "win_helper_dump_uia", "win_helper_scan_windows", "win_helper_self_check", "win_helper_version"}
|
||||||
if !reflect.DeepEqual(gotNames, wantNames) {
|
if !reflect.DeepEqual(gotNames, wantNames) {
|
||||||
t.Fatalf("server tool names = %#v, want %#v", gotNames, wantNames)
|
t.Fatalf("server tool names = %#v, want %#v", gotNames, wantNames)
|
||||||
}
|
}
|
||||||
|
|||||||
96
internal/tools/isphere_files.go
Normal file
96
internal/tools/isphere_files.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
|
"isphere-ai-bridge/internal/isphere"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterISphereFileTools(server *mcp.Server, source ReceiveMessagesSource) {
|
||||||
|
if source == nil {
|
||||||
|
source = isphere.EncryptedPacketLogSource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
mcp.AddTool[ReceiveFilesArgs, map[string]any](server, &mcp.Tool{
|
||||||
|
Name: ToolNameReceiveFiles,
|
||||||
|
Description: "List iSphere file metadata candidates from the configured read-only message source; download is deferred until cache mapping is validated.",
|
||||||
|
}, func(ctx context.Context, _ *mcp.CallToolRequest, input ReceiveFilesArgs) (*mcp.CallToolResult, map[string]any, error) {
|
||||||
|
started := time.Now().UTC()
|
||||||
|
mode := strings.ToLower(strings.TrimSpace(input.Mode))
|
||||||
|
if mode == "" {
|
||||||
|
mode = "list"
|
||||||
|
}
|
||||||
|
if mode != "list" {
|
||||||
|
return nil, nil, fmt.Errorf("isphere_receive_files mode %q is not available; only list is supported", input.Mode)
|
||||||
|
}
|
||||||
|
messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
files := isphere.ListFilesFromMessages(messages.Messages, isphere.ListFilesQuery{
|
||||||
|
ConversationID: input.ConversationID,
|
||||||
|
MessageID: input.MessageID,
|
||||||
|
FileID: input.FileID,
|
||||||
|
NameContains: input.NameContains,
|
||||||
|
Limit: input.Limit,
|
||||||
|
})
|
||||||
|
return nil, receiveFilesResultToMap(files, started, time.Now().UTC()), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveFilesResultToMap(result isphere.ListFilesResult, started time.Time, finished time.Time) map[string]any {
|
||||||
|
files := make([]map[string]any, 0, len(result.Files))
|
||||||
|
for _, file := range result.Files {
|
||||||
|
files = append(files, map[string]any{
|
||||||
|
"file_id": file.FileID,
|
||||||
|
"message_id": nullableString(file.MessageID),
|
||||||
|
"conversation_id": nullableString(file.ConversationID),
|
||||||
|
"file_name": file.FileName,
|
||||||
|
"size_bytes": file.SizeBytes,
|
||||||
|
"mime_type": file.MimeType,
|
||||||
|
"created_at": nullableString(file.CreatedAt),
|
||||||
|
"download_ref": nullableString(file.DownloadRef),
|
||||||
|
"saved_path": nullableString(file.SavedPath),
|
||||||
|
"sha256": nullableString(file.SHA256),
|
||||||
|
"source": file.Source,
|
||||||
|
"raw_ref": file.RawRef,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"ok": true,
|
||||||
|
"mode": "list",
|
||||||
|
"files": files,
|
||||||
|
"next_cursor": nil,
|
||||||
|
"audit": map[string]any{
|
||||||
|
"tool": ToolNameReceiveFiles,
|
||||||
|
"source": "local_readonly",
|
||||||
|
"execution_mode": "read",
|
||||||
|
"started_at": started.Format(time.RFC3339Nano),
|
||||||
|
"finished_at": finished.Format(time.RFC3339Nano),
|
||||||
|
"result": "ok",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullableString(value string) any {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
90
internal/tools/isphere_files_test.go
Normal file
90
internal/tools/isphere_files_test.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
|
"isphere-ai-bridge/internal/isphere"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestISphereReceiveFilesToolReturnsLogBackedFiles(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", "limit": 5},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("call %s: %v", ToolNameReceiveFiles, err)
|
||||||
|
}
|
||||||
|
if callResult.IsError {
|
||||||
|
t.Fatalf("call result is error: %+v", callResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Files []struct {
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
MessageID string `json:"message_id"`
|
||||||
|
ConversationID string `json:"conversation_id"`
|
||||||
|
FileName string `json:"file_name"`
|
||||||
|
SizeBytes *int64 `json:"size_bytes"`
|
||||||
|
MimeType string `json:"mime_type"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
DownloadRef *string `json:"download_ref"`
|
||||||
|
SavedPath *string `json:"saved_path"`
|
||||||
|
SHA256 *string `json:"sha256"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
} `json:"files"`
|
||||||
|
NextCursor any `json:"next_cursor"`
|
||||||
|
Audit map[string]any `json:"audit"`
|
||||||
|
}
|
||||||
|
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 !decoded.OK || decoded.Mode != "list" {
|
||||||
|
t.Fatalf("ok/mode = %v/%q in %s", decoded.OK, decoded.Mode, payload)
|
||||||
|
}
|
||||||
|
if len(decoded.Files) != 1 {
|
||||||
|
t.Fatalf("files = %+v, want one", decoded.Files)
|
||||||
|
}
|
||||||
|
file := decoded.Files[0]
|
||||||
|
if file.FileID != "msg-file-1:redacted-report.docx" || file.MessageID != "msg-file-1" || file.ConversationID != "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou" {
|
||||||
|
t.Fatalf("unexpected file identity: %+v", file)
|
||||||
|
}
|
||||||
|
if file.FileName != "redacted-report.docx" || file.MimeType != "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || file.CreatedAt != "1783423807000" {
|
||||||
|
t.Fatalf("unexpected file metadata: %+v", file)
|
||||||
|
}
|
||||||
|
if file.SizeBytes != nil || file.DownloadRef != nil || file.SavedPath != nil || file.SHA256 != nil {
|
||||||
|
t.Fatalf("download/cache fields should be unknown: %+v", file)
|
||||||
|
}
|
||||||
|
if file.Source != "local_readonly" {
|
||||||
|
t.Fatalf("source = %q", file.Source)
|
||||||
|
}
|
||||||
|
if decoded.Audit["tool"] != ToolNameReceiveFiles || decoded.Audit["result"] != "ok" || decoded.Audit["execution_mode"] != "read" {
|
||||||
|
t.Fatalf("audit = %+v", decoded.Audit)
|
||||||
|
}
|
||||||
|
if len(fake.queries) != 1 || fake.queries[0].Limit != 0 {
|
||||||
|
t.Fatalf("source queries = %+v, want one unbounded receive query", fake.queries)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ $expectedTools = @(
|
|||||||
"win_helper_dump_uia",
|
"win_helper_dump_uia",
|
||||||
"isphere_receive_messages",
|
"isphere_receive_messages",
|
||||||
"isphere_search_contacts",
|
"isphere_search_contacts",
|
||||||
"isphere_search_groups"
|
"isphere_search_groups",
|
||||||
|
"isphere_receive_files"
|
||||||
)
|
)
|
||||||
|
|
||||||
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("isphere-go-mcp-verify-" + [guid]::NewGuid().ToString("N"))
|
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("isphere-go-mcp-verify-" + [guid]::NewGuid().ToString("N"))
|
||||||
@@ -83,9 +84,10 @@ var expectedTools = []string{
|
|||||||
"isphere_receive_messages",
|
"isphere_receive_messages",
|
||||||
"isphere_search_contacts",
|
"isphere_search_contacts",
|
||||||
"isphere_search_groups",
|
"isphere_search_groups",
|
||||||
|
"isphere_receive_files",
|
||||||
}
|
}
|
||||||
|
|
||||||
var forbiddenTerms = []string{"send", "file", "login", "conversation", "upload", "download", "autologin"}
|
var forbiddenTerms = []string{"send", "login", "conversation", "upload", "download", "autologin"}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
@@ -138,7 +140,7 @@ func main() {
|
|||||||
sort.Strings(toolNames)
|
sort.Strings(toolNames)
|
||||||
want := append([]string(nil), expectedTools...)
|
want := append([]string(nil), expectedTools...)
|
||||||
sort.Strings(want)
|
sort.Strings(want)
|
||||||
if len(toolNames) != 7 || !reflect.DeepEqual(toolNames, want) {
|
if len(toolNames) != 8 || !reflect.DeepEqual(toolNames, want) {
|
||||||
fail("tools/list", fmt.Errorf("tool names = %#v, want %#v", toolNames, want))
|
fail("tools/list", fmt.Errorf("tool names = %#v, want %#v", toolNames, want))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +192,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
defaultContactCount := verifySearchContacts(ctx, session, "sender", "tools/call isphere_search_contacts default", 0)
|
defaultContactCount := verifySearchContacts(ctx, session, "sender", "tools/call isphere_search_contacts default", 0)
|
||||||
defaultGroupCount := verifySearchGroups(ctx, session, "project", "tools/call isphere_search_groups default", 0)
|
defaultGroupCount := verifySearchGroups(ctx, session, "project", "tools/call isphere_search_groups default", 0)
|
||||||
configuredReceiveCount, configuredContactCount, configuredGroupCount := verifyConfiguredReceiveSource(ctx)
|
defaultFileCount := verifyReceiveFiles(ctx, session, "report", "tools/call isphere_receive_files default", 0)
|
||||||
|
configuredReceiveCount, configuredContactCount, configuredGroupCount, configuredFileCount := verifyConfiguredReceiveSource(ctx)
|
||||||
|
|
||||||
result := map[string]any{
|
result := map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
@@ -202,9 +205,11 @@ func main() {
|
|||||||
"receive_message_count": len(messagesValue),
|
"receive_message_count": len(messagesValue),
|
||||||
"contact_count": defaultContactCount,
|
"contact_count": defaultContactCount,
|
||||||
"group_count": defaultGroupCount,
|
"group_count": defaultGroupCount,
|
||||||
|
"file_count": defaultFileCount,
|
||||||
"configured_receive_message_count": configuredReceiveCount,
|
"configured_receive_message_count": configuredReceiveCount,
|
||||||
"configured_contact_count": configuredContactCount,
|
"configured_contact_count": configuredContactCount,
|
||||||
"configured_group_count": configuredGroupCount,
|
"configured_group_count": configuredGroupCount,
|
||||||
|
"configured_file_count": configuredFileCount,
|
||||||
"packet_log_file_configured": os.Getenv("ISPHERE_PACKET_LOG_FILE") != "",
|
"packet_log_file_configured": os.Getenv("ISPHERE_PACKET_LOG_FILE") != "",
|
||||||
"real_isphere_login_required": false,
|
"real_isphere_login_required": false,
|
||||||
"python_runtime_required": false,
|
"python_runtime_required": false,
|
||||||
@@ -215,13 +220,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int) {
|
func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int, int) {
|
||||||
plaintext := "--------------------------------------------------------------------------------------------------------------------------------------------\n" +
|
plaintext := "--------------------------------------------------------------------------------------------------------------------------------------------\n" +
|
||||||
"2026/7/7 15:30:07\n" +
|
"2026/7/7 15:30:07\n" +
|
||||||
"<message id=\"msg-fixture-1\" from=\"project-room@conference.imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"sender@imopenfire1-lanzhou\" type=\"groupchat\">\n" +
|
"<message id=\"msg-fixture-1\" from=\"project-room@conference.imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"sender@imopenfire1-lanzhou\" type=\"groupchat\">\n" +
|
||||||
" <body>redacted fixture</body>\n" +
|
" <body>redacted-report.docx</body>\n" +
|
||||||
" <received xmlns=\"urn:xmpp:receipts\" id=\"receipt-fixture-1\" type=\"1\" stamp=\"\" />\n" +
|
" <received xmlns=\"urn:xmpp:receipts\" id=\"receipt-fixture-1\" type=\"1\" stamp=\"\" />\n" +
|
||||||
" <subject>VERIFY_FIXTURE</subject>\n" +
|
" <subject>FILE_TRANSFER_CANCEL</subject>\n" +
|
||||||
" <isphere xmlns=\"isphere.im\" type=\"1001\" sendtime=\"1783423807000\" version=\"1\" />\n" +
|
" <isphere xmlns=\"isphere.im\" type=\"1001\" sendtime=\"1783423807000\" version=\"1\" />\n" +
|
||||||
" <readed />\n" +
|
" <readed />\n" +
|
||||||
"</message>"
|
"</message>"
|
||||||
@@ -306,7 +311,7 @@ func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
fail("fixture/assert", fmt.Errorf("message was not object: %#v", messagesValue[0]))
|
fail("fixture/assert", fmt.Errorf("message was not object: %#v", messagesValue[0]))
|
||||||
}
|
}
|
||||||
if message["id"] != "msg-fixture-1" || message["text"] != "redacted fixture" || message["subject"] != "VERIFY_FIXTURE" {
|
if message["id"] != "msg-fixture-1" || message["text"] != "redacted-report.docx" || message["subject"] != "FILE_TRANSFER_CANCEL" {
|
||||||
fail("fixture/assert", fmt.Errorf("unexpected message: %#v", message))
|
fail("fixture/assert", fmt.Errorf("unexpected message: %#v", message))
|
||||||
}
|
}
|
||||||
if message["conversation_id"] != "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou" {
|
if message["conversation_id"] != "project-room@conference.imopenfire1-lanzhou|sender@imopenfire1-lanzhou" {
|
||||||
@@ -314,7 +319,8 @@ func verifyConfiguredReceiveSource(ctx context.Context) (int, int, int) {
|
|||||||
}
|
}
|
||||||
contactCount := verifySearchContacts(ctx, session, "sender", "fixture/call isphere_search_contacts", 1)
|
contactCount := verifySearchContacts(ctx, session, "sender", "fixture/call isphere_search_contacts", 1)
|
||||||
groupCount := verifySearchGroups(ctx, session, "project", "fixture/call isphere_search_groups", 1)
|
groupCount := verifySearchGroups(ctx, session, "project", "fixture/call isphere_search_groups", 1)
|
||||||
return len(messagesValue), contactCount, groupCount
|
fileCount := verifyReceiveFiles(ctx, session, "report", "fixture/call isphere_receive_files", 1)
|
||||||
|
return len(messagesValue), contactCount, groupCount, fileCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int {
|
func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int {
|
||||||
@@ -388,6 +394,45 @@ func verifySearchGroups(ctx context.Context, session *mcp.ClientSession, query s
|
|||||||
return len(groupsValue)
|
return len(groupsValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func verifyReceiveFiles(ctx context.Context, session *mcp.ClientSession, nameContains string, step string, wantCount int) int {
|
||||||
|
callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_files", Arguments: map[string]any{"mode": "list", "name_contains": nameContains, "limit": 5}})
|
||||||
|
if err != nil {
|
||||||
|
fail(step, err)
|
||||||
|
}
|
||||||
|
if callResult.IsError {
|
||||||
|
fail(step, fmt.Errorf("tool returned isError=true: %#v", callResult.Content))
|
||||||
|
}
|
||||||
|
var filesPayload map[string]any
|
||||||
|
encoded, _ := json.Marshal(callResult.StructuredContent)
|
||||||
|
if err := json.Unmarshal(encoded, &filesPayload); err != nil {
|
||||||
|
fail(step, fmt.Errorf("decode structuredContent %s: %w", encoded, err))
|
||||||
|
}
|
||||||
|
if filesPayload["ok"] != true || filesPayload["mode"] != "list" {
|
||||||
|
fail(step, fmt.Errorf("ok/mode mismatch: %s", encoded))
|
||||||
|
}
|
||||||
|
filesValue, ok := filesPayload["files"].([]any)
|
||||||
|
if !ok {
|
||||||
|
fail(step, fmt.Errorf("files missing or not array: %s", encoded))
|
||||||
|
}
|
||||||
|
if len(filesValue) != wantCount {
|
||||||
|
fail(step, fmt.Errorf("files count = %d, want %d: %s", len(filesValue), wantCount, encoded))
|
||||||
|
}
|
||||||
|
if wantCount > 0 {
|
||||||
|
file, ok := filesValue[0].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
fail(step, fmt.Errorf("file was not object: %#v", filesValue[0]))
|
||||||
|
}
|
||||||
|
if file["file_id"] != "msg-fixture-1:redacted-report.docx" || file["file_name"] != "redacted-report.docx" || file["source"] != "local_readonly" {
|
||||||
|
fail(step, fmt.Errorf("unexpected file: %#v", file))
|
||||||
|
}
|
||||||
|
if file["download_ref"] != nil || file["saved_path"] != nil || file["sha256"] != nil {
|
||||||
|
fail(step, fmt.Errorf("download/cache fields should be nil: %#v", file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(filesValue)
|
||||||
|
}
|
||||||
|
|
||||||
func encryptPacketLogLineForHarness(plaintext string) (string, error) {
|
func encryptPacketLogLineForHarness(plaintext string) (string, error) {
|
||||||
block, err := des.NewCipher([]byte("hyhccdtm"))
|
block, err := des.NewCipher([]byte("hyhccdtm"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -436,13 +481,15 @@ func fail(step string, err error) {
|
|||||||
|
|
||||||
Assert-True ($script:harnessJson.ok -eq $true) "SDK harness did not return ok=true"
|
Assert-True ($script:harnessJson.ok -eq $true) "SDK harness did not return ok=true"
|
||||||
Assert-True ($script:harnessJson.helper_name -eq "ISphereWinHelper") "SDK harness helper_name mismatch: $($script:harnessJson.helper_name)"
|
Assert-True ($script:harnessJson.helper_name -eq "ISphereWinHelper") "SDK harness helper_name mismatch: $($script:harnessJson.helper_name)"
|
||||||
Assert-True ($script:harnessJson.tool_count -eq 7) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)"
|
Assert-True ($script:harnessJson.tool_count -eq 8) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)"
|
||||||
Assert-True ($script:harnessJson.receive_message_count -eq 0) "SDK harness receive_message_count mismatch: $($script:harnessJson.receive_message_count)"
|
Assert-True ($script:harnessJson.receive_message_count -eq 0) "SDK harness receive_message_count mismatch: $($script:harnessJson.receive_message_count)"
|
||||||
Assert-True ($script:harnessJson.contact_count -eq 0) "SDK harness contact_count mismatch: $($script:harnessJson.contact_count)"
|
Assert-True ($script:harnessJson.contact_count -eq 0) "SDK harness contact_count mismatch: $($script:harnessJson.contact_count)"
|
||||||
Assert-True ($script:harnessJson.group_count -eq 0) "SDK harness group_count mismatch: $($script:harnessJson.group_count)"
|
Assert-True ($script:harnessJson.group_count -eq 0) "SDK harness group_count mismatch: $($script:harnessJson.group_count)"
|
||||||
|
Assert-True ($script:harnessJson.file_count -eq 0) "SDK harness file_count mismatch: $($script:harnessJson.file_count)"
|
||||||
Assert-True ($script:harnessJson.configured_receive_message_count -eq 1) "SDK harness configured_receive_message_count mismatch: $($script:harnessJson.configured_receive_message_count)"
|
Assert-True ($script:harnessJson.configured_receive_message_count -eq 1) "SDK harness configured_receive_message_count mismatch: $($script:harnessJson.configured_receive_message_count)"
|
||||||
Assert-True ($script:harnessJson.configured_contact_count -eq 1) "SDK harness configured_contact_count mismatch: $($script:harnessJson.configured_contact_count)"
|
Assert-True ($script:harnessJson.configured_contact_count -eq 1) "SDK harness configured_contact_count mismatch: $($script:harnessJson.configured_contact_count)"
|
||||||
Assert-True ($script:harnessJson.configured_group_count -eq 1) "SDK harness configured_group_count mismatch: $($script:harnessJson.configured_group_count)"
|
Assert-True ($script:harnessJson.configured_group_count -eq 1) "SDK harness configured_group_count mismatch: $($script:harnessJson.configured_group_count)"
|
||||||
|
Assert-True ($script:harnessJson.configured_file_count -eq 1) "SDK harness configured_file_count mismatch: $($script:harnessJson.configured_file_count)"
|
||||||
|
|
||||||
[ordered]@{
|
[ordered]@{
|
||||||
ok = $true
|
ok = $true
|
||||||
@@ -454,9 +501,11 @@ func fail(step string, err error) {
|
|||||||
receive_message_count = $script:harnessJson.receive_message_count
|
receive_message_count = $script:harnessJson.receive_message_count
|
||||||
contact_count = $script:harnessJson.contact_count
|
contact_count = $script:harnessJson.contact_count
|
||||||
group_count = $script:harnessJson.group_count
|
group_count = $script:harnessJson.group_count
|
||||||
|
file_count = $script:harnessJson.file_count
|
||||||
configured_receive_message_count = $script:harnessJson.configured_receive_message_count
|
configured_receive_message_count = $script:harnessJson.configured_receive_message_count
|
||||||
configured_contact_count = $script:harnessJson.configured_contact_count
|
configured_contact_count = $script:harnessJson.configured_contact_count
|
||||||
configured_group_count = $script:harnessJson.configured_group_count
|
configured_group_count = $script:harnessJson.configured_group_count
|
||||||
|
configured_file_count = $script:harnessJson.configured_file_count
|
||||||
packet_log_file_configured = $script:harnessJson.packet_log_file_configured
|
packet_log_file_configured = $script:harnessJson.packet_log_file_configured
|
||||||
python_runtime_required = $false
|
python_runtime_required = $false
|
||||||
real_isphere_login_required = $false
|
real_isphere_login_required = $false
|
||||||
|
|||||||
Reference in New Issue
Block a user