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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ Notable changes to Cloudfall. The format follows

### Added

- `cloudfall agent investigate --alert TEXT --base-url URL --model NAME`
lets a model work an alert through the operations catalog, over any
OpenAI-compatible endpoint (the API key from `--api-key-from-env`,
`--api-key-from-file` or `CLOUDFALL_API_KEY`). The model sees the same
operation tools `mcp serve --repository` gives any client: a read
operation runs and returns what the hosts reported, any other operation is
proposed in check mode and recorded as a decision, and no tool approves
anything. A call repeated with the same arguments is answered from the
first result instead of running again. The investigation is recorded in
`investigations/` as an `AgentInvestigation`: the alert, the model, the
tokens, every call and how it ended, and the finding, with any decision id
the model named but never recorded kept apart under `unrecorded`. Exit 92
`MODEL_UNAVAILABLE` when the endpoint refuses, exit 93
`INVESTIGATION_INCOMPLETE` with the record when the model runs out of
`--max-turns` or does not answer in the asked-for shape. Proven on a
disposable host with Nemotron 3 Super on Nebius Token Factory: a full disk
behind a PostgreSQL alert was traced to unrotated application logs in five
turns, and the two proposals it recorded, once approved, brought the
database back
- An operation tool of `mcp serve --repository` describes a read operation
as running and returning what the hosts reported, instead of saying it
runs check mode only
- `operations propose` returns a read operation's play output in
`data.output`, so an agent acts on what the operation reported without
opening the decision's `.diff` file; the record keeps the file as before.
Expand Down
171 changes: 171 additions & 0 deletions config/schemas/v1/agent-investigation.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://cloudfall.internal/schemas/v1/agent-investigation.schema.json",
"title": "Cloudfall agent investigation",
"description": "One alert worked by one model through the operations catalog: every call, the tokens, and the finding.",
"type": "object",
"properties": {
"apiVersion": {
"$ref": "common.schema.json#/$defs/apiVersion"
},
"kind": {
"const": "AgentInvestigation"
},
"metadata": {
"$ref": "common.schema.json#/$defs/metadata"
},
"spec": {
"type": "object",
"properties": {
"alert": {
"type": "string",
"minLength": 1,
"maxLength": 4000
},
"model": {
"type": "object",
"properties": {
"endpoint": {
"type": "string",
"pattern": "^https?://",
"maxLength": 512
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 256
}
},
"required": [
"endpoint",
"name"
],
"additionalProperties": false
},
"startedAt": {
"type": "string",
"format": "date-time"
},
"finishedAt": {
"type": "string",
"format": "date-time"
},
"status": {
"enum": [
"answered",
"unstructured",
"turn-limit"
]
},
"turns": {
"type": "integer",
"minimum": 0
},
"tokens": {
"type": "object",
"properties": {
"prompt": {
"type": "integer",
"minimum": 0
},
"completion": {
"type": "integer",
"minimum": 0
}
},
"required": [
"prompt",
"completion"
],
"additionalProperties": false
},
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tool": {
"type": "string",
"maxLength": 128
},
"arguments": {
"type": "object"
},
"outcome": {
"enum": [
"ran",
"proposed",
"failed",
"invalid",
"repeated",
"unknown-tool"
]
},
"decision": {
"$ref": "common.schema.json#/$defs/resourceId"
}
},
"required": [
"tool",
"arguments",
"outcome"
],
"additionalProperties": false
}
},
"answer": {
"type": "string"
},
"finding": {
"type": "object",
"properties": {
"rootCause": {
"type": "string"
},
"proposed": {
"type": "array",
"items": {
"$ref": "common.schema.json#/$defs/resourceId"
}
},
"unrecorded": {
"type": "array",
"items": {
"type": "string"
}
},
"why": {
"type": "string"
}
},
"required": [
"rootCause",
"proposed",
"unrecorded",
"why"
],
"additionalProperties": false
}
},
"required": [
"alert",
"model",
"startedAt",
"finishedAt",
"status",
"turns",
"tokens",
"steps",
"answer"
],
"additionalProperties": false
}
},
"required": [
"apiVersion",
"kind",
"metadata",
"spec"
],
"additionalProperties": false
}
7 changes: 5 additions & 2 deletions sdk/src/cloudfall/agent_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
InventorySource,
inventory_from_config,
)
from cloudfall.catalog import CATALOG_DIRECTORY
from cloudfall.catalog import CATALOG_DIRECTORY, RiskLevel
from cloudfall.decision import DECISION_DIRECTORY
from cloudfall.project import (
PROJECT_DIRECTORY_VARIABLE,
Expand Down Expand Up @@ -260,7 +260,10 @@ def operation_tool_description(operation: Operation) -> str:
else f"Run the {operation.operation_id} operation",
"",
f"Risk: {operation.risk.value}. Targets: {operation.targets.value}.",
"Calling this runs check mode only and records what it would change; "
"Calling this runs it and returns what the hosts reported; nothing "
"changes and no approval is needed."
if operation.risk is RiskLevel.READ
else "Calling this runs check mode only and records what it would change; "
f"a person approves the result with `{APPROVAL_COMMAND}`.",
]
if operation.inputs:
Expand Down
Loading
Loading