Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .changeset/gmail-draft-only-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---

Implement robust Gmail draft-only mode with centralized policy enforcement in the executor layer to prevent bypasses.
75 changes: 75 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ path = "src/main.rs"
[dependencies]
aes-gcm = "0.10"
anyhow = "1"
clap = { version = "4", features = ["derive", "string"] }
clap = { version = "4", features = ["derive", "string", "env"] }
dirs = "5"
dotenvy = "0.15"
hostname = "0.4"
Expand Down Expand Up @@ -80,5 +80,6 @@ inherits = "release"
lto = "thin"

[dev-dependencies]
assert_cmd = "2"
serial_test = "3.4.0"
tempfile = "3"
9 changes: 9 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ pub fn build_cli(doc: &RestDescription) -> Command {
.value_name("TEMPLATE")
.global(true),
)
.arg(
clap::Arg::new("draft-only")
.long("draft-only")
.help("Gmail draft-only mode: block sending and strictly allow only draft creation/updates. Also reads GOOGLE_WORKSPACE_GMAIL_DRAFT_ONLY env var.")
.action(clap::ArgAction::SetTrue)
.env("GOOGLE_WORKSPACE_GMAIL_DRAFT_ONLY")
.global(true),
)

.arg(
clap::Arg::new("dry-run")
.long("dry-run")
Expand Down
14 changes: 6 additions & 8 deletions src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,7 @@ async fn build_http_request(
async fn handle_json_response(
body_text: &str,
pagination: &PaginationConfig,
sanitize_template: Option<&str>,
sanitize_mode: &crate::helpers::modelarmor::SanitizeMode,
policy: &crate::helpers::modelarmor::ExecutionPolicy,
output_format: &crate::formatter::OutputFormat,
pages_fetched: &mut u32,
page_token: &mut Option<String>,
Expand All @@ -229,7 +228,7 @@ async fn handle_json_response(
*pages_fetched += 1;

// Run Model Armor sanitization if --sanitize is enabled
if let Some(template) = sanitize_template {
if let Some(ref template) = policy.template {
let text_to_check = serde_json::to_string(&json_val).unwrap_or_default();
match crate::helpers::modelarmor::sanitize_text(template, &text_to_check).await {
Ok(result) => {
Expand All @@ -238,7 +237,7 @@ async fn handle_json_response(
eprintln!("⚠️ Model Armor: prompt injection detected (filterMatchState: MATCH_FOUND)");
}

if is_match && *sanitize_mode == crate::helpers::modelarmor::SanitizeMode::Block
if is_match && policy.mode == crate::helpers::modelarmor::SanitizeMode::Block
{
let blocked = serde_json::json!({
"error": "Content blocked by Model Armor",
Expand Down Expand Up @@ -377,13 +376,13 @@ pub async fn execute_method(
upload_content_type: Option<&str>,
dry_run: bool,
pagination: &PaginationConfig,
sanitize_template: Option<&str>,
sanitize_mode: &crate::helpers::modelarmor::SanitizeMode,
policy: &crate::helpers::modelarmor::ExecutionPolicy,
output_format: &crate::formatter::OutputFormat,
capture_output: bool,
) -> Result<Option<Value>, GwsError> {
let input = parse_and_validate_inputs(doc, method, params_json, body_json, upload_path)?;


if dry_run {
let dry_run_info = json!({
"dry_run": true,
Expand Down Expand Up @@ -470,8 +469,7 @@ pub async fn execute_method(
let should_continue = handle_json_response(
&body_text,
pagination,
sanitize_template,
sanitize_mode,
policy,
output_format,
&mut pages_fetched,
&mut page_token,
Expand Down
5 changes: 2 additions & 3 deletions src/helpers/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ TIPS:
&'a self,
doc: &'a crate::discovery::RestDescription,
matches: &'a ArgMatches,
_sanitize_config: &'a crate::helpers::modelarmor::SanitizeConfig,
_policy: &'a crate::helpers::modelarmor::ExecutionPolicy,
) -> Pin<Box<dyn Future<Output = Result<bool, GwsError>> + Send + 'a>> {
Box::pin(async move {
if let Some(matches) = matches.subcommand_matches("+insert") {
Expand Down Expand Up @@ -182,8 +182,7 @@ TIPS:
None,
matches.get_flag("dry-run"),
&executor::PaginationConfig::default(),
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
_policy,
&crate::formatter::OutputFormat::default(),
false,
)
Expand Down
5 changes: 2 additions & 3 deletions src/helpers/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ TIPS:
&'a self,
doc: &'a crate::discovery::RestDescription,
matches: &'a ArgMatches,
_sanitize_config: &'a crate::helpers::modelarmor::SanitizeConfig,
_policy: &'a crate::helpers::modelarmor::ExecutionPolicy,
) -> Pin<Box<dyn Future<Output = Result<bool, GwsError>> + Send + 'a>> {
// We use `Box::pin` to create a pinned future on the heap.
// This is necessary because the `Helper` trait returns a generic `Future`,
Expand Down Expand Up @@ -112,8 +112,7 @@ TIPS:
None,
matches.get_flag("dry-run"),
&pagination,
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
_policy,
&crate::formatter::OutputFormat::default(),
false,
)
Expand Down
5 changes: 2 additions & 3 deletions src/helpers/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ TIPS:
&'a self,
doc: &'a crate::discovery::RestDescription,
matches: &'a ArgMatches,
_sanitize_config: &'a crate::helpers::modelarmor::SanitizeConfig,
_policy: &'a crate::helpers::modelarmor::ExecutionPolicy,
) -> Pin<Box<dyn Future<Output = Result<bool, GwsError>> + Send + 'a>> {
Box::pin(async move {
if let Some(matches) = matches.subcommand_matches("+write") {
Expand Down Expand Up @@ -102,8 +102,7 @@ TIPS:
None,
matches.get_flag("dry-run"),
&pagination,
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
_policy,
&crate::formatter::OutputFormat::default(),
false,
)
Expand Down
5 changes: 2 additions & 3 deletions src/helpers/drive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ TIPS:
&'a self,
doc: &'a crate::discovery::RestDescription,
matches: &'a ArgMatches,
_sanitize_config: &'a crate::helpers::modelarmor::SanitizeConfig,
_policy: &'a crate::helpers::modelarmor::ExecutionPolicy,
) -> Pin<Box<dyn Future<Output = Result<bool, GwsError>> + Send + 'a>> {
Box::pin(async move {
if let Some(matches) = matches.subcommand_matches("+upload") {
Expand Down Expand Up @@ -113,8 +113,7 @@ TIPS:
None,
matches.get_flag("dry-run"),
&executor::PaginationConfig::default(),
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
_policy,
&crate::formatter::OutputFormat::default(),
false,
)
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ TIPS:
&'a self,
doc: &'a crate::discovery::RestDescription,
matches: &'a ArgMatches,
_sanitize_config: &'a crate::helpers::modelarmor::SanitizeConfig,
_policy: &'a crate::helpers::modelarmor::ExecutionPolicy,
) -> Pin<Box<dyn Future<Output = Result<bool, GwsError>> + Send + 'a>> {
Box::pin(async move {
if let Some(sub_matches) = matches.subcommand_matches("+subscribe") {
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/gmail/forward.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use super::*;
pub(super) async fn handle_forward(
doc: &crate::discovery::RestDescription,
matches: &ArgMatches,
policy: &crate::helpers::modelarmor::ExecutionPolicy,
) -> Result<(), GwsError> {
let config = parse_forward_args(matches);
let dry_run = matches.get_flag("dry-run");
Expand Down Expand Up @@ -54,6 +55,7 @@ pub(super) async fn handle_forward(
&raw,
Some(&original.thread_id),
token.as_deref(),
policy,
)
.await
}
Expand Down
Loading
Loading