Skip to content
Open
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
16 changes: 8 additions & 8 deletions .ci/readability-baseline.env
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Generated by scripts/readability-ratchet.sh
PROD_RS_TOTAL=325
PROD_FILES_GT300=108
PROD_FILES_GT500=51
PROD_FILES_GT1000=3
PROD_MAX_FILE_LINES=1823
PROD_MAX_FILE_PATH=crates/temper-server/src/observe/evolution/insight_generator.rs
PROD_RS_TOTAL=349
PROD_FILES_GT300=111
PROD_FILES_GT500=45
PROD_FILES_GT1000=1
PROD_MAX_FILE_LINES=1625
PROD_MAX_FILE_PATH=crates/temper-server/src/channels/discord.rs
ALLOW_CLIPPY_COUNT=23
ALLOW_DEAD_CODE_COUNT=9
PROD_PRINTLN_COUNT=176
PROD_UNWRAP_CI_OK_COUNT=115
PROD_PRINTLN_COUNT=230
PROD_UNWRAP_CI_OK_COUNT=116
407 changes: 407 additions & 0 deletions .claude/plans/shiny-tumbling-thimble-agent-afffe854b7ee1a307.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ ui/observe/components/Graph3D.tsx
scripts/discord-clean-view.js
scripts/generate-graph-json.js

