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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Changed
- `guardrails.py` and `agent-validator.py` hooks now exit immediately when the working directory is not a Salesforce project (no `sfdx-project.json`, `force-app/`, or `aiAuthoringBundles/` in cwd or any ancestor). When the plugin is installed globally, hooks no longer observe Bash commands or file edits in unrelated projects. Behavior inside Salesforce projects is unchanged.
- **Mode C is Mode A + Mode B with security content — it ships no generator scripts.** `/agentforce-test` Mode C now works exactly like the functional modes: the coding agent reads the customer's `.agent` file and **writes the `AiEvaluationDefinition` security spec YAML itself** (C1, deployed with the same `sf agent test create` / `run` / `results` commands as Mode B), and probes live with the same `sf agent preview start` / `send` / `end --authoring-bundle` calls as Mode A (C2). The only Mode-C-specific machinery is reference material. Consequences:
- Reading the `.agent` file is no longer a flag, it is the method. The skill tells the coding agent to find the file itself (glob, then `GenAiPlannerDefinition` query, then `sf project retrieve start --metadata "AiAuthoringBundle:<name>"`) and to state the inferred business domain in the confirmation gate so the user can correct a misclassification before any case is written.
- The sandbox gate, the simulated-actions default, and the confirmation gate are stated as **instructions the agent must follow** (`SELECT IsSandbox, Name, OrganizationType FROM Organization` before probing, fail closed if it cannot be determined) rather than argument parsing inside a runner.
Expand Down
15 changes: 15 additions & 0 deletions shared/hooks/scripts/agent-validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@
import sys
from pathlib import Path

SF_PROJECT_MARKERS = ("sfdx-project.json", "force-app", "aiAuthoringBundles")


def is_salesforce_project(start: Path | None = None) -> bool:
"""Return True if cwd or any ancestor contains a Salesforce project marker."""
p = (start or Path.cwd()).resolve()
for d in (p, *p.parents):
if any((d / m).exists() for m in SF_PROJECT_MARKERS):
return True
return False


try:
from stdin_utils import read_stdin_safe
except ImportError:
Expand Down Expand Up @@ -487,6 +499,9 @@ def _auto_resolve_placeholder(self):

def main():
"""Main entry point for the PostToolUse hook."""
if not is_salesforce_project():
sys.exit(0)

input_data = read_stdin_safe(timeout_seconds=0.1)
if not input_data:
sys.exit(0)
Expand Down
20 changes: 20 additions & 0 deletions shared/hooks/scripts/guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@
import sys
from pathlib import Path

SF_PROJECT_MARKERS = ("sfdx-project.json", "force-app", "aiAuthoringBundles")


def is_salesforce_project(start: Path | None = None) -> bool:
"""Return True if cwd or any ancestor contains a Salesforce project marker.

Used as a project-relevance gate so this hook is a no-op when the plugin is
installed globally but the user is working in an unrelated project.
"""
p = (start or Path.cwd()).resolve()
for d in (p, *p.parents):
if any((d / m).exists() for m in SF_PROJECT_MARKERS):
return True
return False


try:
from stdin_utils import read_stdin_safe
except ImportError:
Expand Down Expand Up @@ -118,6 +134,10 @@ def is_sf_context(command: str) -> bool:


def main():
if not is_salesforce_project():
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow"}}))
sys.exit(0)

input_data = read_stdin_safe(timeout_seconds=0.1)
if not input_data:
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow"}}))
Expand Down