Skip to content

Commit 9b21777

Browse files
Merge pull request #16 from kevinbackhouse/yaml-always-recurse
Use globally unique filekeys for referencing other yaml files
2 parents fe79430 + 104a974 commit 9b21777

33 files changed

Lines changed: 270 additions & 114 deletions

.github/workflows/smoketest.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Smoke test - run the examples to check for errors
2+
3+
on:
4+
issue_comment:
5+
types: [created] # Add "smoke test" as a PR comment to trigger this workflow.
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
Linux:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: branch-deploy
15+
id: branch-deploy
16+
uses: github/branch-deploy@48285b12b35e47e2dde0c27d2abb33daa846d98b # v11.0.0
17+
with:
18+
trigger: "smoke test"
19+
reaction: "eyes"
20+
stable_branch: "main"
21+
22+
- name: Setup Python
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: '3.11'
26+
27+
- name: Checkout
28+
uses: actions/checkout@v3
29+
30+
- name: Setup Python venv
31+
run: |
32+
python -m venv .venv
33+
source .venv/bin/activate
34+
python -m pip install -r requirements.txt
35+
36+
- name: Run tests
37+
env:
38+
COPILOT_TOKEN: ${{ secrets.COPILOT_TOKEN }}
39+
run: |
40+
source .venv/bin/activate
41+
python main.py -p GitHubSecurityLab/seclab-taskflow-agent/personalities/assistant 'explain modems to me please'
42+
python main.py -p GitHubSecurityLab/seclab-taskflow-agent/personalities/c_auditer 'explain modems to me please'
43+
python main.py -p GitHubSecurityLab/seclab-taskflow-agent/personalities/examples/echo 'explain modems to me please'
44+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/CVE-2023-2283/CVE-2023-2283
45+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/echo
46+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example
47+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example_globals
48+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example_inputs
49+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example_large_list_result_iter
50+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example_repeat_prompt
51+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example_repeat_prompt_async
52+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example_repeat_prompt_dictionary
53+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example_reusable_prompt
54+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example_reusable_taskflows
55+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/example_triage_taskflow
56+
python main.py -t GitHubSecurityLab/seclab-taskflow-agent/taskflows/examples/single_step_taskflow

README.md

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ Example:
149149

150150
```yaml
151151
# personalities define the system prompt level directives for this Agent
152+
seclab-taskflow-agent:
153+
version: 1
154+
filetype: personality
155+
filekey: personalities/examples/echo
156+
152157
personality: |
153158
You are a simple echo bot. You use echo tools to echo things.
154159
@@ -157,7 +162,7 @@ task: |
157162
158163
# personality toolboxes map to mcp servers made available to this Agent
159164
toolboxes:
160-
- echo
165+
- toolboxes/echo
161166
```
162167
163168
## Toolboxes
@@ -168,6 +173,11 @@ Example stdio config:
168173

169174
```yaml
170175
# stdio mcp server configuration
176+
seclab-taskflow-agent:
177+
version: 1
178+
filetype: toolbox
179+
filekey: toolboxes/echo
180+
171181
server_params:
172182
kind: stdio
173183
command: python
@@ -184,6 +194,11 @@ A sequence of interdependent tasks performed by a set of Agents. Configured thro
184194
Example:
185195

186196
```yaml
197+
seclab-taskflow-agent:
198+
version: 1
199+
filetype: taskflow
200+
filekey: taskflows/examples/example.yaml
201+
187202
taskflow:
188203
- task:
189204
# taskflows can optionally choose any of the support CAPI models for a task
@@ -194,18 +209,14 @@ taskflow:
194209
must_complete: true
195210
# taskflows can set a primary (first entry) and handoff (additional entries) agent
196211
agents:
197-
- c_auditer
198-
- fruit_expert
212+
- personalities/c_auditer.yaml
213+
- personalities/examples/fruit_expert.yaml
199214
user_prompt: |
200215
Store an example vulnerable C program that uses `strcpy` in the
201216
`vulnerable_c_example` memory key and explain why `strcpy`
202217
is insecure in the C programming language. Do this before handing off
203218
to any other agent.
204219

