Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/devel_doc/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -21349,7 +21349,7 @@
}
],
"title": "Provider",
"description": "Default provider config: {name, model, credentials_secret}."
"description": "Default provider config: {name, model}. The stack chooses the credential; credentials_secret is rejected."
},
"sandbox_image": {
"anyOf": [
Expand Down
475 changes: 475 additions & 0 deletions examples/workflows/README.md

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions examples/workflows/admin-inline-mcp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# =============================================================================
# admin-inline-mcp.yaml — trusted-admin workflow using an inline MCP server.
#
# Inline MCP is the exception path (role agent-admin, rules[].inline_mcp):
# - https only, no userinfo in the URL, host must be in inline_mcp_hosts
# - secret_headers carry REGISTRY refs ({name: ...}); the stack validates the
# ref and rewrites it to cloud-agents' {secret_name, key} form. A raw
# {secret_name, key} from a caller is rejected (400).
# - the (principal, server, ref) triple must be granted via mcp_secrets
# Prefer named catalog entries; use this only for one-off trusted pipelines.
# =============================================================================
apiVersion: cloudagents/v1
kind: AgentWorkflow
metadata:
name: admin-inline-mcp
description: Query an operator-approved ad-hoc MCP endpoint.

spec:
input_prompt: "List the open incidents."
spawn: ephemeral
service_account: workflow-runner
timeout_seconds: 600
steps:
- name: query
type: agent
agent: triage-agent
prompt: Use the incident tools to list open incidents.
output_key: incidents
mcp_servers:
- name: incidents
url: https://mcp.internal.example.com/incidents
secret_headers:
Authorization: {name: mcp/incidents}
timeout_seconds: 300
permissions:
max_tokens: 20000
318 changes: 318 additions & 0 deletions examples/workflows/cases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
# =============================================================================
# cases.yaml — the behavioural contract for stack#51 (feature-level TDD).
#
# Each case is a request (or a config) and the status the gate must return.
# verify.py runs them against reference_gate.py today; as phases land they
# should run against the real endpoint and the reference code is deleted.
#
# workflow: base definition file; `set` patches it (dotted path, list items
# addressed by index or by their `name`); `body_set` patches the
# request body; `generate` builds oversized inputs
# as: roles of the caller (default per workflow below)
# expect: HTTP status, or {pre269: N, post269: N} when the stages differ
# reason: substring that must appear in the returned reasons
# spawner: false = stack has no spawner_configuration
# Statuses: 400 unknown to the deployment / forbidden field, 403 exists but not
# for this caller, 413 too large, 422 shape or count, 202 accepted.
# =============================================================================
defaults:
triage-github-issue:
as: [agent-user]
provider: {name: claude-prod, model: claude-sonnet-4-5}
kb-answer-with-approval:
as: [agent-support]
provider: {name: openai-team-b, model: gpt-4o}
admin-inline-mcp:
as: [agent-admin]
provider: {name: claude-prod, model: claude-sonnet-4-5}

workflow_cases:
# ---- accepted -------------------------------------------------------------
- {name: triage as agent-user, workflow: triage-github-issue, expect: 202}
- {name: kb answer as agent-support, workflow: kb-answer-with-approval, expect: 202}
- {name: admin inline MCP with registry ref, workflow: admin-inline-mcp, expect: 202}
- name: credential_ref equal to the entry's credential is accepted
workflow: triage-github-issue
body_set: {provider.credential_ref: {name: inference/anthropic-prod}}
expect: 202
- name: omitted provider uses workflow_engine defaults
workflow: triage-github-issue
body_set: {provider: null}
expect: 202
- name: step may pick another model on the same entry
workflow: kb-answer-with-approval
set: {spec.steps.respond.inference_provider: {name: openai-team-b, model: gpt-4o-mini}}
expect: 202
- name: admin may run spawn none (no secret_headers servers)
workflow: triage-github-issue
as: [agent-admin]
set: {spec.spawn: none, spec.mcp_servers: [], spec.allowed_skills: []}
expect: 202
- name: admin may use the admin-only azure entry with its pinned model
workflow: triage-github-issue
as: [agent-admin]
body_set: {provider: {name: azure-eu, model: gpt-4o}}
set: {spec.mcp_servers: [], spec.allowed_skills: []}
expect: 202

# ---- caller cannot choose credentials (G1, G2) ----------------------------
- name: run provider credentials_secret (env var name) is rejected
workflow: triage-github-issue
body_set: {provider.credentials_secret: DATABASE_URL}
expect: 400
reason: credentials_secret
- name: run provider credentials_secret is rejected even when null
workflow: triage-github-issue
body_set: {provider.credentials_secret: null}
expect: 400
- name: credentials_secret is rejected for admins too
workflow: admin-inline-mcp
body_set: {provider.credentials_secret: ANTHROPIC_API_KEY}
expect: 400
- name: definition.provider credentials_secret is rejected
workflow: kb-answer-with-approval
set: {provider.credentials_secret: DATABASE_URL}
expect: 400
- name: credential_ref for another entry's credential is rejected
workflow: triage-github-issue
body_set: {provider.credential_ref: {name: inference/openai-team-b}}
expect: 400
reason: credential_ref
- name: unknown keys in the provider are rejected
workflow: triage-github-issue
body_set: {provider.base_url: https://evil.example.com}
expect: 400

# ---- catalog: unknown to the deployment (400) ------------------------------
- name: unknown provider
workflow: triage-github-issue
body_set: {provider.name: gpt-everything}
expect: 400
reason: unknown provider
- name: model outside allowed_models
workflow: triage-github-issue
body_set: {provider.model: claude-opus-4}
expect: 400
reason: not allowed
- name: step provider on another catalog entry (same-entry rule)
workflow: triage-github-issue
set: {spec.steps.triage.inference_provider: {name: openai-team-b, model: gpt-4o}}
as: [agent-admin]
expect: 400
reason: same catalog entry
- name: definition.provider on another catalog entry (same-entry rule)
workflow: triage-github-issue
set: {provider: {name: openai-team-b, model: gpt-4o}}
as: [agent-admin]
expect: 400
reason: same catalog entry
- name: unknown MCP server name
workflow: triage-github-issue
set: {spec.mcp_servers: [github-readwrite]}
expect: 400
reason: unknown MCP
- name: inline MCP with a raw secret_name/key header (no registry ref)
workflow: admin-inline-mcp
set:
spec.steps.query.mcp_servers.0.secret_headers:
Authorization: {secret_name: lightspeed-postgres, key: password}
expect: 400
reason: registry refs
- name: inline MCP with an unknown registry ref
workflow: admin-inline-mcp
set: {spec.steps.query.mcp_servers.0.secret_headers.Authorization.name: mcp/nope}
expect: 400

# ---- policy: exists, not for this caller (403) -----------------------------
- name: agent-user cannot use the openai-team-b entry
workflow: triage-github-issue
body_set: {provider: {name: openai-team-b, model: gpt-4o}}
expect: 403
reason: not granted
- name: agent-support cannot use the admin-only azure entry
workflow: kb-answer-with-approval
body_set: {provider: {name: azure-eu, model: gpt-4o}}
set:
provider: {name: azure-eu, model: gpt-4o}
spec.steps.research.inference_provider: {name: azure-eu, model: gpt-4o}
expect: 403
- name: agent-support cannot run the triage flow (provider, MCP, skill, tool)
workflow: triage-github-issue
as: [agent-support]
expect: 403
- name: caller without workflow_start
workflow: triage-github-issue
as: []
expect: 403
reason: workflow_start
- name: skill not granted
workflow: triage-github-issue
set: {spec.steps.triage.allowed_skills: [kb-search]}
expect: 403
reason: skill
- name: tool not granted
workflow: triage-github-issue
set: {spec.steps.triage.tools: [github.write]}
expect: 403
reason: tool
- name: service account not granted
workflow: triage-github-issue
set: {spec.service_account: kb-runner}
expect: 403
reason: service account
- name: namespace not granted
workflow: triage-github-issue
set: {spec.steps.triage.target_namespaces: [kube-system]}
expect: 403
reason: namespace
- name: step timeout over the role limit
workflow: triage-github-issue
set: {spec.steps.triage.timeout_seconds: 3600}
expect: 403
reason: timeout
- name: step max_tokens over the role limit
workflow: triage-github-issue
set: {spec.steps.triage.permissions.max_tokens: 900000}
expect: 403
reason: max_tokens
- name: step max_retries over the role limit
workflow: triage-github-issue
set: {spec.steps.triage.max_retries: 9}
expect: 403
reason: max_retries
- name: workflow timeout over the role limit
workflow: triage-github-issue
set: {spec.timeout_seconds: 7200}
expect: 403

# ---- spawn modes, images, advisory -----------------------------------------
- name: spawn none as agent-user
workflow: triage-github-issue
set: {spec.spawn: none, spec.mcp_servers: [], spec.allowed_skills: []}
expect: 403
reason: spawn
- name: spawn local on one step as agent-user
workflow: triage-github-issue
set: {spec.steps.report.spawn: local}
expect: 403
- name: ephemeral steps without spawner_configuration
workflow: triage-github-issue
spawner: false
expect: 403
reason: spawner_configuration
- name: custom image on the request
workflow: triage-github-issue
body_set: {sandbox_image: evil.example.com/sandbox:latest}
expect: 403
reason: image
- name: custom image on a step spawn_config
workflow: triage-github-issue
set: {spec.steps.triage.spawn_config.sandbox_image: evil.example.com/sandbox:latest}
expect: 403
- name: custom skills image
workflow: triage-github-issue
set: {skills: {image: evil.example.com/skills:latest}}
expect: 403
- name: advisory mode as agent-user
workflow: triage-github-issue
set: {advisory: true}
expect: 403
reason: advisory
- name: advisory mode as admin
workflow: triage-github-issue
as: [agent-admin]
set: {advisory: true}
expect: 202
- name: secret_headers MCP on spawn none (needs cloud-agents#269)
workflow: triage-github-issue
as: [agent-admin]
set: {spec.spawn: none}
expect: {pre269: 403, post269: 202}
reason: secret_headers

# ---- inline MCP -------------------------------------------------------------
- name: inline MCP as agent-user
workflow: admin-inline-mcp
as: [agent-user]
expect: 403
reason: inline MCP
- name: inline MCP host not on the allow-list
workflow: admin-inline-mcp
set: {spec.steps.query.mcp_servers.0.url: "https://evil.example.com/mcp"}
expect: 403
reason: host
- name: inline MCP over plain http
workflow: admin-inline-mcp
set: {spec.steps.query.mcp_servers.0.url: "http://mcp.internal.example.com/incidents"}
expect: 403
reason: https
- name: inline MCP with credentials in the URL
workflow: admin-inline-mcp
set: {spec.steps.query.mcp_servers.0.url: "https://user:pw@mcp.internal.example.com/incidents"}
expect: 403
reason: userinfo
- name: inline MCP with a literal header value
workflow: admin-inline-mcp
set: {spec.steps.query.mcp_servers.0.headers: {Authorization: "Bearer ghp_x"}}
expect: 403
reason: literal header

# ---- limits (G9) -------------------------------------------------------------
- name: definition over the byte cap
workflow: triage-github-issue
generate: {pad_bytes: 300000}
expect: 413
- name: too many steps
workflow: triage-github-issue
generate: {steps: 65}
expect: 422
- name: too many MCP servers on the workflow default
workflow: triage-github-issue
generate: {mcp_servers: 17}
expect: 422
- name: too many secret_headers on an inline server
workflow: admin-inline-mcp
generate: {secret_headers: 33}
expect: 422
- name: missing spec.steps
workflow: triage-github-issue
set: {spec.steps: []}
expect: 422
- name: legacy step "provider" alias is a shape error
workflow: triage-github-issue
set: {spec.steps.triage.provider: {name: claude-prod}}
expect: 422

# Config-load failures. `set` patches the parsed lightspeed-stack.yaml.
config_cases:
- name: allowed_models [] is rejected
set: {workflow_engine.providers.claude-prod.allowed_models: []}
error: allowed_models
- name: executor_type outside APPROVED_INFERENCE_PROVIDERS
set: {workflow_engine.providers.claude-prod.executor_type: ollama}
error: not approved
- name: credential without a secrets binding
set: {workflow_engine.providers.claude-prod.credential.name: inference/missing}
error: no secrets binding
- name: pre-269 inference credential must be env-backed
set: {workflow_engine.secrets.inference/anthropic-prod: {name: inference/anthropic-prod, backend: k8s, secret_name: s, key: k}}
stage: pre269
error: must be env
- name: default_provider must be in the catalog
set: {workflow_engine.default_provider: gone}
error: default_provider
- name: default_model must pass the default provider's allowed_models
set: {workflow_engine.default_model: claude-opus-4}
error: default_model
- name: workflow_enabled MCP with request-bound auth
set: {mcp_servers.github-readonly.authorization_headers: {Authorization: client}}
error: workflow_enabled
- name: workflow_enabled MCP with propagated headers
set: {mcp_servers.kb-search.headers: [x-rh-identity]}
error: propagated
- name: MCP secret ref without a binding
set: {mcp_servers.kb-search.secret_headers.X-API-Key.name: mcp/missing}
error: no secrets binding
- name: policy rule naming an undefined provider
set: {workflow_engine.policy.rules.0.providers: [ghost]}
error: unknown provider
Loading
Loading