docs(tickets): PermissionのチケットとTUIのmd表示

This commit is contained in:
Keisuke Hirata 2026-05-05 17:16:03 +09:00
parent 96daebff30
commit 64814c2e15
4 changed files with 420 additions and 14 deletions

View File

@ -19,6 +19,7 @@
- 巻き戻されたターンの入力テキストを編集領域に復元 → [tickets/tui-empty-turn-restore.md](tickets/tui-empty-turn-restore.md)
- セッションコンテキスト長 / ウィンドウ占有率の常時表示 → [tickets/tui-context-usage-indicator.md](tickets/tui-context-usage-indicator.md)
- Compaction 進行中のライブ表示 → [tickets/tui-compact-progress.md](tickets/tui-compact-progress.md)
- Assistant 応答の Markdown スタイル表示 → [tickets/tui-assistant-markdown.md](tickets/tui-assistant-markdown.md)
- Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md)
- Prune: 保護境界を turn 数から末尾 token budget に置き換え → [tickets/prune-token-budget.md](tickets/prune-token-budget.md)
- メモリ機構

285
docs/manifest.toml Normal file
View File

@ -0,0 +1,285 @@
# ============================================================================
# Pod Manifest リファレンス
# ============================================================================
# Pod の宣言的設定 (`PodManifest` / `PodManifestConfig`)。
#
# カスケード層は下から順に
# 1. builtin defaults (`manifest::defaults`)
# 2. user manifest (`<config_dir>/manifest.toml`)
# 3. project manifest (cwd から上方向に探す `.insomnia/manifest.toml`)
# 4. programmatic overlay (呼び出し側が差し込む)
# 上の層が同名フィールドを上書き、scope rule と skills.directories は
# 累積マージ、tool_output.per_tool は key 単位でマージ。
#
# パス解決: 相対パスは「その層の manifest ファイルが置かれているディレクトリ」
# を base に絶対パスへ解決される (overlay 層は cwd)。マージは絶対化済みの
# 値同士で行われる。
#
# 凡例:
# - 必須 … 値が無いと resolve エラー
# - 任意 … 省略可
# - デフォルト … 省略時に採用される値 (None なら "なし")
# - 値 … enum 等で取り得る値
# ----------------------------------------------------------------------------
# ===== [pod] ================================================================
# Pod メタデータ。
[pod]
# 必須。Pod の表示名 (ResolveError::MissingField("pod.name") の対象)。
name = "example-agent"
# 任意。デフォルト: なし。
# PromptCatalog の 4 つ目の overlay 層として読み込む TOML pack のパス。
# 相対パスは manifest base 起点で解決。`worker.instruction` (`$prefix/...`)
# とは別系統の単なるファイルパス。
# prompt_pack = "./prompts.local.toml"
# ===== [model] ==============================================================
# LLM モデル設定。次の 3 形態を受ける:
# (a) `ref` 単独 — カタログから全部解決
# (b) `ref` + 一部 override — auth など個別差し替え
# (c) `scheme` + `model_id` 直書き — カタログを使わない inline 指定
# (b) / (c) では `ref` 未指定なら `scheme` / `model_id` / `auth` が必須。
# (実際の必須判定は `crates/provider` の resolve 側で行う)
[model]
# 任意。形式: "<provider_id>/<model_id_in_ref>"。
# 最初の `/` だけで split されるので、`openrouter/anthropic/claude-sonnet-4`
# のように内部 model_id に `/` が含まれる場合もそのまま書ける。
ref = "anthropic/claude-sonnet-4-6"
# 任意 (ただし ref 未指定時は実質必須)。デフォルト: なし。
# 値: "anthropic" | "openai_chat" | "openai_responses" | "gemini"
# scheme = "anthropic"
# 任意 (ref 未指定時は実質必須)。デフォルト: なし。
# プロバイダが受け付けるモデル ID 文字列。
# model_id = "claude-sonnet-4-20250514"
# 任意。デフォルト: scheme ごとの組み込み既定 URL。
# base_url = "https://api.anthropic.com"
# 任意 (ref 未指定時は実質必須)。デフォルト: なし。
# kind の値: "none" | "api_key" | "codex_oauth"
# - "none" … 認証不要 (ローカル Ollama 等)
# - "api_key" … env / file のいずれかで key を渡す。両方指定なら env 優先。
# env 未指定時は scheme ごとの既定環境変数:
# Anthropic -> INSOMNIA_API_KEY_ANTHROPIC
# OpenaiChat / OpenaiResponses -> INSOMNIA_API_KEY_OPENAI
# Gemini -> INSOMNIA_API_KEY_GEMINI
# file 指定時、相対パスは manifest base 起点で解決。
# - "codex_oauth" … ChatGPT OAuth (`~/.codex/auth.json`)。追加フィールドなし。
# auth = { kind = "none" }
# auth = { kind = "api_key" } # env のみ既定使用
# auth = { kind = "api_key", env = "MY_ANTHROPIC_KEY" }
# auth = { kind = "api_key", file = "./sk-ant.local" }
# auth = { kind = "codex_oauth" }
# 任意。デフォルト: モデルカタログ → provider.default_capability → scheme 既定
# の順で解決される。明示 override したいときだけ書く。
# [model.capability]
# # 値: "none" | "sequential" | "parallel"
# tool_calling = "parallel"
# # 値: "none" | "json_object" | "json_schema"
# structured_output = "json_schema"
# # 任意。値: 省略 (= None) | "effort" | "budget_tokens" | "both"
# reasoning = "both"
# # 任意。デフォルト: false
# vision = false
# # 値: { kind = "explicit", max_breakpoints = <u8> } | { kind = "auto" }
# prompt_caching = { kind = "explicit", max_breakpoints = 4 }
# ===== [worker] =============================================================
# ワーカーの生成パラメータ等。セクション自体省略可 (全フィールド任意)。
[worker]
# 任意。デフォルト: "$insomnia/default" (`defaults::DEFAULT_INSTRUCTION`)。
# システムプロンプト本体の `PromptLoader` 参照。
# プレフィクス: "$insomnia/..." | "$user/..." | "$workspace/..."
# instruction = "$insomnia/default"
# 任意。デフォルト: なし (プロバイダ任せ)。
# 1 レスポンスあたりの出力 token 上限。
# max_tokens = 4096
# 任意。デフォルト: なし (無制限)。
# 1 セッションの最大ターン数。NonZeroU32 — 0 は parse エラー。
# max_turns = 50
# 任意。デフォルト: なし (プロバイダ既定)。
# temperature = 0.3
# 任意。デフォルト: なし (プロバイダ既定)。
# top_p = 0.9
# 任意。デフォルト: なし (プロバイダ既定)。
# top_k = 40
# 任意。デフォルト: 空配列。
# stop_sequences = ["\n\n", "</stop>"]
# 任意。デフォルト: なし。
# 値:
# - 文字列 effort: "minimal" | "low" | "medium" | "high" | "xhigh"
# あるいは provider-native な任意ラベル文字列
# - 整数: Anthropic 系 thinking.budget_tokens (-1 で dynamic)
# reasoning = "medium"
# reasoning = -1
# 任意。tool 実行 content の byte 長キャップ。
# セクション省略時は default_max_bytes = 16 * 1024、per_tool 空。
# [worker.tool_output]
# # 任意。デフォルト: 16384 (`defaults::TOOL_OUTPUT_MAX_BYTES` = 16 KiB)。
# default_max_bytes = 16384
#
# # 任意。デフォルト: 空マップ。tool 名キーで個別キャップ上書き。
# # キーは tool の登録名 ("Read", "Grep", "Glob", ...)。
# [worker.tool_output.per_tool]
# Read = 32768
# Grep = 4096
# ===== [scope] ==============================================================
# Pod がアクセスできるディレクトリ/ファイル範囲。
# - allow: 最低 1 件必要 (空だと ResolveError / ScopeError::EmptyAllow)。
# 複数 allow がマッチした場合は最大の permission が採用される。
# - deny : 任意。マッチした deny の最小 permission *未満* に effective を
# 押し下げる (deny.read で完全遮断、deny.write で Read 止まり)。
# `target` は最終的に絶対パスでなければならない。manifest 内では相対 OK
# (manifest base 起点で resolve)。
# 必須: 最低 1 件の allow ルール。
[[scope.allow]]
# 必須。manifest 内では相対 OK (base 起点で絶対化)。
target = "./"
# 必須。値: "read" | "write"
permission = "write"
# 任意。デフォルト: true (再帰的にマッチ)。
# false の場合、ルール自身および直下の child のみマッチ。
# recursive = true
# allow は何件でも書ける。
# [[scope.allow]]
# target = "/abs/docs"
# permission = "read"
# recursive = false
# 任意。アクセスを *ルール内 permission 未満* に押し下げる。
# [[scope.deny]]
# target = "./secrets"
# permission = "write" # write を禁止 → 該当パスは Read までに降格
# recursive = true
#
# [[scope.deny]]
# target = "./secrets/key"
# permission = "read" # read 自体を禁止 → アクセス完全遮断
# recursive = true
# ===== [compaction] =========================================================
# コンテキスト圧縮 (Prune / Compact)。セクション省略で両方無効。
# セクションを書いた時点で Prune は有効化、Compact は閾値が None なら無効。
# [compaction]
#
# # 任意。デフォルト: 3 (`defaults::PRUNE_PROTECTED_TURNS`)。
# # pruning から保護する末尾ターン数。
# prune_protected_turns = 3
#
# # 任意。デフォルト: 4096 (`defaults::PRUNE_MIN_SAVINGS`)。
# # prune が発火するための最低節約 token 推定値。
# prune_min_savings = 4096
#
# # 任意。デフォルト: なし (proactive compact 無効)。
# # ターン間チェック (Controller post-run)。占有 token > これ で次ターン前に compact。
# compact_threshold = 80000
#
# # 任意。デフォルト: なし (safety-net compact 無効)。
# # ターン中チェック (PodInterceptor::pre_llm_request)。期待される関係:
# # compact_threshold < compact_request_threshold (proactive を先に発火)。
# # 逆順設定は許容するが warn ログを出す。
# compact_request_threshold = 90000
#
# # 任意。デフォルト: 8000 (`defaults::COMPACT_RETAINED_TOKENS`)。
# # compaction 後の history 末尾に verbatim で残す token budget。
# compact_retained_tokens = 8000
#
# # 任意。デフォルト: 8000 (`defaults::COMPACT_AUTO_READ_BUDGET`)。
# # compact worker が `mark_read_required` で取り込める累計 token。
# compact_auto_read_budget = 8000
#
# # 任意。デフォルト: 50000 (`defaults::COMPACT_WORKER_MAX_INPUT_TOKENS`)。
# # compact worker 自身の累積入力 token cap。超過で abort (circuit breaker)。
# compact_worker_max_input_tokens = 50000
#
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
# # compact 専用モデルを使う場合のみ書く ([model] と同じ形式)。
# # [compaction.model]
# # ref = "anthropic/claude-haiku-4-5"
# ===== [memory] =============================================================
# Memory subsystem の opt-in。
# - セクションが *ある* … memory tools (MemoryRead/Write/Edit) を登録、
# `<workspace>/memory/` と `<workspace>/knowledge/`
# の通常 write を Pod 自体に対して deny する。
# - セクションが *無い* … 何も起きない (legacy 動作)。
# `[memory]` だけ書いて中身を省略するのも有効 (全フィールド既定値で有効化)。
# [memory]
#
# # 任意。デフォルト: Pod の pwd (構築時)。
# # 必ず絶対パス (相対なら manifest base 起点で resolve)。
# workspace_root = "/abs/path/to/workspace"
#
# # 任意。デフォルト: tool 側既定 = 20。
# # MemoryQuery / KnowledgeQuery が 1 回に返す最大件数。
# query_result_limit = 20
#
# # 任意。デフォルト: tool 側既定 = 3。
# # 各マッチ前後に表示するコンテキスト行数。`query` 省略時は無視。
# query_excerpt_lines = 3
#
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
# # Phase 1 (extract) ワーカーのモデル ([model] と同じ形式)。
# # Haiku / 4o-mini / Flash クラスの軽量 reasoning モデル推奨。
# # [memory.extract_model]
# # ref = "anthropic/claude-haiku-4-5"
#
# # 任意。デフォルト: なし (Phase 1 自動発火を完全停止)。
# # 前回 extract pointer 以降の累積入力 token がこの値を超えると Phase 1 起動。
# # ※ memory tools と resident injection は extract_threshold が None でも動く。
# extract_threshold = 30000
#
# # 任意。デフォルト: 30000 (`defaults::MEMORY_EXTRACT_WORKER_MAX_INPUT_TOKENS`)。
# # extract worker 自身の累積入力 token cap (超過で abort)。
# extract_worker_max_input_tokens = 30000
#
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
# # Phase 2 (consolidation) ワーカーのモデル。reasoning クラス推奨。
# # [memory.consolidation_model]
# # ref = "anthropic/claude-sonnet-4-6"
#
# # 任意。デフォルト: なし。
# # `_staging/` のエントリ数がこの値以上で Phase 2 発火 (files / bytes は OR)。
# consolidation_threshold_files = 50
#
# # 任意。デフォルト: なし。
# # `_staging/` の総バイト数がこの値以上で Phase 2 発火 (files / bytes は OR)。
# # files / bytes の両方が None だと Phase 2 完全無効。
# consolidation_threshold_bytes = 1048576
# ===== [skills] =============================================================
# 外部 Agent Skills (`SKILL.md`) を Workflow として読み込むディレクトリ群。
# セクション省略 = 何もロードしない (implicit な `$config_dir/skills/` 検索や
# builtin probe は存在しない)。
# [skills]
#
# # 任意。デフォルト: 空配列。
# # 各エントリは skills *root* (root の child 各々が `<name>/SKILL.md` を持つ
# # skill バンドル)。root 自身は skill ではない。
# # 相対パスは manifest base 起点で解決。マージ時は層を跨いで concat される。
# directories = [".claude/skills", ".cursor/skills"]

