From d05579855682f33015c8bbd4d569ce4bfa45d2ce Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 17 Jun 2026 18:54:25 +0000 Subject: [PATCH 1/4] fix(payment-manager): warn loudly at add-time when auto-payment is enabled Auto-payment is enabled by default so an agent can transparently settle 402 Payment Required responses, but that means it can move money with no human in the loop. The add flow now emits a prominent stderr warning naming the per-session spend limit and the --auto-payment false opt-out, so the unattended-spend posture is never enabled silently. --- src/cli/primitives/PaymentManagerPrimitive.ts | 12 ++++++++++ .../__tests__/PaymentManagerPrimitive.test.ts | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/cli/primitives/PaymentManagerPrimitive.ts b/src/cli/primitives/PaymentManagerPrimitive.ts index 90fe3ef9e..eed58a0b8 100644 --- a/src/cli/primitives/PaymentManagerPrimitive.ts +++ b/src/cli/primitives/PaymentManagerPrimitive.ts @@ -8,6 +8,7 @@ import { PaymentManagerSchema, } from '../../schema'; import type { RemoveResult } from '../commands/remove/types'; +import { ANSI } from '../constants'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, SchemaChange } from '../operations/remove/types'; import { getTemplatePath } from '../templates/templateRoot'; @@ -189,6 +190,17 @@ export class PaymentManagerPrimitive extends BasePrimitive { expect(result.error.message).toBe('disk read failure'); } }); + + it('warns on stderr when auto-payment is enabled (default)', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject({ runtimes: [] })); + const stderr = vi.spyOn(process.stderr, 'write').mockReturnValue(true); + + const result = await primitive.add({ name: 'mgr1', authorizerType: 'AWS_IAM' }); + + expect(result.success).toBe(true); + const out = stderr.mock.calls.map(c => String(c[0])).join(''); + expect(out).toMatch(/auto-payment is enabled/i); + expect(out).toContain('--auto-payment false'); + stderr.mockRestore(); + }); + + it('does not warn when auto-payment is explicitly disabled', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject({ runtimes: [] })); + const stderr = vi.spyOn(process.stderr, 'write').mockReturnValue(true); + + await primitive.add({ name: 'mgr2', authorizerType: 'AWS_IAM', autoPayment: false }); + + const out = stderr.mock.calls.map(c => String(c[0])).join(''); + expect(out).not.toMatch(/auto-payment is enabled/i); + stderr.mockRestore(); + }); }); describe('remove()', () => { From fda04b66d0b53f884357b76d1e18437bb2224801 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 17 Jun 2026 18:55:36 +0000 Subject: [PATCH 2/4] fix(deploy): warn post-deploy when a payment manager has auto-payment enabled Adds a per-manager post-deploy warning naming the per-session spend limit whenever auto-payment is active, so the unattended-spend posture is surfaced at deploy time as well as at add time. Set --auto-payment false on the manager to require manual approval. --- src/cli/commands/deploy/actions.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index b3e349f69..168fc34a0 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -768,6 +768,19 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise Date: Wed, 17 Jun 2026 19:40:07 +0000 Subject: [PATCH 3/4] fix(payment-manager): return auto-payment warning instead of writing to stderr add() is shared by the CLI and the Ink TUI flow (useCreatePayment), so writing the auto-payment warning to process.stderr corrupted the TUI's rendered frame. Return the warning text on the AddResult instead and let each caller render it through its own channel: the CLI action prints it to stderr, and the TUI confirm screen surfaces it as a warning field. --- src/cli/primitives/PaymentManagerPrimitive.ts | 29 +++++++++++-------- .../__tests__/PaymentManagerPrimitive.test.ts | 21 ++++++-------- .../tui/screens/payment/AddPaymentFlow.tsx | 16 ++++++++-- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/cli/primitives/PaymentManagerPrimitive.ts b/src/cli/primitives/PaymentManagerPrimitive.ts index eed58a0b8..bfd636665 100644 --- a/src/cli/primitives/PaymentManagerPrimitive.ts +++ b/src/cli/primitives/PaymentManagerPrimitive.ts @@ -129,7 +129,7 @@ export class PaymentManagerPrimitive extends BasePrimitive> { + ): Promise> { try { const project = await this.readProjectSpec(); // payments is optional in the schema (absent on projects with no payment @@ -190,18 +190,20 @@ export class PaymentManagerPrimitive extends BasePrimitive 0) { console.warn( `\nWarning: payment capability auto-wiring skipped for non-Strands runtime(s): ${result.skippedRuntimes.join(', ')}.` diff --git a/src/cli/primitives/__tests__/PaymentManagerPrimitive.test.ts b/src/cli/primitives/__tests__/PaymentManagerPrimitive.test.ts index 88b1cc8f8..c98497e82 100644 --- a/src/cli/primitives/__tests__/PaymentManagerPrimitive.test.ts +++ b/src/cli/primitives/__tests__/PaymentManagerPrimitive.test.ts @@ -208,28 +208,25 @@ describe('PaymentManagerPrimitive', () => { } }); - it('warns on stderr when auto-payment is enabled (default)', async () => { + it('returns an auto-payment warning when enabled (default)', async () => { mockReadProjectSpec.mockResolvedValue(makeProject({ runtimes: [] })); - const stderr = vi.spyOn(process.stderr, 'write').mockReturnValue(true); const result = await primitive.add({ name: 'mgr1', authorizerType: 'AWS_IAM' }); expect(result.success).toBe(true); - const out = stderr.mock.calls.map(c => String(c[0])).join(''); - expect(out).toMatch(/auto-payment is enabled/i); - expect(out).toContain('--auto-payment false'); - stderr.mockRestore(); + if (!result.success) throw new Error('expected success'); + expect(result.autoPaymentWarning).toMatch(/auto-payment is enabled/i); + expect(result.autoPaymentWarning).toContain('--auto-payment false'); }); - it('does not warn when auto-payment is explicitly disabled', async () => { + it('returns no auto-payment warning when explicitly disabled', async () => { mockReadProjectSpec.mockResolvedValue(makeProject({ runtimes: [] })); - const stderr = vi.spyOn(process.stderr, 'write').mockReturnValue(true); - await primitive.add({ name: 'mgr2', authorizerType: 'AWS_IAM', autoPayment: false }); + const result = await primitive.add({ name: 'mgr2', authorizerType: 'AWS_IAM', autoPayment: false }); - const out = stderr.mock.calls.map(c => String(c[0])).join(''); - expect(out).not.toMatch(/auto-payment is enabled/i); - stderr.mockRestore(); + expect(result.success).toBe(true); + if (!result.success) throw new Error('expected success'); + expect(result.autoPaymentWarning).toBeUndefined(); }); }); diff --git a/src/cli/tui/screens/payment/AddPaymentFlow.tsx b/src/cli/tui/screens/payment/AddPaymentFlow.tsx index cea6f19d0..e2c15f574 100644 --- a/src/cli/tui/screens/payment/AddPaymentFlow.tsx +++ b/src/cli/tui/screens/payment/AddPaymentFlow.tsx @@ -348,9 +348,19 @@ export function AddPaymentFlow({ ] : []; - const warningFields = !flow.connectorConfig - ? [{ label: '⚠ Warning', value: 'No connector — deploy will fail until you add one' }] - : []; + const warningFields = [ + ...(flow.managerConfig.autoPayment + ? [ + { + label: '⚠ Warning', + value: `Auto-payment ENABLED — agent settles 402s automatically up to $${flow.managerConfig.defaultSpendLimit}/session with no human approval`, + }, + ] + : []), + ...(!flow.connectorConfig + ? [{ label: '⚠ Warning', value: 'No connector — deploy will fail until you add one' }] + : []), + ]; const allFields = [...managerFields, ...connectorFields, ...warningFields]; From 4b73e03c91904605c836bfe406ff27c21f4d4761 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Fri, 26 Jun 2026 19:58:52 +0000 Subject: [PATCH 4/4] fix(payment-manager): keep auto-payment notice out of --json stdout and deploy exit code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bug-bash findings on the auto-payment warning: - The warning was serialized into `add payment-manager --json` stdout via the autoPaymentWarning result field. Strip it on the --json branch so stdout stays a clean machine-readable result; the notice still renders on the human path. - The deploy-time auto-payment notice was pushed into postDeployWarnings, which the deploy command maps to `process.exit(2)` — so every successful payment deploy exited non-zero (a CI failure signal). Route it through the `notes` channel (exit 0) instead, which is the correct place for informational notices. --- src/cli/commands/deploy/actions.ts | 9 ++++++--- src/cli/primitives/PaymentManagerPrimitive.ts | 10 +++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 168fc34a0..f3a89defb 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -769,11 +769,14 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise 0; const hasInvokable = agentNames.length > 0 || hasHarnesses; const nextSteps = hasInvokable ? [...AGENT_NEXT_STEPS] : [...MEMORY_ONLY_NEXT_STEPS]; - const notes: string[] = []; + const notes: string[] = [...autoPaymentNotices]; const hasPythonAgent = context.projectSpec.runtimes?.some(a => a.entrypoint?.endsWith('.py') || a.entrypoint?.includes('.py:')) ?? false; if ((agentNames.length > 0 || hasGateways) && hasPythonAgent) { diff --git a/src/cli/primitives/PaymentManagerPrimitive.ts b/src/cli/primitives/PaymentManagerPrimitive.ts index bfd636665..5b475c0b9 100644 --- a/src/cli/primitives/PaymentManagerPrimitive.ts +++ b/src/cli/primitives/PaymentManagerPrimitive.ts @@ -456,7 +456,15 @@ export class PaymentManagerPrimitive extends BasePrimitive