From d05579855682f33015c8bbd4d569ce4bfa45d2ce Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 17 Jun 2026 18:54:25 +0000 Subject: [PATCH 1/3] 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/3] 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/3] 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];