View File

@ -10,44 +10,82 @@ OpenCode はパターンベースのルールtool × pattern → allow/deny/a
## 方針
`PreToolCall` Hook として実装する。マニフェストにルールを宣言し、
insomnia 層の Hook 実装がツール呼び出し時に評価する。
Permission の評価点は `PreToolCall` Hook とする。マニフェストにルールを宣言し、
insomnia 層が built-in の `PreToolCall` Hook として登録してツール呼び出し時に評価する。
`deny` はターン全体の Cancel/Abort ではない。対象 tool call を実行せず、
permission denied を表す `is_error = true` の synthetic tool result を履歴に追加してターンを継続する。
これにより provider が要求する `tool_use` / `tool_result` の対応を壊さず、LLM は拒否結果を見て別手段の検討やユーザーへの説明に進める。
`ask``deny` の代替ではなく、ユーザー承認待ちを明示する action として扱う。承認されれば元の tool call を実行し、拒否されれば `deny` と同じ synthetic tool result に落とす。
```toml
[[permission]]
[permissions]
default_action = "allow" # allow | deny | ask
[[permissions.rule]]
tool = "bash"
pattern = "rm *"
action = "deny"
[[permission]]
[[permissions.rule]]
tool = "file_write"
pattern = "*.env"
action = "deny"
```
[[permission]]
tool = "*"
allowlist 型にしたい場合:
```toml
[permissions]
default_action = "deny"
[[permissions.rule]]
tool = "read"
pattern = "*"
action = "allow"
[[permissions.rule]]
tool = "grep"
pattern = "*"
action = "allow"
```
評価順序OpenCode に倣う):
1. 最初にマッチした `deny` → 拒否
2. すべてマッチする `allow` → 許可
3. それ以外 → `ask`(ユーザー確認)
確認待ちを基本にしたい場合:
```toml
[permissions]
default_action = "ask"
[[permissions.rule]]
tool = "bash"
pattern = "rm *"
action = "deny"
```
評価順序:
1. `[permissions]` が無い場合、Permission 層は無効。従来通り実行する
2. `[permissions]` がある場合、`default_action` は必須
3. `[[permissions.rule]]` は宣言順に評価し、最初に `tool``pattern` が一致した rule の `action` を採用する
4. 一致する rule が無ければ `permissions.default_action` を採用する
## 設計ポイント
- 設計原則3: 新しい trait は作らない。`PreToolCall` Hook として実装
- 設計原則2: マニフェストに宣言した以上、insomnia 層が解決する
- `ask` アクションは Pod Protocol の拡張が必要Method に `PermissionReply` を追加)
- Permission Hook は Pod が自動登録する built-in Hook とし、ユーザー追加 Hook より先に評価する
- `deny``PreToolAction::Abort` / 既存 `Skip` では表現しない。tool call 単位の拒否結果を履歴へ返すため、Worker 側に synthetic tool result を返せる action が必要
- `ask` アクションは Pod Protocol の拡張が必要Event に `PermissionRequest`、Method に `PermissionReply` を追加)
- `ask` を処理できない実行環境では暗黙に待機しない。設定時に validation error とするか、fail closed で `deny` 相当の synthetic tool result に落とす
- `Scope` との関係: Scope は書き込みの物理的境界、Permission はツール実行のポリシー。補完関係
- ルール評価はパターンマッチのみ。コンテキスト依存の判断はしない(シンプルに保つ)
## 段階的実装
1. **拡張ポイントの記録**(今): docs/pod.md の拡張ポイント表に追加
2. **deny/allow の実装**(ツール実装時): PreToolCall Hook でパターン評価
3. **ask の実装**Protocol 拡張時): Method/Event に Permission 関連メッセージを追加
2. **deny/allow の実装**(ツール実装時): `default_action` と rule 評価を manifest に追加し、built-in `PreToolCall` Hook でパターン評価
3. **拒否 tool result の実装**: `deny` が turn Abort ではなく synthetic error tool result として履歴に入るよう Worker の pre-tool action を拡張
4. **ask の実装**Protocol 拡張時): Method/Event に Permission 関連メッセージを追加し、承認後に元 tool call を実行、拒否時は synthetic error tool result を返す
## 受け皿になる外部仕様
@ -55,7 +93,7 @@ action = "allow"
`tickets/agent-skills.md` で ingest した SKILL.md の frontmatter には agent-skills 仕様の experimental field である `allowed-tools` (例: `["Read", "Bash"]`) が含まれる場合がある。`crates/memory/src/skill.rs::parse_skill_md` 時点では `tracing::warn!` で受け流しているだけで、実効化していない。
本チケットの Permission 層が固まった時点で、Skill 由来 Workflow を実行中のみ当該 skill の `allowed-tools` リストに含まれるツールしか走れない形で反映する。スコープは「Workflow 実行中」相当 (Workflow の system message が context に乗っているターン) に限定する想定。skill 単位で local な permission 集合を持つので、グローバルな `[[permission]]` ルールとは独立に評価する。
本チケットの Permission 層が固まった時点で、Skill 由来 Workflow を実行中のみ当該 skill の `allowed-tools` リストに含まれるツールしか走れない形で反映する。スコープは「Workflow 実行中」相当 (Workflow の system message が context に乗っているターン) に限定する想定。skill 単位で local な permission 集合を持つので、グローバルな `[[permissions.rule]]` ルールとは独立に評価する。
実装上の足がかり:
- `WorkflowRecord` の出所は `WorkflowSource::Skill { dir }` で識別済み (`crates/memory/src/workflow.rs`)。`dir` は manifest `[skills] directories` に書かれた skill ルートそのもの