205-
Then provide a summary of a high impact CVE ID that involved a `strcpy`
206-
based buffer overflow based on your GHSA knowledge as an additional
207-
example.
208-
209220
Finally, why are apples and oranges healthy to eat?
210221

211222
# taskflows can set temporary environment variables, these support the general
@@ -217,16 +228,16 @@ taskflow:
217228
MEMCACHE_STATE_DIR: "example_taskflow/"
218229
MEMCACHE_BACKEND: "dictionary_file"
219230
# taskflows can optionally override personality toolboxes, in this example
220-
# kevin normally only has the memcache toolbox, but we extend it here with
231+
# this normally only has the memcache toolbox, but we extend it here with
221232
# the GHSA toolbox
222233
toolboxes:
223-
- ghsa
224-
- memcache
234+
- toolboxes/memcache.yaml
235+
- toolboxes/codeql.yaml
225236
- task:
226237
must_complete: true
227238
model: gpt-4.1
228239
agents:
229-
- c_auditer
240+
- personalities/c_auditer.yaml
230241
user_prompt: |
231242
Retrieve C code for security review from the `vulnerable_c_example`
232243
memory key and perform a review.
@@ -236,13 +247,58 @@ taskflow:
236247
MEMCACHE_STATE_DIR: "example_taskflow/"
237248
MEMCACHE_BACKEND: "dictionary_file"
238249
toolboxes:
239-
- memcache
250+
- toolboxes/memcache.yaml
251+
# headless mode does not prompt for tool call confirms configured for a server
252+
# note: this will auto-allow, if you want control over potentially dangerous
253+
# tool calls, then you should NOT run a task in headless mode (default: false)
254+
headless: true
255+
- task:
256+
# tasks can also run shell scripts that return e.g. json output for repeat prompt iterable
257+
must_complete: true
258+
run: |
259+
echo '["apple", "banana", "orange"]'
260+
- task:
261+
repeat_prompt: true
262+
agents:
263+
- personalities/assistant.yaml
264+
user_prompt: |
265+
What kind of fruit is {{ RESULT }}?
240266
```
241267
242268
Taskflows support [Agent handoffs](https://openai.github.io/openai-agents-python/handoffs/). Handoffs are useful for implementing triage patterns where the primary Agent can decide to handoff a task to any subsequent Agents in the `Agents` list.
243269

244270
See the [taskflow examples](taskflows/examples) for other useful Taskflow patterns such as repeatable and asynchronous templated prompts.
245271

272+
## Notes about the yaml syntax
273+
274+
Every personality, toolbox, and taskflow is defined by a YAML file, which
275+
should always include a header like this:
276+
277+
```
278+
seclab-taskflow-agent:
279+
version: 1
280+
filetype: taskflow
281+
filekey: taskflows/examples/example
282+
```
283+
284+
The "filetype" determines whether the file defines a personality, toolbox, or
285+
taskflow. This means that different types of files can be stored in the same directory.
286+
287+
The "filekey" is a unique name for the file. It is used to allow
288+
cross-referencing between files. For example, a taskflow can reference
289+
a personality by its filekey. Because filekeys are used for
290+
cross-referencing (rather than file paths), it means that you can move
291+
a file to a different directory without breaking the links. This also
292+
means that you can easily import new files by dropping them into a sub-directory.
293+
We recommend including something like your
294+
GitHub "username/reponame" in your filekeys to make them globably unique.
295+
296+
The "version" number in the header should always be 1. It means that the
297+
file uses version 1 of the seclab-taskflow-agent syntax. If we ever need
298+
to make a major change to the syntax, then we'll update the version number.
299+
This will hopefully enable us to make changes without breaking backwards
300+
compatibility.
301+
246302
## License
247303

248304
This project is licensed under the terms of the MIT open source license. Please refer to the [LICENSE](./LICENSE) file for the full terms.

available_tools.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,18 @@
33
class VersionException(Exception):
44
pass
55

6+
class FileIDException(Exception):
7+
pass
8+
69
class FileTypeException(Exception):
710
pass
811

12+
def add_yaml_to_dict(table, key, yaml):
13+
"""Add the yaml to the table, but raise an error if the id isn't unique """
14+
if key in table:
15+
raise FileIDException(str(key))
16+
table.update({key: yaml})
17+
918
class AvailableTools:
1019
"""
1120
This class is used for storing dictionaries of all the available
@@ -30,20 +39,23 @@ def __init__(self, yamls: dict):
3039
version = header['version']
3140
if version != 1:
3241
raise VersionException(str(version))
33-
filetype = header['type']
42+
filekey = header['filekey']
43+
filetype = header['filetype']
3444
if filetype == 'personality':
35-
self.personalities.update({path: yaml})
45+
add_yaml_to_dict(self.personalities, filekey, yaml)
3646
elif filetype == 'taskflow':
37-
self.taskflows.update({path: yaml})
47+
add_yaml_to_dict(self.taskflows, filekey, yaml)
3848
elif filetype == 'prompt':
39-
self.prompts.update({path: yaml})
49+
add_yaml_to_dict(self.prompts, filekey, yaml)
4050
elif filetype == 'toolbox':
41-
self.toolboxes.update({path: yaml})
51+
add_yaml_to_dict(self.toolboxes, filekey, yaml)
4252
else:
4353
raise FileTypeException(str(filetype))
4454
except KeyError as err:
4555
logging.error(f'{path} does not contain the key {err.args[0]}')
4656
except VersionException as err:
4757
logging.error(f'{path}: seclab-taskflow-agent version {err.args[0]} is not supported')
58+
except FileIDException as err:
59+
logging.error(f'{path}: file ID {err.args[0]} is not unique')
4860
except FileTypeException as err:
4961
logging.error(f'{path}: seclab-taskflow-agent file type {err.args[0]} is not supported')

main.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import re
1111
import json
1212
import uuid
13+
import pathlib
1314

1415
from agent import DEFAULT_MODEL, TaskRunHooks, TaskAgentHooks
1516
#from agents.run import DEFAULT_MAX_TURNS # XXX: this is 10, we need more than that
@@ -23,7 +24,7 @@
2324
from typing import Any
2425

2526
from shell_utils import shell_tool_call
26-
from mcp_utils import DEFAULT_MCP_CLIENT_SESSION_TIMEOUT, ReconnectingMCPServerStdio, AsyncDebugMCPServerStdio, MCPNamespaceWrap, mcp_client_params, mcp_system_prompt, StreamableMCPThread
27+
from mcp_utils import DEFAULT_MCP_CLIENT_SESSION_TIMEOUT, ReconnectingMCPServerStdio, AsyncDebugMCPServerStdio, MCPNamespaceWrap, mcp_client_params, mcp_system_prompt, StreamableMCPThread, compress_name
2728
from render_utils import render_model_output, flush_async_output
2829
from env_utils import TmpEnv
2930
from yaml_parser import YamlParser
@@ -255,7 +256,7 @@ async def mcp_session_task(
255256
for handoff_agent in list(agents.keys())[1:]:
256257
handoffs.append(TaskAgent(
257258
# XXX: name has to be descriptive for an effective handoff
258-
name=handoff_agent,
259+
name=compress_name(handoff_agent),
259260
instructions=prompt_with_handoff_instructions(
260261
mcp_system_prompt(
261262
agents[handoff_agent]['personality'],
@@ -399,7 +400,7 @@ async def on_handoff_hook(
399400
if p:
400401
personality = available_tools.personalities.get(p)
401402
if personality is None:
402-
raise ValueError("No such personality!")
403+
raise ValueError(f"No such personality: {p}")
403404

404405
await deploy_task_agents(
405406
available_tools,
@@ -413,7 +414,7 @@ async def on_handoff_hook(
413414

414415
taskflow = available_tools.taskflows.get(t)
415416
if taskflow is None:
416-
raise ValueError("No such taskflow!")
417+
raise ValueError(f"No such taskflow: {t}")
417418

418419
await render_model_output(f"** 🤖💪 Running Task Flow: {t}\n")
419420

@@ -628,11 +629,12 @@ async def _deploy_task_agents(resolved_agents, prompt):
628629
break
629630

630631
if __name__ == '__main__':
632+
cwd = pathlib.Path.cwd()
631633
available_tools = AvailableTools(
632-
YamlParser('personalities').get_yaml_dict() |
633-
YamlParser('taskflows').get_yaml_dict() |
634-
YamlParser('prompts').get_yaml_dict(dir_namespace=True) |
635-
YamlParser('toolboxes').get_yaml_dict(recurse=True))
634+
YamlParser(cwd).get_yaml_dict((cwd/'personalities').rglob('*')) |
635+
YamlParser(cwd).get_yaml_dict((cwd/'taskflows').rglob('*')) |
636+
YamlParser(cwd).get_yaml_dict((cwd/'prompts').rglob('*')) |
637+
YamlParser(cwd).get_yaml_dict((cwd/'toolboxes').rglob('*')))
636638

637639
p, t, l, user_prompt, help_msg = parse_prompt_args(available_tools)
638640

mcp_utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os
1010
import socket
1111
import signal
12+
import hashlib
1213
from urllib.parse import urlparse
1314

1415
from mcp.types import CallToolResult, TextContent
@@ -18,6 +19,16 @@
1819

1920
DEFAULT_MCP_CLIENT_SESSION_TIMEOUT = 120
2021

22+
# The openai API complains if the name of a tool is longer than 64
23+
# chars. But we're encouraging people to use long descriptive
24+
# filekeys to avoid accidental collisions, so it's very easy to go
25+
# over the limit. So this function converts a name to a 12 character
26+
# hash.
27+
def compress_name(name):
28+
m = hashlib.sha256()
29+
m.update(name.encode('utf-8'))
30+
return m.hexdigest()[:12]
31+
2132
# A process management class for running in-process MCP streamable servers
2233
class StreamableMCPThread(Thread):
2334
"""Process management for local streamable MCP servers"""
@@ -221,7 +232,7 @@ class MCPNamespaceWrap:
221232
def __init__(self, confirms, obj):
222233
self.confirms = confirms
223234
self._obj = obj
224-
self.namespace = f"{obj.name.upper().replace(' ', '_')}_"
235+
self.namespace = compress_name(obj.name)
225236

226237
def __getattr__(self, name):
227238
attr = getattr(self._obj, name)

personalities/assistant.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
seclab-taskflow-agent:
2-
type: personality
32
version: 1
3+
filetype: personality
4+
filekey: GitHubSecurityLab/seclab-taskflow-agent/personalities/assistant
45

56
personality: |
67
You are a helpful assistant.

personalities/c_auditer.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
seclab-taskflow-agent:
2-
type: personality
32
version: 1
3+
filetype: personality
4+
filekey: GitHubSecurityLab/seclab-taskflow-agent/personalities/c_auditer
45

56
personality: |
67
Your name is Ronald. You are a C programming language security expert.
@@ -14,5 +15,5 @@ task: |
1415
your findings where possible.
1516
1617
toolboxes:
17-
- memcache
18-
- codeql
18+
- GitHubSecurityLab/seclab-taskflow-agent/toolboxes/memcache
19+
- GitHubSecurityLab/seclab-taskflow-agent/toolboxes/codeql

personalities/examples/apple_expert.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
seclab-taskflow-agent:
2-
type: personality
32
version: 1
3+
filetype: personality
4+
filekey: GitHubSecurityLab/seclab-taskflow-agent/personalities/examples/apple_expert
45

56
personality: |
67
You are an apples expert.

personalities/examples/banana_expert.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
seclab-taskflow-agent:
2-
type: personality
32
version: 1
3+
filetype: personality
4+
filekey: GitHubSecurityLab/seclab-taskflow-agent/personalities/examples/banana_expert
45

56
personality: |
67
You are a bananas expert.

0 commit comments

Comments
 (0)