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
5 changes: 5 additions & 0 deletions .changeset/generate-arazzo-respect-hint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@redocly/cli': minor
---

The `generate-arazzo` command now prints a ready-to-run `respect` command after generation, including an `--input` placeholder for every workflow input.
7 changes: 7 additions & 0 deletions .changeset/generate-arazzo-with-ai.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@redocly/cli': minor
---

Added `--with-ai`, `--ai-provider`, and `--max-workflows` options to the `generate-arazzo` command.
With `--with-ai`, the generated one-workflow-per-operation skeleton is redesigned by a locally installed AI CLI (`claude`, `codex`, or `cursor`) into realistic multi-step workflows that chain operations through outputs and runtime expressions, using the OpenAPI description as context.
The AI designs at most `--max-workflows` workflows (default 10), and the generated file is marked as AI-inferred.
7 changes: 7 additions & 0 deletions .changeset/respect-scheme-case.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@redocly/cli': patch
'@redocly/respect-core': patch
'@redocly/openapi-core': patch
---

Fixed `respect` and the `x-security-scheme-required-values` rule rejecting `x-security` HTTP schemes written with non-lowercase casing (such as `Basic`, `Bearer`, or `Digest`) — RFC 7235 scheme names are case-insensitive.
55 changes: 54 additions & 1 deletion docs/@v2/commands/generate-arazzo.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ It acts as a starting point for a test file and needs to be extended to be funct

The first HTTP response is used as the success criteria for each step.

After writing the file, the command prints a ready-to-run [`respect`](./respect.md) command, including an `--input` placeholder for every workflow input — replace the placeholder values with real ones before running it.

