fix: relocate out-of-allowed outbound attachments so files actually send

Agent-generated files written to an arbitrary working path (e.g. TTS audio
under /home/claude/jarvis-tts) were rejected by validateOutboundAttachments as
"outside-allowed-dirs". The rejection was only logged; the MEDIA: directive had
already been stripped from the text, so the user got a message claiming a file
was attached with no file and no error.

- Stage attachments outside the room's allowed dirs into a safe per-group dir
  (data/attachments/outbound/<group>) at the universal delivery choke point, so
  the path delivery uses and revalidates is one the validator accepts. Files
  already inside an allowed dir are untouched, preserving isolation checks.
- Surface any still-rejected attachment in the visible Discord body via
  appendRejectionNotice, so delivery can never again silently drop a file.

Verified: outbound-attachments 22/22, discord 46/46 (incl. new integration test
asserting the notice lands in the sent body), final-delivery 5/5, tsc clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-12 00:39:46 +09:00
parent aec70bc388
commit 9c46cf6761
5 changed files with 285 additions and 8 deletions

View File

@@ -977,6 +977,28 @@ describe('sendMessage', () => {
fs.rmSync(dir, { recursive: true, force: true });
});
it('surfaces rejected attachments in the sent message instead of dropping them silently', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);
await channel.connect();
const mockChannel = {
send: vi.fn().mockResolvedValue({ id: 'discord-message-1' }),
sendTyping: vi.fn(),
};
currentClient().channels.fetch.mockResolvedValue(mockChannel);
await channel.sendMessage('dc:1234567890123456', '샘플을 첨부합니다.', {
attachments: [{ path: '/home/claude/missing-sample.wav' }],
});
expect(mockChannel.send).toHaveBeenCalledTimes(1);
const sent = mockChannel.send.mock.calls[0][0];
expect(sent.files).toBeUndefined();
expect(sent.content).toContain('샘플을 첨부합니다.');
expect(sent.content).toContain('첨부 1건을 전송하지 못했습니다');
expect(sent.content).toContain('missing-sample.wav (파일을 찾을 수 없음)');
});
it('uses legacy image tags as Discord attachment fallback', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);