feat: harden send file idempotency audit
This commit is contained in:
@@ -100,7 +100,7 @@ func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
for _, key := range []string{"ISPHERE_PACKET_LOG_FILE", "ISPHERE_PACKET_LOG_DIR", "ISPHERE_MSGLIB_SIDECAR_EXE", "ISPHERE_MSGLIB_SQLITE_DLL", "ISPHERE_MSGLIB_DB", "ISPHERE_MSGLIB_PASSWORD", "ISPHERE_SEND_FILE_ALLOWED_DIR"} {
|
||||
for _, key := range []string{"ISPHERE_PACKET_LOG_FILE", "ISPHERE_PACKET_LOG_DIR", "ISPHERE_MSGLIB_SIDECAR_EXE", "ISPHERE_MSGLIB_SQLITE_DLL", "ISPHERE_MSGLIB_DB", "ISPHERE_MSGLIB_PASSWORD", "ISPHERE_SEND_FILE_ALLOWED_DIR", "ISPHERE_SEND_FILE_AUDIT_PATH", "ISPHERE_SEND_FILE_IDEMPOTENCY_PATH"} {
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
fail("server/env", err)
|
||||
}
|
||||
@@ -116,6 +116,12 @@ func main() {
|
||||
if err := os.Setenv("ISPHERE_SEND_IDEMPOTENCY_PATH", filepath.Join(sendStateDir, "send-idempotency.jsonl")); err != nil {
|
||||
fail("server/send_state", err)
|
||||
}
|
||||
if err := os.Setenv("ISPHERE_SEND_FILE_AUDIT_PATH", filepath.Join(sendStateDir, "send-file-preview.jsonl")); err != nil {
|
||||
fail("server/send_file_state", err)
|
||||
}
|
||||
if err := os.Setenv("ISPHERE_SEND_FILE_IDEMPOTENCY_PATH", filepath.Join(sendStateDir, "send-file-idempotency.jsonl")); err != nil {
|
||||
fail("server/send_file_state", err)
|
||||
}
|
||||
sendFileDir, err := os.MkdirTemp("", "isphere-send-file-*")
|
||||
if err != nil {
|
||||
fail("server/send_file_state", err)
|
||||
@@ -235,7 +241,7 @@ func main() {
|
||||
defaultGroupCount := verifySearchGroups(ctx, session, "project", "tools/call isphere_search_groups default", 0)
|
||||
defaultFileCount := verifyReceiveFiles(ctx, session, "report", "tools/call isphere_receive_files default", 0, "", "")
|
||||
sendPreviewOK, productionSendEnabled := verifySendMessagePreview(ctx, session)
|
||||
sendFilePreviewOK, sendFileProductionEnabled := verifySendFilePreview(ctx, session, sendFilePath)
|
||||
sendFilePreviewOK, sendFileProductionEnabled, sendFileDuplicateDetected, sendFileConflictBlocked := verifySendFilePreview(ctx, session, sendFilePath)
|
||||
configuredReceiveCount, configuredContactCount, configuredGroupCount, configuredFileCount := verifyConfiguredReceiveSource(ctx)
|
||||
configuredDirReceiveCount, configuredDirContactCount, configuredDirGroupCount, configuredDirFileCount := verifyConfiguredDirectorySource(ctx)
|
||||
|
||||
@@ -267,6 +273,8 @@ func main() {
|
||||
"production_send_enabled": productionSendEnabled,
|
||||
"send_file_preview_tool_present": sendFilePreviewOK,
|
||||
"send_file_production_enabled": sendFileProductionEnabled,
|
||||
"send_file_duplicate_detected": sendFileDuplicateDetected,
|
||||
"send_file_conflict_blocked": sendFileConflictBlocked,
|
||||
"msglib_configured": false,
|
||||
}
|
||||
encoded, _ := json.Marshal(result)
|
||||
@@ -586,9 +594,9 @@ func verifySendMessagePreview(ctx context.Context, session *mcp.ClientSession) (
|
||||
return true, false
|
||||
}
|
||||
|
||||
func verifySendFilePreview(ctx context.Context, session *mcp.ClientSession, filePath string) (bool, bool) {
|
||||
func verifySendFilePreview(ctx context.Context, session *mcp.ClientSession, filePath string) (bool, bool, bool, bool) {
|
||||
fileHash := sha256FileHexForHarness(filePath)
|
||||
callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_send_file", Arguments: map[string]any{
|
||||
previewArgs := map[string]any{
|
||||
"target_type": "direct",
|
||||
"target_id": "sender@imopenfire1-lanzhou",
|
||||
"file_path": filePath,
|
||||
@@ -596,7 +604,8 @@ func verifySendFilePreview(ctx context.Context, session *mcp.ClientSession, file
|
||||
"idempotency_key": "verify-file-preview-idempotency-key",
|
||||
"execution_mode": "preview",
|
||||
"preview": true,
|
||||
}})
|
||||
}
|
||||
callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_send_file", Arguments: previewArgs})
|
||||
if err != nil {
|
||||
fail("tools/call isphere_send_file preview", err)
|
||||
}
|
||||
@@ -619,6 +628,62 @@ func verifySendFilePreview(ctx context.Context, session *mcp.ClientSession, file
|
||||
fail("tools/call isphere_send_file preview", fmt.Errorf("unexpected send-file side effects: %s", encoded))
|
||||
}
|
||||
|
||||
duplicateResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_send_file", Arguments: previewArgs})
|
||||
if err != nil {
|
||||
fail("tools/call isphere_send_file duplicate", err)
|
||||
}
|
||||
if duplicateResult.IsError {
|
||||
fail("tools/call isphere_send_file duplicate", fmt.Errorf("tool returned isError=true: %#v", duplicateResult.Content))
|
||||
}
|
||||
var duplicatePayload map[string]any
|
||||
duplicateEncoded, _ := json.Marshal(duplicateResult.StructuredContent)
|
||||
if err := json.Unmarshal(duplicateEncoded, &duplicatePayload); err != nil {
|
||||
fail("tools/call isphere_send_file duplicate", fmt.Errorf("decode structuredContent %s: %w", duplicateEncoded, err))
|
||||
}
|
||||
duplicateIdempotency, ok := duplicatePayload["idempotency"].(map[string]any)
|
||||
if !ok || duplicatePayload["ok"] != true || duplicatePayload["send_status"] != "planned" || duplicatePayload["real_send_attempted"] != false || duplicateIdempotency["duplicate_detected"] != true || duplicateIdempotency["conflict_detected"] != false {
|
||||
fail("tools/call isphere_send_file duplicate", fmt.Errorf("duplicate idempotency mismatch: %s", duplicateEncoded))
|
||||
}
|
||||
|
||||
conflictPath := filepath.Join(filepath.Dir(filePath), "redacted-conflict.txt")
|
||||
if err := os.WriteFile(conflictPath, []byte("world"), 0o600); err != nil {
|
||||
fail("tools/call isphere_send_file conflict_fixture", err)
|
||||
}
|
||||
conflictKey := "verify-file-conflict-idempotency-key"
|
||||
if _, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_send_file", Arguments: map[string]any{
|
||||
"target_type": "direct",
|
||||
"target_id": "sender@imopenfire1-lanzhou",
|
||||
"file_path": filePath,
|
||||
"file_sha256": fileHash,
|
||||
"idempotency_key": conflictKey,
|
||||
"execution_mode": "preview",
|
||||
}}); err != nil {
|
||||
fail("tools/call isphere_send_file conflict_first", err)
|
||||
}
|
||||
conflictResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_send_file", Arguments: map[string]any{
|
||||
"target_type": "direct",
|
||||
"target_id": "sender@imopenfire1-lanzhou",
|
||||
"file_path": conflictPath,
|
||||
"file_sha256": sha256FileHexForHarness(conflictPath),
|
||||
"idempotency_key": conflictKey,
|
||||
"execution_mode": "preview",
|
||||
}})
|
||||
if err != nil {
|
||||
fail("tools/call isphere_send_file conflict", err)
|
||||
}
|
||||
if conflictResult.IsError {
|
||||
fail("tools/call isphere_send_file conflict", fmt.Errorf("tool returned isError=true: %#v", conflictResult.Content))
|
||||
}
|
||||
var conflictPayload map[string]any
|
||||
conflictEncoded, _ := json.Marshal(conflictResult.StructuredContent)
|
||||
if err := json.Unmarshal(conflictEncoded, &conflictPayload); err != nil {
|
||||
fail("tools/call isphere_send_file conflict", fmt.Errorf("decode structuredContent %s: %w", conflictEncoded, err))
|
||||
}
|
||||
conflictIdempotency, ok := conflictPayload["idempotency"].(map[string]any)
|
||||
if !ok || conflictPayload["ok"] != false || conflictPayload["send_status"] != "blocked" || conflictPayload["real_send_attempted"] != false || conflictIdempotency["duplicate_detected"] != false || conflictIdempotency["conflict_detected"] != true {
|
||||
fail("tools/call isphere_send_file conflict", fmt.Errorf("conflict idempotency mismatch: %s", conflictEncoded))
|
||||
}
|
||||
|
||||
productionResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_send_file", Arguments: map[string]any{
|
||||
"target_type": "direct",
|
||||
"target_id": "sender@imopenfire1-lanzhou",
|
||||
@@ -645,7 +710,7 @@ func verifySendFilePreview(ctx context.Context, session *mcp.ClientSession, file
|
||||
if !ok || productionSideEffects["sent_file"] != false || productionSideEffects["uploaded_file"] != false || productionSideEffects["captured_network"] != false {
|
||||
fail("tools/call isphere_send_file production_blocked", fmt.Errorf("unexpected production file side effects: %s", productionEncoded))
|
||||
}
|
||||
return true, false
|
||||
return true, false, true, true
|
||||
}
|
||||
|
||||
func verifySearchContacts(ctx context.Context, session *mcp.ClientSession, query string, step string, wantCount int) int {
|
||||
@@ -856,6 +921,8 @@ func fail(step string, err error) {
|
||||
Assert-True ($script:harnessJson.production_send_enabled -eq $false) "SDK harness production_send_enabled mismatch: $($script:harnessJson.production_send_enabled)"
|
||||
Assert-True ($script:harnessJson.send_file_preview_tool_present -eq $true) "SDK harness send_file_preview_tool_present mismatch: $($script:harnessJson.send_file_preview_tool_present)"
|
||||
Assert-True ($script:harnessJson.send_file_production_enabled -eq $false) "SDK harness send_file_production_enabled mismatch: $($script:harnessJson.send_file_production_enabled)"
|
||||
Assert-True ($script:harnessJson.send_file_duplicate_detected -eq $true) "SDK harness send_file_duplicate_detected mismatch: $($script:harnessJson.send_file_duplicate_detected)"
|
||||
Assert-True ($script:harnessJson.send_file_conflict_blocked -eq $true) "SDK harness send_file_conflict_blocked mismatch: $($script:harnessJson.send_file_conflict_blocked)"
|
||||
|
||||
[ordered]@{
|
||||
ok = $true
|
||||
@@ -880,6 +947,8 @@ func fail(step string, err error) {
|
||||
production_send_enabled = $script:harnessJson.production_send_enabled
|
||||
send_file_preview_tool_present = $script:harnessJson.send_file_preview_tool_present
|
||||
send_file_production_enabled = $script:harnessJson.send_file_production_enabled
|
||||
send_file_duplicate_detected = $script:harnessJson.send_file_duplicate_detected
|
||||
send_file_conflict_blocked = $script:harnessJson.send_file_conflict_blocked
|
||||
packet_log_file_configured = $script:harnessJson.packet_log_file_configured
|
||||
packet_log_dir_configured = $script:harnessJson.packet_log_dir_configured
|
||||
python_runtime_required = $false
|
||||
|
||||
Reference in New Issue
Block a user