Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/cli/commands/deploy/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,22 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep

const postDeployWarnings: string[] = [];

// Auto-payment is a money-movement default; surface its posture per manager
// so an unattended-spend configuration is never silent at deploy time. This
// is an informational notice, not a failure — it goes through `notes` (exit
// 0), NOT postDeployWarnings (which signals partial failure and exits 2).
const autoPaymentNotices: string[] = [];
for (const manager of context.projectSpec.payments ?? []) {
if (manager.autoPayment !== false) {
const limit = manager.defaultSpendLimit ?? '10.00';
autoPaymentNotices.push(
`Payment manager "${manager.name}": auto-payment is ENABLED — the agent will settle ` +
`402 responses automatically up to the per-session spend limit ($${limit}) with no ` +
`human approval. Set --auto-payment false on the manager to require manual approval.`
);
}
}

// Post-deploy: Enable online eval configs that have enableOnCreate (CFN deploys them as DISABLED).
// Only enable configs that are newly deployed — skip configs that already existed before this
// deploy run, so we don't re-enable configs a customer intentionally disabled.
Expand Down Expand Up @@ -878,7 +894,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
const hasHarnesses = (context.projectSpec.harnesses ?? []).length > 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) {
Expand Down
31 changes: 28 additions & 3 deletions src/cli/primitives/PaymentManagerPrimitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -128,7 +129,7 @@ export class PaymentManagerPrimitive extends BasePrimitive<AddPaymentManagerOpti

async add(
options: AddPaymentManagerOptions
): Promise<AddResult<{ managerName: string; skippedRuntimes?: string[] }>> {
): Promise<AddResult<{ managerName: string; skippedRuntimes?: string[]; autoPaymentWarning?: string }>> {
try {
const project = await this.readProjectSpec();
// payments is optional in the schema (absent on projects with no payment
Expand Down Expand Up @@ -189,7 +190,20 @@ export class PaymentManagerPrimitive extends BasePrimitive<AddPaymentManagerOpti
}
}

return { success: true, managerName: options.name, skippedRuntimes };
// Auto-payment lets the agent settle 402 responses with no human in the
// loop, so surface a warning when it is active. Returned (not printed)
// because add() is shared by the CLI and the Ink TUI flow — each caller
// renders it through its own channel rather than writing to stderr
// mid-render.
const effectiveAutoPayment = options.autoPayment ?? DEFAULT_AUTO_PAYMENT;
const autoPaymentWarning = effectiveAutoPayment
? `auto-payment is ENABLED for manager "${options.name}". Agents will automatically settle ` +
`402 Payment Required responses up to the per-session spend limit ` +
`($${options.defaultSpendLimit ?? DEFAULT_SPEND_LIMIT}) with no human approval. ` +
`Re-run with --auto-payment false to require manual approval.`
: undefined;

return { success: true, managerName: options.name, skippedRuntimes, autoPaymentWarning };
} catch (err) {
return { success: false, error: toError(err) };
}
Expand Down Expand Up @@ -442,9 +456,20 @@ export class PaymentManagerPrimitive extends BasePrimitive<AddPaymentManagerOpti
});

if (cliOptions.json) {
console.log(JSON.stringify(serializeResult(result)));
// autoPaymentWarning is a human-facing notice rendered on the
// non-JSON path only; strip it so --json stdout stays a clean
// machine-readable result (mirrors the connector leak warning).
if (result.success) {
const { autoPaymentWarning: _autoPaymentWarning, ...rest } = result;
console.log(JSON.stringify(serializeResult(rest)));
} else {
console.log(JSON.stringify(serializeResult(result)));
}
} else if (result.success) {
console.log(`Added payment manager '${result.managerName}'`);
if (result.autoPaymentWarning) {
console.warn(`${ANSI.yellow}Warning: ${result.autoPaymentWarning}${ANSI.reset}`);
}
if (result.skippedRuntimes && result.skippedRuntimes.length > 0) {
console.warn(
`\nWarning: payment capability auto-wiring skipped for non-Strands runtime(s): ${result.skippedRuntimes.join(', ')}.`
Expand Down
21 changes: 21 additions & 0 deletions src/cli/primitives/__tests__/PaymentManagerPrimitive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,27 @@ describe('PaymentManagerPrimitive', () => {
expect(result.error.message).toBe('disk read failure');
}
});

it('returns an auto-payment warning when enabled (default)', async () => {
mockReadProjectSpec.mockResolvedValue(makeProject({ runtimes: [] }));

const result = await primitive.add({ name: 'mgr1', authorizerType: 'AWS_IAM' });

expect(result.success).toBe(true);
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('returns no auto-payment warning when explicitly disabled', async () => {
mockReadProjectSpec.mockResolvedValue(makeProject({ runtimes: [] }));

const result = await primitive.add({ name: 'mgr2', authorizerType: 'AWS_IAM', autoPayment: false });

expect(result.success).toBe(true);
if (!result.success) throw new Error('expected success');
expect(result.autoPaymentWarning).toBeUndefined();
});
});

describe('remove()', () => {
Expand Down
16 changes: 13 additions & 3 deletions src/cli/tui/screens/payment/AddPaymentFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this also have ?? DEFAULT_SPEND_LIMIT like the code above does? So we don't get the possibility of $undefined/session here?

},
]
: []),
...(!flow.connectorConfig
? [{ label: '⚠ Warning', value: 'No connector — deploy will fail until you add one' }]
: []),
];

const allFields = [...managerFields, ...connectorFields, ...warningFields];

Expand Down
Loading