feat: add send file preview tool

This commit is contained in:
zhaoyilun
2026-07-10 21:22:39 +08:00
parent 6781c75f84
commit 1fd10b5df7
9 changed files with 673 additions and 26 deletions

View File

@@ -29,6 +29,7 @@ var expectedTools = []string{
"isphere_search_contacts",
"isphere_search_groups",
"isphere_receive_files",
"isphere_send_file",
"isphere_send_message",
}
@@ -54,6 +55,8 @@ type smokeResult struct {
FixtureDirFileCount int `json:"fixture_dir_file_count"`
SendPreviewToolPresent bool `json:"send_preview_tool_present"`
ProductionSendEnabled bool `json:"production_send_enabled"`
SendFilePreviewToolPresent bool `json:"send_file_preview_tool_present"`
SendFileProductionEnabled bool `json:"send_file_production_enabled"`
LocalLoginRequired bool `json:"local_login_required"`
RealISphereLoginRequired bool `json:"real_isphere_login_required"`
PythonRuntimeRequired bool `json:"python_runtime_required"`
@@ -89,6 +92,17 @@ func run() error {
if err := os.Setenv("ISPHERE_SEND_IDEMPOTENCY_PATH", idempotencyPath); err != nil {
return err
}
sendFileDir := filepath.Join(sendStateDir, "send-file-allowed")
if err := os.MkdirAll(sendFileDir, 0o755); err != nil {
return err
}
sendFilePath := filepath.Join(sendFileDir, "redacted-preview.txt")
if err := os.WriteFile(sendFilePath, []byte("hello"), 0o600); err != nil {
return err
}
if err := os.Setenv("ISPHERE_SEND_FILE_ALLOWED_DIR", sendFileDir); err != nil {
return err
}
session, cleanup, err := newSession(ctx, "isphere-capability-smoke")
if err != nil {
@@ -128,6 +142,10 @@ func run() error {
if err != nil {
return err
}
sendFilePreviewOK, sendFileProductionEnabled, err := verifySendFile(ctx, session, sendFilePath)
if err != nil {
return err
}
fixtureReceiveCount, fixtureContactCount, fixtureGroupCount, fixtureFileCount, err := verifyPacketLogFileFixture(ctx)
if err != nil {
@@ -160,6 +178,8 @@ func run() error {
FixtureDirFileCount: fixtureDirFileCount,
SendPreviewToolPresent: sendPreviewOK,
ProductionSendEnabled: productionSendEnabled,
SendFilePreviewToolPresent: sendFilePreviewOK,
SendFileProductionEnabled: sendFileProductionEnabled,
LocalLoginRequired: false,
RealISphereLoginRequired: false,
PythonRuntimeRequired: false,
@@ -187,6 +207,7 @@ func clearRuntimeEnv() {
"ISPHERE_MSGLIB_SQLITE_DLL",
"ISPHERE_MSGLIB_DB",
"ISPHERE_MSGLIB_PASSWORD",
"ISPHERE_SEND_FILE_ALLOWED_DIR",
} {
_ = os.Unsetenv(key)
}
@@ -416,6 +437,55 @@ func verifySendMessage(ctx context.Context, session *mcp.ClientSession) (bool, b
return true, false, nil
}
func verifySendFile(ctx context.Context, session *mcp.ClientSession, filePath string) (bool, bool, error) {
fileHash, err := sha256FileHex(filePath)
if err != nil {
return false, false, err
}
preview, err := callToolObject(ctx, session, "isphere_send_file", map[string]any{
"target_type": "direct",
"target_id": "sender@imopenfire1-lanzhou",
"file_path": filePath,
"file_sha256": fileHash,
"idempotency_key": "capability-file-preview-idempotency-key",
"execution_mode": "preview",
"preview": true,
})
if err != nil {
return false, false, err
}
if preview["ok"] != true || preview["send_status"] != "planned" || preview["file_sha256"] != fileHash || preview["file_size_bytes"] != float64(5) {
return false, false, fmt.Errorf("unexpected send-file preview payload: %#v", preview)
}
sideEffects, ok := preview["side_effect_flags"].(map[string]any)
if !ok || sideEffects["sent_file"] != false || sideEffects["uploaded_file"] != false || sideEffects["clicked_ui"] != false {
return false, false, fmt.Errorf("send-file preview side effects mismatch: %#v", preview["side_effect_flags"])
}
if preview["production_send_enabled"] != false || preview["real_send_attempted"] != false {
return false, false, fmt.Errorf("send-file preview should not enable or attempt send: %#v", preview)
}
production, err := callToolObject(ctx, session, "isphere_send_file", map[string]any{
"target_type": "direct",
"target_id": "sender@imopenfire1-lanzhou",
"file_path": filePath,
"file_sha256": fileHash,
"idempotency_key": "capability-file-production-idempotency-key",
"execution_mode": "production",
})
if err != nil {
return false, false, err
}
if production["ok"] != false || production["send_status"] != "blocked" || production["production_send_enabled"] != false || production["real_send_attempted"] != false {
return false, false, fmt.Errorf("production send-file was not blocked: %#v", production)
}
productionSideEffects, ok := production["side_effect_flags"].(map[string]any)
if !ok || productionSideEffects["sent_file"] != false || productionSideEffects["uploaded_file"] != false || productionSideEffects["captured_network"] != false {
return false, false, fmt.Errorf("production send-file side effects mismatch: %#v", production["side_effect_flags"])
}
return true, false, nil
}
func verifyPacketLogFileFixture(ctx context.Context) (int, int, int, int, error) {
plaintext := fixturePacketLogPlaintext("msg-fixture-1", "redacted-report.docx")
line, err := encryptPacketLogLine(plaintext)
@@ -568,6 +638,14 @@ func sha256Hex(value string) string {
return hex.EncodeToString(sum[:])
}
func sha256FileHex(path string) (string, error) {
payload, err := os.ReadFile(path)
if err != nil {
return "", err
}
return sha256Hex(string(payload)), nil
}
func replaceEnv(key string, value string) func() {
oldValue, hadOldValue := os.LookupEnv(key)
if value == "" {