| 151 | 151 | | } |
| 152 | 152 | | |
| 153 | 153 | | /// The environment g1t keeps on an Anthropic account: a cloud sandbox that |
| 154 | | − | /// reaches package registries, g1t's MCP server, and g1t itself. |
| 155 | | − | pub fn environment_body(name: &str, hosts: &[String]) -> Value { |
| 154 | + | /// reaches the web as a CI runner does. Limited networking also limited |
| 155 | + | /// web_search and web_fetch to its hosts, so a session could look nothing |
| 156 | + | /// up and couldn't open its own deploy on g1t.page. The token still goes to |
| 157 | + | /// g1t's hosts only (`token_credential`). |
| 158 | + | pub fn environment_body(name: &str) -> Value { |
| 156 | 159 | | json!({ |
| 157 | 160 | | "name": name, |
| 158 | | − | "config": { |
| 159 | | − | "type": "cloud", |
| 160 | | − | "networking": { "type": "limited", "allow_package_managers": true, "allow_mcp_servers": true, "allowed_hosts": hosts }, |
| 161 | | − | }, |
| 161 | + | "config": { "type": "cloud", "networking": { "type": "unrestricted" } }, |
| 162 | 162 | | }) |
| 163 | 163 | | } |
| 164 | 164 | | |
| ⋯ |
| 866 | 866 | | let known = stored.as_ref().map(|stored| own_skills(&stored.skills)).unwrap_or_default(); |
| 867 | 867 | | let (skills, ours) = self.agent_skills(key, &known).await?; |
| 868 | 868 | | let body = agent_body(model, &self.mcp_url(), &skills); |
| 869 | | − | let hash = g1t_secrets::sha256_hex(&body.to_string()); |
| 870 | | − | let hosts = self.g1t_hosts(); |
| 869 | + | let environment = environment_body(&format!("g1t-{}-{}", row.workspace, row.id)); |
| 870 | + | // Both: a change to either reaches accounts at their next session. |
| 871 | + | let hash = g1t_secrets::sha256_hex(&format!("{body}{environment}")); |
| 871 | 872 | | let (agent_id, version, environment_id) = match stored { |
| 872 | 873 | | Some(stored) if stored.config_hash == hash => return Ok(Ok((stored.agent_id, stored.agent_version, stored.environment_id))), |
| 873 | | − | Some(stored) => match self.call_anthropic(key, Method::Post, &format!("/v1/agents/{}", stored.agent_id), Some(body)).await? { |
| 874 | | − | Ok(agent) => (stored.agent_id, agent["version"].as_u64().unwrap_or(1) as u32, stored.environment_id), |
| 875 | | − | Err(problem) => return Ok(Err(problem)), |
| 876 | | − | }, |
| 874 | + | Some(stored) => { |
| 875 | + | if let Err(problem) = self.call_anthropic(key, Method::Post, &format!("/v1/environments/{}", stored.environment_id), Some(environment)).await? { |
| 876 | + | return Ok(Err(problem)); |
| 877 | + | } |
| 878 | + | match self.call_anthropic(key, Method::Post, &format!("/v1/agents/{}", stored.agent_id), Some(body)).await? { |
| 879 | + | Ok(agent) => (stored.agent_id, agent["version"].as_u64().unwrap_or(1) as u32, stored.environment_id), |
| 880 | + | Err(problem) => return Ok(Err(problem)), |
| 881 | + | } |
| 882 | + | } |
| 877 | 883 | | None => { |
| 878 | 884 | | let environment = match self |
| 879 | | − | .call_anthropic(key, Method::Post, "/v1/environments", Some(environment_body(&format!("g1t-{}-{}", row.workspace, row.id), &hosts))) |
| 885 | + | .call_anthropic(key, Method::Post, "/v1/environments", Some(environment)) |
| 880 | 886 | | .await? |
| 881 | 887 | | { |
| 882 | 888 | | Ok(environment) => environment["id"].as_str().unwrap_or_default().to_owned(), |
| ⋯ |
| 1610 | 1616 | | assert!(prompt.contains("commit the first version straight to the default branch and push it there"), "an empty repository's first push is asked for"); |
| 1611 | 1617 | | assert!(prompt.contains("echo ses_1 > /workspace/web/.git/g1t/session")); |
| 1612 | 1618 | | assert!(prompt.contains("G1t-Session: ses_1")); |
| 1613 | | − | let environment = environment_body("g1t-acme-con_1", &hosts); |
| 1614 | | − | assert_eq!(environment["config"]["networking"]["allowed_hosts"], json!(hosts)); |
| 1615 | | − | assert_eq!(environment["config"]["networking"]["allow_mcp_servers"], true); |
| 1619 | + | // The sandbox reaches the web; the token, only g1t's hosts. |
| 1620 | + | let environment = environment_body("g1t-acme-con_1"); |
| 1621 | + | assert_eq!(environment["config"]["networking"], json!({ "type": "unrestricted" })); |
| 1616 | 1622 | | } |
| 1617 | 1623 | | |
| 1618 | 1624 | | #[test] |
| ⋯ |
| 1751 | 1757 | | let hosts = vec!["g1t.sh".to_owned(), "api.g1t.sh".to_owned(), "mcp.g1t.sh".to_owned()]; |
| 1752 | 1758 | | let bodies = json!({ |
| 1753 | 1759 | | "agent": agent_body(DEFAULT_MODEL, "https://mcp.g1t.sh", &[]), |
| 1754 | | − | "environment": environment_body("g1t-smoke-check", &hosts), |
| 1760 | + | "environment": environment_body("g1t-smoke-check"), |
| 1755 | 1761 | | "vault": vault_body("smoke"), |
| 1756 | 1762 | | "mcp_credential": mcp_credential("https://mcp.g1t.sh", "g1t_placeholder_not_a_token"), |
| 1757 | 1763 | | "token_credential": token_credential("g1t_placeholder_not_a_token", &hosts), |