a3s 0.10.5

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use std::sync::Arc;

use async_trait::async_trait;
use tokio::sync::Mutex;

use super::credential_store::{
    CredentialStoreError, PrivateFileCredentialStore, WeixinCredentialStore, WeixinCredentials,
};
use a3s_boot::ilink::SecretValue;

fn credentials() -> WeixinCredentials {
    credentials_with_token("bot-token-canary")
}

fn credentials_with_token(token: &str) -> WeixinCredentials {
    WeixinCredentials::new(
        SecretValue::new(token).unwrap(),
        SecretValue::new("bot-id-canary").unwrap(),
        SecretValue::new("owner-id-canary").unwrap(),
        "https://ilinkai.weixin.qq.com",
        Some(SecretValue::new("context-token-canary").unwrap()),
    )
    .unwrap()
}

#[tokio::test]
#[cfg(unix)]
async fn weixin_credential_store_round_trips_private_file_and_deletes_it() {
    use std::os::unix::fs::PermissionsExt;

    let temporary = tempfile::tempdir().unwrap();
    let root = std::fs::canonicalize(temporary.path()).unwrap();
    let directory = root.join("weixin");
    let store = PrivateFileCredentialStore::new(&directory);
    let expected = credentials();

    assert!(store.load().await.unwrap().is_none());
    store.save(&expected).await.unwrap();

    let path = store.path_for_test();
    let file_metadata = std::fs::symlink_metadata(&path).unwrap();
    let directory_metadata = std::fs::symlink_metadata(&directory).unwrap();
    assert_eq!(file_metadata.permissions().mode() & 0o777, 0o600);
    assert_eq!(directory_metadata.permissions().mode() & 0o777, 0o700);
    assert_eq!(store.load().await.unwrap(), Some(expected.clone()));
    let rendered = format!("{:?}", store.load().await.unwrap().unwrap());
    for canary in [
        "bot-token-canary",
        "bot-id-canary",
        "owner-id-canary",
        "context-token-canary",
        "ilinkai.weixin.qq.com",
    ] {
        assert!(!rendered.contains(canary));
    }

    let replacement = credentials_with_token("replacement-token-canary");
    store.save(&replacement).await.unwrap();
    assert_eq!(store.load().await.unwrap(), Some(replacement));
    assert_eq!(
        std::fs::symlink_metadata(&path)
            .unwrap()
            .permissions()
            .mode()
            & 0o777,
        0o600
    );

    store.delete().await.unwrap();
    assert!(!path.exists());
    assert!(store.load().await.unwrap().is_none());
}

#[tokio::test]
#[cfg(unix)]
async fn weixin_credential_store_rejects_symlinks_and_permissive_files() {
    use std::os::unix::fs::{symlink, PermissionsExt};

    let temporary = tempfile::tempdir().unwrap();
    let root = std::fs::canonicalize(temporary.path()).unwrap();
    let real_directory = root.join("real");
    std::fs::create_dir(&real_directory).unwrap();
    std::fs::set_permissions(&real_directory, std::fs::Permissions::from_mode(0o700)).unwrap();
    let linked_directory = root.join("linked");
    symlink(&real_directory, &linked_directory).unwrap();
    let linked_store = PrivateFileCredentialStore::new(&linked_directory);
    assert!(matches!(
        linked_store.save(&credentials()).await,
        Err(CredentialStoreError::UnsafePath)
    ));

    let nested_link_store = PrivateFileCredentialStore::new(linked_directory.join("nested"));
    assert!(matches!(
        nested_link_store.save(&credentials()).await,
        Err(CredentialStoreError::UnsafePath)
    ));

    let store = PrivateFileCredentialStore::new(&real_directory);
    store.save(&credentials()).await.unwrap();
    std::fs::set_permissions(
        store.path_for_test(),
        std::fs::Permissions::from_mode(0o644),
    )
    .unwrap();
    assert!(matches!(
        store.load().await,
        Err(CredentialStoreError::UnsafePermissions)
    ));

    let permissive_parent = root.join("permissive-parent");
    std::fs::create_dir(&permissive_parent).unwrap();
    std::fs::set_permissions(&permissive_parent, std::fs::Permissions::from_mode(0o777)).unwrap();
    let nested_store = PrivateFileCredentialStore::new(permissive_parent.join("weixin"));
    assert!(matches!(
        nested_store.save(&credentials()).await,
        Err(CredentialStoreError::UnsafePermissions)
    ));

    let relative_store = PrivateFileCredentialStore::new("relative-weixin-state");
    assert!(matches!(
        relative_store.save(&credentials()).await,
        Err(CredentialStoreError::UnsafePath)
    ));
}

struct MemoryCredentialStore {
    value: Mutex<Option<WeixinCredentials>>,
}

impl MemoryCredentialStore {
    fn new() -> Self {
        Self {
            value: Mutex::new(None),
        }
    }
}

#[async_trait]
impl WeixinCredentialStore for MemoryCredentialStore {
    async fn load(&self) -> Result<Option<WeixinCredentials>, CredentialStoreError> {
        Ok(self.value.lock().await.clone())
    }

    async fn save(&self, credentials: &WeixinCredentials) -> Result<(), CredentialStoreError> {
        *self.value.lock().await = Some(credentials.clone());
        Ok(())
    }

    async fn delete(&self) -> Result<(), CredentialStoreError> {
        *self.value.lock().await = None;
        Ok(())
    }
}

#[tokio::test]
async fn weixin_credential_store_trait_supports_in_memory_test_double() {
    let store: Arc<dyn WeixinCredentialStore> = Arc::new(MemoryCredentialStore::new());
    let expected = credentials();

    store.save(&expected).await.unwrap();
    assert_eq!(store.load().await.unwrap(), Some(expected));
    store.delete().await.unwrap();
    assert!(store.load().await.unwrap().is_none());
}