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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ jobs:
run: cargo fmt --all -- --check

- name: Check example generated artifacts
run: cd examples/notes && cargo run -p hydra-codegen --bin hydra-codegen -- check
run: |
cd examples/notes && cargo run -p hydra-codegen --bin hydra-codegen -- check
cd ../security-scan && cargo run -p hydra-codegen --bin hydra-codegen -- check

creed:
name: Creed context drift
Expand Down
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"crates/hydra-mcp-stdio",
"crates/hydra-codegen",
"examples/notes",
"examples/security-scan",
]

[workspace.package]
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ validation and persistence. Hydra does not own source-specific event models,
batch hashing, idempotency, or transactions. `examples/notes` contains a
tested `ingest_batch` reference operation and is the pattern Iris should use.

### Security-scanner consumer boundary

`examples/security-scan` is a deliberately small, fixture-backed consumer
reference. Its explicit `run_security_scan` operation projects to CLI, HTTP,
and MCP, while the consumer—not Hydra—owns a typed `SecurityScanner` trait and
the single dispatch function. The checked-in fixture only accepts
`fixture:demo-repo` and the optional `baseline` profile. It never treats input
as a command, filesystem path, or URL.

The fixture needs no configuration. A future live adapter must read only
`DEEPSEC_ENDPOINT` (an absolute HTTPS URL) and `DEEPSEC_TOKEN` (a non-empty
credential) from its environment. Neither belongs in source, generated output,
logs, requests, or public errors. Consumer adapters must map private failures
to the fixed public `invalid_request`, `scanner_unavailable`, or `scan_failed`
error codes without serializing vendor details, credentials, headers, endpoints,
or raw scanner output.

## Raw-request (webhook) operations

Operations that must see the exact wire representation — signature-verified
Expand Down
23 changes: 23 additions & 0 deletions examples/security-scan/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "security-scan-example"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Fixture-backed security scanner adapter boundary projected by Hydra"
publish = false

[lints]
workspace = true

[dependencies]
hydra-mcp-stdio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true }
clap = { workspace = true }
axum = { workspace = true }

[dev-dependencies]
tower = { version = "0.5", features = ["util"] }
19 changes: 19 additions & 0 deletions examples/security-scan/api/operations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
operations:
- name: run_security_scan
description: Run the configured security scanner against an allowed target.
method: POST
path: /security/scans
read: false
output_type: SecurityScanResult
surfaces: [cli, http, mcp]
parameters:
- name: target
description: Closed target identifier accepted by this tracer bullet.
type: string
required: true
location: body
- name: profile
description: Explicit scanner profile; omission selects baseline.
type: string
required: false
location: body
36 changes: 36 additions & 0 deletions examples/security-scan/generated/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Code generated by hydra. DO NOT EDIT.
// CLI command structs generated from the API definition

use clap::{Args, Subcommand};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Subcommand)]
pub enum GeneratedCommand {
/// Run the configured security scanner against an allowed target.
RunSecurityScan(RunSecurityScanArgs),
}

impl GeneratedCommand {
pub const fn operation_name(&self) -> &'static str {
match self {
Self::RunSecurityScan(_) => "run_security_scan",
}
}

pub fn parameters_json(&self) -> serde_json::Value {
match self {
Self::RunSecurityScan(args) => serde_json::json!({"target": args.target.clone(), "profile": args.profile.clone()}),
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, Args)]
pub struct RunSecurityScanArgs {
/// Closed target identifier accepted by this tracer bullet.
#[arg(long)]
pub target: String,
/// Explicit scanner profile; omission selects baseline.
#[arg(long)]
pub profile: Option<String>,
}

47 changes: 47 additions & 0 deletions examples/security-scan/generated/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Code generated by hydra. DO NOT EDIT.
// HTTP route handlers generated from the API definition

use std::collections::BTreeMap;

use axum::{extract::{State}, response::Response, routing::{post}, Router};
use axum::Json;
use serde_json::Value;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GeneratedRoute {
pub name: &'static str,
pub method: &'static str,
pub path: &'static str,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GeneratedOperationInput {
pub path: BTreeMap<String, String>,
pub query: BTreeMap<String, String>,
pub body: Value,
}

pub const GENERATED_ROUTES: &[GeneratedRoute] = &[
GeneratedRoute { name: "run_security_scan", method: "POST", path: "/security/scans" },
];

pub fn generated_router() -> Router<crate::AppState> {
Router::new()
.route("/security/scans", post(run_security_scan))
}

async fn run_security_scan(
State(state): State<crate::AppState>,
Json(body): Json<Value>,
) -> Response {
crate::execute_operation_http(
&state,
"run_security_scan",
GeneratedOperationInput {
path: BTreeMap::new(),
query: BTreeMap::new(),
body,
},
)
.await
}
31 changes: 31 additions & 0 deletions examples/security-scan/generated/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"locations": {
"run_security_scan": {
"profile": "body",
"target": "body"
}
},
"tools": [
{
"description": "Run the configured security scanner against an allowed target.",
"inputSchema": {
"additionalProperties": false,
"properties": {
"profile": {
"description": "Explicit scanner profile; omission selects baseline.",
"type": "string"
},
"target": {
"description": "Closed target identifier accepted by this tracer bullet.",
"type": "string"
}
},
"required": [
"target"
],
"type": "object"
},
"name": "run_security_scan"
}
]
}
3 changes: 3 additions & 0 deletions examples/security-scan/hydra.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Generated surfaces call the consumer-owned, typed dispatch boundary.
http_dispatch_fn: "crate::execute_operation_http"
http_state_type: "crate::AppState"
5 changes: 5 additions & 0 deletions examples/security-scan/src/bin/security-scan-cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Security-scan example CLI binary.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
security_scan_example::run_cli().await
}
5 changes: 5 additions & 0 deletions examples/security-scan/src/bin/security-scan-http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Security-scan example HTTP server binary.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
security_scan_example::run_http().await
}
5 changes: 5 additions & 0 deletions examples/security-scan/src/bin/security-scan-mcp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Security-scan example MCP stdio binary.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
security_scan_example::run_mcp().await
}
Loading
Loading