With `--with-ai`, the generated one-workflow-per-operation skeleton is redesigned by an AI provider into realistic multi-step workflows, using the OpenAPI description as context.
See the [Redesign workflows with AI](#redesign-workflows-with-ai) section.

## Usage

```sh
npx @redocly/cli@latest generate-arazzo <your-OAS-description-file> [-o | --output-file]
npx @redocly/cli@latest generate-arazzo <your-OAS-description-file> --with-ai [--ai-provider=<option>]
```

## Options
Expand All @@ -37,6 +43,24 @@ npx @redocly/cli@latest generate-arazzo <your-OAS-description-file> [-o | --outp
- string
- Name for the generated output file. Defaults to `auto-generated.arazzo.yaml` **If the file already exists, it's overwritten.** See the [specify output file](#specify-output-file) section.

---

- --with-ai
- boolean
- Redesign the generated workflows with an AI provider, using the OpenAPI description as context. Default value is `false`. See the [redesign workflows with AI](#redesign-workflows-with-ai) section.

---

- --ai-provider
- string
- AI provider used with `--with-ai`. Runs the corresponding CLI in non-interactive mode.<br/>**Possible values:** `claude`, `codex`, `cursor`. Default value is `claude`.

---

- --max-workflows
- number
- Most workflows the AI may design with `--with-ai`, so the output contains the most likely scenarios instead of every combination. Default value is `10`.

{% /table %}

## Examples
Expand All @@ -48,7 +72,7 @@ The command generates an `auto-generated.arazzo.yaml` file in the current direct
The contents of the generated file are:

```yaml {% title="auto-generated.arazzo.yaml" %}
arazzo: 1.0.1
arazzo: 1.1.0
info:
title: Warp API
version: 1.0.0
Expand Down Expand Up @@ -123,6 +147,35 @@ By default, the CLI tool writes the generated file as `auto-generated.arazzo.yam
redocly generate-arazzo <your-OAS-description-file> --output-file=arazzo-custom.yaml
```

### Redesign workflows with AI

Without AI, the generated file contains one workflow per operation and no dependencies between them.
With `--with-ai`, the OpenAPI description and the generated skeleton are sent to an AI provider, which redesigns the workflows into realistic scenarios: related operations are grouped into multi-step workflows (for example create, read, update, then delete a resource), steps pass values to each other through `outputs` and runtime expressions, and workflows declare `inputs` for values a caller must provide.

The AI designs at most `--max-workflows` workflows (default `10`), preferring to cover every operation and otherwise choosing the most likely scenarios.

The AI's answer is never trusted blindly: the `arazzo`, `info`, and `sourceDescriptions` fields always come from the generated baseline, every step must reference an operation that exists in the OpenAPI description, the workflow count must stay within `--max-workflows`, and the result must pass validation with the `spec` ruleset.
If the answer is rejected, the provider fails, or the description is too large to prompt with, the command keeps the auto-generated workflows.

The generated file starts with a comment marking the workflows as AI-inferred — they are a guess derived from the description, not verified behavior, so review them before use.
The result also varies between runs: the same description can produce different workflows each time.

```bash
redocly generate-arazzo openapi.yaml --with-ai --ai-provider claude --max-workflows 5
```

{% admonition type="warning" name="Data sharing" %}
`--with-ai` sends the resolved OpenAPI description to the selected AI provider.
Make sure it contains no secrets or personal data you are not allowed to share.
{% /admonition %}

#### AI providers

The workflows are designed by a locally installed AI CLI running in non-interactive mode: `claude` (Claude Code), `codex` (Codex CLI), or `cursor` (Cursor CLI).
The selected CLI must be installed and authenticated on the machine running the command — no API key is passed to or stored by Redocly CLI.

The provider runs in isolation: project context the CLIs normally load (such as `CLAUDE.md`, `AGENTS.md`, or `.cursor/rules`) does not apply.

## Resources

- [Learn more about Arazzo](/learn/arazzo/what-is-arazzo).
Expand Down
128 changes: 124 additions & 4 deletions packages/cli/src/__tests__/commands/generate-arazzo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { generate } from '@redocly/respect-core';
import { writeFileSync } from 'node:fs';
import { vi, describe, it, expect, beforeEach } from 'vitest';

import { generateWorkflowsWithAi } from '../../commands/generate-arazzo/ai/generate-workflows.js';
import {
buildRespectHint,
type GenerateArazzoCommandArgv,
handleGenerateArazzo,
} from '../../commands/generate-arazzo.js';
} from '../../commands/generate-arazzo/index.js';

vi.mock('@redocly/respect-core', async () => {
const actual =
Expand All @@ -18,12 +20,17 @@ vi.mock('@redocly/respect-core', async () => {
};
});

vi.mock('../../commands/generate-arazzo/ai/generate-workflows.js', () => ({
generateWorkflowsWithAi: vi.fn(),
}));

vi.mock('@redocly/openapi-core', async () => {
const actual = await vi.importActual('@redocly/openapi-core');
return {
...actual,
logger: {
info: vi.fn(),
warn: vi.fn(),
},
stringifyYaml: vi.fn(() => 'mocked yaml'),
};
Expand All @@ -37,7 +44,7 @@ vi.mock('node:fs', () => ({
describe('handleGenerateArazzo', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(generate).mockResolvedValue('{"mocked": "arazzo"}');
vi.mocked(generate).mockResolvedValue({ workflows: [] });
});

it('should call generate with the correct arguments', async () => {
Expand All @@ -56,7 +63,7 @@ describe('handleGenerateArazzo', () => {
expect(generate).toHaveBeenCalledWith({
outputFile: 'auto-generated.arazzo.yaml',
descriptionPath: 'openapi.yaml',
collectSpecData: commandArgs.collectSpecData,
collectSpecData: expect.any(Function),
version: '1.0.0',
config: mockConfig,
});
Expand All @@ -80,7 +87,7 @@ describe('handleGenerateArazzo', () => {
expect(generate).toHaveBeenCalledWith({
outputFile: 'custom.arazzo.yaml',
descriptionPath: 'openapi.yaml',
collectSpecData: commandArgs.collectSpecData,
collectSpecData: expect.any(Function),
version: '1.0.0',
config: mockConfig,
});
Expand All @@ -106,4 +113,117 @@ describe('handleGenerateArazzo', () => {
);
expect(writeFileSync).not.toHaveBeenCalled();
});

it('should write the AI-designed workflows when --with-ai succeeds', async () => {
const mockConfig = await openapiCore.createConfig({});
vi.mocked(generateWorkflowsWithAi).mockResolvedValueOnce({
yaml: 'ai yaml',
workflows: 2,
});
const commandArgs = {
argv: {
descriptionPath: 'openapi.yaml',
'with-ai': true,
'ai-provider': 'claude',
'max-workflows': 10,
} as GenerateArazzoCommandArgv,
config: mockConfig,
version: '1.0.0',
collectSpecData: vi.fn(),
};

await handleGenerateArazzo(commandArgs);

expect(generateWorkflowsWithAi).toHaveBeenCalledWith({
provider: 'claude',
baseline: { workflows: [] },
description: undefined,
maxWorkflows: 10,
});
expect(writeFileSync).toHaveBeenCalledWith(
'auto-generated.arazzo.yaml',
'# The workflows below were inferred by AI (--with-ai). Verify before use.\nai yaml'
);
});

it('should keep the auto-generated workflows when --with-ai fails', async () => {
const mockConfig = await openapiCore.createConfig({});
vi.mocked(generateWorkflowsWithAi).mockRejectedValueOnce(new Error('no usable answer'));
const commandArgs = {
argv: {
descriptionPath: 'openapi.yaml',
'with-ai': true,
'ai-provider': 'claude',
'max-workflows': 10,
} as GenerateArazzoCommandArgv,
config: mockConfig,
version: '1.0.0',
collectSpecData: vi.fn(),
};

await handleGenerateArazzo(commandArgs);

expect(writeFileSync).toHaveBeenCalledWith('auto-generated.arazzo.yaml', 'mocked yaml');
expect(openapiCore.logger.warn).toHaveBeenCalledWith(
expect.stringContaining('AI workflow design failed')
);
});

it('should print a hint with the respect command', async () => {
const mockConfig = await openapiCore.createConfig({});
const commandArgs = {
argv: {
descriptionPath: 'openapi.yaml',
} as GenerateArazzoCommandArgv,
config: mockConfig,
version: '1.0.0',
collectSpecData: vi.fn(),
};

await handleGenerateArazzo(commandArgs);

expect(openapiCore.logger.info).toHaveBeenCalledWith(
expect.stringContaining('npx @redocly/cli@latest respect auto-generated.arazzo.yaml')
);
});
});

describe('buildRespectHint', () => {
it('lists a placeholder for every workflow input, resolving component refs', () => {
const resultYaml = [
'workflows:',
' - workflowId: first',
' inputs:',
' $ref: "#/components/inputs/bearerAuth"',
' - workflowId: second',
' inputs:',
' type: object',
' properties:',
' userEmail:',
' type: string',
'components:',
' inputs:',
' bearerAuth:',
' type: object',
' properties:',
' bearerAuth:',
' type: string',
].join('\n');

const hint = buildRespectHint(resultYaml, 'museum.arazzo.yaml');

expect(hint).toContain(
'npx @redocly/cli@latest respect museum.arazzo.yaml --input bearerAuth=YOUR_BEARERAUTH --input userEmail=YOUR_USEREMAIL'
);
expect(hint).toContain('Replace the YOUR_* values');
expect(hint).toContain('REDOCLY_CLI_RESPECT_INPUT');
});

it('omits input flags when the workflows declare no inputs', () => {
const hint = buildRespectHint('workflows:\n - workflowId: first\n', 'museum.arazzo.yaml');

expect(hint).toContain('npx @redocly/cli@latest respect museum.arazzo.yaml\n');
expect(hint).not.toContain('--input');
expect(hint).not.toContain('Replace the YOUR_* values');
});
});
45 changes: 0 additions & 45 deletions packages/cli/src/commands/generate-arazzo.ts

This file was deleted.

Loading
Loading