View File

@ -0,0 +1,82 @@
# TUI: Assistant 応答の Markdown スタイル表示
## 背景
LLM の出力は実質 Markdown だが、TUI は `Block::AssistantText { text }`
`push_padded_lines` で 1 行ずつ素のテキストとして
`Style(MessageKind::Assistant)` に流しているだけで、`**強調**` /
`` `code` `` / `# 見出し` / `- list` 等の記号がそのまま見える状態になっている
`crates/tui/src/ui.rs:592-595, 640-648`)。スタイルが付かないため、
構造のあるアシスタント応答は読みにくい。
ratatui 0.30 の `Vec<Line<'static>>` で表現できる範囲のスタイル付けで
十分目的を満たせる。既存の `wrap_line_into``crates/tui/src/ui.rs:473-`)が
span 単位のラップを既に実装しているため、Markdown レンダラは
スタイル付きの `Vec<Line>` を返すだけでよく、ラップスクロールoverview
畳み込みの仕組みを変える必要はない。
## 方針
- `pulldown-cmark``tui` クレートの依存に追加し、Event ストリームを
既存の `MessageKind` / `ratatui::style::Style` 体系へ畳み込む小さな
自前レンダラを `crates/tui/src/markdown.rs` に置く。
- レンダラの公開面は `render(text: &str, base: Style) -> Vec<Line<'static>>`
程度の 1 関数。`Block::AssistantText` の `Mode::Detail` / `Mode::Normal`
描画から呼ぶ。`Mode::Overview` は現行通り 1 行畳み込みMarkdown 記号
含めて表示しても情報量はほぼ同じなので素のテキストでよい)。
- ストリーミング中の不完全要素(未閉鎖の `**` や開きっぱなしのフェンス)
は CommonMark の流儀テキスト扱いEOF で閉じる)に任せる。挙動が
破綻する場合だけ末尾要素を素のテキストにフォールバックする小さな
後処理を入れる余地を残す。
- `tui-markdown` クレートは採用しない。syntect 依存でビルドが肥大する
割にカスタマイズが効かず、本クレートの色味(`MessageKind`
パレット)との整合を握りにくいため。
## 対応する Markdown 要素
最小限の "対応できる範囲" を以下に限定する。CommonMark + GFM の一部。
- 強調: `**bold**` / `*italic*` / `~~strike~~`GFM
- インラインコード: `` `code` ``
- フェンスコードブロック: ` ```lang ` / ` ``` `(言語タグは無視、
ブロック全体を等幅・低彩度の背景/前景で塗る)
- 見出し: H1〜H4H5/H6 は H4 と同等)
- 箇条書きリスト: `-` / `*` / `+`、ネスト可(深さ分インデント)
- 順序リスト: `1.` / `1)`、ネスト可(番号は元の値で表示)
- 引用: `> ...`(ネスト可)
- 水平線: `---` / `***`
- リンク: `[text](url)``text` をリンク色で着色URL は表示しない)
## 範囲外
- 表GFM table
- 画像 `![alt](src)`(テキストとしても表示しない)
- HTML パススルー(タグはそのまま生テキストで出る)
- 数式(`$...$` / `$$...$$`
- コードブロックの syntax highlighting
- リンクのターミナルクリックOSC 8URL の自動表示
- `Thinking` 本文 / `SystemMessage` への適用
(同じ `markdown::render` を後で差せばよい。本チケットは
`Block::AssistantText` のみ)
- ライブストリーム最中の "途中要素のフォールバック" の作り込み
CommonMark のデフォルト挙動で破綻が見えたら別チケット)
## 完了条件
- アシスタント応答に含まれる上記要素が、それぞれ視認可能な
スタイルで描画される。
- ストリーミング中、テキストが追記されるたびに描画が更新され、
フェンスコードブロックの開きが先に着いて中身が後から流れる
ようなケースでも、テキスト全体の見た目が大きく崩れない。
- `Mode::Detail` / `Mode::Normal` で Markdown スタイルが、
`Mode::Overview` では従来通りの 1 行畳み込みが出る。
- 既存の `wrap_line_into` によるラップ・右パディング・スクロール
が引き続き機能する(行幅計算が乱れない)。
## 影響範囲
- `crates/tui/Cargo.toml`: `pulldown-cmark` を追加(`cargo add` 経由)。
- `crates/tui/src/markdown.rs`: 新設。`render(&str, Style) -> Vec<Line<'static>>`。
- `crates/tui/src/ui.rs`: `Block::AssistantText` 分岐で Markdown
レンダラを呼ぶ。`Mode::Overview` は現行のまま。
- `crates/tui/src/main.rs` または `lib.rs`: 新モジュールの宣言。