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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ __pycache__/
.pytest_cache/
.coverage
.venv/

# Browser verification drops screenshots wherever it is run from.
# Repo images live in images/ and stay tracked.
*.png
!images/*.png
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- **LogScale query recipe** — Complete `NGSIEM.start_search()` / `get_search_status()` pattern for querying LogScale from Foundry functions. Documents the `search-all` repository requirement (specific repo names cause 403), the `search=` keyword requirement (FalconPy documents `body=` but its guard never honors it — see [falconpy#1491](https://github.com/CrowdStrike/falconpy/issues/1491)), the `resources` vs `body` response-key asymmetry between `start_search` and `get_search_status`, and clarifies that `NGSIEM` is the query class while `FoundryLogScale` is ingestion-only. Adds `humio-auth-proxy:read` to the scope reference table, verified against a live CID.
- **Function I/O schema requirements** — Functions called from workflows must be created with `--input-schema` and `--output-schema`. Schemas bind only at creation time; the CLI writes `null` for both without these flags, even when `--wf-expose` is set. Functions without a response schema produce no visible output in Fusion actions.
- **Workflow deletion warning** — Documents that deleting a workflow and recreating it with the same name causes `409 name must be unique for an app` followed by `400 dependent artifact failed`, blocking all further deploys. Recovery requires a fresh app.
- **Cross-plugin redirect to fusion-skills** — The development-workflow orchestrator now recognizes standalone Falcon Fusion workflow requests (trigger + actions, no UI/function/collection/API integration) and advises the `crowdstrike-falcon-fusion` plugin instead of scaffolding a Foundry app. Adds `detect_fusion_redirect.py` classifier with unit tests.
- **Cross-plugin redirect to the Falcon Fusion plugin** — The development-workflow orchestrator now recognizes standalone Falcon Fusion workflow requests (trigger + actions, no UI/function/collection/API integration) and advises the `crowdstrike-falcon-fusion` plugin instead of scaffolding a Foundry app. Adds `detect_fusion_redirect.py` classifier with unit tests.
- **GraphQL APIs use case** — Integrate GraphQL APIs (Falcon Identity Protection, GitHub, Snyk) into Foundry apps using FalconPy or HTTP POST. Covers zero-arg auth for Falcon GraphQL endpoints and the security tradeoff of env vars vs API integrations for third-party APIs.
- **`scripts/action_search.py`** — API-based action discovery script that works in headless/CI environments where the CLI's interactive `actions view` prompt fails. Uses FalconPy with FQL fuzzy matching and prints action IDs with `version_constraint` values.
- **CLI guard for `actions view` / `triggers view`** — Hook now catches missing `--no-prompt` on these commands to prevent TTY hangs.
- **`foundry apps list` in prerequisite check** — New CLI 2.0.2 command that lists all deployed apps on the CID from any directory. Added to Step 3 to help avoid name collisions.
- **Collection description validation constraints** — Documents the 3–500 character length limit, alphanumeric-start requirement, and allowed character set for collection descriptions.
- **Function logs in testing-patterns reference** — Added function logs (viewing in UI and Advanced Event Search) to the reference table entry for testing patterns.
- **Query parameter type matching for API integrations** — Documents that `apiIntegration().execute()` types params as `Record<string, unknown>`, so a quoted number like `limit: '25'` passes type-checking and fails server-side with `got string want integer`. The extension still renders, so the failure reads as an API or credential error rather than a code bug.
- **Content regression tests** — `tests/test_skill_content.py` guards critical documentation (LogScale recipe, schema requirements, workflow deletion warning) against accidental removal.

### Changed

- **Fusion redirect names the plugin, not the repo** — The cross-plugin advisory pointed users at the `fusion-skills` GitHub repo. It now names the plugin (`crowdstrike-falcon-fusion`) with the `/plugin install` command and the marketplace link, since most users install from the marketplace and a repo detour is confusing to anyone unfamiliar with GitHub. `detect_fusion_redirect.py` reports `target` as `crowdstrike-falcon-fusion` / `crowdstrike-falcon-foundry` rather than the repo names.
- **Gemini CLI → Antigravity CLI** — Google transitioned Gemini CLI to Antigravity CLI (binary: `agy`). Updated README with new command, skills paths (`~/.gemini/antigravity-cli/skills/` for user scope, `.agents/skills/` for workspace scope). Removed `GEMINI.md` since we never shipped Gemini CLI support; Antigravity reads `AGENTS.md` directly.
- **Codex docs link** — Updated from `developers.openai.com/codex/skills` to `learn.chatgpt.com/docs/build-skills`.
- **Renamed Python scripts to snake_case** — `scripts/adapt-spec-for-foundry.py` → `adapt_spec_for_foundry.py` and `scripts/test-adapt-spec.py` → `test_adapt_spec.py`, matching the repo's `snake_case` lint convention and allowing the test to import the module directly. The PreToolUse hook and all skill docs reference the new names; no behavior changed. If you invoked the old path directly in your own tooling, update it to the underscore name.

### Fixed

- **Fusion redirect was never wired to a hook** — `detect_fusion_redirect.py` shipped as a standalone script that nothing invoked, so its verdict never reached the agent at runtime. The `fusion-redirect` eval passed only 1 of 5 trials: in three runs the agent declined to scaffold an app but never mentioned the Fusion plugin, and in one it scaffolded an app anyway. The skill router now runs the classifier on Foundry-matched prompts and injects an explicit redirect advisory when it fires. The advisory in `development-workflow` also states that naming the plugin is *required output* — declining to scaffold is only half a redirect — and that hand-writing the workflow YAML defeats the purpose, since the Fusion plugin discovers real action IDs, validates against the platform schema, and imports to the CID.
- **Fusion redirect classifier mishandled negation** — `detect_fusion_redirect.py` matched app-capability keywords without regard to negation, so a prompt saying "no Foundry app, no UI, no functions" registered `UI` and `Foundry app` as *requests* for those capabilities and suppressed the redirect. Standalone Fusion workflow requests that explicitly ruled out app capabilities — the clearest possible case for redirecting — were the ones most likely to be kept in this plugin. Negated spans are now stripped before app signals are matched, and the verdict reports `negated_app_signals` so the reasoning stays visible. Caught by the `fusion-redirect` eval, which failed 0/5 trials before this fix.
- **Removed "delete and re-create" advice** — The old guidance for fixing missing `workflow_integration` said to delete and recreate the function. This is technically correct (schemas only bind at creation), but was misleading about workflows: you must never delete and recreate a *workflow* to refresh a binding. Both skills now give consistent guidance — recreate the function, update the workflow YAML reference in place.
- **Action discovery guidance** — Updated all `actions view` examples to include `--no-prompt` and pointed to `action_search.py` as the primary fallback. The CLI ignores `--no-prompt` for these commands (tracked upstream), so the script is the reliable path.
- **Alert and detection query routing (population vs. enrich)** — The orchestrator and workflows skills now distinguish two cases. Fetching a *population* the workflow doesn't already have ("summarize all high-severity alerts") goes to a source-of-truth API — a native platform action (e.g. Cases → Search Cases) first, or a FalconPy `Alerts`/`Detects` function when none fits — since an Event Query against NG-SIEM can silently return nothing (repo contents are connector-dependent). *Enriching* a detection the workflow already holds (query by its ID) stays an Event Query, as does historical/aggregate telemetry. New reference [event-query-vs-api.md](skills/workflows-development/references/event-query-vs-api.md); the functions-falcon-api example keeps the verified `severity_name` + `created_timestamp` FQL filter.
Expand Down
27 changes: 27 additions & 0 deletions hooks/foundry-skill-router.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,33 @@ case "$HOOK_EVENT" in
# Write marker so PreToolUse hook knows to inject advisory context
echo "$$" > "$MARKER"

# Standalone Fusion workflow? Advise the sibling plugin instead of
# steering into app scaffolding. Without this the classifier is never
# consulted at runtime and the agent tends to quietly author the workflow
# itself, never telling the user a better-suited plugin exists.
REDIRECT_SCRIPT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/scripts/detect_fusion_redirect.py"
if [ -f "$REDIRECT_SCRIPT" ]; then
VERDICT=$(printf '%s' "$USER_PROMPT" | python3 "$REDIRECT_SCRIPT" 2>/dev/null || true)
if echo "$VERDICT" | grep -q '"redirect": true'; then
# If the sibling plugin is already installed, say "use it" rather than
# "install it". Best-effort: the file may be absent, in which case we
# fall back to the install wording. Mirrors the check in
# fusion-skills' fusion-foundry-bridge.sh.
FUSION_HINT="Advise the crowdstrike-falcon-fusion plugin — /plugin install crowdstrike-falcon-fusion, or https://claude.com/plugins/crowdstrike-falcon-fusion."
if [ -f "$HOME/.claude/plugins/installed_plugins.json" ] &&
grep -q "crowdstrike-falcon-fusion" "$HOME/.claude/plugins/installed_plugins.json" 2>/dev/null; then
FUSION_HINT="The crowdstrike-falcon-fusion plugin is already installed — hand off to its workflows skill."
fi
jq -n --arg hint "$FUSION_HINT" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: ("STANDALONE FUSION WORKFLOW DETECTED: this request looks like a trigger plus actions that already exist in the CID — no UI, function, collection, or API integration to build. It does NOT need a Foundry app. " + $hint + " Do NOT scaffold a Foundry app. Naming the plugin is required output — declining to scaffold is only half the redirect, and hand-writing the workflow YAML yourself defeats the purpose since that plugin discovers real action IDs, validates against the platform schema, and imports to the CID. This detection is advisory: if the request genuinely needs an app capability built, proceed with crowdstrike-falcon-foundry:development-workflow instead.")
}
}'
exit 0
fi
fi

jq -n '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
Expand Down
19 changes: 19 additions & 0 deletions run-ab-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@ RED_DIR="$AB_RESULTS_DIR/red-runs"
GREEN_DIR="$AB_RESULTS_DIR/green-runs"
BASELINE_JSON="$AB_RESULTS_DIR/baseline.json"

# ── Validate the baseline ref up front ───────────────────────
# Without this an unknown ref fails silently: rev-parse is muted by 2>/dev/null,
# `git archive` then writes nothing, and its failure is masked because it is piped
# into tar (pipefail does not help when the last command in the pipe succeeds).
# The script runs on with an empty baseline and exits 0 having printed nothing,
# which reads as "the script did nothing" rather than "that ref is not here".
if [ "$NO_SKILL" != "1" ]; then
if ! git -C "$REPO_ROOT" rev-parse --verify --quiet "${BASELINE_REF}^{commit}" >/dev/null 2>&1; then
echo "ERROR: baseline ref '$BASELINE_REF' does not exist in $REPO_ROOT" >&2
echo "" >&2
echo " Release tags available here:" >&2
git -C "$REPO_ROOT" tag --list 'v*' --sort=-v:refname 2>/dev/null | head -5 | sed 's/^/ /' >&2
echo "" >&2
echo " Pass one of the above with --ref, or a branch or commit SHA." >&2
exit 1
fi
fi


# Delete Foundry apps from a phase directory
cleanup_phase_apps() {
local phase_dir="$1"
Expand Down
48 changes: 42 additions & 6 deletions scripts/detect_fusion_redirect.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/usr/bin/env python3
"""Classify whether a request is a standalone Fusion workflow (redirect to
fusion-skills) or a Falcon Foundry app (handle here in foundry-skills).
the crowdstrike-falcon-fusion plugin) or a Falcon Foundry app (handle here).

A standalone Fusion workflow needs only a trigger plus actions that already
exist in the CID — no UI, no serverless function, no collection, no custom API
integration to be built, no manifest.yml. Those app-only capabilities are what
keep a request in foundry-skills. When none are present, the request belongs to
the sibling fusion-skills (crowdstrike-falcon-fusion) plugin.
the sibling crowdstrike-falcon-fusion plugin.

This is a heuristic used to *advise* a redirect; it never blocks. The routing
decision ultimately rests with the orchestrator skill (development-workflow),
Expand Down Expand Up @@ -68,19 +68,54 @@ def _matches(patterns, text):
return hits


# Negation cues that *withdraw* a capability rather than request it. A prompt
# saying "no UI, no functions" is describing what it does NOT need, so those
# words must not count as app signals — the naive read inverts the verdict and
# keeps a standalone-workflow request in foundry-skills.
_NEGATION_CUE = (
r"(?:no|not|without|excluding|skip|omit|"
r"(?:do(?:n't|es not|esn't| not)? (?:need|want|require))|"
r"(?:no need for)|(?:isn't|is not|aren't|are not))"
)

# A negation scopes over the capabilities that follow it, including across a
# comma/"or"/"and" series: "no UI, no functions" and "without a UI or function".
# Stop at sentence-ending punctuation so a later clause isn't swallowed.
_NEGATED_SPAN = re.compile(
rf"\b{_NEGATION_CUE}\b[^.;!?\n]*",
re.IGNORECASE,
)


def _strip_negated_spans(text):
"""Blank out spans where capabilities are explicitly ruled out.

Replaces with spaces rather than deleting so surrounding word boundaries
survive and offsets stay comparable.
"""
return _NEGATED_SPAN.sub(lambda m: " " * len(m.group(0)), text or "")


def classify(request):
"""Classify a natural-language request.

Returns a dict:
redirect - True if it looks like a standalone Fusion workflow
target - "fusion-skills" when redirecting, else "foundry-skills"
target - "crowdstrike-falcon-fusion" when redirecting, else
"crowdstrike-falcon-foundry"
is_workflow - whether the request mentions a workflow/automation
app_signals - app-capability patterns that matched (block redirect)
reason - one-line human-readable explanation
"""
text = request or ""
workflow_hits = _matches(WORKFLOW_PATTERNS, text)
app_hits = _matches(APP_CAPABILITY_PATTERNS, text)
# Match app signals only against text with negated spans removed, so
# "no UI, no functions" reads as an absence of those capabilities.
affirmative = _strip_negated_spans(text)
app_hits = _matches(APP_CAPABILITY_PATTERNS, affirmative)
negated_app_hits = [
p for p in _matches(APP_CAPABILITY_PATTERNS, text) if p not in app_hits
]

is_workflow = bool(workflow_hits)
# Redirect only when it's clearly a workflow AND carries no app-only signal.
Expand All @@ -96,14 +131,15 @@ def classify(request):
else:
reason = (
"Standalone Fusion workflow (trigger + existing actions, no app "
"capability) — advise fusion-skills (crowdstrike-falcon-fusion)."
"capability) — advise the crowdstrike-falcon-fusion plugin."
)

return {
"redirect": redirect,
"target": "fusion-skills" if redirect else "foundry-skills",
"target": "crowdstrike-falcon-fusion" if redirect else "crowdstrike-falcon-foundry",
"is_workflow": is_workflow,
"app_signals": app_hits,
"negated_app_signals": negated_app_hits,
"reason": reason,
}

Expand Down
36 changes: 31 additions & 5 deletions skills/development-workflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,49 @@ Security review → security-patterns
E2E testing / Playwright → e2e-testing

Standalone Fusion workflow (no app — trigger + existing actions only)
└── Advise fusion-skills — see Cross-Plugin Advisory
└── Advise the Falcon Fusion plugin — see Cross-Plugin Advisory
```

## Cross-Plugin Advisory (Fusion vs. Foundry)

A Falcon Fusion workflow can be authored **standalone** (no app wrapper) when it
only needs a trigger plus actions that already exist in the CID. That is the
sibling **fusion-skills** (`crowdstrike-falcon-fusion`) plugin's job, not this one.
sibling **Falcon Fusion** plugin's job (`crowdstrike-falcon-fusion`), not this one.

| Situation | Action |
|-----------|--------|
| Just a workflow: trigger + existing actions, no UI/function/collection/manifest | **Advise fusion-skills** (`claude plugin install crowdstrike-falcon-fusion`). Do NOT scaffold a Foundry app. |
| Just a workflow: trigger + existing actions, no UI/function/collection/manifest | **Advise the Falcon Fusion plugin** — see the required response below. Do NOT scaffold a Foundry app. |
| Workflow needs a UI, function, collection, or custom API integration to be BUILT | **Proceed here** — that's a Foundry app; use the App Creation Flow. |
| A workflow *inside* an app you're already building | **Proceed here** — use `workflows-development`. |

If every action already exists and there's no UI/function/collection, redirect.
Detection is advisory, never blocking.
> **⚠️ MUST NOT silently do the Fusion plugin's job.** The common failure is to
> recognize no app is needed, then hand the user workflow YAML anyway without
> ever telling them a better-suited plugin exists. Declining to scaffold is only
> half the redirect — **naming the plugin is required output.**

When redirecting, your response MUST contain all three:

1. A statement that this needs no Foundry app
2. The plugin name **`crowdstrike-falcon-fusion`** written out
3. How to get it: `/plugin install crowdstrike-falcon-fusion` or
https://claude.com/plugins/crowdstrike-falcon-fusion

Name the plugin, not a GitHub repo — most users install from the marketplace and
a repo link is a detour.

**Do not hand-write the workflow YAML in a redirect.** Producing the artifact
yourself defeats the purpose: the Fusion plugin discovers real action IDs from
the live API, validates against the platform schema, and imports and releases to
the CID. A YAML block with placeholder action IDs is strictly worse than sending
the user somewhere that can finish the job. Offer a one-line sketch of the shape
if it helps, then redirect.

A negated capability is not a request for it. "no UI", "without a function",
"I don't need a collection" all mean the request is *smaller*, not larger — so
they push **toward** redirecting, never away. If every action already exists and
there's no UI/function/collection to build, redirect. The routing decision is
yours; `scripts/detect_fusion_redirect.py` is available as a heuristic
cross-check and never blocks.


## App Creation Flow
Expand Down
Loading