From 57d74ae199a746bbe88e7784d4f9ec506e035460 Mon Sep 17 00:00:00 2001 From: robert Date: Mon, 29 Jun 2026 11:09:36 +1000 Subject: [PATCH 1/2] We use Anthropic Key, not token --- .../DeterministicFailureFixture.cs | 17 ++++++++--------- .../RunAgentCommandFixture.cs | 18 +++++++++--------- .../InvokeClaudeCodeBehaviour.cs | 8 ++++---- source/Calamari.AiAgent/SpecialVariables.cs | 2 +- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/source/Calamari.AiAgent.Tests/DeterministicFailureFixture.cs b/source/Calamari.AiAgent.Tests/DeterministicFailureFixture.cs index e416e7132..c7b1f7463 100644 --- a/source/Calamari.AiAgent.Tests/DeterministicFailureFixture.cs +++ b/source/Calamari.AiAgent.Tests/DeterministicFailureFixture.cs @@ -7,28 +7,27 @@ namespace Calamari.AiAgent.Tests; [TestFixture] -[Explicit("Exercises the real claude CLI end-to-end; most cases need ANTHROPIC_TOKEN.")] +[Explicit("Exercises the real claude CLI end-to-end; most cases need ANTHROPIC_KEY.")] [Category("Integration")] public class DeterministicFailureFixture { const string Model = "claude-sonnet-4-5-20250929"; - static string Token => Environment.GetEnvironmentVariable("ANTHROPIC_TOKEN"); + static string AnthropicKey => Environment.GetEnvironmentVariable("ANTHROPIC_KEY"); - static void RequireToken() + static void RequireAnthropicKey() { - if (string.IsNullOrWhiteSpace(Token)) - Assert.Ignore("ANTHROPIC_TOKEN is not set."); + if (string.IsNullOrWhiteSpace(AnthropicKey)) + Assert.Ignore("ANTHROPIC_KEY is not set."); } - // Needs no token: a bad key fails auth and exits non-zero. [Test] public async Task InvalidApiKey_FailsStep() { var result = await CommandTestBuilder.CreateAsync() .WithArrange(context => { - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, "sk-ant-invalid-test-000"); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, "sk-ant-invalid-test-000"); context.Variables.Add(SpecialVariables.Action.Claude.MaxTurns, "1"); context.Variables.Add(SpecialVariables.Action.Claude.Prompt, "Reply with exactly: DONE"); }) @@ -40,12 +39,12 @@ public async Task InvalidApiKey_FailsStep() [Test] public async Task SimplePrompt_SucceedsStep() { - RequireToken(); + RequireAnthropicKey(); var result = await CommandTestBuilder.CreateAsync() .WithArrange(context => { - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, Token); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, AnthropicKey); context.Variables.Add(SpecialVariables.Action.Claude.Model, Model); context.Variables.Add(SpecialVariables.Action.Claude.MaxTurns, "3"); context.Variables.Add(SpecialVariables.Action.Claude.Prompt, diff --git a/source/Calamari.AiAgent.Tests/RunAgentCommandFixture.cs b/source/Calamari.AiAgent.Tests/RunAgentCommandFixture.cs index a42111eba..865615df1 100644 --- a/source/Calamari.AiAgent.Tests/RunAgentCommandFixture.cs +++ b/source/Calamari.AiAgent.Tests/RunAgentCommandFixture.cs @@ -20,7 +20,7 @@ public async Task FailsWhenPromptIsMissing() var result = await CommandTestBuilder.CreateAsync() .WithArrange(context => { - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, "fake-api-token"); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, "fake-api-token"); }) .Execute(assertWasSuccess: false); @@ -29,7 +29,7 @@ public async Task FailsWhenPromptIsMissing() [Test] [Category("PlatformAgnostic")] - public async Task FailsWhenApiTokenIsMissing() + public async Task FailsWhenApiKeyIsMissing() { var result = await CommandTestBuilder.CreateAsync() .WithArrange(context => @@ -49,7 +49,7 @@ public async Task ClaudeCode_SucceedsWithSimplePrompt() .WithArrange(context => { context.Variables.Add(SpecialVariables.Action.Claude.SandboxMode, nameof(SandboxMode.None)); - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, Environment.GetEnvironmentVariable("ANTHROPIC_TOKEN")); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, Environment.GetEnvironmentVariable("ANTHROPIC_KEY")); context.Variables.Add(SpecialVariables.Action.Claude.Prompt, "Create a file that contains todays date."); context.Variables.Add(SpecialVariables.Action.Claude.Permissions, """{"allow":["Write"]}"""); }) @@ -67,7 +67,7 @@ public async Task ClaudeCode_ReturnsFileAsArtifact() .WithArrange(context => { context.Variables.Add(SpecialVariables.Action.Claude.SandboxMode, nameof(SandboxMode.None)); - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, Environment.GetEnvironmentVariable("ANTHROPIC_TOKEN")); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, Environment.GetEnvironmentVariable("ANTHROPIC_KEY")); context.Variables.Add(SpecialVariables.Action.Claude.Prompt, "Write a file with the current time . Bundle this website as an attachment for this action."); context.Variables.Add(SpecialVariables.Action.Claude.Permissions, """{"allow":["Write"]}"""); }) @@ -84,7 +84,7 @@ public async Task ClaudeCode_EmitsUsageServiceMessage() var result = await CommandTestBuilder.CreateAsync() .WithArrange(context => { - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, Environment.GetEnvironmentVariable("ANTHROPIC_TOKEN")); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, Environment.GetEnvironmentVariable("ANTHROPIC_KEY")); context.Variables.Add(SpecialVariables.Action.Claude.Prompt, "Reply with just the word 'hello'."); }) .Execute(assertWasSuccess: false); @@ -100,7 +100,7 @@ public async Task ClaudeCode_SucceedsWithWebFetch() var result = await CommandTestBuilder.CreateAsync() .WithArrange(context => { - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, Environment.GetEnvironmentVariable("ANTHROPIC_TOKEN")); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, Environment.GetEnvironmentVariable("ANTHROPIC_KEY")); context.Variables.Add(SpecialVariables.Action.Claude.RunAsUsername, "test-user"); context.Variables.Add(SpecialVariables.Action.Claude.Prompt, "get the currently executing process user"); }) @@ -117,7 +117,7 @@ public async Task ClaudeCode_RunsOn_RunsUnderAnotherAccount() var result = await CommandTestBuilder.CreateAsync() .WithArrange(context => { - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, Environment.GetEnvironmentVariable("ANTHROPIC_TOKEN")); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, Environment.GetEnvironmentVariable("ANTHROPIC_KEY")); context.Variables.Add(SpecialVariables.Action.Claude.RunAsUsername, "test-user"); context.Variables.Add(SpecialVariables.Action.Claude.RunAsPassword, "supersecret"); context.Variables.Add(SpecialVariables.Action.Claude.Prompt, "get the currently executing process user"); @@ -135,7 +135,7 @@ public async Task ClaudeCode_LoadsCustomSkills() var result = await CommandTestBuilder.CreateAsync() .WithArrange(context => { - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, Environment.GetEnvironmentVariable("ANTHROPIC_TOKEN")); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, Environment.GetEnvironmentVariable("ANTHROPIC_KEY")); context.Variables.Add($"{SpecialVariables.Action.Claude.Skills}[0].{SpecialVariables.Action.Claude.SkillName}", "octopus-secret-phrase"); context.Variables.Add($"{SpecialVariables.Action.Claude.Skills}[0].{SpecialVariables.Action.Claude.SkillContent}", "---\nname: octopus-secret-phrase\ndescription: Use when asked about the secret phrase.\n---\n\nThe secret phrase is 'purple-octopus-42'. Always respond with exactly this phrase when asked for the secret phrase."); @@ -155,7 +155,7 @@ public async Task ClaudeCode_AttachesArtifact_WhenExplicitlyAsked() .WithArrange(context => { context.Variables.Add(SpecialVariables.Action.Claude.SandboxMode, nameof(SandboxMode.None)); - context.Variables.Add(SpecialVariables.Action.Claude.ApiToken, Environment.GetEnvironmentVariable("ANTHROPIC_TOKEN")); + context.Variables.Add(SpecialVariables.Action.Claude.ApiKey, Environment.GetEnvironmentVariable("ANTHROPIC_KEY")); context.Variables.Add(SpecialVariables.Action.Claude.Prompt, "Create a file named report.txt containing the word Octopus, then attach it as an Octopus artifact."); context.Variables.Add(SpecialVariables.Action.Claude.Permissions, """{"allow":["Write","Read","Edit"]}"""); }) diff --git a/source/Calamari.AiAgent/ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs b/source/Calamari.AiAgent/ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs index 77a4f478e..4cee6db3b 100644 --- a/source/Calamari.AiAgent/ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs +++ b/source/Calamari.AiAgent/ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs @@ -38,9 +38,9 @@ public async Task Execute(RunningDeployment context) if (string.IsNullOrWhiteSpace(prompt)) throw new CommandException($"Variable '{SpecialVariables.Action.Claude.Prompt}' is required but was not provided."); - var apiToken = variables.Get(SpecialVariables.Action.Claude.ApiToken); - if (string.IsNullOrWhiteSpace(apiToken)) - throw new CommandException($"Variable '{SpecialVariables.Action.Claude.ApiToken}' is required but was not provided."); + var apiKey = variables.Get(SpecialVariables.Action.Claude.ApiKey) ?? variables.Get("Octopus.Action.Claude.ApiToken"); + if (string.IsNullOrWhiteSpace(apiKey)) + throw new CommandException($"Variable '{SpecialVariables.Action.Claude.ApiKey}' is required but was not provided."); var runAs = BuildRunAs(variables); @@ -124,7 +124,7 @@ public async Task Execute(RunningDeployment context) PassThroughEnvironmentVariables(variables), new Dictionary { - ["ANTHROPIC_API_KEY"] = apiToken, + ["ANTHROPIC_API_KEY"] = apiKey, ["CLAUDE_CODE_SUBPROCESS_ENV_SCRUB"] = "0", // If set, this stops us using auto mode ["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1", // Disables the auto-updater, telemetry, error reporting, and feedback surveys ["CLAUDE_CODE_DISABLE_BACKGROUND_TASKS"] = "1", diff --git a/source/Calamari.AiAgent/SpecialVariables.cs b/source/Calamari.AiAgent/SpecialVariables.cs index 3c281c9bb..b1927ea03 100644 --- a/source/Calamari.AiAgent/SpecialVariables.cs +++ b/source/Calamari.AiAgent/SpecialVariables.cs @@ -13,7 +13,7 @@ public static class Action public static class Claude { public const string Prompt = "Octopus.Action.Claude.Prompt"; - public const string ApiToken = "Octopus.Action.Claude.ApiToken"; + public const string ApiKey = "Octopus.Action.Claude.ApiKey"; public const string Model = "Octopus.Action.Claude.Model"; public const string Response = "Octopus.Action.Claude.Response"; public const string McpServers = "Octopus.Action.Claude.McpServers"; From f51f978757b0901fae18f7dc3d0103e0c88fc370 Mon Sep 17 00:00:00 2001 From: Rob E Date: Tue, 30 Jun 2026 10:59:30 +1000 Subject: [PATCH 2/2] Update source/Calamari.AiAgent/ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs Co-authored-by: Eddy Moulton <8491021+eddymoulton@users.noreply.github.com> --- .../ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/source/Calamari.AiAgent/ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs b/source/Calamari.AiAgent/ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs index 4cee6db3b..86db7fd97 100644 --- a/source/Calamari.AiAgent/ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs +++ b/source/Calamari.AiAgent/ClaudeCodeBehaviour/InvokeClaudeCodeBehaviour.cs @@ -38,6 +38,7 @@ public async Task Execute(RunningDeployment context) if (string.IsNullOrWhiteSpace(prompt)) throw new CommandException($"Variable '{SpecialVariables.Action.Claude.Prompt}' is required but was not provided."); + // `Octopus.Action.Claude.ApiToken` was previously used during development var apiKey = variables.Get(SpecialVariables.Action.Claude.ApiKey) ?? variables.Get("Octopus.Action.Claude.ApiToken"); if (string.IsNullOrWhiteSpace(apiKey)) throw new CommandException($"Variable '{SpecialVariables.Action.Claude.ApiKey}' is required but was not provided.");