Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,37 @@ quieter, and Fleet runs can be checked before they spend anything.
(a word for an on/off switch, text for a number, a choice outside the list)
instead of saving it ([#6568](https://github.com/Hmbown/Codewhale/pull/6568),
thanks @dajiaohuang).
- Receipts: `/receipts`, `codewhale receipts [ID|--last] [--format md|json]`,
and `GET /v1/threads/{id}/receipt` (plus a per-turn form) list what a session
did, one line per action: files changed with line counts, commands with exit
codes, web and MCP calls, agents, approvals and who gave them, and failures.
They also count what ran without asking and name the posture each turn ran
under, read from the turn's own record. A call Codewhale blocked before it
started (Auto-Review or guardian, a tool policy, a refused sandbox
escalation, invalid input, a missing tool) is listed as blocked, with the
reason, and is not counted as run or as ran without asking. Only
Codewhale's own refusal text counts: an MCP server, a fetched page, or a
program cannot make a call that ran read as blocked. A terminal
session's receipt also lists the files a command changed in each turn,
from the workspace snapshots taken before and after it (not ignored files
or anything outside the workspace), with control characters in paths
escaped so a file name cannot forge a receipt line. All three read the
records Codewhale already keeps and say what those records do not hold
([docs/RECEIPTS.md](docs/RECEIPTS.md)). `audit.log` is not that record: it
logs security events, and it logs an approval only when one is requested,
which under Full Access is almost never.
Comment on lines +80 to +98

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Leave changelog entries for merge

This PR edits both changelogs, although contribution guidance reserves those entries for a batched commit on main. Strip both hunks before merge.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.


### Fixed

- Approvals now record who decided: you, a session rule, or the posture. An
automatic approval used to be saved exactly like one you gave, and an app
approval that expired was saved as your denial. `GET /v1/approvals` now
returns `decided_by`.
- Network audit lines now go to the same `audit.log` as every other audit
event (`$CODEWHALE_HOME` included), and test runs no longer append to your
real one.
- Auto-Review verdicts now reach `audit.log`, as `/permissions` said they
did. They were written only when `CODEWHALE_TOOL_AUDIT_LOG` was set.
- A turn that stops producing output now reports itself: the turn loop records
its phase and last progress, and an overdue phase surfaces instead of
hanging silently until the stream idle timeout. A delegated agent's final result is
Expand Down
10 changes: 10 additions & 0 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ enum Commands {
Speech(TuiPassthroughArgs),
/// List saved sessions.
Sessions(TuiPassthroughArgs),
/// Show what a session did: files, commands, web and MCP calls, agents,
/// approvals, and failures. `codewhale receipts [ID|--last] [--format md|json]`.
#[command(visible_alias = "receipt")]
Receipts(TuiPassthroughArgs),
/// Resume a saved session.
Resume(TuiPassthroughArgs),
/// Launch an interactive session and hand it to the Codewhale web app.
Expand Down Expand Up @@ -2126,6 +2130,12 @@ fn run() -> Result<()> {
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
run_tui_in_process(&cli, &resolved_runtime, tui_args("sessions", args))
}
Some(Commands::Receipts(args)) => {
// Read-only: resolve the runtime without first-run setup side effects.
let resolved_runtime =
resolve_runtime_for_diagnostic_dispatch(&store, &runtime_overrides);
run_tui_in_process(&cli, &resolved_runtime, tui_args("receipts", args))
}
Some(Commands::Resume(args)) => {
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
run_resume_command(&cli, &resolved_runtime, args)
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@
"CmdConstitutionDescription": "Gestiona la llei constitucional permanent i les previsualitzacions",
"CmdContextDescription": "Obre l'inspector de context o l'informe del mapa de fonts",
"CmdCostDescription": "Mostra el desglossament de costos de la sessió",
"CmdReceiptsDescription": "Mostra què ha fet aquesta sessió: fitxers, ordres, aprovacions",
"CmdDiffDescription": "Mostra els canvis en fitxers des de l'inici de la sessió",
"CmdEditDescription": "Revisa i torna a enviar l'últim missatge",
"CmdExitDescription": "Surt de l'aplicació",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@
"CmdConstitutionDescription": "Geltende Verfassungsregeln und Vorschauen verwalten",
"CmdContextDescription": "Kontext-Inspektor oder Source-Map-Bericht öffnen",
"CmdCostDescription": "Sitzungskosten-Aufschlüsselung anzeigen",
"CmdReceiptsDescription": "Anzeigen, was diese Sitzung getan hat: Dateien, Befehle, Freigaben",
"CmdDiffDescription": "Dateiänderungen seit Sitzungsbeginn anzeigen",
"CmdEditDescription": "Letzte Nachricht überarbeiten und erneut senden",
"CmdExitDescription": "Anwendung beenden",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
"CmdConstitutionDescription": "See and amend your constitution",
"CmdContextDescription": "Open the context inspector",
"CmdCostDescription": "Show session cost breakdown",
"CmdReceiptsDescription": "Show what this session did: files, commands, approvals",
"CmdDiffDescription": "Show changes since this session started",
"CmdEditDescription": "Edit and resend your last message",
"CmdExitDescription": "Quit",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/es-419.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
"CmdConstitutionDescription": "Administrar la constitución permanente y vistas previas",
"CmdContextDescription": "Abrir el inspector compacto de contexto de la sesión",
"CmdCostDescription": "Mostrar el desglose de costo de la sesión",
"CmdReceiptsDescription": "Mostrar lo que hizo esta sesión: archivos, comandos, aprobaciones",
"CmdDiffDescription": "Mostrar cambios en archivos desde el inicio de la sesión",
"CmdEditDescription": "Revisar y reenviar el último mensaje",
"CmdExitDescription": "Salir de la aplicación",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@
"CmdConstitutionDescription": "Gérer la constitution permanente et ses aperçus",
"CmdContextDescription": "Ouvrir l'inspecteur de contexte ou le rapport source-map",
"CmdCostDescription": "Afficher le détail des coûts de la session",
"CmdReceiptsDescription": "Afficher ce que cette session a fait : fichiers, commandes, approbations",
"CmdDiffDescription": "Afficher les modifications de fichiers depuis le début de la session",
"CmdEditDescription": "Réviser et renvoyer le dernier message",
"CmdExitDescription": "Quitter l'application",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@
"CmdConstitutionDescription": "स्थायी संविधान कानून और पूर्वावलोकन प्रबंधित करें",
"CmdContextDescription": "संदर्भ निरीक्षक या सोर्स-मैप रिपोर्ट खोलें",
"CmdCostDescription": "सत्र लागत का विवरण दिखाएँ",
"CmdReceiptsDescription": "दिखाएँ कि इस सत्र ने क्या किया: फ़ाइलें, कमांड, स्वीकृतियाँ",
"CmdDiffDescription": "सत्र शुरू होने के बाद के फ़ाइल बदलाव दिखाएँ",
"CmdEditDescription": "अंतिम संदेश संशोधित कर पुनः सबमिट करें",
"CmdExitDescription": "एप्लिकेशन से बाहर निकलें",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@
"CmdConstitutionDescription": "Kelola hukum konstitusi tetap dan pratinjaunya",
"CmdContextDescription": "Buka inspektor konteks atau laporan source-map",
"CmdCostDescription": "Tampilkan rincian biaya sesi",
"CmdReceiptsDescription": "Tampilkan apa yang dilakukan sesi ini: berkas, perintah, persetujuan",
"CmdDiffDescription": "Tampilkan perubahan file sejak awal sesi",
"CmdEditDescription": "Revisi dan kirim ulang pesan terakhir",
"CmdExitDescription": "Keluar dari aplikasi",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
"CmdConstitutionDescription": "恒久憲法ルールとプレビューを管理",
"CmdContextDescription": "コンパクトなセッションコンテキスト検査ツールを開く",
"CmdCostDescription": "セッションのコスト内訳を表示",
"CmdReceiptsDescription": "このセッションの実行内容を表示: ファイル、コマンド、承認",
"CmdDiffDescription": "セッション開始以降のファイル変更を表示",
"CmdEditDescription": "最後のメッセージを編集して再送信",
"CmdExitDescription": "アプリを終了",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
"CmdConstitutionDescription": "상시 헌법 규칙과 미리보기를 관리합니다",
"CmdContextDescription": "컨텍스트 인스펙터 또는 소스맵 리포트를 엽니다",
"CmdCostDescription": "세션 비용 내역을 표시합니다",
"CmdReceiptsDescription": "이 세션이 한 일을 표시합니다: 파일, 명령, 승인",
"CmdDiffDescription": "세션 시작 이후 파일 변경 사항을 표시합니다",
"CmdEditDescription": "마지막 메시지를 수정해서 다시 보냅니다",
"CmdExitDescription": "애플리케이션을 종료합니다",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
"CmdConstitutionDescription": "Gerenciar a constituição permanente e pré-visualizações",
"CmdContextDescription": "Abrir o inspetor compacto de contexto da sessão",
"CmdCostDescription": "Exibir o detalhamento de custo da sessão",
"CmdReceiptsDescription": "Exibir o que esta sessão fez: arquivos, comandos, aprovações",
"CmdDiffDescription": "Mostrar alterações em arquivos desde o início da sessão",
"CmdEditDescription": "Revisar e reenviar a última mensagem",
"CmdExitDescription": "Sair do aplicativo",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@
"CmdConstitutionDescription": "Управление постоянной конституцией и предпросмотрами",
"CmdContextDescription": "Открыть инспектор контекста или отчёт source-map",
"CmdCostDescription": "Показать разбивку стоимости сессии",
"CmdReceiptsDescription": "Показать, что сделала эта сессия: файлы, команды, одобрения",
"CmdDiffDescription": "Показать изменения файлов с начала сессии",
"CmdEditDescription": "Изменить и повторно отправить последнее сообщение",
"CmdExitDescription": "Выйти из приложения",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@
"CmdConstitutionDescription": "Керувати чинним конституційним правом і попередніми переглядами",
"CmdContextDescription": "Відкрити інспектор контексту або звіт карти джерел",
"CmdCostDescription": "Показати розбивку вартості сеансу",
"CmdReceiptsDescription": "Показати, що зробив цей сеанс: файли, команди, схвалення",
"CmdDiffDescription": "Показати зміни файлів від початку сеансу",
"CmdEditDescription": "Змінити й повторно надіслати останнє повідомлення",
"CmdExitDescription": "Вийти з застосунку",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
"CmdConstitutionDescription": "Quản lý hiến pháp cố định và bản xem trước",
"CmdContextDescription": "Mở trình kiểm tra ngữ cảnh phiên thu gọn",
"CmdCostDescription": "Hiển thị chi tiết chi phí của phiên làm việc",
"CmdReceiptsDescription": "Hiển thị những gì phiên làm việc này đã làm: tệp, lệnh, phê duyệt",
"CmdDiffDescription": "Hiển thị các thay đổi của tệp kể từ khi bắt đầu phiên",
"CmdEditDescription": "Chỉnh sửa và gửi lại tin nhắn gần nhất",
"CmdExitDescription": "Thoát ứng dụng",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
"CmdConstitutionDescription": "管理长期宪章与预览",
"CmdContextDescription": "打开压缩会话上下文检查器或源映射报告",
"CmdCostDescription": "显示本次会话的费用明细",
"CmdReceiptsDescription": "显示本次会话做了什么:文件、命令、审批",
"CmdDiffDescription": "显示会话开始以来的文件变更",
"CmdEditDescription": "修改并重新提交最后一条消息",
"CmdExitDescription": "退出应用",
Expand Down
1 change: 1 addition & 0 deletions crates/localization/locales/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@
"CmdCostCoverage": "已涵蓋:{turns} 個計費回合中有 {priced} 個已定價。",
"CmdCostCoverageUnknownLegacy": "涵蓋範圍:未知。此工作階段儲存時尚未記錄逐回合定價涵蓋,因此無法確定上述金額已計入多少。",
"CmdCostDescription": "顯示本工作階段的費用明細",
"CmdReceiptsDescription": "顯示本工作階段做了什麼:檔案、指令、核准",
"CmdCostEstimateOnly": "這是估算值而非帳單:依據供應商回報的 token 用量與公開價格在本機計算,可能與實際帳單不同。",
"CmdCostLivePricingDowngraded": "無法驗證此路由的即時供應商價格({defects});改用內建的公開價格。",
"CmdCostLivePricingUnavailable": "無法驗證此路由的即時供應商價格({defects}),且沒有可用的內建費率;此筆支出未知。",
Expand Down
2 changes: 2 additions & 0 deletions crates/localization/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ pub enum MessageId {
CmdConstitutionDescription,
CmdContextDescription,
CmdCostDescription,
CmdReceiptsDescription,
CmdDiffDescription,
CmdEditDescription,
CmdExitDescription,
Expand Down Expand Up @@ -2983,6 +2984,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CmdConstitutionDescription,
MessageId::CmdContextDescription,
MessageId::CmdCostDescription,
MessageId::CmdReceiptsDescription,
MessageId::CmdDiffDescription,
MessageId::CmdEditDescription,
MessageId::CmdExitDescription,
Expand Down
31 changes: 31 additions & 0 deletions crates/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,37 @@ quieter, and Fleet runs can be checked before they spend anything.
(a word for an on/off switch, text for a number, a choice outside the list)
instead of saving it ([#6568](https://github.com/Hmbown/Codewhale/pull/6568),
thanks @dajiaohuang).
- Receipts: `/receipts`, `codewhale receipts [ID|--last] [--format md|json]`,
and `GET /v1/threads/{id}/receipt` (plus a per-turn form) list what a session
did, one line per action: files changed with line counts, commands with exit
codes, web and MCP calls, agents, approvals and who gave them, and failures.
They also count what ran without asking and name the posture each turn ran
under, read from the turn's own record. A call Codewhale blocked before it
started (Auto-Review or guardian, a tool policy, a refused sandbox
escalation, invalid input, a missing tool) is listed as blocked, with the
reason, and is not counted as run or as ran without asking. Only
Codewhale's own refusal text counts: an MCP server, a fetched page, or a
program cannot make a call that ran read as blocked. A terminal
session's receipt also lists the files a command changed in each turn,
from the workspace snapshots taken before and after it (not ignored files
or anything outside the workspace), with control characters in paths
escaped so a file name cannot forge a receipt line. All three read the
records Codewhale already keeps and say what those records do not hold
([docs/RECEIPTS.md](docs/RECEIPTS.md)). `audit.log` is not that record: it
logs security events, and it logs an approval only when one is requested,
which under Full Access is almost never.

### Fixed

- Approvals now record who decided: you, a session rule, or the posture. An
automatic approval used to be saved exactly like one you gave, and an app
approval that expired was saved as your denial. `GET /v1/approvals` now
returns `decided_by`.
- Network audit lines now go to the same `audit.log` as every other audit
event (`$CODEWHALE_HOME` included), and test runs no longer append to your
real one.
- Auto-Review verdicts now reach `audit.log`, as `/permissions` said they
did. They were written only when `CODEWHALE_TOOL_AUDIT_LOG` was set.
- A turn that stops producing output now reports itself: the turn loop records
its phase and last progress, and an overdue phase surfaces instead of
hanging silently until the stream idle timeout. A delegated agent's final result is
Expand Down
35 changes: 34 additions & 1 deletion crates/tui/src/approval_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ pub(crate) enum ApprovalOutcome {
},
}

/// Who resolved an approval request. Recorded on the decision half so a
/// receipt says "approved by you" only when a person answered. Records
/// written before this field existed carry no decider; readers report it as
/// not recorded rather than guessing.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ApprovalDecider {
/// A person answered the prompt: the terminal card, the app, the web
/// mirror, or a Runtime API client acting for them.
User,
/// A remembered "allow/deny for this session" rule answered it.
SessionRule,
/// The active mode or permission posture answered it without a prompt.
Posture,
/// The host resolved it without a person: the turn had ended, was
/// cancelled, or the decision channel closed.
Host,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "phase", rename_all = "snake_case")]
pub(crate) enum ApprovalReceipt {
Expand All @@ -40,6 +59,8 @@ pub(crate) enum ApprovalReceipt {
tool_call_id: String,
outcome: ApprovalOutcome,
created_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
decided_by: Option<ApprovalDecider>,
},
}

Expand All @@ -54,13 +75,23 @@ impl ApprovalReceipt {
}
}

/// A decision whose decider this call site does not know.
pub(crate) fn decided(tool_call_id: impl Into<String>, outcome: ApprovalOutcome) -> Self {
Self::decided_with(tool_call_id, outcome, None)
}

pub(crate) fn decided_with(
tool_call_id: impl Into<String>,
outcome: ApprovalOutcome,
decided_by: Option<ApprovalDecider>,
) -> Self {
let tool_call_id = tool_call_id.into();
Self::Decided {
approval_id: tool_call_id.clone(),
tool_call_id,
outcome,
created_at: Utc::now(),
decided_by,
}
}

Expand Down Expand Up @@ -96,6 +127,7 @@ pub(crate) struct CompletedApproval {
pub(crate) ask: ApprovalReceipt,
pub(crate) outcome: ApprovalOutcome,
pub(crate) decided_at: DateTime<Utc>,
pub(crate) decided_by: Option<ApprovalDecider>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
Expand Down Expand Up @@ -142,7 +174,7 @@ impl ApprovalReplay {
tool_call_id,
outcome,
created_at,
..
decided_by,
} => {
if approval_id != tool_call_id {
return Err(format!(
Expand All @@ -159,6 +191,7 @@ impl ApprovalReplay {
ask,
outcome: outcome.clone(),
decided_at: *created_at,
decided_by: *decided_by,
});
}
}
Expand Down
Loading
Loading