.proof/
.proof/*
!.proof/
!.proof/*.md
.code-review-pass
.dst-review-pass
.vercel
Expand Down
929 changes: 929 additions & 0 deletions .proof/temper-agent-e2e-proof.md

Large diffs are not rendered by default.

481 changes: 481 additions & 0 deletions .vision/AGENT_ECOSYSTEM_RESEARCH_2026_03.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.lock

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

9 changes: 9 additions & 0 deletions crates/temper-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ enum Commands {
/// per entity. Exit 0 = pass; non-zero or timeout = failure.
#[arg(long)]
verify_subprocess: bool,
/// Discord bot token for channel transport (enables Discord Gateway).
/// Falls back to DISCORD_BOT_TOKEN env var if not provided.
#[arg(long)]
discord_bot_token: Option<String>,
},
/// Run the verification cascade on IOA TOML source read from stdin.
///
Expand Down Expand Up @@ -149,6 +153,7 @@ async fn main() -> anyhow::Result<()> {
tenant,
skill,
verify_subprocess,
discord_bot_token,
} => {
let storage_explicit =
std::env::args().any(|arg| arg == "--storage" || arg.starts_with("--storage="));
Expand All @@ -166,6 +171,8 @@ async fn main() -> anyhow::Result<()> {
{
apps.push((tenant.clone(), dir.clone()));
}
let discord_token =
discord_bot_token.or_else(|| std::env::var("DISCORD_BOT_TOKEN").ok()); // determinism-ok: read once at startup
serve::run(
port,
apps,
Expand All @@ -174,6 +181,8 @@ async fn main() -> anyhow::Result<()> {
storage_explicit,
!no_observe,
verify_subprocess,
discord_token,
tenant,
)
.await?
}
Expand Down
166 changes: 157 additions & 9 deletions crates/temper-cli/src/serve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct LoadedTenantSpecs {
/// 1. Storage init 2. Registry build 3. Auto-reload 4. Webhooks
/// 5. Persistence wiring 6. Entity hydration 7. Policy/WASM recovery
/// 8. Tenant bootstrap 9. Server start
#[allow(clippy::too_many_arguments)]
pub async fn run(
port: u16,
apps: Vec<(String, String)>,
Expand All @@ -61,6 +62,8 @@ pub async fn run(
storage_explicit: bool,
observe: bool,
verify_subprocess: bool,
discord_bot_token: Option<String>,
tenant: String,
) -> Result<()> {
let _otel_guard = init_observability("temper-platform");
temper_authz::init_metrics();
Expand Down Expand Up @@ -115,16 +118,24 @@ pub async fn run(
}

// Phase 5b: Secrets vault
if let Ok(key_b64) = std::env::var("TEMPER_VAULT_KEY") {
// determinism-ok: read once at startup
{
use base64::Engine as _;
let key_bytes = base64::engine::general_purpose::STANDARD
.decode(&key_b64)
.expect("TEMPER_VAULT_KEY must be valid base64");
assert_eq!(key_bytes.len(), 32, "TEMPER_VAULT_KEY must be 32 bytes");
let vault = temper_server::secrets::vault::SecretsVault::new(
key_bytes.as_slice().try_into().unwrap(), // ci-ok: length asserted == 32 above
);
let key_bytes: [u8; 32] = if let Ok(key_b64) = std::env::var("TEMPER_VAULT_KEY") {
// determinism-ok: read once at startup
let decoded = base64::engine::general_purpose::STANDARD
.decode(&key_b64)
.expect("TEMPER_VAULT_KEY must be valid base64");
assert_eq!(decoded.len(), 32, "TEMPER_VAULT_KEY must be 32 bytes");
decoded.try_into().unwrap() // ci-ok: length asserted == 32 above
} else {
// No explicit key — generate an ephemeral one for in-memory secret caching.
// determinism-ok: OsRng used once at startup for vault key generation
use rand::RngCore as _;
let mut key = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut key);
key
};
let vault = temper_server::secrets::vault::SecretsVault::new(&key_bytes);
state.server.secrets_vault = Some(std::sync::Arc::new(vault));
println!(" Secrets vault: configured");
}
Expand All @@ -137,6 +148,95 @@ pub async fn run(
bootstrap::recover_wasm_modules(&state).await;
bootstrap::recover_secrets(&state).await;

// Seed secrets from env into the vault for all tenants.
if let Some(ref vault) = state.server.secrets_vault {
// ANTHROPIC_API_KEY — makes {secret:anthropic_api_key} resolve in LLM integrations.
if let Ok(key) = std::env::var("ANTHROPIC_API_KEY") {
// determinism-ok: env var read at startup for configuration
let _ = vault.cache_secret("default", "anthropic_api_key", key.clone());
if tenant != "default" {
let _ = vault.cache_secret(&tenant, "anthropic_api_key", key);
}
}

// blob_endpoint — points blob_adapter at the server's internal blob storage
// when no external blob endpoint (R2/S3) is configured.
// determinism-ok: env var read at startup for configuration
if std::env::var("BLOB_ENDPOINT").is_err() {
let blob_url = format!("http://127.0.0.1:{port}/_internal/blobs");
let _ = vault.cache_secret("default", "blob_endpoint", blob_url.clone());
if tenant != "default" {
let _ = vault.cache_secret(&tenant, "blob_endpoint", blob_url);
}
}

// temper_api_url — points WASM modules at this server for TemperFS calls.
{
let api_url = format!("http://127.0.0.1:{port}");
let _ = vault.cache_secret("default", "temper_api_url", api_url.clone());
if tenant != "default" {
let _ = vault.cache_secret(&tenant, "temper_api_url", api_url);
}
}

// sandbox_url — local sandbox for tool execution.
// Uses SANDBOX_URL env var if set, otherwise auto-starts local_sandbox.py.
// determinism-ok: env var read at startup for configuration
{
let sandbox_url = if let Ok(url) = std::env::var("SANDBOX_URL") {
println!(" Sandbox: {url} (from SANDBOX_URL)");
url
} else {
let sandbox_port = port + 10; // e.g., 3000 → 3010
let sandbox_url = format!("http://127.0.0.1:{sandbox_port}");

// Find the local sandbox script relative to the binary or os-apps.
let sandbox_script =
std::path::Path::new("os-apps/temper-agent/sandbox/local_sandbox.py");
if sandbox_script.exists() {
// Use /tmp/temper-sandbox as the base; create /workspace for tool_runner
// which sends cwd="/workspace" by default (matching E2B's layout).
let _ = std::fs::create_dir_all("/tmp/temper-sandbox");
let _ = std::fs::create_dir_all("/workspace");

// determinism-ok: subprocess spawn at startup for local dev sandbox
match std::process::Command::new("python3")
.arg(sandbox_script)
.arg("--port")
.arg(sandbox_port.to_string())
.arg("--workdir")
.arg("/tmp/temper-sandbox")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(_child) => {
println!(" Local sandbox: {sandbox_url} (auto-started)");
}
Err(e) => {
eprintln!(" Warning: failed to start local sandbox: {e}");
eprintln!(
" Run manually: python3 {sandbox_script:?} --port {sandbox_port}"
);
}
}
} else {
eprintln!(" Warning: local sandbox script not found at {sandbox_script:?}");
eprintln!(
" Set SANDBOX_URL env var or ensure os-apps/temper-agent/sandbox/local_sandbox.py exists"
);
}

sandbox_url
};

let _ = vault.cache_secret("default", "sandbox_url", sandbox_url.clone());
if tenant != "default" {
let _ = vault.cache_secret(&tenant, "sandbox_url", sandbox_url);
}
}
}

// Startup banner
println!("Starting Temper platform server...");
println!();
Expand Down Expand Up @@ -177,6 +277,29 @@ pub async fn run(
spawn_actor_passivation_loop(&state);
state.server.spawn_runtime_metrics_loop();

// Channel transports: spawn persistent connections to external messaging platforms.
// Resolve Discord bot token: CLI/env → vault fallback.
let discord_token_resolved = discord_bot_token.or_else(|| {
state
.server
.secrets_vault
.as_ref()
.and_then(|v| v.get_secret(&tenant, "discord_bot_token"))
});
if let Some(ref token) = discord_token_resolved {
// Seed into vault so WASM modules can also access it.
if let Some(ref vault) = state.server.secrets_vault {
let _ = vault.cache_secret("default", "discord_bot_token", token.clone());
if tenant != "default" {
let _ = vault.cache_secret(&tenant, "discord_bot_token", token.clone());
}
}
spawn_channel_transport_discord(&state, token.clone(), &tenant);
} else {
println!(" Discord transport: not configured");
println!(" Set DISCORD_BOT_TOKEN env var or store 'discord_bot_token' in vault");
}

println!("Listening on http://0.0.0.0:{actual_port}");
axum::serve(listener, router)
.await
Expand Down Expand Up @@ -392,6 +515,31 @@ fn spawn_observe_ui(api_port: u16) {
});
}

/// Spawn the Discord channel transport as a background task.
///
/// Connects to Discord Gateway via WebSocket, routes inbound messages to
/// Channel entities, and delivers outbound replies via Discord REST API.
fn spawn_channel_transport_discord(state: &PlatformState, bot_token: String, tenant: &str) {
use temper_server::channels::discord::{DiscordTransport, DiscordTransportConfig};
use temper_server::channels::discord_types::intents;

let server = state.server.clone();
let tenant = tenant.to_string();
println!(" Discord channel transport: connecting (tenant={tenant})...");
tokio::spawn(async move {
// determinism-ok: WebSocket for channel transport
let config = DiscordTransportConfig {
bot_token,
tenant,
intents: intents::DEFAULT,
};
let transport = DiscordTransport::new(config, server);
if let Err(e) = transport.run().await {
eprintln!(" [discord] Transport fatal error: {e}");
}
});
}

fn is_ephemeral_metadata_error(err: &str) -> bool {
err.contains("explicit ephemeral mode")
}
Expand Down
12 changes: 7 additions & 5 deletions crates/temper-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ pub struct McpConfig {
/// Full URL of a remote Temper server (e.g. `https://api.temper.build`).
/// Mutually exclusive with `temper_port`.
pub temper_url: Option<String>,
/// Agent instance ID. Resolved from the credential registry via
/// `TEMPER_API_KEY` at startup (ADR-0033). Only used as an override
/// when credential resolution is not available.
/// Optional local agent label. When `TEMPER_API_KEY` resolves through
/// the credential registry (ADR-0033), the verified platform-assigned
/// agent ID replaces this value. This field does not grant HTTP identity.
pub agent_id: Option<String>,
/// Agent software classification (e.g. `claude-code`). Resolved from
/// the credential registry's `AgentType` entity at startup (ADR-0033).
/// Optional local agent type label (e.g. `claude-code`). When
/// `TEMPER_API_KEY` resolves through the credential registry, the
/// verified platform-assigned type replaces this value. This field does
/// not grant HTTP identity.
pub agent_type: Option<String>,
/// Session ID (`X-Session-Id`). Auto-derived from `CLAUDE_SESSION_ID`.
pub session_id: Option<String>,
Expand Down
Loading
Loading