✓ Human-authored analysis; AI used for formatting and proofreading.
We built a cloud security CLI, LLM-assisted, standard Go tooling. We followed Unix philosophy from day one because: small tools, single responsibility, standard formats between steps, compose via pipes.
We looked at the pipeline and realized every step is automatable by an AI agent, because the same properties that make tools composable for humans make them composable for agents.
The pipeline
The tool is Stave, a static analysis engine for cloud infrastructure configurations. The pipeline looks like this:
steampipe query → transform script → stave validate → stave apply → stave export-sir → z3
(cloud inventory) (schema mapping) (contract check) (evaluation) (fact export) (proof)
Six steps. Six separate tools. Each reads a known input format, produces a known output format, and has a verifiable postcondition. The orchestrator such as a shell script, a Makefile, or an AI agent doesn't need to understand what's inside the data. It needs to know whether the step succeeded and whether the output conforms to the next step's contract.
Replace shell pipe with agent OODA loop and the architecture is identical:
Shell: cat snapshot.json | stave validate | stave apply
Agent: generate snapshot → assert validation passes → assert findings match expectations
Same shape. Different orchestrator. The tools don't know or care which one is driving.
What makes a step agent-automatable
A step is agent-automatable when three conditions hold:
1. The input contract is machine-readable. A JSON Schema the agent can parse, understand, and generate conforming output against. If the contract is prose documentation, the agent has to interpret natural language to produce output. If the contract is JSON Schema, the agent generates structured data that a standard validator checks.
2. The postcondition is binary. The step either succeeded or it didn't. Exit code 0 or non-zero. Schema validation passes or fails. The finding exists or it doesn't. No subjective quality judgment required. An agent can't evaluate "is this output good enough?" but it can evaluate "did stave validate --strict exit 0?"
3. The error message is actionable. When the postcondition fails, the error names the specific field that's wrong and what was expected. "Missing required property: properties.storage.kind" is actionable. The agent knows what to add. "Validation failed" is not, the agent has to guess.
If all three hold, the agent's workflow is mechanical: generate → check postcondition → if fail, read error → fix the named field → check again. No human in the loop. No subjective decision.
How Unix philosophy produces all three
Unix philosophy is a handful of principles that reinforce each other.
Write programs that do one thing and do it well
A tool that does one thing has one input contract and one postcondition. A tool that does many things has many contracts, many postconditions, and ambiguous failure modes ("did the collection fail or the evaluation fail?"). An agent working with a single-responsibility tool knows exactly what went wrong when something fails.
Stave's separation: steampipe collects. stave validate checks the contract. stave apply evaluates. stave export-sir exports facts. z3 proves. Five tools, five clear failure modes. An agent debugging a failed pipeline knows which step to retry.
A monolithic tool that collects, evaluates, and reports in one binary gives the agent one error: "scan failed." The agent can't isolate which stage broke.
Write programs to handle text streams
Unix chose text because it's the universal interface. Every tool can read and produce it. JSON is the modern equivalent. When every step reads JSON and produces JSON, the agent can inspect intermediate results, validate them against schemas, and modify them with standard tools (jq, any JSON library in any language).
Stave's observation contract is JSON Schema (Draft 2020-12). The fact export is JSONL triples. The SMT-LIB export is a text format every solver reads. An agent working with these formats uses the same libraries it uses for everything else. No SDK, client library or proprietary format that requires a vendor's parser.
Make each program do one thing well
Well means deterministic. Same input, same output, every time. This is the property that lets an agent verify its own work: run the tool, check the output, run it again, compare. If the output differs, the tool is broken. If the output matches, the postcondition holds.
stave apply is deterministic. Same snapshot produces the same findings in the same order with the same severities. An agent that generates a transform, runs stave apply, and checks for a specific finding knows with certainty whether the transform is correct. No flaky tests, probabilistic results or "run it three times and take the majority vote."
The contracts that emerged
We set out to build a tool one person could maintain. The contracts emerged from practical constraints:
JSON Schema for observations — because customers use Python, Node, Go, Ruby, and shell. We needed a language-independent contract they could validate with their existing tools. That same language-independence means any agent framework can validate against it.
Exit codes for control flow — because CI/CD pipelines branch on exit codes. Exit 0 means success. Exit 2 means input error. Exit 3 means findings exist. An agent branches on the same codes.
stave validate with structured errors — because a customer who gets "validation failed" files a support ticket. A customer who gets "missing required property: properties.storage.kind at assets[0]" fixes the field. That same specificity means an agent reads the error and fixes the field without asking a human.
stave readiness with a coverage number — because a customer who hears "you should improve your security" doesn't know what to do. A customer who sees "41% of detection capability is active. Add data_classification tags to reach 75%" knows exactly what to do. That same quantification means an agent has an objective function to optimize: maximize the readiness score.
stave gaps with a prioritized task queue — because a customer facing 100 possible improvements needs to know which one to do first. That same prioritization means an agent has an ordered list of tasks it can execute sequentially, checking the readiness score after each one.
Every contract was built for human productivity. The agent-automation was free.
The OODA loop closes automatically
An agent automating this pipeline runs a simple loop:
# Agent workflow: produce valid Stave observations from a new data source
while True:
# Generate: produce observation JSON from the data source
observation = generate_observation(source_data, schema)
write_json(observation, "output.obs.json")
# Assert: validate against the contract
result = run("stave validate --in output.obs.json --strict")
if result.exit_code == 0:
break # postcondition holds — done
# Fix: read the structured error, modify the observation
error = parse_validation_error(result.stderr)
observation = fix_field(observation, error.path, error.expected)
# Loop: try again with the fixed version
The agent doesn't need cloud security knowledge. It doesn't need to understand what properties.storage.kind means. It needs to know that the field is missing, that the schema says it's required, and that adding it makes the validation pass. The domain expertise is encoded in the schema. The agent is a generic generate-assert-fix loop.
This is the same pattern a developer uses:
# Human developer workflow
vim output.obs.json # generate
stave validate --in output.obs.json --strict # assert
# read error, fix the field # fix
# repeat # loop
Same loop. Different operator. The tools are identical in both cases because they were designed for machine-readable contracts.
Three agent workflows this enables
1. Collector agent
"Produce observations for aws_s3_bucket from this Steampipe table."
The agent reads the per-asset-type JSON Schema, queries Steampipe for the table schema, generates the field mapping, produces observation JSON, and validates. Correctness assertion: stave validate exits 0. Coverage assertion: stave readiness score increased. The agent iterates until both hold.
2. Control authoring agent
"Write a control that detects S3 buckets with public access and no data classification tag."
The agent writes a YAML control file with a CEL predicate, generates a writeup fixture (observation that should trigger the control) and a remediated fixture (observation that shouldn't), runs stave apply on both, and asserts: finding exists on writeup, absent on remediated. The agent iterates the predicate until both assertions hold.
No Go code or compilation. The control is a YAML file. The test is two JSON fixtures. The assertion is an exit code.
3. Chain authoring agent
"Compose these three controls into a compound chain with threshold 2-of-3."
The agent writes a chain YAML file, runs stave apply on a fixture set where exactly 2 of 3 controls fire, and asserts: compound finding exists. Then runs on a fixture set where only 1 fires, and asserts: compound finding absent. The agent iterates the threshold and scope_field until both hold.
Each workflow is the same pattern: generate YAML → run tool → check binary postcondition → fix if needed. The domain-specific knowledge (what predicate to write, what threshold to set) is where the LLM adds value. The verification (does it work?) is where the contract adds value. The two complement each other.
Why monoliths can't retrofit this
A monolithic security tool where collection, evaluation, and reporting are one binary with one proprietary data format can add an AI assistant to its UI. It can add natural language querying. It can add LLM-generated remediation suggestions. These are useful features.
What it can't add is agent-automatable composition. Because composition requires:
- Separate steps with independent failure modes (monolith has one step)
- Standard formats between steps that any tool can read (monolith has a proprietary internal format)
- Machine-verifiable contracts at each boundary (monolith validates internally, opaquely)
- Deterministic output that an agent can assert against (monolith may cache, rate-limit, or return different results based on server state)
Adding these properties to a monolith means decomposing it into separate tools with published contracts which means abandoning the monolithic architecture. The Unix philosophy isn't a feature you bolt on. It's a structural decision made at the beginning that produces emergent properties you didn't plan for.
The composability gap monoliths can't close
Even if a monolithic tool replicates every agent workflow above with collector agent, control authoring agent, chain authoring agent, it still can't do the one thing enterprises need: compose with tools the customer already has.
Enterprise environments don't start from zero. They have existing collectors, existing SIEMs, existing compliance pipelines, existing identity providers. They may not use Steampipe. They may use AWS Config, or Terraform state files, or a homegrown inventory system, or a commercial CMDB that exports CSV. The pipeline has to work with what they have, not require them to replace it.
A monolithic scanner says: use our collector, evaluator, dashboard and export format. If the customer's existing collector produces data in a different format, the monolith can't consume it. If the customer wants to feed findings into a SIEM the monolith doesn't integrate with, they write a custom adapter. Every custom integration is a maintenance burden that grows with each tool in the stack.
A contract-based pipeline says: produce JSON that validates against this schema. We don't care how you produce it. Steampipe, AWS CLI, Terraform state, a Python script you wrote in 2019, a commercial CMDB with a CSV export that you run through jq. Any source that produces conforming observations works. The contract is the interface, not the tool.
Customer A: Steampipe → stave validate → stave apply → Splunk
Customer B: AWS Config → stave validate → stave apply → Jira
Customer C: Terraform state → stave validate → stave apply → PagerDuty
Customer D: Homegrown CMDB → stave validate → stave apply → Neo4j → custom dashboard
Four customers, different collectors and different downstream consumers. The same evaluation engine in the middle. Zero custom integration code inside Stave for any of them. The JSON Schema contract absorbs the variation at the input boundary. The JSONL/SARIF/SMT-LIB export absorbs the variation at the output boundary. Stave never knows or cares what produced the input or what consumes the output.
This is the composability gap. A monolith can add agent support for its own pipeline. It cannot add agent support for an arbitrary pipeline composed of tools it doesn't control. The enterprise customer's pipeline is always arbitrary. It's whatever they already have, plus whatever they're adding next quarter, minus whatever they're decommissioning this quarter. A tool that requires the customer to use its collector is a tool that can't survive the customer's next infrastructure change.
The agent implication is direct: an agent automating a monolithic pipeline is locked to that monolith's capabilities. An agent automating a contract-based pipeline can swap any step, replace Steampipe with AWS Config, replace Neo4j with a custom graph database, replace Z3 with cvc5 without changing the agent's logic. The contracts are stable. The tools behind them are interchangeable. The agent targets the contracts, not the tools.
This is why "bring your own collector" isn't a convenience feature. It's the architectural property that makes the pipeline enterprise-viable. The customer doesn't adopt Stave's stack. They plug Stave into their stack. The difference is who adapts to whom.
A real-world agent development playground
Stave's pipeline is a real-world project with every property a learning agent needs:
Contracts. The JSON Schema tells the agent exactly what to produce. No ambiguity or "figure out the format from examples." The schema is the spec.
Binary assertions. stave validate exits 0 or it doesn't. The agent knows immediately whether it succeeded. No subjective evaluation or "does this look right?"
Graduated difficulty. Start with the collector agent. Transform one Steampipe table into valid observations. Then the control authoring agent. Write a YAML control that fires on a specific misconfiguration. Then the chain authoring agent. Compose controls into compound attack paths. Each step requires more domain reasoning but the verification mechanism is identical: generate, assert, fix.
Real-world feedback. stave readiness gives the agent a coverage number that increases as it produces more valid observations. stave gaps gives it a prioritized task queue. They measure security detection capability. The agent is doing useful work while learning.
Open source, local, safe. The entire pipeline runs on the developer's machine. No cloud credentials or API rate limits (except the optional LLM call). No risk of breaking production. Fork the repo, build agents against it, break things, fix things, learn.
The developer who builds a collector agent against Stave's observation schema has practiced: JSON Schema validation, structured error parsing, OODA self-correction loops, objective function optimization, and contract-driven development. Those skills transfer to any agent project such as logistics, finance, healthcare, manufacturing. The domain is cloud security. The engineering discipline is universal.
This is the other side of the Unix philosophy payoff. Tools built for composability enable enterprise automation and become the training ground where the next generation of agent developers learns the craft on a real system with real contracts and real consequences for getting the contracts wrong.
A portfolio piece for an emerging field
Job seekers in AI and DevOps face a credibility problem: everyone lists "experience with LLMs" on their resume. Few can point to a working agent system with verifiable outcomes deployed against a real-world domain.
Building an agent against Stave's pipeline produces a portfolio piece with properties hiring managers can evaluate:
The code is public. The agent lives in the developer's GitHub. The hiring manager reads the code, runs it, and sees whether stave validate passes on the output. No "trust me, it worked". The assertion is reproducible.
The outcome is measurable. "I built an agent that increased Stave readiness from 12% to 68% by generating valid observations for 7 AWS services" is a concrete, verifiable claim. The hiring manager runs stave readiness before and after the agent's output and confirms the number.
The tool choice is the developer's. Build the agent with LangChain, Google ADK, CrewAI, AutoGen, raw API calls, or a shell script with curl. The pipeline doesn't care. The contracts are the same regardless of the agent framework. The portfolio demonstrates the developer's skill with their chosen tool.
The domain is transferable. The agent engineering skills such as contract-driven generation, OODA self-correction, structured error parsing, objective function optimization, multi-step pipeline composition apply to any domain. The developer who builds a Steampipe collector agent can adapt the same patterns to the company's internal proprietary tools, multi-cloud environments, or entirely different verticals. The domain is cloud security. The skill set is universal.
Every open-source tool in the pipeline is a building block the developer can showcase: Steampipe for data collection, Stave for evaluation, Z3 for formal proofs, Soufflé for graph reasoning, Clingo for declarative rules, Neo4j for graph analysis. An agent that orchestrates three of these into a working pipeline demonstrates more engineering judgment than a chatbot that wraps one API.
The career path this serves is real and growing. Companies are hiring for "AI Engineering," "Agent Developer," "LLM Platform Engineer", roles that didn't exist two years ago and that most candidates can only demonstrate with toy projects. A working agent pipeline that produces verifiable security findings against a published schema is the antidote to "I completed the LangChain tutorial" on a resume.
Once the developer gets the job, the pattern ports directly. The company's internal CMDB replaces Steampipe. The company's compliance framework replaces Stave's control catalog. The company's proprietary cloud environment replaces the demo fixtures. The contracts, the OODA loop, the assertion-driven development, those stay the same. The developer built the skill on an open-source project and applies it to proprietary infrastructure on day one.
The agent-automatable pipeline is a consequence of architectural decisions made on day one by a solo developer who needed composable tools because one person can't maintain a monolith:
- Air-gapped snapshots (no live API dependency) → agent runs locally, repeatedly, safely
- JSON Schema contracts (language-independent) → agent validates with standard tools
- Deterministic evaluation (same input = same output) → agent asserts without human judgment
- Structured errors (field-level, actionable) → agent fixes without guessing
- Quantified readiness (coverage number, not prose) → agent has an objective function
- Prioritized gaps (ordered task queue) → agent knows what to work on next
Every decision was made for human productivity. The agent architecture was produced as an emergent property. Unix proved this 50 years ago: if you make tools composable for humans via standard interfaces, you get scriptability for free. Scripts are just agents with less autonomy. Agents are just scripts with more initiative.
How this relates to existing compliance mods
The Unix-philosophy lens makes the pipeline composition with existing tools explicit. turbot/steampipe-mod-aws-compliance already follows the same composability discipline: Steampipe plugins do collection (one job, standard SQL interface), Powerpipe does rendering (one job, standard dashboard format), the mod itself is the framework-control content piped between them. Stave plugs into the same composition. Its CLI reads observation JSON, writes finding JSON, exits with a status code, and the same Powerpipe dashboard renders the result. Two tools obeying the same composability rules; the agent calling both gets framework-coverage findings AND compositional findings from the same script, without either tool needing to know the other exists. That's the Unix property the article describes, applied across the tool boundary. Comparison: aws-compliance-mod.
Stave is an open-source intent verification engine for cloud infrastructure. 3,000+ controls, 585 compound chains, 40 per-asset-type JSON Schemas, 9 reasoning engines. Every step in the pipeline has a machine-verifiable contract. Try it: bash examples/demo-ai-security/run.sh
Top comments (0)