diff --git a/README.md b/README.md index 0d604a8..e0919f4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The README uses a GIF so the preview renders directly on GitHub. The docs homepa | --- | --- | --- | | TUI | Dashboard, inventory, findings, analysis, fix plan, backup preview, action queue | Read-only until a confirmed action is applied | | CLI | Scriptable scan, doctor, policy, SARIF, snapshot, schedule, backup, and action commands | Read-only unless explicit output/export paths or `--confirm` actions are requested | -| MCP server | Stdio tools/resources/prompts for AI clients | Direct apply only through shared action-registry IDs with disclosure, confirmation, redaction, and audit logging | +| MCP server | Stdio tools/resources/prompts for AI clients | Read-only; can list/preview actions, but writes must be applied in CLI/TUI/Raycast | | Raycast | macOS companion commands plus confirmed Nightward Actions | Clipboard/report-folder actions plus confirmation-gated writes | | GitHub Action | Workspace policy and SARIF checks | Writes only requested CI outputs | | Trunk plugin | Local workspace policy/analyze linters | Emits SARIF to stdout | @@ -296,7 +296,7 @@ Secret values are never emitted in scan JSON, findings output, fix-plan JSON, Ma `nw analyze` turns scan findings and classifications into explainable signals. It does not claim a package, server, binary, or URL is safe. It reports what Nightward can prove from local structure, why it matters, and how confident the signal is. -Default analysis is offline and built in. Optional providers are discovered by `providers doctor`; Nightward does not call online services unless a user explicitly selects providers and opts into network-capable behavior. The CLI/TUI/Raycast/MCP action layer can install known provider CLIs after confirmation. Explicit local providers are `gitleaks`, `trufflehog`, `semgrep`, and `syft`. Online-capable providers are `trivy`, `osv-scanner`, `grype`, `scorecard`, and `socket`, and they require explicit online-provider opt-in. Socket support creates a remote Socket scan artifact from dependency manifest metadata; Nightward does not fetch or normalize remote Socket reports in v1. +Default analysis is offline and built in. Optional providers are discovered by `providers doctor`; Nightward does not call online services unless a user explicitly selects providers and opts into network-capable behavior. The CLI/TUI/Raycast action layer can install known provider CLIs after confirmation. MCP can list and preview those actions, but cannot apply local writes. Explicit local providers are `gitleaks`, `trufflehog`, `semgrep`, and `syft`. Online-capable providers are `trivy`, `osv-scanner`, `grype`, `scorecard`, and `socket`, and they require explicit online-provider opt-in. Socket support creates a remote Socket scan artifact from dependency manifest metadata; Nightward does not fetch or normalize remote Socket reports in v1. Provider runs use explicit skip/block/ready states, timeouts, bounded output capture, and redacted metadata only. Oversized provider stdout fails closed as a provider warning instead of being partially parsed. Semgrep execution requires a repo-local config file so Nightward does not use automatic rule discovery by default. @@ -356,7 +356,7 @@ Nightward can expose local context and bounded Nightward action workflows to MCP } ``` -The server supports scan, doctor, findings, finding/signal explanation, analysis, fix-plan, policy-check, report history/diff, action list/preview/apply, rules, providers, resources, and prompts. It uses stdio only, does not open a network listener, and cannot rewrite arbitrary MCP or agent config. `nightward_action_apply` can apply only shared action-registry IDs after disclosure acceptance, `confirm: true`, action availability checks, redacted output, and audit logging. +The server supports scan, doctor, findings, finding/signal explanation, analysis, fix-plan, policy-check, report history/diff, action list/preview, rules, providers, resources, and prompts. It uses stdio only, does not open a network listener, and cannot rewrite arbitrary MCP or agent config. MCP clients cannot apply local writes because tool-call arguments are not an out-of-band local confirmation channel; use the CLI, TUI, or Raycast extension to apply previewed actions. ## GitHub Action diff --git a/crates/nightward-core/src/mcpserver.rs b/crates/nightward-core/src/mcpserver.rs index ab2fd3b..16cc4d5 100644 --- a/crates/nightward-core/src/mcpserver.rs +++ b/crates/nightward-core/src/mcpserver.rs @@ -1,4 +1,4 @@ -use crate::actions::{self, ApplyOptions}; +use crate::actions; use crate::analysis::{run as analyze, Options as AnalysisOptions}; use crate::fixplan::{plan as fix_plan, Selector}; use crate::inventory::{home_dir_from_env, load_report, redact_text, scan_home, scan_workspace}; @@ -97,7 +97,7 @@ fn initialize_result(requested: Option<&str>) -> Value { "version": env!("CARGO_PKG_VERSION"), "description": "Local-first AI agent, MCP, provider, and dotfiles security posture." }, - "instructions": "Nightward returns redacted local security posture. Write-capable MCP calls are limited to the shared Nightward action registry and require disclosure acceptance plus explicit confirmation." + "instructions": "Nightward returns redacted local security posture. MCP is read-only: it can list and preview bounded actions, but local writes must be applied out-of-band in the Nightward CLI, TUI, or Raycast extension." }) } @@ -187,13 +187,6 @@ fn tools() -> Vec { schema_action_id(), read_only_annotations("Action preview", false), ), - tool( - "nightward_action_apply", - "Action Apply", - "Apply one bounded Nightward action after disclosure acceptance and confirm: true.", - schema_action_apply(), - write_annotations("Action apply", true, true), - ), tool( "nightward_rules", "Nightward Rules", @@ -349,16 +342,6 @@ fn read_only_annotations(title: &str, open_world: bool) -> Value { }) } -fn write_annotations(title: &str, destructive: bool, open_world: bool) -> Value { - json!({ - "title": title, - "readOnlyHint": false, - "destructiveHint": destructive, - "idempotentHint": false, - "openWorldHint": open_world - }) -} - fn read_resource(params: Value, home: &Path) -> Result { let uri = params .get("uri") @@ -401,7 +384,7 @@ fn read_prompt(params: Value) -> Result { let finding_id = string_arg(&args, "finding_id"); let text = match name { "audit_my_ai_setup" => { - "Use Nightward MCP tools to run nightward_scan, nightward_analysis, and nightward_policy_check with compact output. Explain the highest-risk AI/MCP configuration issues, provider posture, and the safest next actions. Do not apply actions unless I explicitly ask and confirm them." + "Use Nightward MCP tools to run nightward_scan, nightward_analysis, and nightward_policy_check with compact output. Explain the highest-risk AI/MCP configuration issues, provider posture, and the safest next actions. MCP is read-only, so preview any relevant action and tell me the CLI/TUI/Raycast path for applying it." } "explain_top_risks" => { "Use nightward_findings and nightward_analysis to identify the top risks. Explain what can actually break or leak, what is probably just review noise, and what should be fixed first." @@ -410,7 +393,7 @@ fn read_prompt(params: Value) -> Result { return Ok(prompt_result( "Generate a safe Nightward fix workflow.", format!( - "Use nightward_explain_finding and nightward_fix_plan for finding `{}`. If a bounded registry action is relevant, use nightward_action_preview first. Do not call nightward_action_apply unless I explicitly accept the disclosure and provide confirm: true.", + "Use nightward_explain_finding and nightward_fix_plan for finding `{}`. If a bounded registry action is relevant, use nightward_action_preview first. MCP cannot apply local writes, so tell me how to apply the previewed action in the Nightward CLI, TUI, or Raycast extension.", if finding_id.is_empty() { "" } else { @@ -420,7 +403,7 @@ fn read_prompt(params: Value) -> Result { )); } "set_up_providers" => { - "Use nightward_providers and nightward_actions_list to show missing, blocked, selected, and online-capable providers. Recommend provider.install/provider.enable actions only through nightward_action_preview, and call out online/network behavior before any apply." + "Use nightward_providers and nightward_actions_list to show missing, blocked, selected, and online-capable providers. Recommend provider.install/provider.enable actions only through nightward_action_preview, call out online/network behavior, and tell me to apply writes in the Nightward CLI, TUI, or Raycast extension." } "compare_reports" => { "Use nightward_report_history and nightward_report_changes to compare the last two reports. Summarize new, removed, and changed findings, then recommend which changes actually matter." @@ -456,6 +439,11 @@ fn call_tool_inner(params: Value, home: &Path) -> Result { .get("name") .and_then(Value::as_str) .ok_or_else(|| anyhow!("tools/call requires a tool name"))?; + if name == "nightward_action_apply" { + return Err(anyhow!( + "nightward_action_apply is disabled in MCP because MCP clients cannot provide out-of-band local confirmation; use nightward_action_preview, then apply writes in the Nightward CLI, TUI, or Raycast extension" + )); + } let args = validate_tool_args( name, params @@ -611,25 +599,6 @@ fn call_tool_inner(params: Value, home: &Path) -> Result { } tool_result(sanitized_value(&actions::preview(home, &id)?)?) } - "nightward_action_apply" => { - let id = string_arg(&args, "action_id"); - if id.is_empty() { - return Err(anyhow!("action_id is required")); - } - let result = actions::apply( - home, - &id, - ApplyOptions { - confirm: bool_arg(&args, "confirm", false), - executable: string_arg(&args, "executable"), - policy_path: string_arg(&args, "policy_path"), - finding_id: string_arg(&args, "finding_id"), - rule: string_arg(&args, "rule"), - reason: string_arg(&args, "reason"), - }, - )?; - tool_result(sanitized_value(&result)?) - } "nightward_rules" => tool_result(json!({ "schema_version": 1, "rules": rules::all_rules() @@ -643,7 +612,6 @@ fn call_tool_inner(params: Value, home: &Path) -> Result { enum ToolArgKind { String, Bool, - ConfirmTrue, Limit, Severity, StringList, @@ -712,13 +680,6 @@ fn validate_arg_value(tool: &str, spec: ToolArgSpec, value: &Value) -> Result<() Err(anyhow!("{tool} argument `{}` must be a boolean", spec.name)) } } - ToolArgKind::ConfirmTrue => { - if value.as_bool() == Some(true) { - Ok(()) - } else { - Err(anyhow!("{tool} argument `{}` must be true", spec.name)) - } - } ToolArgKind::Limit => { let Some(value) = value.as_u64() else { return Err(anyhow!( @@ -830,15 +791,6 @@ fn tool_arg_specs(name: &str) -> Result> { ToolArgSpec::optional("head", String), ], "nightward_action_preview" => vec![ToolArgSpec::required("action_id", String)], - "nightward_action_apply" => vec![ - ToolArgSpec::required("action_id", String), - ToolArgSpec::required("confirm", ConfirmTrue), - ToolArgSpec::optional("executable", String), - ToolArgSpec::optional("policy_path", String), - ToolArgSpec::optional("finding_id", String), - ToolArgSpec::optional("rule", String), - ToolArgSpec::optional("reason", String), - ], _ => return Err(anyhow!("unknown tool {name}")), }; Ok(specs) @@ -1310,21 +1262,6 @@ fn schema_action_id() -> Value { ) } -fn schema_action_apply() -> Value { - schema_object( - json!({ - "action_id": { "type": "string" }, - "confirm": { "type": "boolean", "const": true }, - "executable": { "type": "string", "description": "Nightward executable path for schedule install actions." }, - "policy_path": { "type": "string", "description": "Optional policy path under NIGHTWARD_HOME for policy actions." }, - "finding_id": { "type": "string", "description": "Finding ID for policy.ignore." }, - "rule": { "type": "string", "description": "Rule ID for policy.ignore." }, - "reason": { "type": "string", "description": "Required reviewed reason for policy.ignore." } - }), - &["action_id", "confirm"], - ) -} - #[cfg(test)] mod tests { use super::*; @@ -1379,17 +1316,23 @@ mod tests { "nightward_report_changes", "nightward_actions_list", "nightward_action_preview", - "nightward_action_apply", "nightward_rules", "nightward_providers", ] { assert!(names.contains(name), "missing {name}"); } + assert!(!names.contains("nightward_action_apply")); assert!(tools .iter() .all(|tool| tool["inputSchema"]["additionalProperties"] == false)); assert!(tools.iter().all(|tool| tool.get("outputSchema").is_some())); assert!(tools.iter().all(|tool| tool.get("annotations").is_some())); + assert!(tools + .iter() + .all(|tool| tool["annotations"]["readOnlyHint"] == true)); + assert!(tools + .iter() + .all(|tool| tool["annotations"]["destructiveHint"] == false)); let resources_response = handle_request_with_home( json!({"jsonrpc":"2.0","id":2,"method":"resources/list"}), @@ -1450,17 +1393,19 @@ mod tests { } #[test] - fn action_apply_is_disclosure_gated_tool_result_error() { + fn action_apply_is_disabled_and_cannot_accept_disclosure() { let home = tempfile::tempdir().expect("temp home"); let response = handle_request_with_home( - json!({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"nightward_action_apply","arguments":{"action_id":"backup.snapshot","confirm":true}}}), + json!({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"nightward_action_apply","arguments":{"action_id":"disclosure.accept","confirm":true}}}), home.path(), ); assert_eq!(response["result"]["isError"], true); assert!(response["result"]["content"][0]["text"] .as_str() .unwrap() - .contains("disclosure")); + .contains("disabled in MCP")); + assert!(!state::disclosure_status(home.path()).accepted); + assert!(!state::settings_path(home.path()).exists()); } #[test] @@ -1483,10 +1428,6 @@ mod tests { json!({"name":"nightward_findings","arguments":{"severity":"urgent"}}), "known severity", ), - ( - json!({"name":"nightward_action_apply","arguments":{"action_id":"backup.snapshot","confirm":false}}), - "confirm` must be true", - ), ] { let response = handle_request_with_home( json!({"jsonrpc":"2.0","id":1,"method":"tools/call","params":arguments}), @@ -1567,7 +1508,7 @@ mod tests { } #[test] - fn action_apply_can_directly_apply_shared_registry_action() { + fn action_apply_remains_disabled_after_out_of_band_disclosure() { let home = tempfile::tempdir().expect("temp home"); fs::create_dir_all(home.path().join(".codex")).expect("codex dir"); fs::write(home.path().join(".codex/config.toml"), "model = \"test\"\n").expect("config"); @@ -1587,15 +1528,12 @@ mod tests { home.path(), ); - assert_eq!(response["result"]["isError"], false); - let writes = response["result"]["structuredContent"]["writes"] - .as_array() - .unwrap(); - assert!(!writes.is_empty()); - assert!(PathBuf::from(writes[0].as_str().unwrap()) - .join(".codex/config.toml") - .is_file()); - assert!(state::audit_path(home.path()).is_file()); + assert_eq!(response["result"]["isError"], true); + assert!(response["result"]["content"][0]["text"] + .as_str() + .unwrap() + .contains("disabled in MCP")); + assert!(!state::state_dir(home.path()).join("snapshots").exists()); } #[test] diff --git a/docs/mcp-server.md b/docs/mcp-server.md index a5ddedc..cdf7acb 100644 --- a/docs/mcp-server.md +++ b/docs/mcp-server.md @@ -6,17 +6,17 @@ Nightward includes a stdio MCP server: nw mcp serve ``` -The server is a first-class Nightward surface for AI clients. It exposes scan, analysis, policy, report, provider, rule, prompt, and bounded action workflows without requiring users to copy CLI commands out of chat. +The server is a first-class Nightward surface for AI clients. It exposes scan, analysis, policy, report, provider, rule, prompt, and bounded action preview workflows without granting AI clients local write access. ## Protocol Behavior - Negotiates MCP `2025-11-25` and remains compatible with `2025-06-18`. - Declares `tools`, `resources`, and `prompts` capabilities. - Provides strict tool input schemas with `additionalProperties: false`. -- Enforces those input schemas server-side, including unknown-key rejection, required fields, type checks, severity enums, `confirm: true`, and integer bounds. +- Enforces those input schemas server-side, including unknown-key rejection, required fields, type checks, severity enums, and integer bounds. - Returns `structuredContent` plus text fallback for tool results. - Reports tool execution failures as MCP tool results with `isError: true`. -- Adds output schemas and tool annotations for read-only, destructive, idempotent, and open-world hints. +- Adds output schemas and tool annotations for read-only, idempotent, and open-world hints. ## Exposed Tools @@ -32,19 +32,10 @@ The server is a first-class Nightward surface for AI clients. It exposes scan, a - `nightward_report_changes` - `nightward_actions_list` - `nightward_action_preview` -- `nightward_action_apply` - `nightward_rules` - `nightward_providers` -`nightward_action_apply` is intentionally narrow. It can apply only shared Nightward action-registry IDs, such as disclosure acceptance, policy init/ignore-with-reason, schedule install/remove where supported, backup snapshot, report/cache cleanup, provider install/enable/disable, and online-provider toggles. It cannot rewrite arbitrary MCP or agent config. - -Apply calls require: - -- accepted Nightward responsibility disclosure -- `confirm: true` -- action availability checks -- redacted output -- audit logging under Nightward state +MCP is read-only. It can show the shared action registry and preview exact write targets, command previews, risk levels, and blocked reasons. Applying those actions must happen out-of-band through the Nightward CLI, TUI, or Raycast extension, where Nightward can receive a local user confirmation that did not come from the MCP client. Cached or manual `nightward_action_apply` calls return an MCP tool-result error before the action registry is reached. ## Exposed Resources @@ -111,7 +102,7 @@ CI validates that `server.json` and `packages/npm/package.json` agree before the - No telemetry. - No default network calls. - Online-capable providers remain blocked unless explicitly allowed. -- Direct apply is limited to the shared action registry. +- MCP cannot apply local writes; action application is limited to CLI/TUI/Raycast surfaces with local confirmation. - No live MCP/agent config mutation in MCP v1. - Workspace and explicit report-diff paths must stay under `NIGHTWARD_HOME`, exist as the expected regular file or directory type, avoid symlink components, and pass the existing bounded report-size checks. - Tool/resource/prompt output is bounded and redacted before it reaches the client. diff --git a/docs/privacy-model.md b/docs/privacy-model.md index 544eb32..11a1322 100644 --- a/docs/privacy-model.md +++ b/docs/privacy-model.md @@ -13,7 +13,7 @@ Nightward is designed around local custody. The scanner inspects local file meta - No agent config mutation in scan, doctor, findings, fix, policy, or backup-plan commands. - The TUI can apply only shared action-registry operations after disclosure acceptance and an explicit confirmation keypress. - The Raycast extension exposes the same shared action registry and uses Raycast confirmation prompts before applying actions. -- The MCP server is stdio-only. It exposes read-only scan, analysis, policy, report, rule, provider, prompt, and resource context plus one write-capable path: `nightward_action_apply`, which can apply only shared action-registry operations after disclosure acceptance and `confirm: true`. +- The MCP server is stdio-only and read-only. It exposes scan, analysis, policy, report, rule, provider, prompt, resource, action-list, and action-preview context, but cannot apply local writes. ## Write Paths @@ -35,7 +35,7 @@ Confirmed action writes append audit events under `~/.local/state/nightward/audi Nightward-owned state writes reject symlinked directories and symlinked files before writing settings, audit logs, schedules, snapshots, or action-managed policy files. -`nw mcp serve` can write only through the shared action registry. MCP clients cannot request arbitrary file edits, live MCP/agent config rewrites, restore operations, Git pushes, or secret sync. Direct apply requires disclosure acceptance, an explicit `confirm: true` argument, action availability checks, redacted output, and audit logging. MCP tool arguments are validated server-side against strict schemas, and MCP workspace/report paths must stay under `NIGHTWARD_HOME`, exist as regular files or directories as appropriate, and avoid symlink components. +`nw mcp serve` cannot apply local writes. MCP clients cannot accept the Nightward disclosure, request arbitrary file edits, apply shared registry actions, live MCP/agent config rewrites, restore operations, Git pushes, or secret sync. MCP can list and preview shared action-registry operations so the user can apply them out-of-band in the CLI, TUI, or Raycast extension. MCP tool arguments are validated server-side against strict schemas, and MCP workspace/report paths must stay under `NIGHTWARD_HOME`, exist as regular files or directories as appropriate, and avoid symlink components. The TUI docs action opens an http(s) documentation URL through the OS default opener after the user presses `o`; Nightward itself does not fetch docs content. diff --git a/docs/testing.md b/docs/testing.md index ef6fecc..943d004 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -87,7 +87,7 @@ make verify - Badge artifact tests must cover pass/fail shape, policy summary fields, optional SARIF URL, and no-write stdout mode. - Golden-style tests should stay stable for JSON/SARIF shape, not timestamps or host-specific paths. Scan-summary goldens must keep item buckets separate from finding buckets. - MCP fixture tests should cover command servers, URL-shaped servers, sensitive headers, local/private endpoints, Docker/socket exposure, package provenance hints, stale configs, app-owned state, and unsupported shapes. -- MCP server protocol tests should cover initialize negotiation, tools/resources/prompts lists, strict input schemas, server-side invalid-argument rejection, MCP path scoping, structured output, annotations, tool-result errors, disclosure-gated apply, successful action-registry apply, and report-change failure paths. +- MCP server protocol tests should cover initialize negotiation, tools/resources/prompts lists, strict input schemas, server-side invalid-argument rejection, MCP path scoping, structured output, annotations, tool-result errors, disabled MCP action apply, disclosure self-accept rejection, out-of-band disclosure still not enabling MCP writes, and report-change failure paths. - Parser fuzz harnesses live under `fuzz/` and cover MCP JSON/TOML/YAML parsing, URL/header redaction, symlink traversal, huge-file handling, and malformed config cases. Run a bounded local fuzz check with `make fuzz-check`; run a single target directly with `cargo fuzz run mcp_config_formats -- -runs=1024`. - Provider contract tests use `testdata/providers/*` fixtures for `gitleaks`, `trufflehog`, `semgrep`, `trivy`, `osv-scanner`, `grype`, `syft`, `scorecard`, and `socket`. - Scheduler tests verify generated launchd, systemd user timer, and cron text without installing schedules. diff --git a/docs/threat-model.md b/docs/threat-model.md index 0eb4a54..1c6cae6 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -15,7 +15,7 @@ Nightward inspects local AI agent and devtool state, so its primary risk is acci - Local filesystem input is untrusted. Config files may be malformed, hostile, huge, symlinked, or privacy-sensitive. - CLI/TUI/Raycast output is a disclosure boundary. Secret values must not cross it. - Optional providers are execution boundaries. They are discovered, selected, and installed only through explicit action paths, unselected providers are skipped, online-capable providers are blocked until explicitly allowed, and provider timeout/output-cap failures are surfaced as warnings instead of clean results. Trivy, Grype, OSV-Scanner, OpenSSF Scorecard, and Socket are treated as online-capable when their normal operation can contact external services. -- MCP clients are agent boundaries. `nw mcp serve` exposes local context and bounded action application through stdio, so returned tool/resource/prompt content must stay redacted and writes must flow only through the action registry. +- MCP clients are agent boundaries. `nw mcp serve` exposes local context and bounded action previews through stdio, so returned tool/resource/prompt content must stay redacted and the server must not perform local writes. - GitHub Actions and Trunk integrations treat repository contents and PR input as untrusted. - Scheduler install/remove is explicit, confirmation-gated, and user-level only. - Release automation and npm publishing are privileged publishing boundaries. @@ -28,7 +28,7 @@ Nightward inspects local AI agent and devtool state, so its primary risk is acci - MCP execution ambiguity: flag shell wrappers, broad filesystem access, unpinned package execution, package-name impersonation risk, remote package sources, Docker/socket exposure, local/private endpoints, sensitive headers/env, token paths, stale configs, app-owned state, and unknown shapes. - Supply-chain compromise: pin GitHub Actions by full SHA, use Renovate, run Gitleaks/OSV/CodeQL/Clippy/Trunk, keep release artifacts signed, and keep the npm package as a no-postinstall launcher that verifies archive checksums, rejects unsafe archive entries, and can require Sigstore verification in strict environments. - Malformed config denial-of-service: keep parser/fuzz coverage for MCP JSON/TOML/YAML, URL/header redaction, symlink traversal, huge-file handling, and malformed configs. -- Agent overreach through MCP: expose write capability only through `nightward_action_apply`, not through arbitrary config mutation. Direct apply requires disclosure acceptance, `confirm: true`, registry action availability, redacted output, and audit logging. Tool inputs are validated against strict server-side schemas, and explicit workspace/report paths are scoped under `NIGHTWARD_HOME` with no-symlink regular-file/directory checks. Do not expose live MCP/agent config mutation, restore, sync, or HTTP listener behavior through MCP v1. +- Agent overreach through MCP: keep MCP read-only. It can list and preview registry actions, but it cannot accept the responsibility disclosure or apply local writes because MCP tool arguments are not an out-of-band user confirmation channel. Tool inputs are validated against strict server-side schemas, and explicit workspace/report paths are scoped under `NIGHTWARD_HOME` with no-symlink regular-file/directory checks. Do not expose live MCP/agent config mutation, restore, sync, HTTP listener behavior, or local write apply through MCP v1. ## Non-Goals diff --git a/site/index.md b/site/index.md index bb7008f..fe68526 100644 --- a/site/index.md +++ b/site/index.md @@ -92,7 +92,7 @@ The sample report below is generated from the committed `testdata/homes/policy` | Report history | Compare scan JSON files, inspect latest-report status, render filterable diff-aware HTML, and generate a static local report index. | | Policy and CI | Reason-required ignores, policy badges, SARIF output, GitHub Action mode, and Trunk plugin support. | | Providers | Local [Gitleaks](https://github.com/gitleaks/gitleaks), [TruffleHog](https://github.com/trufflesecurity/trufflehog), [Semgrep](https://semgrep.dev/), and [Syft](https://oss.anchore.com/syft/); online-gated [Trivy](https://trivy.dev/), [OSV-Scanner](https://google.github.io/osv-scanner/), [Grype](https://oss.anchore.com/grype/), [OpenSSF Scorecard](https://github.com/ossf/scorecard), and remote [Socket](https://socket.dev/) scan creation. | -| MCP server | Stdio tools/resources/prompts for local AI clients; direct apply only through shared action-registry IDs with disclosure, confirmation, redaction, and audit logging. | +| MCP server | Stdio tools/resources/prompts for local AI clients; read-only action list/preview with local writes applied through CLI/TUI/Raycast. | ## Trust Posture diff --git a/site/integrations/mcp-server.md b/site/integrations/mcp-server.md index 2d7590d..f1fc69f 100644 --- a/site/integrations/mcp-server.md +++ b/site/integrations/mcp-server.md @@ -2,13 +2,13 @@ -Nightward ships a stdio [Model Context Protocol](https://modelcontextprotocol.io/) server so AI clients can scan, explain, compare, plan, and apply bounded Nightward actions without sending users back to raw CLI commands. +Nightward ships a stdio [Model Context Protocol](https://modelcontextprotocol.io/) server so AI clients can scan, explain, compare, plan, and preview bounded Nightward actions. ```sh nw mcp serve ``` -The MCP surface is action-capable, but it is not an arbitrary file editor. Applies go only through the shared Nightward action registry and require the responsibility disclosure, `confirm: true`, redacted output, and audit logging. +The MCP surface is read-only. It can list and preview the shared Nightward action registry, but local writes must be applied out-of-band in the Nightward CLI, TUI, or Raycast extension. ## Client Setup @@ -32,7 +32,7 @@ VS Code-style clients use a different key: } ``` -Restart or reload the AI client after editing its MCP config. A useful first prompt is: "Audit my AI setup with Nightward, explain the top risks, and do not apply actions unless I explicitly confirm them." +Restart or reload the AI client after editing its MCP config. A useful first prompt is: "Audit my AI setup with Nightward, explain the top risks, and preview any relevant actions without applying writes." ## Tools @@ -50,7 +50,6 @@ Restart or reload the AI client after editing its MCP config. A useful first pro | `nightward_report_changes` | Compare two report files or the latest two saved reports. | Read-only. | | `nightward_actions_list` | List bounded registry actions. | Read-only. | | `nightward_action_preview` | Preview one registry action. | Read-only. | -| `nightward_action_apply` | Apply one registry action, including policy init/ignore-with-reason and report/cache cleanup actions. | Requires disclosure acceptance and `confirm: true`. | | `nightward_rules` | List rules and remediation metadata. | Read-only. | | `nightward_providers` | List provider capabilities and status. | Read-only. | @@ -85,7 +84,7 @@ Compact mode keeps pass/fail, threshold, summary counts, and bounded finding or - `set_up_providers` - `compare_reports` -These prompts are workflow starters for clients that expose MCP prompts. They are deliberately cautious: they tell the assistant to preview registry actions first and avoid apply unless the user explicitly confirms. +These prompts are workflow starters for clients that expose MCP prompts. They are deliberately cautious: they tell the assistant to preview registry actions and send actual writes through the CLI, TUI, or Raycast extension. ## Safety Model @@ -95,12 +94,12 @@ These prompts are workflow starters for clients that expose MCP prompts. They ar - Strict tool input schemas, server-side invalid-argument rejection, and structured output. - Tool execution failures return `isError: true`, not protocol crashes. - Online-capable providers stay blocked unless explicitly allowed. -- Direct apply is limited to shared action-registry IDs. +- MCP cannot apply local writes; action application is limited to CLI/TUI/Raycast surfaces with local confirmation. - No arbitrary MCP/agent config mutation in MCP v1. - Explicit workspace and report-diff paths must stay under `NIGHTWARD_HOME`, exist as the expected regular file or directory type, and avoid symlink components. -- Apply output is redacted and every successful apply appends an audit event. +- Preview output is redacted and exposes write targets before any out-of-band apply. -Use `nightward_action_preview` before `nightward_action_apply`. For package-manager provider installs, read the command, provider privacy boundary, and rollback expectations before confirming. +Use `nightward_action_preview` before applying an action in the CLI, TUI, or Raycast extension. For package-manager provider installs, read the command, provider privacy boundary, and rollback expectations before confirming outside MCP. ## Registry Package diff --git a/site/reference/distribution.md b/site/reference/distribution.md index cc84ff8..dfe8131 100644 --- a/site/reference/distribution.md +++ b/site/reference/distribution.md @@ -12,7 +12,7 @@ Nightward v0.1.4 is distributed through signed GitHub Releases and the npm launc | [GitHub Action](/integrations/github-action) | Shipped | Uses release tags for CI policy/SARIF workflows. | | [Trunk plugin import](/integrations/trunk) | Shipped | Imports the in-repo plugin from release tags. | | [Raycast extension](/integrations/raycast) | Development-ready | Local Raycast extension commands and menu-bar status; store PR still pending. | -| [MCP server](/integrations/mcp-server) | Shipped in CLI | Stdio tools/resources/prompts plus bounded direct apply through shared action-registry IDs. Registry metadata lives in `server.json`. | +| [MCP server](/integrations/mcp-server) | Shipped in CLI | Stdio tools/resources/prompts plus bounded read-only action list/preview. Registry metadata lives in `server.json`. | ## Later Channels diff --git a/site/reference/output-surfaces.md b/site/reference/output-surfaces.md index d81742c..ce91a67 100644 --- a/site/reference/output-surfaces.md +++ b/site/reference/output-surfaces.md @@ -15,7 +15,7 @@ | Fix export | `nw fix export --format markdown` | stdout only | | Actions list/preview | `nw actions list --json`, `nw actions preview --json` | stdout only | | Actions apply | `nw actions apply --confirm` | disclosure-accepted, confirmation-gated provider, policy, schedule, backup, or settings writes | -| MCP server | `nw mcp serve` | stdio JSON-RPC only; read tools plus shared action-registry apply with disclosure acceptance, `confirm: true`, and audit logging | +| MCP server | `nw mcp serve` | stdio JSON-RPC only; read tools plus shared action-registry list/preview, no local writes | | Schedule install/remove | `nw schedule install --confirm`, `nw schedule remove --confirm` | user-level launchd/systemd files only | | Backup snapshot | `nw backup create --confirm` | local snapshot under Nightward state | @@ -26,5 +26,5 @@ Labels used in docs: - `online-capable`: can invoke provider behavior that contacts a network service. - `plan-only`: generates review material without mutating live config. - `confirmed action`: mutates only after explicit preview and confirmation. -- `mcp direct apply`: mutates only through a shared Nightward action ID; never arbitrary agent/MCP config rewrites. +- `mcp action preview`: shows shared Nightward action write targets and risks without applying local writes. - `future/not shipped`: documented as roadmap, not a current interface. diff --git a/site/reference/support-matrix.md b/site/reference/support-matrix.md index 7fe9dec..9838f38 100644 --- a/site/reference/support-matrix.md +++ b/site/reference/support-matrix.md @@ -46,7 +46,7 @@ Run `nw adapters list --json` or `nw adapters explain ` for the exact path | --- | --- | --- | | CLI/TUI | Shipped | Local read-only scan/report flows plus explicit output/export files | | [Raycast](/integrations/raycast) | Shipped in-repo | Read-only commands, menu-bar status, clipboard/report-folder actions | -| [MCP server](/integrations/mcp-server) | Shipped | Stdio-only tools/resources/prompts; direct apply only through shared action-registry actions with disclosure, confirmation, redaction, and audit logging | +| [MCP server](/integrations/mcp-server) | Shipped | Stdio-only tools/resources/prompts; read-only action list/preview, with writes applied through CLI/TUI/Raycast | | [GitHub Action](/integrations/github-action) | Shipped | CI policy/SARIF output against repository fixtures/workspaces | | [Trunk](/integrations/trunk) | Shipped | Repo-owned plugin definition; users pin to a Nightward tag or SHA | diff --git a/site/security/threat-model.md b/site/security/threat-model.md index 4e474a8..40e3f36 100644 --- a/site/security/threat-model.md +++ b/site/security/threat-model.md @@ -16,7 +16,7 @@ Nightward's primary asset is local AI/devtool state: config files, MCP server de - Redaction across JSON, SARIF, Markdown, TUI, and Raycast output. - No default network calls. - Explicit online-provider opt-in. -- MCP direct apply is limited to registry action IDs with disclosure acceptance, `confirm: true`, redaction, and audit logging. +- MCP is read-only: it can list and preview registry actions, but cannot accept disclosure or apply local writes. - MCP tool inputs are validated server-side, and explicit workspace/report paths are scoped under `NIGHTWARD_HOME` with no-symlink regular-file/directory checks. - GitHub Actions pinned by full SHA. - Signed release checksums and SBOMs. diff --git a/site/test/docs-contract.test.mjs b/site/test/docs-contract.test.mjs index faa5804..c5bd016 100644 --- a/site/test/docs-contract.test.mjs +++ b/site/test/docs-contract.test.mjs @@ -106,7 +106,7 @@ test("MCP docs list every runtime tool, resource, and prompt", { timeout: 60000 const missing = [...tools, ...resources, ...prompts].filter((value) => !docs.includes(value)); assert.deepEqual(missing, []); - assert.equal(tools.length, 15); + assert.equal(tools.length, 14); assert.equal(resources.length, 8); assert.equal(prompts.length, 5); } finally {