KeiSeiKit-1.0/_primitives/_rust/kei-store/src/github.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

167 lines
5.7 KiB
Rust

//! GitHubStore — git-over-SSH/HTTPS.
//!
//! Wraps FilesystemStore for local ops, adds push/pull to a configured
//! remote. SSH auth via `KEI_MEMORY_SSH_KEY` (path to key); HTTPS via
//! `KEI_MEMORY_PAT` (token). Exactly the pattern used in v0.11
//! `kei-sleep-setup.sh`.
//!
//! v0.14.1: pushes to `github.com` are blocked by default under RULE 0.1
//! (patent-IP protection). Forks on Forgejo / Gitea / self-hosted are
//! unaffected since they do not resolve to `github.com`. Override for a
//! genuinely public repo: `KEI_STORE_ALLOW_GITHUB_PUSH=1`.
use crate::config::GitRemoteCfg;
use crate::filesystem::FilesystemStore;
use crate::store_trait::MemoryStore;
use anyhow::{bail, Context, Result};
use std::path::PathBuf;
pub struct GitHubStore {
inner: FilesystemStore,
remote: GitRemoteCfg,
name: &'static str,
}
impl GitHubStore {
pub fn new(local: PathBuf, remote: GitRemoteCfg) -> Result<Self> {
Self::with_name(local, remote, "github")
}
pub fn with_name(local: PathBuf, remote: GitRemoteCfg, name: &'static str) -> Result<Self> {
let inner = FilesystemStore::new(local)?;
Ok(Self { inner, remote, name })
}
fn callbacks(&self) -> git2::RemoteCallbacks<'_> {
let cfg = self.remote.clone();
let mut cbs = git2::RemoteCallbacks::new();
cbs.credentials(move |_url, user, _types| credential(&cfg, user));
cbs
}
fn remote_url(&self) -> Result<&str> {
self.remote
.url
.as_deref()
.context("remote url missing from config")
}
}
fn credential(cfg: &GitRemoteCfg, user: Option<&str>) -> std::result::Result<git2::Cred, git2::Error> {
if let Some(var) = cfg.ssh_key_env.as_ref() {
if let Ok(key_path) = std::env::var(var) {
let u = user.unwrap_or("git");
return git2::Cred::ssh_key(u, None, std::path::Path::new(&key_path), None);
}
}
if let Some(var) = cfg.pat_env.as_ref() {
if let Ok(token) = std::env::var(var) {
return git2::Cred::userpass_plaintext(user.unwrap_or("x-access-token"), &token);
}
}
git2::Cred::default()
}
impl MemoryStore for GitHubStore {
fn read(&self, path: &str) -> Result<Vec<u8>> {
self.inner.read(path)
}
fn write(&self, path: &str, bytes: &[u8]) -> Result<()> {
self.inner.write(path, bytes)
}
fn list(&self, dir: &str) -> Result<Vec<String>> {
self.inner.list(dir)
}
fn branch(&self, name: &str) -> Result<()> {
self.inner.branch(name)
}
fn commit(&self, message: &str) -> Result<String> {
self.inner.commit(message)
}
fn push(&self, branch: &str) -> Result<()> {
let url = self.remote_url()?;
enforce_github_push_guard(url)?;
let repo = git2::Repository::open(&self.inner.root)?;
let mut remote = match repo.find_remote("origin") {
Ok(r) => r,
Err(_) => repo.remote("origin", url)?,
};
let mut opts = git2::PushOptions::new();
opts.remote_callbacks(self.callbacks());
let refspec = format!("refs/heads/{b}:refs/heads/{b}", b = branch);
remote.push(&[&refspec], Some(&mut opts))?;
Ok(())
}
fn pull(&self, branch: &str) -> Result<()> {
let repo = git2::Repository::open(&self.inner.root)?;
let url = self.remote_url()?;
let mut remote = match repo.find_remote("origin") {
Ok(r) => r,
Err(_) => repo.remote("origin", url)?,
};
let mut opts = git2::FetchOptions::new();
opts.remote_callbacks(self.callbacks());
remote.fetch(&[branch], Some(&mut opts), None)?;
Ok(())
}
fn backend_name(&self) -> &'static str {
self.name
}
}
/// RULE 0.1 enforcement point for the kei-store push path.
///
/// Blocks pushes whose URL contains `github.com` unless the caller
/// explicitly opts-in via `KEI_STORE_ALLOW_GITHUB_PUSH=1`. Forks on
/// Forgejo / Gitea / self-hosted remain unaffected — only the literal
/// `github.com` host is gated.
pub(crate) fn enforce_github_push_guard(url: &str) -> Result<()> {
if !url.contains("github.com") {
return Ok(());
}
if std::env::var("KEI_STORE_ALLOW_GITHUB_PUSH").is_ok() {
return Ok(());
}
bail!(
"push to github.com blocked by RULE 0.1 (patent-IP protection). \
Set KEI_STORE_ALLOW_GITHUB_PUSH=1 if this is a public-safe release. \
See ~/.claude/rules/security.md for banned-project criteria."
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_env::env_lock;
#[test]
fn test_github_push_blocked_without_env_var() {
let _guard = env_lock();
std::env::remove_var("KEI_STORE_ALLOW_GITHUB_PUSH");
let err = enforce_github_push_guard("git@github.com:owner/repo.git").unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("github.com"), "unexpected err: {msg}");
assert!(msg.contains("RULE 0.1"), "unexpected err: {msg}");
}
#[test]
fn test_github_push_allowed_with_env_var() {
let _guard = env_lock();
std::env::set_var("KEI_STORE_ALLOW_GITHUB_PUSH", "1");
let ok = enforce_github_push_guard("git@github.com:owner/repo.git");
std::env::remove_var("KEI_STORE_ALLOW_GITHUB_PUSH");
assert!(ok.is_ok(), "should allow with opt-in env var");
}
#[test]
fn test_non_github_push_always_allowed() {
// Non-github URLs should always pass regardless of env state, but we
// still take the lock so we don't observe a half-set var mid-test.
let _guard = env_lock();
enforce_github_push_guard("ssh://git@forgejo.local:2222/user/repo.git").unwrap();
enforce_github_push_guard("https://gitea.example.com/user/repo.git").unwrap();
}
}