-
Notifications
You must be signed in to change notification settings - Fork 57
feat(sec-core): route prompt scan to daemon and add prompt model preload #786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/daemon/handlers/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Daemon method handlers.""" |
120 changes: 120 additions & 0 deletions
120
src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/daemon/handlers/prompt_scan.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| """Daemon handler for the scan-prompt CLI-compatible method.""" | ||
|
|
||
| import asyncio | ||
| from typing import Any | ||
|
|
||
| from agent_sec_cli.daemon.errors import UnavailableError | ||
| from agent_sec_cli.daemon.protocol import DaemonRequest | ||
| from agent_sec_cli.daemon.registry import ( | ||
| HandlerResult, | ||
| MethodRegistry, | ||
| MethodSpec, | ||
| ) | ||
| from agent_sec_cli.daemon.runtime import DaemonRuntime | ||
|
|
||
|
|
||
| def register_prompt_scan_methods(registry: MethodRegistry) -> None: | ||
| """Register prompt scanner daemon methods.""" | ||
| registry.register( | ||
| MethodSpec( | ||
| method="scan-prompt", | ||
| handler=prompt_scan_handler, | ||
| lifecycle="security action", | ||
| queue="prompt-scan", | ||
| timeout_ms=30_000, | ||
| access_log=True, | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| async def prompt_scan_handler( | ||
| request: DaemonRequest, runtime: DaemonRuntime | ||
| ) -> HandlerResult: | ||
| """Execute prompt scanning through security middleware.""" | ||
| prompt_scan_state = runtime.prompt_scan_state | ||
| if prompt_scan_state.status != "ready" or not prompt_scan_state.loaded: | ||
| raise UnavailableError(_prompt_unavailable_message(runtime)) | ||
|
|
||
| params = request.params | ||
| result = await asyncio.to_thread( | ||
| _invoke_prompt_scan, | ||
| trace_context=request.trace_context, | ||
| text=_string_param(params, "text"), | ||
| mode=_string_param(params, "mode", default="standard"), | ||
| source=_string_param(params, "source"), | ||
| ) | ||
| return _action_result_to_handler_result(result) | ||
|
|
||
|
|
||
| def _invoke_prompt_scan( | ||
| *, | ||
| trace_context: dict[str, Any], | ||
| text: str, | ||
| mode: str, | ||
| source: str, | ||
| ) -> Any: | ||
| from agent_sec_cli.security_middleware import ( # noqa: PLC0415 - lazy import: daemon handler execution only | ||
| invoke_with_context, | ||
| ) | ||
|
|
||
| return invoke_with_context( | ||
| "prompt_scan", | ||
| caller="daemon", | ||
| trace_context=trace_context, | ||
| text=text, | ||
| mode=mode, | ||
| source=source, | ||
| ) | ||
|
|
||
|
|
||
| def _action_result_to_handler_result(result: Any) -> HandlerResult: | ||
| return HandlerResult( | ||
| data=result.data, | ||
| stdout=result.stdout, | ||
| stderr=result.error, | ||
| exit_code=result.exit_code, | ||
| ) | ||
|
|
||
|
|
||
| def _string_param( | ||
| params: dict[str, Any], | ||
| name: str, | ||
| default: str = "", | ||
| ) -> str: | ||
| value = params.get(name, default) | ||
| if value is None: | ||
| return default | ||
| return str(value) | ||
|
|
||
|
|
||
| def _prompt_unavailable_message(runtime: DaemonRuntime) -> str: | ||
| prompt_scan_state = runtime.prompt_scan_state.to_dict() | ||
| status = prompt_scan_state.get("status", "unknown") | ||
| model = prompt_scan_state.get("model") | ||
| last_error = prompt_scan_state.get("last_error") | ||
|
|
||
| if status == "downloading": | ||
| parts = [ | ||
| "prompt scanner is not ready: model download is still in progress", | ||
| "status=downloading", | ||
| ] | ||
| elif status == "loading": | ||
| parts = [ | ||
| "prompt scanner is not ready: model download completed and the model is loading", | ||
| "status=loading", | ||
| ] | ||
| elif status == "degraded": | ||
| parts = [ | ||
| "prompt scanner preload failed", | ||
| "retry with `agent-sec-cli scan-prompt warmup`", | ||
| "then restart the agent-sec daemon process", | ||
| "status=degraded", | ||
| ] | ||
| else: | ||
| parts = [f"prompt scanner is not ready: status={status}"] | ||
|
|
||
| if model: | ||
| parts.append(f"model={model}") | ||
| if last_error: | ||
| parts.append(f"last_error={last_error}") | ||
| return ", ".join(parts) | ||
169 changes: 169 additions & 0 deletions
169
...ec-core/agent-sec-cli/src/agent_sec_cli/daemon/handlers/prompt_scan_protocol.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| # scan-prompt daemon protocol | ||
|
|
||
| This document defines the response contract for the daemon `scan-prompt` method. | ||
| It is the method-level contract for callers such as the CLI, Cosh hooks, and | ||
| other subprocess clients. | ||
|
|
||
| ## Response layers | ||
|
|
||
| `scan-prompt` responses have three layers. Callers must handle them in this | ||
| order. | ||
|
|
||
| ### 1. Transport failure: no `DaemonResponse` | ||
|
|
||
| The client did not receive a valid daemon response. | ||
|
|
||
| Examples: | ||
|
|
||
| - daemon socket does not exist | ||
| - daemon connection or request timeout | ||
| - daemon process exits before writing a response | ||
| - daemon response is not valid protocol data | ||
| - daemon response exceeds the configured size limit | ||
| - daemon runtime path cannot be resolved, for example when `XDG_RUNTIME_DIR` is | ||
| not set and no explicit daemon socket path is provided | ||
|
|
||
| Caller behavior: | ||
|
|
||
| ```python | ||
| try: | ||
| response = client.call("scan-prompt", params=params) | ||
| except (DaemonClientError, DaemonRuntimePathError): | ||
| # Daemon is unreachable, the protocol is broken, or the local runtime | ||
| # socket path cannot be resolved. | ||
| exit(1) | ||
| ``` | ||
|
|
||
| ### 2. Daemon failure: `ok=false` | ||
|
|
||
| The daemon received the request, but the method could not be dispatched or | ||
| executed at the daemon/method boundary. | ||
|
|
||
| `ok=false` responses are not scan results. Callers must not parse `data` or | ||
| `stdout` as an action result. | ||
|
|
||
| Expected shape: | ||
|
|
||
| ```json | ||
| { | ||
| "id": "req-1", | ||
| "ok": false, | ||
| "data": {}, | ||
| "stdout": "", | ||
| "stderr": "prompt scanner is not ready: status=loading", | ||
| "exit_code": 1, | ||
| "error": { | ||
| "code": "unavailable", | ||
| "message": "prompt scanner is not ready: status=loading" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| `scan-prompt` daemon failures include: | ||
|
|
||
| - unknown daemon method | ||
| - malformed daemon request | ||
| - prompt scanner runtime unavailable, including preload states such as | ||
| `pending`, `downloading`, `loading`, or `degraded` | ||
| - daemon method timeout | ||
| - unexpected handler crash | ||
|
|
||
| Unexpected handler crashes should be logged by the daemon and returned as | ||
| `internal_error` without exposing arbitrary exception details to callers. | ||
|
|
||
| Caller behavior: | ||
|
|
||
| ```python | ||
| if not response.ok: | ||
| echo_error(response.stderr or response.error["message"]) | ||
| exit(response.exit_code or 1) | ||
| ``` | ||
|
|
||
| ### 3. Action result: `ok=true` | ||
|
|
||
| The daemon successfully dispatched `scan-prompt`, and the handler returned a | ||
| scan action result. | ||
|
|
||
| For `ok=true`, `exit_code` is the action/CLI semantic exit code. It may be | ||
| non-zero even though daemon dispatch succeeded. | ||
|
|
||
| Expected successful scan shape: | ||
|
|
||
| ```json | ||
| { | ||
| "id": "req-1", | ||
| "ok": true, | ||
| "data": { | ||
| "ok": true, | ||
| "verdict": "pass" | ||
| }, | ||
| "stdout": "{...same scan result as JSON...}", | ||
| "stderr": "", | ||
| "exit_code": 0 | ||
| } | ||
| ``` | ||
|
|
||
| Expected scanner error result shape: | ||
|
|
||
| ```json | ||
| { | ||
| "id": "req-1", | ||
| "ok": true, | ||
| "data": { | ||
| "ok": false, | ||
| "verdict": "error", | ||
| "summary": "Scanner error: model exploded" | ||
| }, | ||
| "stdout": "{...same error verdict as JSON...}", | ||
| "stderr": "Scanner error: model exploded", | ||
| "exit_code": 1 | ||
| } | ||
| ``` | ||
|
|
||
| `scan-prompt` action results include: | ||
|
|
||
| - `PASS`, `WARN`, and `DENY` scan verdicts: `ok=true`, `exit_code=0` | ||
| - backend validation failures, such as missing/empty `text` or invalid `mode`: | ||
| `ok=true`, `exit_code=1`, with `stderr` describing the validation error | ||
| - scanner-produced `ERROR` verdicts: `ok=true`, `exit_code=1`, with structured | ||
| error verdict data | ||
| - scanner domain exceptions that can be converted to an error verdict: | ||
| `ok=true`, `exit_code=1`, with structured error verdict data | ||
|
|
||
| Caller behavior: | ||
|
|
||
| ```python | ||
| if response.ok: | ||
| rendered = render_action_output_if_present(response) | ||
| if response.exit_code != 0: | ||
| if not rendered: | ||
| echo_error(response.stderr or "scan-prompt failed") | ||
| exit(response.exit_code) | ||
| exit(0) | ||
| ``` | ||
|
|
||
| Callers should render structured action output before exiting with a non-zero | ||
| action `exit_code`, so JSON consumers can still parse the error verdict. If an | ||
| action failure has no structured output, callers should display `stderr`. | ||
|
|
||
| ## Request parameters | ||
|
|
||
| `scan-prompt` request params: | ||
|
|
||
| ```json | ||
| { | ||
| "text": "prompt text to scan", | ||
| "mode": "fast|standard|strict", | ||
| "source": "optional input source label" | ||
| } | ||
| ``` | ||
|
|
||
| Rules: | ||
|
|
||
| - `text` is required and must contain non-whitespace content. | ||
| - `mode` is optional and defaults to `standard`. | ||
| - `mode` must be one of `fast`, `standard`, or `strict`. | ||
| - `source` is optional and defaults to an empty string. | ||
|
|
||
| Invalid `text` or `mode` is handled by the prompt scan backend and returned as | ||
| an action failure: `ok=true`, `exit_code=1`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
src/agent-sec-core/agent-sec-cli/src/agent_sec_cli/daemon/jobs/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| """Daemon background job package.""" | ||
|
|
||
| from agent_sec_cli.daemon.jobs.base import ( | ||
| BackgroundJob, | ||
| JobManager, | ||
| JobStatus, | ||
| PeriodicBackgroundJob, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "BackgroundJob", | ||
| "JobManager", | ||
| "JobStatus", | ||
| "PeriodicBackgroundJob", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.