Skip to content

Commit cb367d9

Browse files
Merge pull request #28 from cloudfall-dev/feat/agent-investigate
Let a model investigate an alert through the operations catalog
2 parents 59abe10 + d4ca6a6 commit cb367d9

7 files changed

Lines changed: 1530 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@ Notable changes to Cloudfall. The format follows
88

99
### Added
1010

11+
- `cloudfall agent investigate --alert TEXT --base-url URL --model NAME`
12+
lets a model work an alert through the operations catalog, over any
13+
OpenAI-compatible endpoint (the API key from `--api-key-from-env`,
14+
`--api-key-from-file` or `CLOUDFALL_API_KEY`). The model sees the same
15+
operation tools `mcp serve --repository` gives any client: a read
16+
operation runs and returns what the hosts reported, any other operation is
17+
proposed in check mode and recorded as a decision, and no tool approves
18+
anything. A call repeated with the same arguments is answered from the
19+
first result instead of running again. The investigation is recorded in
20+
`investigations/` as an `AgentInvestigation`: the alert, the model, the
21+
tokens, every call and how it ended, and the finding, with any decision id
22+
the model named but never recorded kept apart under `unrecorded`. Exit 92
23+
`MODEL_UNAVAILABLE` when the endpoint refuses, exit 93
24+
`INVESTIGATION_INCOMPLETE` with the record when the model runs out of
25+
`--max-turns` or does not answer in the asked-for shape. Proven on a
26+
disposable host with Nemotron 3 Super on Nebius Token Factory: a full disk
27+
behind a PostgreSQL alert was traced to unrotated application logs in five
28+
turns, and the two proposals it recorded, once approved, brought the
29+
database back
30+
- An operation tool of `mcp serve --repository` describes a read operation
31+
as running and returning what the hosts reported, instead of saying it
32+
runs check mode only
1133
- `operations propose` returns a read operation's play output in
1234
`data.output`, so an agent acts on what the operation reported without
1335
opening the decision's `.diff` file; the record keeps the file as before.
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://cloudfall.internal/schemas/v1/agent-investigation.schema.json",
4+
"title": "Cloudfall agent investigation",
5+
"description": "One alert worked by one model through the operations catalog: every call, the tokens, and the finding.",
6+
"type": "object",
7+
"properties": {
8+
"apiVersion": {
9+
"$ref": "common.schema.json#/$defs/apiVersion"
10+
},
11+
"kind": {
12+
"const": "AgentInvestigation"
13+
},
14+
"metadata": {
15+
"$ref": "common.schema.json#/$defs/metadata"
16+
},
17+
"spec": {
18+
"type": "object",
19+
"properties": {
20+
"alert": {
21+
"type": "string",
22+
"minLength": 1,
23+
"maxLength": 4000
24+
},
25+
"model": {
26+
"type": "object",
27+
"properties": {
28+
"endpoint": {
29+
"type": "string",
30+
"pattern": "^https?://",
31+
"maxLength": 512
32+
},
33+
"name": {
34+
"type": "string",
35+
"minLength": 1,
36+
"maxLength": 256
37+
}
38+
},
39+
"required": [
40+
"endpoint",
41+
"name"
42+
],
43+
"additionalProperties": false
44+
},
45+
"startedAt": {
46+
"type": "string",
47+
"format": "date-time"
48+
},
49+
"finishedAt": {
50+
"type": "string",
51+
"format": "date-time"
52+
},
53+
"status": {
54+
"enum": [
55+
"answered",
56+
"unstructured",
57+
"turn-limit"
58+
]
59+
},
60+
"turns": {
61+
"type": "integer",
62+
"minimum": 0
63+
},
64+
"tokens": {
65+
"type": "object",
66+
"properties": {
67+
"prompt": {
68+
"type": "integer",
69+
"minimum": 0
70+
},
71+
"completion": {
72+
"type": "integer",
73+
"minimum": 0
74+
}
75+
},
76+
"required": [
77+
"prompt",
78+
"completion"
79+
],
80+
"additionalProperties": false
81+
},
82+
"steps": {
83+
"type": "array",
84+
"items": {
85+
"type": "object",
86+
"properties": {
87+
"tool": {
88+
"type": "string",
89+
"maxLength": 128
90+
},
91+
"arguments": {
92+
"type": "object"
93+
},
94+
"outcome": {
95+
"enum": [
96+
"ran",
97+
"proposed",
98+
"failed",
99+
"invalid",
100+
"repeated",
101+
"unknown-tool"
102+
]
103+
},
104+
"decision": {
105+
"$ref": "common.schema.json#/$defs/resourceId"
106+
}
107+
},
108+
"required": [
109+
"tool",
110+
"arguments",
111+
"outcome"
112+
],
113+
"additionalProperties": false
114+
}
115+
},
116+
"answer": {
117+
"type": "string"
118+
},
119+
"finding": {
120+
"type": "object",
121+
"properties": {
122+
"rootCause": {
123+
"type": "string"
124+
},
125+
"proposed": {
126+
"type": "array",
127+
"items": {
128+
"$ref": "common.schema.json#/$defs/resourceId"
129+
}
130+
},
131+
"unrecorded": {
132+
"type": "array",
133+
"items": {
134+
"type": "string"
135+
}
136+
},
137+
"why": {
138+
"type": "string"
139+
}
140+
},
141+
"required": [
142+
"rootCause",
143+
"proposed",
144+
"unrecorded",
145+
"why"
146+
],
147+
"additionalProperties": false
148+
}
149+
},
150+
"required": [
151+
"alert",
152+
"model",
153+
"startedAt",
154+
"finishedAt",
155+
"status",
156+
"turns",
157+
"tokens",
158+
"steps",
159+
"answer"
160+
],
161+
"additionalProperties": false
162+
}
163+
},
164+
"required": [
165+
"apiVersion",
166+
"kind",
167+
"metadata",
168+
"spec"
169+
],
170+
"additionalProperties": false
171+
}

‎sdk/src/cloudfall/agent_server.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
InventorySource,
3232
inventory_from_config,
3333
)
34-
from cloudfall.catalog import CATALOG_DIRECTORY
34+
from cloudfall.catalog import CATALOG_DIRECTORY, RiskLevel
3535
from cloudfall.decision import DECISION_DIRECTORY
3636
from cloudfall.project import (
3737
PROJECT_DIRECTORY_VARIABLE,
@@ -260,7 +260,10 @@ def operation_tool_description(operation: Operation) -> str:
260260
else f"Run the {operation.operation_id} operation",
261261
"",
262262
f"Risk: {operation.risk.value}. Targets: {operation.targets.value}.",
263-
"Calling this runs check mode only and records what it would change; "
263+
"Calling this runs it and returns what the hosts reported; nothing "
264+
"changes and no approval is needed."
265+
if operation.risk is RiskLevel.READ
266+
else "Calling this runs check mode only and records what it would change; "
264267
f"a person approves the result with `{APPROVAL_COMMAND}`.",
265268
]
266269
if operation.inputs:

0 commit comments

Comments
 (0)