From snowglobe-skills
End-to-end onboarding for a user who has never used Snowglobe before. Installs the `snowglobe` pip package, authenticates the CLI (skipping if already authed), registers the agent (POST to the Snowglobe API for new agents, or `snowglobe-connect init` to wire up existing ones), then creates the starter wrapper file via `init`. Scans the project to draft a chatbot description, augments the wrapper with `@snowglobe_tool` decorated functions, a `tool_defs()` function, `TOOLS_MAP`, and a tool-call loop in `completion()`, verifies with `snowglobe-connect test`, starts the wrapper with `snowglobe-connect start`, and hands the user a dashboard deep link. Trigger when the user says "set up snowglobe for my project", "bootstrap my snowglobe agent", "get started with snowglobe", "create a snowglobe agent", "connect my chatbot to snowglobe", "scaffold my agent", "add tools to my agent", or any similar bootstrap / onboarding request.
How this skill is triggered — by the user, by Claude, or both
Slash command
/snowglobe-skills:bootstrap-snowglobe-agentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Your job is to bootstrap a Snowglobe agent for the user end-to-end. By the end, the user has:
Your job is to bootstrap a Snowglobe agent for the user end-to-end. By the end, the user has:
snowglobe Python package installed and the snowglobe-connect CLI on their PATH.snowglobe/config.rc).snowglobe/agents.json mapping the
wrapper file to its UUID — both produced by snowglobe-connect init@snowglobe_tool, tool_defs(),
TOOLS_MAP, full completion() loop)https://guardrailsai.com/snowglobe/app/agents/<UUID>You may be operating in either of two modes:
init creates the
starter wrapper + writes agents.json. You then augment the wrapper with tools in Step 8.agents.json entry. Skip
init (Step 3) and go straight to augmenting it.Detect which mode you're in at the start of Step 3.
The bootstrap touches the user's filesystem, their pip environment, their browser (for auth), and their Snowglobe dashboard. Before running anything, print a short, numbered roadmap so the user can see what's coming. This is for visibility, not approval — once you've printed the plan, immediately start Step 1. Don't wait for the user to say "go" or "ok"; their invoking the skill is the consent.
A good preamble for a from-scratch run looks like:
Here's the plan:
- Install
snowglobe(pip) — needed for the CLI- Authenticate with
snowglobe-connect auth— opens a browser briefly (skipping if you're already authed)- Register the agent — I'll ask whether you want a new agent (created via the Snowglobe API) or an existing one from your dashboard, then run
snowglobe-connect initto wire it up locally- Scan your project to find existing tools, system prompts, and LLM client
- Draft a chatbot description for your dashboard (I'll ask about gaps; feel free to skip)
- Augment the wrapper file with
@snowglobe_tooldecorators,tool_defs(),TOOLS_MAP, and a tool-call loop- Verify with
snowglobe-connect test, then hand you the deep link to launch a simulationStarting now.
If you've already detected transforming-existing mode (existing wrapper file + matching
agents.json entry), trim the plan accordingly — drop Step 3 and frame Step 6 as "augmenting
your existing wrapper at <path>." If snowglobe is already installed and the user is already
authed, drop those too.
Between steps, give a one-line update before running the command so the user knows where you are in the plan. Examples:
initsnowglobe_agent.py with tool_defs, @snowglobe_tool, TOOLS_MAP, and the tool-call loop in completion()."Brief is better than verbose — one line per step is plenty. The goal is that the user can follow the bootstrap as it runs without having to inspect each tool call.
If a step requires a user answer (the new-vs-existing question in Step 3, the description gap-fill in Step 6), stop the turn after asking. Don't run other tools, don't scan files, don't try to "fill the wait" with parallel work. Send only the question and end the turn.
Concretely: never produce a message that contains both a question and a tool call (or an announcement of one). Don't say "While you answer, I'll scan the project for context" — that splits the user's attention and means you'll rework the scan if their answer changes anything. The flow is strictly sequential: ask → user answers → resume with the next step.
This applies to every user-facing question in the skill. Single in-flight question per turn, no side work.
Before anything else, make sure the snowglobe Python package is installed and the
snowglobe-connect CLI is available.
Detect first:
snowglobe-connect --help
If that succeeds, the CLI is installed — skip to Step 2.
If not installed, announce what you're doing in one short sentence ("Installing the
snowglobe package…") and run the install without asking permission first. The user
invoked this skill to set up Snowglobe; that is the consent.
pip install snowglobe
(If the project uses uv, poetry, or another tool, prefer that. E.g. uv pip install snowglobe,
poetry add snowglobe. Detect by checking for pyproject.toml + lockfile, or uv.lock.)
Verify the install by running snowglobe-connect --help again. If it still isn't on PATH after a
successful install, the user's shell may need to re-source — tell them to open a new terminal or
run hash -r (zsh/bash) and try again.
The user needs an authenticated Snowglobe CLI session before init can register an agent.
Detect existing auth first. Skip this step entirely if any of these is true:
.snowglobe/config.rc exists and contains a non-empty SNOWGLOBE_API_KEY=... lineSNOWGLOBE_API_KEY environment variable is set in the user's shellTo check the file:
test -f .snowglobe/config.rc && grep -q '^SNOWGLOBE_API_KEY=..*' .snowglobe/config.rc && echo authed
To check the env var:
[ -n "$SNOWGLOBE_API_KEY" ] && echo authed
If either reports authed, mention "already authenticated, skipping auth" and continue to Step 3.
If not authed, announce what you're doing in one short sentence ("Running
snowglobe-connect auth — a browser will open for OAuth…") and run the command without
asking permission first. The user invoked this skill to set up Snowglobe; that is the consent.
snowglobe-connect auth
This creates .snowglobe/config.rc containing SNOWGLOBE_API_KEY=....
If auth fails (e.g. headless environment, browser unavailable, callback timeout), then fall
back to telling the user to set the API-key env var manually:
export SNOWGLOBE_API_KEY=<key_from_dashboard>
…and re-run the skill. Don't preemptively offer the env-var path before attempting auth.
After auth, verify .snowglobe/config.rc exists (or the env var is set) before moving on.
init for existing)The agent has to be registered in the Snowglobe dashboard before init can wire up the local
wrapper file. snowglobe-connect init only picks from existing dashboard agents — it cannot
create a new one from the CLI. So the flow forks:
init with the new name to
wire it up locally.init with the name piped to its stdin.Both paths converge on init to produce the starter wrapper file (snowglobe_agent.py) and
the .snowglobe/agents.json entry.
Detect skip condition first. Skip this step entirely if a wrapper file already exists with
a matching agents.json entry — the user is in transforming-existing mode and just wants tools
added.
test -f .snowglobe/agents.json && \
python -c "import json; d=json.load(open('.snowglobe/agents.json')); print(any((__import__('os').path.exists(k) for k in d)))"
If a wrapper + entry exist, set the target file path from the matching agents.json key and
proceed to Step 4. (If multiple wrappers are listed, ask the user which one to augment.)
Otherwise, ask the scope question. End the turn with only this question — no scanning, drafting, or other tool calls until the user answers:
"Are you creating a new Snowglobe agent for this project, or wiring up to an existing one in your dashboard?"
init can't create new apps, so create the agent via the public API first. Pick a sensible
default name from the project (the package's directory name in kebab-case — e.g.
customer-support-agent). Confirm with the user in one line and end the turn:
"I'll create it as
customer-support-agentand use 🤖 as the icon — sound good?"
After they confirm (or counter-suggest), do this in two ordered steps — the GET must come
before the POST. The agent-create endpoint requires x-snowglobe-org-id in addition to
x-api-key, and the org id isn't in config.rc.
Step 3a — fetch the org id (must run before the POST):
GET /api/users/me returns {"id": "<user-id>", "organizations": [{"id": "<org-id>"}, ...]}.
Take the first org's id:
KEY=${SNOWGLOBE_API_KEY:-$(grep -m1 '^SNOWGLOBE_API_KEY=' .snowglobe/config.rc | cut -d= -f2)}
ORG_ID=$(curl -sS https://api.snowglobe.guardrailsai.com/api/users/me \
-H "x-api-key: $KEY" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print((d.get('organizations') or [{}])[0].get('id',''))")
# Validate before POSTing — if empty, the user has no org and the POST will fail anyway
if [ -z "$ORG_ID" ]; then
echo "no organization on this account" >&2
exit 1
fi
If $ORG_ID is empty, the user's account isn't in any organization. Stop and tell them:
"Your account isn't in a Snowglobe organization yet. Add yourself to one in the dashboard at
https://guardrailsai.com/snowglobe/app, then re-run me." Don't proceed to the POST and don't
guess an org id.
Step 3b — only now POST the agent:
curl -sS -X POST https://api.snowglobe.guardrailsai.com/api/agents \
-H "x-api-key: $KEY" \
-H "x-snowglobe-org-id: $ORG_ID" \
-H "Content-Type: application/json" \
-d '{"name": "customer-support-agent", "icon": "🤖"}'
The endpoint, schema, and auth headers are documented at
/Users/zaydsimjee/workspace/threat-tester-demo/threat-tester-control-plane/src/sdk-spec.json
under AgentCreateSchema. Required body fields are name (string) and icon (string — any
short emoji works). Description is optional.
The response is a JSON Agent object with an id field — that's the UUID. Capture it; you'll
also use the agent's name when running init next.
If the POST fails (4xx/5xx), surface the error to the user and stop. Common failures:
User must belong to an organization! — you skipped Step 3a or the x-snowglobe-org-id
header didn't get set. Re-run the GET and confirm $ORG_ID is non-empty before retrying.422 — validation error (name conflict, missing icon). Show the error message verbatim.401/403 — auth failed. Likely a stale .snowglobe/config.rc; ask the user to re-run
snowglobe-connect auth.499 — quota exceeded. The user has too many apps; they'll need to delete some.After the POST succeeds, run init with the new agent's name piped to stdin so it picks the
just-created app from the (now-updated) dashboard list:
printf '%s\n' "customer-support-agent" | snowglobe-connect init
If init asks additional prompts (file path, etc.), extend the payload with \n-separated
answers. Empty lines accept defaults.
Ask in one line and end the turn:
"What's the agent called in your dashboard? A name or fragment is enough — I'll match it."
Once you have a name, run init with it piped:
printf '%s\n' "<name-or-fragment>" | snowglobe-connect init
If init errors with "no match" or "ambiguous," surface the error and ask them to
disambiguate. Don't fall back to "show me the list" — at 100+ apps that's useless.
init completesRead .snowglobe/agents.json to learn the wrapper file path it created (the most recent
entry) — that path is your target file for Step 8. Note the UUID too; you'll use it for the
deep link in Step 9.
If init fails for any reason, surface the error clearly and stop — don't try to substitute a
manual flow (asking the user for a UUID, hand-writing agents.json, scaffolding a wrapper
file from scratch). The whole point of using init is to avoid those substitutions.
Scan the working directory for existing chat/agent application code. This grounds every later step — domain vocabulary, tool implementations, system prompts, LLM client choice, model name, and the description you draft for the dashboard.
Run something like:
find . -name "*.py" | head -40
Read any files that look like chat or agent apps. Cast a wide net — the project may use any framework or LLM provider. Signals to look for:
App / server patterns (any of these count):
flask, fastapi, django, aiohttp,
starlette, sanic, plain http.server, etc.)Tool / function definitions (any format counts):
TOOLS list ("type": "function" entries)@tool or StructuredTool definitions"name", "input_schema")LLM client setup (any provider counts):
openai, litellm, anthropic, cohere, mistral, groq, together, ollama,
google.generativeai, boto3 (Bedrock), azure, huggingface_hub, etc."gpt-4o-mini", "claude-opus-4-7", "mistral-large")Description signals:
Lift names, schemas, and implementations from the project rather than inventing placeholders.
If the project already has an LLM client/model the user is committed to, prefer it over the
default init template.
If the user named tools they want, use those. If the project has tool-like functions (per Step 4), lift their names and signatures.
If no tools are specified and none can be lifted, scaffold with a single placeholder named
example_tool, clearly marked # TODO, so the user has a pattern to follow.
For each tool you'll scaffold, you need three things by Step 8:
TOOLS list (with parameters, returns, and examples)@snowglobe_tool decorated implementation functionTOOLS_MAPSnowglobe uses the chatbot description to generate realistic personas and scenarios for simulation, so the description quality directly drives test quality. Per the Snowglobe description guide, a good description naturally answers four questions:
Style: 2–3 sentences is enough to start. Specific but not exhaustive. User-value language, not technical jargon. Don't include conversation examples — the dashboard has a separate slot for historical data.
Use what you found in Step 4 to draft each of the four answers:
For any of the four answers you can't confidently fill in from code, ask the user — but tell them up front they can skip any question and you'll fall back to your inference. Frame it like:
"I drafted a description from your code. I'm confident on functionality and scope, but wanted to check two things — feel free to skip either:
- Audience: I assumed
<X>. Anyone specific you have in mind?- Boundaries: anything the bot is not supposed to handle? (escalations, refused topics)"
Keep the final description to 2–6 sentences. Don't list every tool — describe capabilities at the level of user value.
You will present this description to the user in Step 9 and ask them to paste it into the Snowglobe dashboard.
Before augmenting the wrapper file, read the actual implementation of each tool you identified
in Step 5. Snowglobe uses the description, returns, and examples fields to mock realistic
tool responses during simulation, so they need to encode behavior — not just interface shape.
For each tool:
Read the full function body. If the implementation lives in a separate file (e.g. api.py),
read that file. Don't skim — look at every branch, every return, every data structure built.
Extract:
status
is always "confirmed" / "pending" / "cancelled")"FL-1234", price ranges){'error': ...}
when the flight number doesn't exist")Rewrite the description field so a simulator reading only the description could
produce plausible mocks. Plain prose, no JSON inside the string.
Good:
"Look up a flight by flight number and return its current status and seat availability. Returns a dict with keys:
flight_number(string, e.g. 'FL-101'),origin(IATA code),destination(IATA code),departure_time(ISO 8601),status(one of 'on_time', 'delayed', 'cancelled'),available_seats(integer ≥ 0). If the flight number isn't found, returns{'error': 'Flight not found'}. Seat counts typically range 0–30."
Poor:
"Look up a flight."
Update examples to use values drawn from the implementation (hardcoded data, enum
values, real ID formats).
If no real implementation exists (truly a stub), note in the description: "Mock implementation — replace with real behavior before deploying." Don't invent implementation details that aren't in the code.
Open the wrapper file (created by init in Step 3, or pre-existing in transforming mode) and
add tools to it in place. Preserve everything init already put there: the imports, the LLM
client setup, and the existing completion() function.
What you're adding/modifying:
from snowglobe.tools import snowglobe_tool (and import json if missing)TOOLS list: full specs with parameters, returns, and examples (Step 7)tool_defs() function: returns TOOLS. The runner introspects it at startup. Don't
call register_tools() yourself — the runner does that internally.@snowglobe_tool decorated implementations: one per tool, keyword-only args, dict returnTOOLS_MAP: string name → function, for the dispatch loopcompletion() body: replace the basic LLM call with a tool-call loop that uses TOOLS
in the request and dispatches via TOOLS_MAP when the model returns tool calls. Preserve
the system prompt and LLM client/model that init (or the user) chose.The structure should look like this:
# Imports — preserve existing; add snowglobe.tools
from snowglobe.client import CompletionRequest, CompletionFunctionOutputs
from snowglobe.tools import snowglobe_tool
import json
# ... existing imports (openai, litellm, etc.) ...
TOOLS = [
{
"type": "function",
"function": {
"name": "<tool_name>",
"description": "<enriched description from Step 7>",
"parameters": {
"type": "object",
"description": "<overall description>",
"properties": {
"<param>": {"type": "<json_type>", "description": "<...>"}
},
"required": ["<required_params>"]
},
"returns": {
"type": "object",
"description": "<return description>",
"properties": { ... }
},
"examples": [{"input": { ... }, "output": { ... }}]
}
},
# ... more tools ...
]
# tool_defs() — REQUIRED. Runner introspects this name at startup.
def tool_defs():
return TOOLS
@snowglobe_tool
def <tool_name>(*, <param>: <type>, ...) -> dict:
# TODO: replace with real implementation
return { ... } # mock response matching the "returns" schema
TOOLS_MAP = {
"<tool_name>": <tool_function>,
}
def completion(request: CompletionRequest) -> CompletionFunctionOutputs:
system_prompt = "..." # preserve existing or add placeholder
messages = request.to_openai_messages(system_prompt=system_prompt)
while True:
response = <client>.chat.completions.create(
model="<model>",
messages=messages,
tools=TOOLS,
)
message = response.choices[0].message
if not message.tool_calls:
return CompletionFunctionOutputs(response=message.content)
messages.append(message)
for tool_call in message.tool_calls:
if tool_call.function.name not in TOOLS_MAP:
result = f"Error: Tool {tool_call.function.name} not found"
else:
result = TOOLS_MAP[tool_call.function.name](
**json.loads(tool_call.function.arguments)
)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result),
})
from snowglobe.client import CompletionRequest, CompletionFunctionOutputs
from snowglobe.tools import snowglobe_tool
from openai import OpenAI
import json
import os
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
TOOLS = [
{
"type": "function",
"function": {
"name": "get_menu",
"description": "Return the current pizza menu, optionally filtered by category. "
"Returns {'items': [{'name': str, 'price': float}, ...]}. Categories "
"are 'pizza', 'sides', 'drinks', or 'desserts'. If category is empty, "
"returns all items.",
"parameters": {
"type": "object",
"description": "Parameters for fetching the menu",
"properties": {
"category": {
"type": "string",
"description": "Optional category filter: 'pizza', 'sides', 'drinks', or 'desserts'"
}
},
"required": []
},
"returns": {
"type": "object",
"description": "Menu items grouped by category",
"properties": {
"items": {"type": "array", "description": "List of menu items"}
}
},
"examples": [{
"input": {"category": "pizza"},
"output": {"items": [
{"name": "Margherita", "price": 12.99},
{"name": "Pepperoni", "price": 14.99}
]}
}]
}
},
{
"type": "function",
"function": {
"name": "place_order",
"description": "Place a pizza order and return a confirmation. Returns "
"{'order_id': str (e.g. 'ORD-001'), 'estimated_time': int (minutes), "
"'total': float}. Payment methods are 'card', 'cash', or 'online'.",
"parameters": {
"type": "object",
"description": "Order details",
"properties": {
"customer_name": {"type": "string", "description": "Full name of the customer"},
"items": {"type": "array", "description": "List of {name, quantity} dicts"},
"delivery_address": {"type": "string", "description": "Delivery address"},
"payment_method": {"type": "string", "description": "'card', 'cash', or 'online'"}
},
"required": ["customer_name", "items", "delivery_address", "payment_method"]
},
"returns": {
"type": "object",
"description": "Order confirmation details",
"properties": {
"order_id": {"type": "string", "description": "Unique order identifier"},
"estimated_time": {"type": "integer", "description": "Estimated delivery time in minutes"},
"total": {"type": "number", "description": "Total order cost"}
}
},
"examples": [{
"input": {
"customer_name": "Jane Smith",
"items": [{"name": "Pepperoni", "quantity": 1}],
"delivery_address": "123 Main St",
"payment_method": "card"
},
"output": {"order_id": "ORD-001", "estimated_time": 30, "total": 14.99}
}]
}
}
]
def tool_defs():
return TOOLS
@snowglobe_tool
def get_menu(*, category: str = "") -> dict:
# TODO: replace with real implementation
return {"items": [
{"name": "Margherita", "price": 12.99},
{"name": "Pepperoni", "price": 14.99},
]}
@snowglobe_tool
def place_order(*, customer_name: str, items: list, delivery_address: str, payment_method: str) -> dict:
# TODO: replace with real implementation
return {"order_id": "ORD-001", "estimated_time": 30, "total": 14.99}
TOOLS_MAP = {
"get_menu": get_menu,
"place_order": place_order,
}
SYSTEM_PROMPT = "You are a helpful pizza ordering assistant. Help customers browse the menu and place orders."
def completion(request: CompletionRequest) -> CompletionFunctionOutputs:
messages = request.to_openai_messages(system_prompt=SYSTEM_PROMPT)
while True:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
)
message = response.choices[0].message
if not message.tool_calls:
return CompletionFunctionOutputs(response=message.content)
messages.append(message)
for tool_call in message.tool_calls:
if tool_call.function.name not in TOOLS_MAP:
result = f"Error: Tool {tool_call.function.name} not found"
else:
result = TOOLS_MAP[tool_call.function.name](
**json.loads(tool_call.function.arguments)
)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result),
})
@snowglobe_tool functions"type": "string" → str"type": "integer" → int"type": "number" → float"type": "boolean" → bool"type": "array" → list (or list[dict] if items are objects)"type": "object" → dictrequired) → add = "" or = None defaultAll parameters must be keyword-only (after *).
TOOLS spec rulesEvery entry must have:
"type": "function" wrapper"name" and "description" inside "function""parameters" with "type": "object", "properties", and "required" (empty list OK)"returns" block describing the return shape — Snowglobe uses this to generate mock outputs"examples" entry with realistic "input" and "output"Tell the user the bootstrap is complete and walk them through verify → start → launch.
snowglobe-connect test <wrapper-file-path>
This sends a minimal request through completion() and checks the response shape. If it fails:
TOOLS_MAP keys match the name fields in your TOOLS specsOPENAI_API_KEY (or other provider key) → tell them to export itsnowglobe is installed and the file imports run cleanlyAfter test passes, the user runs snowglobe-connect start to bring the wrapper up. This is
what makes the agent reachable to the Snowglobe control plane during a simulation. It's a
long-running command — leave it running in a terminal.
snowglobe-connect start
Tell the user explicitly: the simulation won't run unless this process is up. They should keep it running in one terminal while they launch a simulation from the dashboard.
Tell the user:
What was created/changed:
snowglobe package installed (or already present).snowglobe/config.rc (from auth, or already present)# TODO markers flagged).snowglobe/agents.json entry (from init)The drafted chatbot description. Show it as a quoted block and tell them to paste it into the dashboard at the agent URL (next bullet).
Ask them to double-check the wrapper works — particularly the tool implementations you stubbed. Recommend a quick smoke run if they have a real provider key.
Remind them to start the wrapper with snowglobe-connect start before launching a
simulation, and to keep that terminal open.
The deep link to launch a simulation: read the UUID from .snowglobe/agents.json for
the wrapper file you just augmented.
Once
snowglobe-connect startis running, open your agent here to launch a simulation: https://guardrailsai.com/snowglobe/app/agents/\<UUID from agents.json>
Assumptions you made — preserved LLM client, model string, any inferred tool names/types — and invite corrections.
init chose one; preserve it.register_tools() from the wrapper file. The runner does that internally; the
user just defines def tool_defs() and the runner finds it by introspection..snowglobe/agents.json,
keyed by file path; the runner reads it from there.CompletionFunctionOutputs(content=...) — the field is named response.POST /api/agents, for existing via init matching by name.snowglobe-connect init — init only picks from
existing dashboard agents. New agents must be created via POST https://api.snowglobe.guardrailsai.com/api/agents
first (headers: x-api-key: $SNOWGLOBE_API_KEY and x-snowglobe-org-id: <org-id>,
body {"name": "...", "icon": "..."})./api/agents without the x-snowglobe-org-id header. The API rejects creates
without it ("User must belong to an organization!"). Fetch the org id first via
GET /api/users/me (returns {"organizations": [{"id": "..."}]}); take the first org's id.
This GET must complete and return a non-empty org id BEFORE the POST runs — never POST first
and try to "fix" by adding the header on retry.init writes a working default; if the
project has its own LLM client (per Step 4), prefer that. Either way, you're not asking.pip install snowglobe without first checking whether it's already installed.snowglobe-connect auth without first checking whether the user is already authed.auth or pip install. Announce what you're doing in
one short sentence and run.init bare. Always pipe the agent name to its stdin
(printf '%s\n' "<name>" | snowglobe-connect init). Reading and forwarding init's
interactive prompts dumps the user's full app list back to them and stalls the flow.init fails (asking for UUID, hand-writing agents.json).
Surface the init error and stop.snowglobe-connect start and keep
it running. Without that, simulations can't reach the wrapper.Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub guardrails-ai/snowglobe-skills --plugin snowglobe-skills