オフィスドキュメントスイート
ファイルを説明する段落ではなく、本物の.docxを手渡すエージェント——マーカープロトコル、フェイルクローズのパスフェンス、そしてモデルがマーカーを忘れた日のためのセーフティネット付き。
DuDuClawのエージェントは実際のオフィスファイル——Word (.docx)、Excel (.xlsx)、PowerPoint (.pptx)、PDF——を生成し、リクエストが届いたチャットチャネルへネイティブ添付ファイルとして届けられます。同梱の4つのドキュメントスキルが各フォーマットの作り方をエージェントに教えます。ファイルが存在した後のすべて——検証、チャネル配送、アーカイブ、ダッシュボードプレビュー——はゲートウェイが担います。
このスイートは受信方向でも機能します。メッセージにドキュメント添付が付いている場合、決定論的な拡張子テーブル(office_docs.rs)が適切なスキルへマッピングし——doc/docx → docx、xls/csv/xlsx → xlsx、ppt/pptx → pptx、pdf → pdf——プログレッシブスキルランカーのアクティブセットにそのスキルを強制投入します。純粋なルックアップであり、ドキュメントが添付されていなければコストはゼロです。
📎DELIVERプロトコル
Section titled “📎DELIVERプロトコル”ファイルを生成した後、エージェントは返信にファイルごとに1行のマーカーを追加します:
📎DELIVER:/home/u/.duduclaw/agents/sales/q3-report.docxゲートウェイは次を行います:
- ユーザーに見えるテキストからすべてのマーカー行を除去します。
- パスをフェイルクローズで検証します:絶対パスであること、通常ファイルとして存在すること、そして
..とシンボリックリンクを解決する正規化(canonicalization)の後、エージェント自身のディレクトリまたは共有attachments/フォールバックの内側にあること。agents/me/../victim/secretのようなトラバーサルは、正規化するとエージェントルートの外に出るため拒否されます。 - バイト列を読み取り、チャネルの
send_documentを通して送信します。
マーカーのない返信はバイト単位でそのまま返されます——最も一般的なケースは、プロンプトキャッシュとフォーマットの安定性を保ったままです。検証・読み取り・送信のいずれの失敗も正直にデグレードします:ファイルの場所を示すテキスト注記が追加されるため、成果物がどこへ行ったのかユーザーが分からなくなることはありません。サイレントドロップという結末はありません。
推測ではなくマーカーを使う理由
Section titled “推測ではなくマーカーを使う理由”返信テキストは、すべてのランタイム——Claude CLI、Codex、Gemini、direct API——が共有する唯一の受け渡しポイントです。返信内のマーカーによって、モデルが意図を宣言しつつ、実際にサンドボックスの外へ出るものについての権限はゲートウェイが握り続けます。その代替案——ゲートウェイが散文から成果物を推測する——こそ、このプロトコルが避けるために存在する失敗モードです。
このプロトコルは元々オフィススキルのSKILL.mdファイルにしか書かれておらず、実際のインシデント(2026-07-28)がその隙間を露呈しました:エージェントは本物の.docxを生成して~/Desktopに書き込みましたが、マーカーを一度も出力しませんでした。ユーザーが受け取ったのはファイルが存在すると主張する散文だけで、ゲートウェイには送信もアーカイブもするものがありませんでした。2つの修正が実装されました:
- 常時有効なシステムルール(
channel_reply.rsのdeliver_rules):すべてのチャネルシステムプロンプトに含まれる、静的でプロンプトキャッシュに優しいブロック——ファイルは作業ディレクトリ内に保存しなければならず、各ファイルは自分の📎DELIVER:行を持ち、「テキストでファイルを説明する」ことは配送に数えられません。姉妹の常時有効ブロック(pacing_rules)は関連する現場報告を修正します:重いタスクのターンの後、単なる挨拶には短い返信を返すべきであり、前のタスクを再実行してはなりません。 - 下記のスイープ:プロンプトルールでは保証できない残り半分のために。
スイープのセーフティネット
Section titled “スイープのセーフティネット”sweep_undeclared_deliverablesは、「作業ディレクトリにファイルを書いたがマーカーを忘れた」場合のための決定論的なネットです。生成されたドキュメントに言及しているマーカーなしの返信(キーワードゲート:拡張子、“Word”/“Excel”/“PowerPoint”、檔案/簡報/報表などのzh-TW用語)の後、ゲートウェイはエージェントディレクトリをスキャンし、見つけたものを宣言されていたかのようにそのまま配送します:
| 制約 | 値 |
|---|---|
| ファイルタイプ | docx xlsx pptx pdf csv odt ods odpのみ——.md/.txt/.jsonは決して対象外(エージェントはこれらを内部状態として書くため) |
| 新しさのウィンドウ | mtimeが15分以内 |
| 再帰の深さ | ≤ 3、attachments/、sessions/、logs/、memory/、隠しディレクトリはスキップ |
| 返信ごとの上限 | 3ファイル、新しい順 |
| 重複排除 | サニタイズ済みの名前+サイズがすでにアーカイブに存在するファイルは、以前のターンで配送済み——スキップ |
キーワードゲートは配送のヒューリスティックであり、セキュリティ判断ではありません。フェンスはあくまでvalidate_deliver_pathです。スイープの失敗はログに記録されてスキップされます——このネット自体が返信の新たな失敗モードになることは決してありません。
アーカイブ、ファイルページ、プレビュー
Section titled “アーカイブ、ファイルページ、プレビュー”配送されるすべてのファイルは、チャネル送信の前にエージェントのattachments/ディレクトリへコピーされます。そのため送信が失敗しても、成果物はダッシュボードのファイルページで閲覧可能なままです(すでにattachments/内にあるファイルは重複コピーされません)。
ファイルページはGET /api/files/preview経由でオフィスドキュメントをブラウザ内でプレビューします:
- PDFと画像はネイティブにインラインでストリームされます。
docx/xlsx/pptx/odt/ods/odp/csvはLibreOffice(soffice --headless)でPDFに変換されます。mtime検証付きキャッシュ(<home>/cache/preview/<agent>/配下)、分離されたLOプロファイル(デフォルトプロファイルに対する並列変換はそのロックを奪い合うため)、60秒タイムアウト付き。- LibreOfficeが見つからない場合→明示的な503 JSONメッセージを返し、壊れたバイトストリームは決して返しません。
- ダウンロードエンドポイントと同じJWT認証とパスフェンスが適用されます。
Provenance (I-2b)
Section titled “Provenance (I-2b)”attachments/に置かれるすべてのファイルは、以前はフラットなディレクトリ一覧の中の匿名の1行にすぎませんでした——顧客からの受信添付と、エージェントがgoal taskのために生成したレポートは区別がつきませんでした。provenance台帳(artifacts.jsonl。tool_calls.jsonlやtask_changes.jsonlと並ぶ場所にあります)が、各ファイルがどこから来たのかを記録するようになりました。書き込まれたその場で記録され、後から推測されることは決してありません:
| 由来 | 意味 |
|---|---|
declared |
エージェントが📎DELIVER:を通じて手渡しで配送したもの。 |
swept |
sweep_undeclared_deliverablesが回収したもの——ファイルは存在するが、マーカーが忘れられていた。 |
uploaded |
人間がチャネル経由で送ってきたもの(受信添付)。 |
produced |
タスク自身のchange ledgerから読み取り時に導出されるもの——artifactsファイル自体には永続化されない。 |
unknown |
証拠がそこまで届かず判断できないもの。誠実な保留であり、推測ではない。 |
タスクへの帰属も同じ考え方で二層に分かれています:その行(またはタスク自身のchange ledger)がタスクを直接名指ししていればexact、claim→reviewの時間窓の慣例(tasks.changesが使うのと同じ窓)だけでそこに置かれた場合はinferredです——ダッシュボードは推論を事実として提示することは決してありません。マージに使うkeyはファイルの実際のbasenameであり、CJKでサニタイズ済みの表示名ではありません。そのため由来の異なる2つのファイルがたまたま同じ文字列にサニタイズされても、1行に統合されることはありません。起動時のbackfillが、台帳が存在する前のファイルの履歴を埋めますが、それも既存の証拠(declared/sweptの記録、task_changes.jsonl)が届く範囲までです——判断できないものは方向を決めつけずunknownのままにされます。
この台帳を読む画面は2つあります:
- タスク詳細 → 成果物(產物)タブ:タスクに紐づく各ファイルを、由来・ラウンドとともに一覧表示し、(アーカイブ済みコピーがある
uploaded/declared/sweptの行には)ダウンロードリンクを添えます。エージェントが書き込んだがアーカイブされなかったファイルは、切れたダウンロードリンクの代わりに書き込まれたパスを表示します。 - ファイルページ:由来(來源)列とタスクフィルターがあり、「先週の成果物を探す」がスクロールではなくフィルター操作になります。
既知の制限:goal-loopのディスパッチ経路は、生成したファイルのダウンロード可能なコピーをまだアーカイブしません——これらの行はproducedとして現れても、背後に添付ファイルがない場合があります。このギャップを埋める作業は今後のwaveで追跡されています。
| 項目 | 制限 |
|---|---|
| 配送ファイルサイズ | 20 MB (media::MAX_FILE_SIZE) |
| 配送パス | 絶対パス、正規化済み、エージェントディレクトリまたは共有attachments/の内側(フェイルクローズ) |
| スイープ | 返信ごとに3ファイル、15分ウィンドウ、深さ≤3、オフィス拡張子のみ |
| プレビュー変換 | オフィス形式にはLibreOfficeが必要。変換ごとに60秒タイムアウト |