diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd1f74..e0d363d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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:"`) 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. diff --git a/shared/hooks/scripts/agent-validator.py b/shared/hooks/scripts/agent-validator.py index b5d3284..01e543b 100644 --- a/shared/hooks/scripts/agent-validator.py +++ b/shared/hooks/scripts/agent-validator.py @@ -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: @@ -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) diff --git a/shared/hooks/scripts/guardrails.py b/shared/hooks/scripts/guardrails.py index 1b0edd8..16a7b89 100644 --- a/shared/hooks/scripts/guardrails.py +++ b/shared/hooks/scripts/guardrails.py @@ -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: @@ -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"}}))