diff --git a/.gitattributes b/.gitattributes index 56862bb..0a1d199 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ schemas/*.json text eol=lf -crates/model-connector/tests/fixtures/wire_v4/* -text +crates/model-connector/tests/fixtures/wire_v3/* -text diff --git a/Cargo.lock b/Cargo.lock index 3709c14..524654e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,13 +139,10 @@ name = "claude-connector" version = "0.1.0" dependencies = [ "anyhow", - "chrono", "cultcache-rs", - "hmac", "libc", "model-connector", "rmp-serde", - "rmpv", "serde", "serde_json", "sha2", diff --git a/README.md b/README.md index 74bd5b5..339e793 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,6 @@ The connector kit for GameCult minds: one typed, encrypted, replay-safe contract between a caller that needs a model turn and a daemon that holds the provider credential, so no project wires its own provider calls. -The connector meters nothing in dollars. It holds the subscription token, lends it, and -publishes the plan's usage windows (`usage_readout`, and `plan_quota` on every lease grant) for the -minds to read and decide their own burn. The one usage-derived refusal is the provider's own -(`ProviderRejected`), repeated until its reset. - Status: the `model-connector` crate and the `claude-connector` backend crate are live. No connector binary exists yet (the Claude connector binary is first, then an OpenAI-compatible one). @@ -16,7 +11,7 @@ connector binary exists yet (the Claude connector binary is first, then an OpenA Crate `crates/model-connector`: -- `src/contract.rs` - the v4 wire contract (`gamecult.model.*.v4`): `ProviderRequest`, `TransportInvocation`, +- `src/contract.rs` - the v3 wire contract (`gamecult.model.*.v3`): `ProviderRequest`, `TransportInvocation`, `TransportResult`, `TransportReceipt`, the typed `StopReason`, `FailureKind` and `Refusal`, `InputItem::ReasoningState` (a provider's opaque reasoning block, replayed by callers verbatim via `TransportResult::transcript_items`; the service refuses another provider's @@ -26,12 +21,9 @@ Crate `crates/model-connector`: - `src/envelope.rs` - the AES-256-GCM `TransportEnvelope` (binary ciphertext), its identity-binding AAD, the length-prefixed framing, and the CultNet carriage: each frame is a `cultnet.operation_request.v0` / `operation_response.v0` for service - `gamecult.model.connector`, operation `invoke` (or `usage_readout`), its `message_id` the - request id and its payload the base64 of the sealed envelope. `usage_readout` answers a - `UsageReadout` (`gamecult.model.usage_readout.v1`): the backend's plan quota windows, each with - utilization, reset, status and age; - `ConnectorClient::usage_readout` asks. -- `schemas/` - one JSON Schema per v4 document, generated from the types. + `gamecult.model.connector`, operation `invoke`, its `message_id` the request id and its + payload the base64 of the sealed envelope. +- `schemas/` - one JSON Schema per v3 document, generated from the types. - `src/client.rs` - `ConnectorClient`, the one-request caller side. - `src/service.rs` (feature `daemon`) - `TransportService`: caller admission, durable replay (a retried request is answered byte-identically and the provider runs once), @@ -45,16 +37,13 @@ Crate `crates/claude-connector` (library only; the binary that wires it to `serv - `ClaudeCodeBackend` implements `ProviderBackend` for provider `claude-code` by driving the unmodified `claude` binary, one child per call, in print stream-json mode with no tools, no MCP servers, no settings sources and the system prompt replaced. Tool-less turns only: a request - with tools or a final tool result is refused before any spawn. A request's output schema goes - to the CLI as `--json-schema` and the result's structured output is the one assistant text. -- Reasoning state is a session handle (`ClaudeCodeHandle`) authenticated with a connector secret - and bound to the caller it was minted for; a handle that cannot be proven to match the request - and its caller re-seeds from the caller's transcript, and every resume forks. + with tools, an output schema or a final tool result is refused before any spawn. +- Reasoning state is a session handle (`ClaudeCodeHandle`); a handle that cannot be proven to + match the request re-seeds from the caller's transcript, and every resume forks. - `ProcessPort` is the one OS seam (`StdProcessPort`, unix, kills the child's whole process group); `SessionStore` and `StderrSink` are the other ports. `ClaudeCodeConfig` is what the - binary hands in; the credential is read from a systemd credential and reaches the child on a - pipe (`CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR`), never in an environment. `QuotaCell` keeps the - plan windows the CLI's `rate_limit_event`s report, for the usage readout and the lease grant. + binary hands in; the credential is read by name from the connector's own environment + (`CLAUDE_CODE_OAUTH_TOKEN`) and copied only into the child. Frame arithmetic is public in `src/envelope.rs`, measured on the carriage frame: `frame_bytes_for_payload(n)` is the frame a caller payload bound of `n` needs, `payload_bytes_for_frame(frame)` is the largest payload bound @@ -63,7 +52,7 @@ a frame allows, and `MAX_IDENTITY_BYTES` (256) bounds caller ids, request ids an needed); the smallest legal frame is `frame_bytes_for_payload(MIN_PAYLOAD_BYTES)`. A request id past the bound is refused on the wire as `Refusal::Malformed`, the reply echoing the id (the connection is dropped only if that reply cannot fit the frame); `ConnectorClient` refuses such an id locally with `RequestIdTooLong`. -The default feature set compiles contract, envelope and client only. The kit speaks v4 +The default feature set compiles contract, envelope and client only. The kit speaks v3 only: a v2 frame gets no answer. ## Verify @@ -73,9 +62,9 @@ cargo test --locked --workspace cargo test --locked --workspace --features model-connector/daemon ``` -`crates/model-connector/tests/wire_golden_v4.rs` pins the v4 documents and one sealed carriage -frame to the fixtures in `crates/model-connector/tests/fixtures/wire_v4` (regenerate with -`WIRE_V4_REGENERATE=1`; see the fixtures' README), and +`crates/model-connector/tests/wire_golden_v3.rs` pins the v3 documents and one sealed carriage +frame to the fixtures in `crates/model-connector/tests/fixtures/wire_v3` (regenerate with +`WIRE_V3_REGENERATE=1`; see the fixtures' README), and `crates/model-connector/tests/published_schemas.rs` fails when `schemas/` differs from the types (`MODEL_CONNECTOR_REGENERATE_SCHEMAS=1` rewrites it). diff --git a/crates/claude-connector/Cargo.toml b/crates/claude-connector/Cargo.toml index 6cb9e45..7f9b042 100644 --- a/crates/claude-connector/Cargo.toml +++ b/crates/claude-connector/Cargo.toml @@ -13,8 +13,6 @@ path = "src/main.rs" [dependencies] anyhow = "1" -chrono = { version = "0.4", default-features = false } -hmac = "0.12" cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a4eae9056152335662f1115f97bf35fb00ab6d23", package = "cultcache-rs" } model-connector = { path = "../model-connector", features = ["daemon"] } rmp-serde = "1" @@ -26,6 +24,3 @@ zeroize = "1" [target.'cfg(unix)'.dependencies] libc = "0.2" - -[dev-dependencies] -rmpv = "1" diff --git a/crates/claude-connector/src/backend.rs b/crates/claude-connector/src/backend.rs index 0bc2d77..c5a8f81 100644 --- a/crates/claude-connector/src/backend.rs +++ b/crates/claude-connector/src/backend.rs @@ -1,41 +1,27 @@ use std::fs::File; use std::io; use std::path::PathBuf; -use std::sync::Arc; use std::time::Instant; use model_connector::{ - FailureKind, InputItem, PlanQuotaSnapshot, ProviderBackend, ProviderCall, ProviderRequest, - TransportEvent, TransportEventPayload, TransportOutcome, TransportResult, Usage, + FailureKind, InputItem, ProviderBackend, ProviderCall, ProviderRequest, TransportEvent, + TransportEventPayload, TransportOutcome, TransportResult, Usage, }; use sha2::{Digest, Sha256}; use zeroize::Zeroizing; use crate::config::{ClaudeCodeConfig, PROVIDER}; -use crate::handle::{ - Binding, ClaudeCodeHandle, MIN_SECRET_BYTES, SessionStore, plan, prefix_digest, stdin_bytes, -}; +use crate::handle::{ClaudeCodeHandle, SessionStore, plan, prefix_digest, stdin_bytes}; use crate::launch::{PromptFile, argv, child_env}; use crate::process::{ProcessPort, RunningChild, StdoutRead}; -use crate::quota::QuotaCell; use crate::stream::{Failure, Finished, Step, StreamState}; -/// The meter's call: the cheapest model, a trivial prompt, and an answer cap -/// that only bounds a runaway. -const METER_MODEL: &str = "claude-haiku-5-5"; -const METER_CALLER: &str = "meter"; -const METER_MAX_OUTPUT_TOKENS: u32 = 16; - /// Where the child's credential comes from, read afresh at every use. The /// connector's own environment is never a source (ruling /// claude-connector-token-credential). pub trait TokenSource: Send + Sync { /// The token, or `None` when there is none or it is empty. fn read(&self) -> Option>; - /// What `config check` names when `read` has no token. - fn missing(&self) -> &'static str { - "token_credential" - } } /// A token in a file named by a systemd credential: the file @@ -44,8 +30,6 @@ pub trait TokenSource: Send + Sync { pub struct CredentialTokenSource { directory: Option, name: String, - /// The configuration field `missing` names when the credential is absent. - field: &'static str, } impl CredentialTokenSource { @@ -56,21 +40,13 @@ impl CredentialTokenSource { Self { directory: std::env::var_os("CREDENTIALS_DIRECTORY").map(PathBuf::from), name: name.into(), - field: "token_credential", } } - /// The same source, naming `field` when its credential is absent. - pub fn for_field(mut self, field: &'static str) -> Self { - self.field = field; - self - } - pub fn in_directory(directory: impl Into, name: impl Into) -> Self { Self { directory: Some(directory.into()), name: name.into(), - field: "token_credential", } } } @@ -83,16 +59,6 @@ impl TokenSource for CredentialTokenSource { token.truncate(trimmed); (!token.is_empty()).then_some(token) } - - /// No directory at all is the unit's environment lacking its credentials, - /// which is not the same fault as a credential the directory lacks. - fn missing(&self) -> &'static str { - if self.directory.is_none() { - "credentials_directory" - } else { - self.field - } - } } /// Where the tail of a child's stderr goes. The backend never puts it in a @@ -109,15 +75,6 @@ pub struct ClaudeCodeBackend { sessions: Box, sink: Box, tokens: Box, - handle_secret: Box, - quota: Arc, -} - -/// The handle secret, or `None` when it is absent or shorter than a key. -fn usable_secret(source: &dyn TokenSource) -> Option> { - source - .read() - .filter(|secret| secret.len() >= MIN_SECRET_BYTES) } impl ClaudeCodeBackend { @@ -131,11 +88,7 @@ impl ClaudeCodeBackend { let tokens = Box::new(CredentialTokenSource::from_environment( config.token_credential.clone(), )); - let handle_secret = Box::new( - CredentialTokenSource::from_environment(config.handle_secret_credential.clone()) - .for_field("handle_secret_credential"), - ); - Self::with_ports(config, port, sessions, sink, tokens, handle_secret) + Self::with_ports(config, port, sessions, sink, tokens) } pub fn with_ports( @@ -144,7 +97,6 @@ impl ClaudeCodeBackend { sessions: Box, sink: Box, tokens: Box, - handle_secret: Box, ) -> Self { Self { config, @@ -152,34 +104,6 @@ impl ClaudeCodeBackend { sessions, sink, tokens, - handle_secret, - quota: Arc::new(QuotaCell::new()), - } - } - - /// The plan windows every stream this backend reads feeds. - pub fn quota(&self) -> Arc { - self.quota.clone() - } - - /// The meter's call: the smallest real one, through the same child, - /// stream and cell as any other. The stream's `rate_limit_event` is what - /// the caller wants; this returns what the call billed, or the failure's - /// kind (never its message). - pub(crate) fn read_meter(&self) -> Result { - let mut request = ProviderRequest::new( - METER_CALLER, - METER_CALLER, - METER_MODEL, - "Reply with the single word ok.", - METER_MAX_OUTPUT_TOKENS, - ); - request.input = vec![InputItem::UserText { - text: "ok?".to_string(), - }]; - match self.run(&request, METER_CALLER, true).1 { - TransportOutcome::Completed { usage, .. } => Ok(usage), - TransportOutcome::Failed { kind, .. } => Err(format!("{kind:?}")), } } @@ -189,28 +113,8 @@ impl ClaudeCodeBackend { binary_matches_pin(&self.config) } - /// `ephemeral` leaves no transcript behind: the meter keeps no handle and - /// resumes nothing, every other call is resumed by its session. - fn run( - &self, - request: &ProviderRequest, - caller: &str, - ephemeral: bool, - ) -> (Vec, TransportOutcome) { - let Some(secret) = usable_secret(self.handle_secret.as_ref()) else { - return failed( - FailureKind::Authentication, - "claude-code: the connector has no handle secret".to_string(), - Some(zero_usage()), - ); - }; - let plan = match plan( - request, - &self.config, - secret.as_bytes(), - caller, - self.sessions.as_ref(), - ) { + fn run(&self, request: &ProviderRequest) -> (Vec, TransportOutcome) { + let plan = match plan(request, &self.config, self.sessions.as_ref()) { Ok(plan) => plan, Err(error) => { return failed( @@ -239,8 +143,8 @@ impl ClaudeCodeBackend { "claude-code: the system prompt could not be staged", )); }; - let argv = argv(&self.config, request, &plan, prompt.path(), ephemeral); - let env = child_env(&self.config, request); + let argv = argv(&self.config, request, &plan, prompt.path()); + let env = child_env(&self.config, request, token); let deadline = Instant::now() + self.config.limits.deadline; let spawned = self.port.spawn( @@ -248,7 +152,6 @@ impl ClaudeCodeBackend { &env, &self.config.work_dir, stdin_bytes(plan.blocks()), - &token, ); let Ok(mut child) = spawned else { return failure(Failure::never_reached( @@ -256,13 +159,7 @@ impl ClaudeCodeBackend { "claude-code: the child could not be spawned", )); }; - let finished = read_stream( - child.as_mut(), - deadline, - &request.model, - request.output_schema_json.as_deref(), - &self.quota, - ); + let finished = read_stream(child.as_mut(), deadline, &request.model); child.kill(); child.wait(); let tail = child.stderr_tail(); @@ -271,7 +168,7 @@ impl ClaudeCodeBackend { } match finished { - Ok(finished) => self.completed(request, caller, secret.as_bytes(), finished), + Ok(finished) => self.completed(request, finished), Err(failure_) => failure(failure_), } } @@ -279,8 +176,6 @@ impl ClaudeCodeBackend { fn completed( &self, request: &ProviderRequest, - caller: &str, - secret: &[u8], finished: Finished, ) -> (Vec, TransportOutcome) { let mut payloads: Vec = finished @@ -295,30 +190,14 @@ impl ClaudeCodeBackend { text: finished.texts.concat(), }); } - // A schema turn leaves the CLI's StructuredOutput tool exchange in its - // session, and a later turn without the schema runs with no tools: it - // must not resume that session. So a schema turn hands out no handle, - // and whatever follows it is seeded from the caller's transcript. - if request.output_schema_json.is_none() { - let handle = ClaudeCodeHandle::Ended { - session_id: finished.session_id.clone(), - instance: self.config.instance.clone(), - prefix_sha256: prefix_digest( - secret, - &Binding { - caller, - instructions: &request.instructions, - model: &request.model, - session_id: &finished.session_id, - instance: &self.config.instance, - items: &prefix, - }, - ), - }; - payloads.push(TransportEventPayload::ReasoningState { - state: handle.encode(), - }); - } + let handle = ClaudeCodeHandle::Ended { + session_id: finished.session_id, + instance: self.config.instance.clone(), + prefix_sha256: prefix_digest(&request.instructions, &request.model, &prefix), + }; + payloads.push(TransportEventPayload::ReasoningState { + state: handle.encode(), + }); let events = payloads .into_iter() @@ -341,11 +220,7 @@ impl ClaudeCodeBackend { impl ProviderBackend for ClaudeCodeBackend { fn ready(&self) -> Result<(), &'static str> { - readiness( - &self.config, - self.tokens.as_ref(), - self.handle_secret.as_ref(), - ) + readiness(&self.config, self.tokens.as_ref()) } fn provider(&self) -> &str { @@ -353,23 +228,17 @@ impl ProviderBackend for ClaudeCodeBackend { } fn execute(&self, call: &ProviderCall) -> TransportResult { - let (events, outcome) = self.run(call.request(), call.caller_runtime_id(), false); + let (events, outcome) = self.run(call.request()); call.transported(events, outcome) } - - fn plan_quota(&self) -> PlanQuotaSnapshot { - self.quota.snapshot() - } } fn read_stream( child: &mut dyn RunningChild, deadline: Instant, model: &str, - schema: Option<&str>, - quota: &QuotaCell, ) -> Result { - let mut state = StreamState::new(model, schema, quota); + let mut state = StreamState::new(model); let finished = loop { match child.read_line(deadline) { StdoutRead::Line(line) => { @@ -416,7 +285,6 @@ fn binary_matches_pin(config: &ClaudeCodeConfig) -> bool { pub(crate) fn readiness( config: &ClaudeCodeConfig, tokens: &dyn TokenSource, - handle_secret: &dyn TokenSource, ) -> Result<(), &'static str> { if !config.work_dir.is_dir() { return Err("work_dir"); @@ -427,17 +295,12 @@ pub(crate) fn readiness( if !config.prompt_dir.is_dir() { return Err("prompt_dir"); } + if tokens.read().is_none() { + return Err("token_credential"); + } if !binary_matches_pin(config) { return Err("claude_binary_sha256"); } - // Last: outside the unit the credential is the one thing always absent, - // and it must not hide the faults the document itself has. - if usable_secret(handle_secret).is_none() { - return Err(handle_secret.missing()); - } - if tokens.read().is_none() { - return Err(tokens.missing()); - } Ok(()) } diff --git a/crates/claude-connector/src/backend/tests.rs b/crates/claude-connector/src/backend/tests.rs index ecd98fa..21a8c72 100644 --- a/crates/claude-connector/src/backend/tests.rs +++ b/crates/claude-connector/src/backend/tests.rs @@ -5,20 +5,17 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use model_connector::{ - CallerAdmission, PlanQuotaSnapshot, ReasoningEffort, StopReason, ToolDefinition, - TransportAdmission, TransportDisposition, TransportInvocation, TransportKey, TransportReceipt, - TransportService, decrypt_result, encrypt_invocation, + CallerAdmission, ModelPrice, PriceTable, Rates, ReasoningEffort, SpendCeiling, SpendWindowMs, + ToolDefinition, TransportAdmission, TransportDisposition, TransportInvocation, TransportKey, + TransportReceipt, TransportService, decrypt_result, encrypt_invocation, }; use super::*; use crate::config::ClaudeCodeLimits; use crate::handle::{FsSessionStore, SessionStore}; -use crate::launch::ChildEnv; +use crate::launch::{ChildEnv, TOKEN_VAR}; const CANARY_TOKEN: &str = "canary-oauth-token-value"; -/// The variable the connector's own environment must never hand on. -const TOKEN_ENV: &str = "CLAUDE_CODE_OAUTH_TOKEN"; -const TOKEN_FD_ENV: &str = "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR"; const MODEL: &str = "claude-haiku-5-5"; const INSTANCE: &str = "persona-a"; const SESSION: &str = "11111111-2222-3333-4444-555555555555"; @@ -61,75 +58,15 @@ const USAGE: &str = r#"{"claude-haiku-5-5":{"inputTokens":2,"outputTokens":4,"ca /// What a resumed session's `modelUsage` says: every turn so far. Every result /// fixture carries it beside the turn's own `model_usage_since_last_result`, -/// so reading the wrong field shows in a count. +/// so reading the wrong field shows in a price. const CUMULATIVE: &str = r#"{"claude-haiku-5-5":{"inputTokens":900000,"outputTokens":900000,"cacheReadInputTokens":900000,"cacheCreationInputTokens":900000,"thinkingTokens":900000,"costUSD":99.0}}"#; fn result_line(stop_reason: &str, since_last: &str) -> String { - result_line_with(stop_reason, since_last, "") -} - -/// A success result with `extra` (a leading comma and fields) appended. -fn result_line_with(stop_reason: &str, since_last: &str, extra: &str) -> String { format!( - r#"{{"type":"result","subtype":"success","is_error":false,"stop_reason":"{stop_reason}","session_id":"{SESSION}","total_cost_usd":99.0,"modelUsage":{CUMULATIVE},"model_usage_since_last_result":{since_last}{extra}}}"# + r#"{{"type":"result","subtype":"success","is_error":false,"stop_reason":"{stop_reason}","session_id":"{SESSION}","total_cost_usd":99.0,"modelUsage":{CUMULATIVE},"model_usage_since_last_result":{since_last}}}"# ) } -// The shapes `claude -p --json-schema` produced on the pinned CLI (2.1.312): -// the answer is a call of the CLI's own StructuredOutput tool, the result -// carries it as `structured_output` with stop_reason `tool_use`, and a -// `rate_limit_event` reports the plan's windows under `unifiedWindows`. -const SCHEMA: &str = r#"{"type":"object","properties":{"answer":{"type":"string"},"n":{"type":"integer"}},"required":["answer","n"],"additionalProperties":false}"#; -const STRUCTURED: &str = r#"{"answer":"say hello","n":0}"#; - -fn structured_call_line(name: &str) -> String { - format!( - r#"{{"type":"assistant","message":{{"id":"msg_s","role":"assistant","content":[{{"type":"tool_use","id":"toolu_01","name":"{name}","input":{STRUCTURED},"caller":{{"type":"direct"}}}}],"stop_reason":null}},"session_id":"{SESSION}"}}"# - ) -} - -fn structured_tool_result_line() -> String { - format!( - r#"{{"type":"user","message":{{"role":"user","content":[{{"tool_use_id":"toolu_01","type":"tool_result","content":"Structured output provided successfully"}}]}},"parent_tool_use_id":null,"session_id":"{SESSION}"}}"# - ) -} - -fn plan_rate_limit_line() -> String { - r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":4102444800,"rateLimitType":"five_hour","overageStatus":"rejected","isUsingOverage":false,"unifiedWindows":{"five_hour":{"utilization":0.08,"resetsAt":4102444800,"status":"allowed"},"seven_day":{"utilization":0.3,"resetsAt":4102531200,"status":"allowed"}}}}"#.to_string() -} - -fn structured_result_line() -> String { - result_line_with( - "tool_use", - USAGE, - &format!(r#","structured_output":{STRUCTURED},"result":"ignored""#), - ) -} - -fn structured_script() -> Script { - Script::lines(vec![ - init_line(MODEL, SESSION), - structured_call_line("StructuredOutput"), - structured_tool_result_line(), - plan_rate_limit_line(), - structured_result_line(), - ]) -} - -fn schema_request() -> ProviderRequest { - let mut request = request(vec![user("hello")]); - request.output_schema_json = Some(SCHEMA.to_string()); - request -} - -fn a_tool() -> ToolDefinition { - ToolDefinition { - name: "lookup".into(), - description: "d".into(), - parameters_json: "{}".into(), - } -} - fn error_result_line(extra: &str) -> String { format!( r#"{{"type":"result","subtype":"error_during_execution","is_error":true,"session_id":"{SESSION}","total_cost_usd":99.0,"modelUsage":{CUMULATIVE},"model_usage_since_last_result":{USAGE}{extra}}}"# @@ -209,8 +146,6 @@ struct Seen { env: Vec<(String, String)>, cwd: PathBuf, stdin: Vec, - /// The token the backend handed the port, which a real port puts on a pipe. - token: String, /// The file `--system-prompt-file` named, as it was when the child started. prompt_file: Option<(PathBuf, String)>, /// Every deadline the backend passed to a read of this child. @@ -238,7 +173,6 @@ impl ProcessPort for ScriptedPort { env: &ChildEnv, cwd: &Path, stdin: Vec, - token: &str, ) -> io::Result> { self.spawns.fetch_add(1, Ordering::SeqCst); let deadlines = Arc::new(Mutex::new(Vec::new())); @@ -257,7 +191,6 @@ impl ProcessPort for ScriptedPort { .collect(), cwd: cwd.to_path_buf(), stdin, - token: token.to_string(), }; let script = self .scripts @@ -333,7 +266,6 @@ fn config(dir: &Path, instance: &str, limits: ClaudeCodeLimits) -> ClaudeCodeCon config_dir: PathBuf::from("/cfg"), prompt_dir: dir.join("prompts"), token_credential: "claude-token".to_string(), - handle_secret_credential: "claude-handle-secret".to_string(), instance: instance.to_string(), limits, } @@ -352,26 +284,6 @@ fn canary_token() -> Box { Box::new(FixedToken(Some(CANARY_TOKEN))) } -/// A handle secret source with a fixed answer; it names its own field. -struct FixedSecret(Option<&'static str>); - -impl TokenSource for FixedSecret { - fn read(&self) -> Option> { - self.0.map(|secret| Zeroizing::new(secret.to_string())) - } - - fn missing(&self) -> &'static str { - "handle_secret_credential" - } -} - -const SECRET: &str = "handle-secret-0000000000000000000000000000000000000000000000"; -const OTHER_SECRET: &str = "handle-secret-1111111111111111111111111111111111111111111111"; - -fn canary_secret() -> Box { - Box::new(FixedSecret(Some(SECRET))) -} - fn limits() -> ClaudeCodeLimits { ClaudeCodeLimits { deadline: Duration::from_secs(30), @@ -390,16 +302,6 @@ struct Rig { } fn rig_for(instance: &str, scripts: Vec