From df9332cd4c870e7a98812c66282f160720e47396 Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 16:12:04 -0500 Subject: [PATCH 01/14] docs: handle bounded MCP lists and strict tenant arguments Explain partial fleet discovery, list limits, cursor-page retry, tenantless tools and the config/settings alias. Keep absent apps from being treated as healthy or missing. Requires the matching N|Solid Console MCP server release. --- skills/ns-advanced-memory-leak-hunter/SKILL.md | 2 +- skills/ns-analyze-asset/SKILL.md | 2 +- skills/ns-analyze-behaviors/SKILL.md | 2 +- skills/ns-analyze-event/SKILL.md | 2 +- skills/ns-analyze-logs/SKILL.md | 4 ++-- skills/ns-analyze-release/SKILL.md | 2 +- skills/ns-analyze-tracing/SKILL.md | 4 ++-- skills/ns-analyze-vulnerabilities/SKILL.md | 6 +++--- skills/ns-configure-agents/SKILL.md | 7 ++++--- skills/ns-cpu-spike-analysis/SKILL.md | 2 +- skills/ns-download-asset/SKILL.md | 2 +- skills/ns-fleet-health/SKILL.md | 14 +++++++++----- skills/ns-generate-asset/SKILL.md | 2 +- skills/ns-generate-sbom/SKILL.md | 4 ++-- skills/ns-manage-alerts/SKILL.md | 8 ++++---- skills/ns-memory-spike-analysis/SKILL.md | 2 +- skills/ns-switch-org/SKILL.md | 7 ++++--- 17 files changed, 39 insertions(+), 33 deletions(-) diff --git a/skills/ns-advanced-memory-leak-hunter/SKILL.md b/skills/ns-advanced-memory-leak-hunter/SKILL.md index d3216db..1786a2d 100644 --- a/skills/ns-advanced-memory-leak-hunter/SKILL.md +++ b/skills/ns-advanced-memory-leak-hunter/SKILL.md @@ -8,7 +8,7 @@ This skill reads N|Solid data through the N|Solid MCP server (`nsolid`) and bund ### Phase 0: Reuse Evidence and Resolve Target 1. If the user already supplied both baseline and peak evidence (asset IDs or local files), skip straight to Phase 4. -2. The N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names. Pass it to every call, and set `NSOLID_TENANT` for the scripts. +2. Most N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Pass it to every call, and set `NSOLID_TENANT` for the scripts. 3. If no `agentId` is supplied, call `fleet_snapshot` with `where: 'app == ""'` and `sort: "used_heap_size"` to find the process; keep the exact `appName` for downloads. Stop if no matching process is connected. 4. A leak can sit in a worker thread while the main thread looks fine: `list_threads` with the `agentId` shows each thread's heap. Capture the thread that grows (`threadId`). diff --git a/skills/ns-analyze-asset/SKILL.md b/skills/ns-analyze-asset/SKILL.md index 904f885..dbc3b9c 100644 --- a/skills/ns-analyze-asset/SKILL.md +++ b/skills/ns-analyze-asset/SKILL.md @@ -11,7 +11,7 @@ This skill reads assets through the N|Solid MCP server (`nsolid`) and bundled sc When the user asks to restore a deleted profile or snapshot, pick the tenant (step 1), then go straight to [Restoring a Deleted Asset](#restoring-a-deleted-asset) instead of steps 2–10. Every other request follows the steps below. ### 1. Pick the Tenant -- The N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names. Pass the same tenant to every call, and set `NSOLID_TENANT` for the scripts. +- Most N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Pass the same tenant to every tenant-scoped call, and set `NSOLID_TENANT` for the scripts. ### 2. Identify the Asset - The user should provide an asset ID or an app name with asset type. diff --git a/skills/ns-analyze-behaviors/SKILL.md b/skills/ns-analyze-behaviors/SKILL.md index afe2a75..98d70ab 100644 --- a/skills/ns-analyze-behaviors/SKILL.md +++ b/skills/ns-analyze-behaviors/SKILL.md @@ -4,7 +4,7 @@ description: >- Investigates what N|Solid's behavioral watch found: npm packages that newly run programs, compile code, use the network, start threads or touch the file system in production, packages that start running at all, release regressions and dependency drift. Use when the user asks about supply-chain attacks at run time, a compromised or suspicious dependency, what a package started doing, a behavior alert, or dependencies that changed without a release. --- -This skill works through the N|Solid MCP server (`nsolid`). The N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names. +This skill works through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. The watch learns what each app's packages do from its CPU profiles, and reports a package doing something it never did before in that app, once the app has a history without it. diff --git a/skills/ns-analyze-event/SKILL.md b/skills/ns-analyze-event/SKILL.md index 8fa94ed..3c18af7 100644 --- a/skills/ns-analyze-event/SKILL.md +++ b/skills/ns-analyze-event/SKILL.md @@ -4,7 +4,7 @@ description: >- Investigates an existing N|Solid event or N|Solid alert and correlates the relevant evidence from N|Solid. Use when the user provides or references an event or alert such as a blocked event loop, a process exit or crash, an uncaught exception, a new vulnerability, a new package behavior, a stack trace, or span data. Also use when the user asks why an agent dropped, disconnected or reconnected, even without a specific event or alert. Routes follow-up to CPU, memory, vulnerability, or tracing workflows when needed. --- -This skill reads N|Solid data through the N|Solid MCP server (`nsolid`). The N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the event or user names. +This skill reads N|Solid data through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the event or user names. **No event, just a drop or reconnect.** When the user asks why an app or process dropped, disconnected or reconnected without giving an event or alert, skip parsing: resolve the app or process they name with `find_agents` to get its actual `agentId` (ask only for the missing app or process when they named neither), then go straight to **Lifecycle** in step 2. Use the time window they gave for `list_events`, or say which recent window you used (such as `-1h`) when they gave none; `agent_sessions` takes no `from`/`to`. diff --git a/skills/ns-analyze-logs/SKILL.md b/skills/ns-analyze-logs/SKILL.md index 92e5bc2..9919856 100644 --- a/skills/ns-analyze-logs/SKILL.md +++ b/skills/ns-analyze-logs/SKILL.md @@ -4,7 +4,7 @@ description: >- Searches and analyzes the logs N|Solid keeps from Node.js apps (nsolid.logger and OpenTelemetry logs): errors and warnings by app, process, text or severity, a request's logs (by its trace, for OpenTelemetry logs), and how they line up with exits, blocked loops and slow traces. Use when the user asks what an app logged, to find errors in production logs, what happened around an incident, or for the logs of a specific request or trace. --- -This skill reads logs through the N|Solid MCP server (`nsolid`). The N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names. +This skill reads logs through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. N|Solid keeps logs for 14 days by default. Apps send them through the N|Solid logger (`nsolid.logger`) or an OpenTelemetry SDK. Only OpenTelemetry logs carry trace and span ids: `nsolid.logger` lines have none yet, so they are found by process and time rather than by trace. @@ -14,7 +14,7 @@ N|Solid keeps logs for 14 days by default. Apps send them through the N|Solid lo - `severity`: the least severe to include (e.g. `warn`); - `agentId`: one process (`find_agents` finds them by app or host); - `traceId`: one request's logs (OpenTelemetry logs only); - - `where`: an expression, e.g. `level >= "error" && service == "checkout"`, `body.contains("ECONNREFUSED")`, `attr["http.route"] == "/orders"` (`expression_help` with context `log` lists the fields). + - `where`: an expression, e.g. `level >= "error" && service == "checkout"`, `body.contains("ECONNREFUSED")`, `attr["http.route"] == "/orders"` (`expression_help` with context `log` lists the fields; it takes no `tenant`). - Start narrow (an app, errors, the incident's window) and widen only as needed. ### 2. Correlate diff --git a/skills/ns-analyze-release/SKILL.md b/skills/ns-analyze-release/SKILL.md index fa0f8aa..6987daa 100644 --- a/skills/ns-analyze-release/SKILL.md +++ b/skills/ns-analyze-release/SKILL.md @@ -4,7 +4,7 @@ description: >- Explains what a release of a Node.js app changed in production, from N|Solid's CPU profiles and package inventories: CPU by package and function against the release before (with confidence), calls between packages that are new or gone, packages added, removed or upgraded, regressions and dependency drift. Use when the user asks whether a deploy made things slower, what changed in version X, why the app behaves differently since a release, or wants releases marked or sourcemaps uploaded from CI. --- -This skill works through the N|Solid MCP server (`nsolid`). The N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names (production and staging are usually separate tenants). +This skill works through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names (production and staging are usually separate tenants). A release is a version of the app as its processes report it (`package.json`'s `version`). N|Solid records a release when its processes first report it, and CI can mark a deploy with its commit and a link. diff --git a/skills/ns-analyze-tracing/SKILL.md b/skills/ns-analyze-tracing/SKILL.md index be888a9..4eb9773 100644 --- a/skills/ns-analyze-tracing/SKILL.md +++ b/skills/ns-analyze-tracing/SKILL.md @@ -4,7 +4,7 @@ description: >- Analyzes N|Solid/OpenTelemetry tracing evidence from N|Solid for request latency, HTTP errors, and service dependencies. Use when the user asks about slow endpoints, API timeouts, distributed traces/spans, N+1 queries, await chains, microservice latency, cascading failures, or trace IDs. Query N|Solid only when no trace data was provided. --- -This skill reads traces through the N|Solid MCP server (`nsolid`). The N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names. +This skill reads traces through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. ### 1. Use Provided Trace Data First - Treat the prompt as primary evidence. Parse any provided trace ID, span list, app name, endpoint name, status code, duration, or error stack before calling tools. @@ -13,7 +13,7 @@ This skill reads traces through the N|Solid MCP server (`nsolid`). The N|Solid t ### 2. Discover Services Only When Needed - **Skip this step if sufficient host-provided trace data is present**, or when the user names the service or app. -- Call `list_apps` for the app names. A span's `service` is its app's name. +- Call `list_apps` for the app names. A span's `service` is its app's name. An app missing from an omitted or truncated answer is not proven absent: pass the named app straight to the tools that take it, or ask the user for its exact name. - The N|Solid server doesn't collect AWS Lambda functions; if the user asks about a serverless function, say so. ### 3. Find Slow Requests Only If No Trace Was Supplied diff --git a/skills/ns-analyze-vulnerabilities/SKILL.md b/skills/ns-analyze-vulnerabilities/SKILL.md index d06bbf5..3cf7a89 100644 --- a/skills/ns-analyze-vulnerabilities/SKILL.md +++ b/skills/ns-analyze-vulnerabilities/SKILL.md @@ -4,13 +4,13 @@ description: >- Analyzes live runtime vulnerabilities in the packages connected N|Solid Node.js processes run, from N|Solid. Use when the user wants to know which CVEs are actually present in running apps, active exploitable/live vulnerabilities, malicious packages, zero-day exposure, supply-chain risk in production, Node.js runtime advisories, or production package risk. For static npm-audit-style project dependency scans, use ns-audit-dependencies instead. --- -This skill reads vulnerability findings through the N|Solid MCP server (`nsolid`). The N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names (production and staging are usually separate tenants). +This skill reads vulnerability findings through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names (production and staging are usually separate tenants). ### 1. High-Level Overview - Call `list_vulnerabilities`: open findings, most severe first. Each names its advisory (GHSA, a malicious-package report MAL, or a Node.js core advisory NODE-CORE), the app, severity, the vulnerable packages with the version that fixes them, the app's own dependencies that bring each in (`via`), and the runtime evidence: `installed`, `loaded` (required by the app) or `running` (seen in CPU profiles). A finding whose CVE CISA lists as exploited in the wild carries `knownExploited`: the CVE, when CISA listed it, its remediation due date, and whether ransomware campaigns use it. -- Narrow large fleets with `app`, `severity` (at least), or `where`, e.g. `severity >= "high" && evidence >= "loaded"`, or `knownExploited` (`expression_help` with context `finding` lists the fields). +- Narrow large fleets with `app`, `severity` (at least), or `where`, e.g. `severity >= "high" && evidence >= "loaded"`, or `knownExploited` (`expression_help` with context `finding` lists the fields; it takes no `tenant`). - Summarize which app has the most critical issues. Lead with malicious packages (MAL-…) when there are any, then known exploited findings in loaded or running code: attackers use those now, whatever their severity. -- Before saying an app has nothing to fix, `vulnerability_data` shows whether the advisory data is current: when the OSV mirror and CISA's catalog were last checked, and any sync error. Say so when it is stale. +- Before saying an app has nothing to fix, `vulnerability_data` (it takes no `tenant`) shows whether the advisory data is current: when the OSV mirror and CISA's catalog were last checked, and any sync error. Say so when it is stale. - For the Node.js runtime itself, `list_runtimes` gives each Node.js version in use: its core advisories, the release that fixes them, and whether its line is at or near end of life. ### 2. Detail Per App diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index e383d4f..d8b0e7f 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -4,18 +4,19 @@ description: >- Changes how N|Solid agents run, safely: tracing on or off, continuous CPU profiling, the metrics interval, the blocked-loop threshold, trace sampling, snapshot redaction, tags. Use when the user wants tracing turned on for an app, continuous profiling for a while, a different metrics interval, asks why a process has a setting, or asks for a named config's history: who changed it and what it held before. Previews every change, prefers changes that expire, and explains where each running value came from. --- -This skill works through the N|Solid MCP server (`nsolid`). The N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names. Changing configuration needs the `nsolid:dynamic-configuration` permission; without it the change tools are missing, and the skill can still explain and preview. +This skill works through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Changing configuration needs the `nsolid:dynamic-configuration` permission; without it the change tools are missing, and the skill can still explain and preview. N|Solid keeps agents at **desired configs**: each targets processes by an expression (or an app, tags or one agent), has a priority, and can expire. Processes started later get them too, and N|Solid confirms each change from the agent's own report. That beats changing one process once, which a restart forgets. ### 1. Understand What Runs Now -- `list_desired_configs`: the configs the tenant keeps, with their targets, settings and priority; ones with `expiresAt` are temporary overrides. +- `list_desired_configs`: the configs the tenant keeps, with their targets, settings (as `config`) and priority; ones with `expiresAt` are temporary overrides. - `agent_config` with an `agentId`: what that process reports, what it should have and which config set each key, what is still to apply, and whether it has converged. Use it to answer "why does this process have that setting?" - `desired_config_history` with a config's `name`: its 50 newest saves and deletes, each with the settings, target, priority, expiry, who made it and when. Deleted configs keep their history. Read it before changing or deleting a named config, to see who set it and what it held before. No tool restores an earlier version: to go back to one, make it a new change and preview and apply it as below. ### 2. Shape the Change +- `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. `list_desired_configs` names those settings `config`; its `where` is an expression tree, while `whereText` is the expression string the input accepts. Do not pass the entire returned row as tool arguments. Selectors and configuration subtrees are kept whole, but their entire entry can be omitted by the byte cap: a missing name in a partial list is not proof it is absent. - Settings: `tracingEnabled`, `traceSampleRate` (0 to 1), `contCpuProfile` (continuous CPU profiling), `interval` (metrics, ms), `blockedLoopThreshold` (ms), `pauseMetrics`, `promiseTracking`, `redactSnapshots`, `assetsEnabled`, `tags`. Turning `contCpuProfile` on needs the tenant's continuous-profiling feature: where N|Solid has it off, the save is refused, and processes stay off whatever a config says. -- Target: `app`, `tags`, one `agentId`, or `where`, an expression over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. +- Target: `app`, `tags`, one `agentId`, or `where`, an expression over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields; it takes no `tenant`). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. - **Prefer an expiring override** for anything diagnostic: `expiresInMinutes` makes it stop applying, and the processes go back to what they had. Tracing or continuous profiling "for the next hour" is an override of 60 minutes. - `priority`: higher wins where configs disagree. - Names use letters, digits, `.`, `_` and `-`: `checkout-tracing-1h`. diff --git a/skills/ns-cpu-spike-analysis/SKILL.md b/skills/ns-cpu-spike-analysis/SKILL.md index 04e23b1..17c2744 100644 --- a/skills/ns-cpu-spike-analysis/SKILL.md +++ b/skills/ns-cpu-spike-analysis/SKILL.md @@ -18,7 +18,7 @@ when the token can use several tenants). - If the user explicitly says read-only, offline, or "analyze this data", never capture a new profile unless they later approve it. ### 2. Pick the Tenant -- The N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user or alert names (production and staging are usually separate tenants). Pass the same tenant to every call, and set `NSOLID_TENANT` for the scripts. +- Most N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user or alert names (production and staging are usually separate tenants). Pass the same tenant to every tenant-scoped call, and set `NSOLID_TENANT` for the scripts. ### 3. Discover Connected Agents Only When Needed - Call `fleet_snapshot` with `sort: "cpu_percent"` to list connected processes, hottest first, with their live metrics. When the user named an app, add `where: 'app == ""'`. diff --git a/skills/ns-download-asset/SKILL.md b/skills/ns-download-asset/SKILL.md index efb30bc..60effff 100644 --- a/skills/ns-download-asset/SKILL.md +++ b/skills/ns-download-asset/SKILL.md @@ -10,7 +10,7 @@ This skill downloads N|Solid assets with a bundled script, finding them through ### 1. Pick the Tenant -- The N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names. Set `NSOLID_TENANT` for the script. +- Most N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Set `NSOLID_TENANT` for the script. ### 2. Identify the Asset diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 2d70100..5f184dc 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -4,25 +4,29 @@ description: >- Gives a health check of the Node.js processes N|Solid watches: what is hot or growing, what is crashing or blocking its event loop, what is alerting, and new vulnerabilities, risky Node.js versions and suspicious dependency behavior. Use when the user asks how production (or an app) is doing, what needs attention, for a daily or weekly briefing, or to find processes matching a condition such as "high memory on Node 20". --- -This skill reads N|Solid through its MCP server (`nsolid`). The N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` once and check each tenant the user cares about (production and staging are usually separate tenants). +This skill reads N|Solid through its MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and check each tenant the user cares about (production and staging are usually separate tenants). ### 1. Scope -- The whole tenant by default, or the app the user names. `list_apps` lists the apps with their processes by status and the app, N|Solid and Node.js versions in use. +- The whole tenant by default, or the app the user names. `list_apps` with `limit: 50` lists the apps with their processes by status and the app, N|Solid and Node.js versions in use. + +### Bounded Lists +- Read `total`, `returned`, `truncated`, `appsOmitted` and any `outputCut` notice before drawing conclusions. List results stay within 20,000 serialized UTF-8 bytes, so fewer entries may fit than the requested limit. An omitted app or process is not proof that it is absent or healthy; say when the briefing covers only part of the tenant. +- Narrow with arguments the tool takes. `list_apps` has no app filter or cursor; for a named app, use the app-scoped calls below. Full lists that cannot be narrowed through MCP need the Console or REST API. Cursor-paginated lists refuse an oversized page whole: retry the same request and cursor with a lower `limit`; if limit 1 still fails, read that record through REST. ### 2. Gather the Picture Run what fits the question; for a full briefing, all of it: - **Hot processes:** `fleet_snapshot` with `sort: "cpu_percent"`, then `sort: "used_heap_size"` and `sort: "loop_estimated_lag"` (limit 5 each; add `where: 'app == ""'` for one app). Note anything far from its peers. - **Growing memory:** `query_metric` with `metric: "used_heap_size"`, `groupBy: "app"` and `from: "-24h"`: a series that only climbs needs a look (`ns-memory-spike-analysis`). - **Latency:** `query_metric` with `metric: "http_server"` (the p99) and `groupBy: "app"`. -- **Crashes and restarts:** `list_ended_processes` with `byApp: true` and `from: "-24h"`: how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. +- **Crashes and restarts:** `list_ended_processes` with `byApp: true`, `limit: 25` and `from: "-24h"`: how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. For one app, pass `app` without `byApp`; tenant-wide `byApp` refuses `app`, `host` and `ending`. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. -- **Custom conditions:** `fleet_snapshot` takes an expression mixing metrics and what a process is, e.g. `rss > 1GiB && nodeVersion < "22"`, `cpu_percent > 80 && "canary" in tags`. `expression_help` with context `fleet` lists the fields. `save_view` keeps a useful expression under a name for people and tools (with the user's agreement). +- **Custom conditions:** `fleet_snapshot` takes an expression mixing metrics and what a process is, e.g. `rss > 1GiB && nodeVersion < "22"`, `cpu_percent > 80 && "canary" in tags`. `expression_help` with context `fleet` lists the fields (it takes no `tenant`). `save_view` keeps a useful expression under a name for people and tools (with the user's agreement). ### 3. Prioritize - Order by impact: crashes and firing critical alerts, then malicious or loaded high-severity vulnerabilities and end-of-life runtimes, then resource and latency outliers, then the rest. diff --git a/skills/ns-generate-asset/SKILL.md b/skills/ns-generate-asset/SKILL.md index ff2ad4a..12a8657 100644 --- a/skills/ns-generate-asset/SKILL.md +++ b/skills/ns-generate-asset/SKILL.md @@ -7,7 +7,7 @@ description: >- This skill captures assets through the N|Solid MCP server (`nsolid`) and saves them with a bundled script. The script needs the server's settings: `NSOLID_API_URL` and `NSOLID_API_TOKEN` in the environment, or `~/.agents/.nsolid-auth.json` with `apiUrl` and `token` (plus `tenant` when the token can use several tenants). ### 1. Pick the Tenant -- The N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names (production and staging are usually separate tenants). Pass the same tenant to every call, and set `NSOLID_TENANT` for the script. +- Most N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names (production and staging are usually separate tenants). Pass the same tenant to every tenant-scoped call, and set `NSOLID_TENANT` for the script. ### 2. Resolve Scope - The target app is already resolved (workspace mapping, explicit `Application: `, or user confirmation). Do not switch apps. diff --git a/skills/ns-generate-sbom/SKILL.md b/skills/ns-generate-sbom/SKILL.md index 2bedc59..1ceae02 100644 --- a/skills/ns-generate-sbom/SKILL.md +++ b/skills/ns-generate-sbom/SKILL.md @@ -9,11 +9,11 @@ This skill reads SBOMs through the N|Solid MCP server (`nsolid`) and a bundled s ## Instructions ### 1. Pick the Tenant -- The N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names. Set `NSOLID_TENANT` for the script. +- Most N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Set `NSOLID_TENANT` for the script. ### 2. Identify Target Application - Determine the specific `app` name you need the SBOM for. -- If the app name is unknown, call `list_apps` for the apps and their process counts. +- If the app name is unknown, call `list_apps` for the apps and their process counts. An app missing from an omitted or truncated answer is not proven absent: pass the named app straight to `get_sbom`, or ask the user for its exact name. - An app's SBOM covers its connected processes together. For one process instead, find its `agentId` with `find_agents` (`app`). ### 3. Know What It Contains diff --git a/skills/ns-manage-alerts/SKILL.md b/skills/ns-manage-alerts/SKILL.md index d1e0024..2098b06 100644 --- a/skills/ns-manage-alerts/SKILL.md +++ b/skills/ns-manage-alerts/SKILL.md @@ -4,12 +4,12 @@ description: >- Defines, reviews, tunes and triages N|Solid alert rules and alerts. Use when the user wants to be alerted or paged on something (CPU, memory, event loop lag, latency, crashes or a crash loop, a process killed or lost, an app that stops reporting, blocked loops, new or known exploited vulnerabilities, a dependency's new behavior or a release regression), asks what alerts exist, which are firing or whether a rule is working, wants to acknowledge or silence an alert or end a silence early, switch a rule off, or wants a noisy rule fixed. Writes rules in N|Solid's expression language and checks what each would have caught before saving it. --- -This skill works through the N|Solid MCP server (`nsolid`). The N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user names. Saving rules, acknowledging or silencing alerts and ending a silence need the `nsolid:manage-alerts` permission; without it those tools are missing, and the skill can still review and draft. +This skill works through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Saving rules, acknowledging or silencing alerts and ending a silence need the `nsolid:manage-alerts` permission; without it those tools are missing, and the skill can still review and draft. ## Start a Newly Connected Service's Baseline -When setting up alerts for a newly connected service, call `list_alert_starters`. -It returns the same starters the web editor offers, each with an id, title, +When setting up alerts for a newly connected service, call `list_alert_starters` +(it takes no `tenant`). It returns the same starters the web editor offers, each with an id, title, reason (`why`) and a `spec` in `set_alert_rule`'s terms. Read the current list; do not keep copies of starter definitions or reconstruct their thresholds. @@ -51,7 +51,7 @@ do not keep copies of starter definitions or reconstruct their thresholds. - **Event rule**: something that happens ("a process crashes", "the loop blocks over a second"). `event.where` over agent events: `type == "exit" && exitCode != 0`, `exitSignal == "SIGABRT"`, `type == "loop_blocked" && blockedFor > 1s`, `type == "lost"` (a process gone 15 minutes without an exit: SIGKILL, the out-of-memory killer, its host lost; the alert comes 15 minutes after it was last heard from). `resolveAfterMinutes` closes it after a quiet spell. `count` with `withinMinutes` fires only once that many matching events of a group came within the window ("3 exits in 10 minutes": `count: 3`, `withinMinutes: 10`); until then it is pending. - **Finding rule**: new vulnerability findings. `finding.where`, e.g. `severity >= "high" && evidence >= "loaded"`, `knownExploited` (CISA lists its CVE as exploited in the wild), `kind == "malicious"`, `kind == "end-of-life"`, `"express" in via`. Only findings that open after the rule is saved alert. - **Behavior rule**: a dependency doing something new, a release regression, or dependency drift. `behavior.where`, e.g. `kind == "capability" && severity >= "high"`, `kind == "regression" && severity >= "medium"`, `kind == "drift" && severity >= "high"`, `versionChange`. -- `expression_help` (contexts `agent`, `event`, `finding`, `behavior`) lists every field when the request needs one this skill doesn't name. +- `expression_help` (contexts `agent`, `event`, `finding`, `behavior`; it takes no `tenant`) lists every field when the request needs one this skill doesn't name. ### 2. Scope It - `scope` limits metric and event rules to some processes, as an agent expression: `app == "checkout"`, `"canary" in tags`, `hostname.endsWith(".prod")`. Finding and behavior rules take no scope: put `app == "…"` in their `where`. diff --git a/skills/ns-memory-spike-analysis/SKILL.md b/skills/ns-memory-spike-analysis/SKILL.md index b58b9bd..364382a 100644 --- a/skills/ns-memory-spike-analysis/SKILL.md +++ b/skills/ns-memory-spike-analysis/SKILL.md @@ -13,7 +13,7 @@ This skill reads N|Solid data through the N|Solid MCP server (`nsolid`) and bund - If the user explicitly says read-only, offline, or "analyze this file", do not capture a new heap asset unless they later approve it. ### 2. Pick the Tenant -- The N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` once and use the tenant the user or alert names. Pass the same tenant to every call, and set `NSOLID_TENANT` for the scripts. +- Most N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user or alert names. Pass the same tenant to every tenant-scoped call, and set `NSOLID_TENANT` for the scripts. ### 3. Find the Bottleneck Only If You Still Need a Target - If no agent is known, call `fleet_snapshot` with `where: 'app == ""'` and `sort: "used_heap_size"`, and keep the exact app name. diff --git a/skills/ns-switch-org/SKILL.md b/skills/ns-switch-org/SKILL.md index 4e5e26b..0fd29b8 100644 --- a/skills/ns-switch-org/SKILL.md +++ b/skills/ns-switch-org/SKILL.md @@ -59,10 +59,11 @@ work around a failed OAuth flow (e.g. by editing The skills that read N|Solid data use the N|Solid MCP server (`nsolid`), which has settings of its own: switching NodeSource organizations doesn't change it. A N|Solid token can reach several tenants, such as an organization's production and -staging. Its tools take a `tenant`, and `list_tenants` shows the tenants the -token reaches. If the user wants N|Solid data from another tenant: +staging. Most of its tools take a `tenant`, and `list_tenants` (which takes +none) shows the tenants the token reaches. If the user wants N|Solid data from +another tenant: -- Pass that tenant to the tools from now on. +- Pass that tenant to the tenant-scoped tools from now on. - To change the default the skills' scripts use, the user sets `NSOLID_TENANT`, or `tenant` in `~/.agents/.nsolid-auth.json`. Tell them; don't edit the file yourself. From 5291b487780af2543890654eb38a7a2d30ad51ec Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 19:49:17 -0500 Subject: [PATCH 02/14] fix: handle partial MCP evidence before saving rules Verify an omitted rule name through a complete authorized source before treating it as new, while reusing authorization for the same action and target. Preserve positive matches from partial lists. Distinguish samples, lower-bound totals and incomplete vulnerability/runtimes evidence in the skills that consume bounded lists. --- skills/ns-advanced-memory-leak-hunter/SKILL.md | 2 +- skills/ns-analyze-vulnerabilities/SKILL.md | 5 +++-- skills/ns-fleet-health/SKILL.md | 10 +++++----- skills/ns-manage-alerts/SKILL.md | 17 +++++++++++++---- skills/ns-node-upgrade/SKILL.md | 3 ++- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/skills/ns-advanced-memory-leak-hunter/SKILL.md b/skills/ns-advanced-memory-leak-hunter/SKILL.md index 1786a2d..9306596 100644 --- a/skills/ns-advanced-memory-leak-hunter/SKILL.md +++ b/skills/ns-advanced-memory-leak-hunter/SKILL.md @@ -8,7 +8,7 @@ This skill reads N|Solid data through the N|Solid MCP server (`nsolid`) and bund ### Phase 0: Reuse Evidence and Resolve Target 1. If the user already supplied both baseline and peak evidence (asset IDs or local files), skip straight to Phase 4. -2. Most N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Pass it to every call, and set `NSOLID_TENANT` for the scripts. +2. Most N|Solid tools take a `tenant`. When the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Pass the same tenant to every tenant-scoped call, and set `NSOLID_TENANT` for the scripts. 3. If no `agentId` is supplied, call `fleet_snapshot` with `where: 'app == ""'` and `sort: "used_heap_size"` to find the process; keep the exact `appName` for downloads. Stop if no matching process is connected. 4. A leak can sit in a worker thread while the main thread looks fine: `list_threads` with the `agentId` shows each thread's heap. Capture the thread that grows (`threadId`). diff --git a/skills/ns-analyze-vulnerabilities/SKILL.md b/skills/ns-analyze-vulnerabilities/SKILL.md index 3cf7a89..71fa6c0 100644 --- a/skills/ns-analyze-vulnerabilities/SKILL.md +++ b/skills/ns-analyze-vulnerabilities/SKILL.md @@ -9,8 +9,9 @@ This skill reads vulnerability findings through the N|Solid MCP server (`nsolid` ### 1. High-Level Overview - Call `list_vulnerabilities`: open findings, most severe first. Each names its advisory (GHSA, a malicious-package report MAL, or a Node.js core advisory NODE-CORE), the app, severity, the vulnerable packages with the version that fixes them, the app's own dependencies that bring each in (`via`), and the runtime evidence: `installed`, `loaded` (required by the app) or `running` (seen in CPU profiles). A finding whose CVE CISA lists as exploited in the wild carries `knownExploited`: the CVE, when CISA listed it, its remediation due date, and whether ransomware campaigns use it. - Narrow large fleets with `app`, `severity` (at least), or `where`, e.g. `severity >= "high" && evidence >= "loaded"`, or `knownExploited` (`expression_help` with context `finding` lists the fields; it takes no `tenant`). -- Summarize which app has the most critical issues. Lead with malicious packages (MAL-…) when there are any, then known exploited findings in loaded or running code: attackers use those now, whatever their severity. -- Before saying an app has nothing to fix, `vulnerability_data` (it takes no `tenant`) shows whether the advisory data is current: when the OSV mirror and CISA's catalog were last checked, and any sync error. Say so when it is stale. +- A result that fills its `limit` or carries a cut notice is partial: the findings it shows stand, but more may exist. Narrow and re-read; don't invent fields or drop known findings to make it fit. +- Summarize which app has the most critical issues. A ranking of apps needs complete results for every app it compares; from partial results, say the ranking is partial. Lead with malicious packages (MAL-…) when there are any, then known exploited findings in loaded or running code: attackers use those now, whatever their severity. +- Before saying an app has nothing to fix, get complete results from `list_vulnerabilities` scoped to that `app`; current advisory data alone isn't enough. `vulnerability_data` (it takes no `tenant`) shows whether the advisory data is current: when the OSV mirror and CISA's catalog were last checked, and any sync error. Say so when it is stale. - For the Node.js runtime itself, `list_runtimes` gives each Node.js version in use: its core advisories, the release that fixes them, and whether its line is at or near end of life. ### 2. Detail Per App diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 5f184dc..629327e 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -10,7 +10,7 @@ This skill reads N|Solid through its MCP server (`nsolid`). Most N|Solid tools t - The whole tenant by default, or the app the user names. `list_apps` with `limit: 50` lists the apps with their processes by status and the app, N|Solid and Node.js versions in use. ### Bounded Lists -- Read `total`, `returned`, `truncated`, `appsOmitted` and any `outputCut` notice before drawing conclusions. List results stay within 20,000 serialized UTF-8 bytes, so fewer entries may fit than the requested limit. An omitted app or process is not proof that it is absent or healthy; say when the briefing covers only part of the tenant. +- Read `total`, `returned`, `truncated`, `more`, `totalAtLeast`, `appsOmitted`, any `outputCut` notice and any cut notice in each content block before drawing conclusions. `more: true` means the entries are a sample, not the whole set; `totalAtLeast` is a lower bound, not a count. List results stay within 20,000 serialized UTF-8 bytes, so fewer entries may fit than the requested limit. An omitted app or process is not proof that it is absent or healthy; say when the briefing covers only part of the tenant. - Narrow with arguments the tool takes. `list_apps` has no app filter or cursor; for a named app, use the app-scoped calls below. Full lists that cannot be narrowed through MCP need the Console or REST API. Cursor-paginated lists refuse an oversized page whole: retry the same request and cursor with a lower `limit`; if limit 1 still fails, read that record through REST. ### 2. Gather the Picture @@ -18,11 +18,11 @@ Run what fits the question; for a full briefing, all of it: - **Hot processes:** `fleet_snapshot` with `sort: "cpu_percent"`, then `sort: "used_heap_size"` and `sort: "loop_estimated_lag"` (limit 5 each; add `where: 'app == ""'` for one app). Note anything far from its peers. - **Growing memory:** `query_metric` with `metric: "used_heap_size"`, `groupBy: "app"` and `from: "-24h"`: a series that only climbs needs a look (`ns-memory-spike-analysis`). - **Latency:** `query_metric` with `metric: "http_server"` (the p99) and `groupBy: "app"`. -- **Crashes and restarts:** `list_ended_processes` with `byApp: true`, `limit: 25` and `from: "-24h"`: how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. For one app, pass `app` without `byApp`; tenant-wide `byApp` refuses `app`, `host` and `ending`. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. +- **Crashes and restarts:** `list_ended_processes` with `byApp: true`, `limit: 25` and `from: "-24h"`: how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. For one app, pass `app` without `byApp`; tenant-wide `byApp` refuses `app`, `host` and `ending`, so don't add filters to it. One app's list of ended processes is a sample, not a total: for the app's event totals over the whole window, use `event_histogram` filtered to that app with the same absolute `from` and `to`. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. -- **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest. +- **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's version, use `find_agents` with `where: 'app == ""'`. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. @@ -30,7 +30,7 @@ Run what fits the question; for a full briefing, all of it: ### 3. Prioritize - Order by impact: crashes and firing critical alerts, then malicious or loaded high-severity vulnerabilities and end-of-life runtimes, then resource and latency outliers, then the rest. -- Say plainly when everything looks fine. +- Say plainly when everything looks fine, and name any check whose results were partial. ### 4. Present the Briefing - `# Fleet Health — — ` diff --git a/skills/ns-manage-alerts/SKILL.md b/skills/ns-manage-alerts/SKILL.md index 2098b06..7132e03 100644 --- a/skills/ns-manage-alerts/SKILL.md +++ b/skills/ns-manage-alerts/SKILL.md @@ -6,6 +6,11 @@ description: >- This skill works through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Saving rules, acknowledging or silencing alerts and ending a silence need the `nsolid:manage-alerts` permission; without it those tools are missing, and the skill can still review and draft. +## Partial Rule Lists and Name Checks +- When `list_alert_rules` comes back cut, partial or with an error (a cut notice, fewer rules than it counts, a refused page), the rules it left out are unknown. A name missing from it is unverified, not unused, and coverage conclusions from it are partial: say which. +- Before presenting or saving a name as new, verify that exact tenant and name through a complete source. A complete, successful `list_alert_rules` response for that tenant (not cut, not partial, no error) is itself a complete source: if the exact name is absent from it, the name is free, and no Console or REST check is needed. Only when the list's coverage is incomplete, or a complete record of the named rule can't otherwise be obtained, fall back to an authorized check of that exact name: the Console's alert rules page, or an authorized `GET /v1/tenants/{tenant}/alert-rules/{name}`. A 404 from an authorized request confirms the name is free. A 200, or the exact name appearing in any rule list, means saving would replace that rule: show it, and if the user approved only creating a rule, get their go-ahead to replace it. The user's existing go-ahead for the same action on the same target still counts; ask nothing more. +- If the exact name can't be verified (no Console or REST access, or the request is refused), don't infer that it is free and don't save it as new: hand the user the drafted spec to check and save themselves. + ## Start a Newly Connected Service's Baseline When setting up alerts for a newly connected service, call `list_alert_starters` @@ -15,7 +20,8 @@ do not keep copies of starter definitions or reconstruct their thresholds. - Call `list_alert_rules` first. Check both names and coverage: a tenant-wide rule may already cover the service. Offer the useful missing starters and - explain their reasons; do not install the whole list automatically. + explain their reasons; do not install the whole list automatically. When + the rule list is partial, say the coverage check is partial too. - Review each chosen `spec` with the user. Starters are enabled, cover every process or app, have no notification channels, and take no diagnostics. Agree on the intended coverage and whether to record alerts only or use @@ -24,7 +30,9 @@ do not keep copies of starter definitions or reconstruct their thresholds. For finding/behavior rules, combine the original `where` with the app expression using parentheses. Use the expression language's escaped string syntax for the app name. Choose an unused rule name for service-specific - rules; a name already in use replaces the whole rule. + rules; a name already in use replaces the whole rule. If the rule list was + partial, verify the exact name as in Partial Rule Lists and Name Checks + before offering it as new. - Preview what it would have caught using step 3 below, and show the final spec. Once the user authorizes saving, pass that spec with the selected `tenant` to `set_alert_rule`; do not pass the starter's id, title or why. @@ -72,13 +80,13 @@ Before saving, show the user what the rule would have done: - `diagnose` makes a firing process capture evidence: `profile` (CPU), `heap_sampling` or `snapshot` (pauses the process). Only rules grouped by `agent` take one: an app's alert is about no one process. A small hourly budget per app (`diagnosePerAppPerHour`, default 2) keeps an incident from profiling the whole fleet; `diagnoseDurationMs` (default 10000) sets how long a profile or sampling runs. Suggest `profile` for CPU and latency rules and `heap_sampling` for memory rules; `snapshot` only when the user asks. A process that profiles continuously isn't asked for a `profile`: the alert keeps its continuous profiles instead. ### 5. Save -- Show the rule as you'll save it (name, kind, condition, scope, channels, diagnostics), and save it with `set_alert_rule` once the user agrees. A name that exists is replaced whole: check `list_alert_rules` first, say so when the user is changing a rule rather than adding one, and pass back every field it shows (they are in `set_alert_rule`'s terms), changing only what the user asked. `enabled` left out keeps the rule's state; a new rule is on. +- Show the rule as you'll save it (name, kind, condition, scope, channels, diagnostics), and save it with `set_alert_rule` once the user agrees. A name that exists is replaced whole: check `list_alert_rules` first (when it is partial, verify the exact name as in Partial Rule Lists and Name Checks before saving it as new), say so when the user is changing a rule rather than adding one, and pass back every field it shows (they are in `set_alert_rule`'s terms), changing only what the user asked. `enabled` left out keeps the rule's state; a new rule is on. - Arguments are checked strictly: a misspelled or unknown key is refused with its name. Fix it rather than dropping it. - A channel that doesn't exist is refused. The saved rule comes back with `warnings`, such as a metric no process reported in the last 7 days (a misspelled name, or an app that hasn't started) or a disabled channel: tell the user. - Names use letters, digits, `.`, `_` and `-`: `checkout-p99-latency`. ## Reviewing and Tuning -- `list_alert_rules` lists every rule with its condition, scope, channels, diagnostics and `health`, which tells a quiet rule from a broken one: +- `list_alert_rules` lists the rules (all of them unless it is cut or partial) with their condition, scope, channels, diagnostics and `health`, which tells a quiet rule from a broken one: - `ok`, with what the last check saw: the groups with a value and how many held (`groups`, `holding`), or for an event rule when an event last came and last matched (`checkedAt`, `matchedAt`); - `no-data`: no process in scope reports the metric (a misspelled name, an empty scope); - `failing`: its last check failed, and `lastError` says why; @@ -97,6 +105,7 @@ Before saving, show the user what the rule would have done: ## Present the Result - For a new or changed rule: the rule as saved, what it would have caught (step 3), and where it notifies. +- For coverage reviews: mark the conclusion partial when `list_alert_rules` left rules out. - For triage: what is firing, since when, what the evidence shows, and the next step. ## Guardrails diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index e600800..e22e8a3 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -36,7 +36,7 @@ Otherwise, check in order: ### 2b. Check What Production Runs (when N|Solid is available) If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). -- Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Production may run a different version than the workspace pins: say so, and plan from what runs. +- Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm a known app's version with `find_agents` scoped to it (`where: 'app == ""'`). Production may run a different version than the workspace pins: say so, and plan from what runs. - Core advisories with an `upgradeTo` on the same line call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. - If the server isn't available, continue from the workspace alone. @@ -71,6 +71,7 @@ If the user asks about a specific version mid-conversation, re-run the detection ## Tools - `getPackageVersions` — check direct dependencies for known incompatibilities with target Node version. - `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them. +- `find_agents` (`nsolid` server, when available) — the processes of one app, with the Node.js version each runs. ## Guardrails - The release schedule table from `fetch-node-releases.cjs` or the injected host table is authoritative. Do not override LTS names, EOL dates, or latest patch versions with training-data values. From ee2d9133336f16884738598b7bdcf5b48ebb4e6b Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 20:01:17 -0500 Subject: [PATCH 03/14] fix: verify bounded-list fallbacks before changes --- skills/ns-configure-agents/SKILL.md | 8 +++++++- skills/ns-fleet-health/SKILL.md | 4 ++-- skills/ns-manage-alerts/SKILL.md | 12 ++++++++---- skills/ns-node-upgrade/SKILL.md | 6 ++++-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index d8b0e7f..87ef2d7 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -21,8 +21,14 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - `priority`: higher wins where configs disagree. - Names use letters, digits, `.`, `_` and `-`: `checkout-tracing-1h`. +#### New Names and Replacing a Config +- Saving under a name that exists replaces that config. A name missing from a bounded or partial `list_desired_configs` result does not authorize replacing anything: before saving a config as new, verify the exact tenant and name in a successful, complete `list_desired_configs` result (no cut notice, nothing omitted, no error), the Console, or an authorized `GET /v1/tenants/{tenant}/configs` (the full REST list; there is no route for one config by name). If none of these is available, don't save it: hand the user the drafted spec to check and save themselves. +- The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now; if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the same change still counts; ask nothing more. +- `preview_desired_config` shows which processes would change; it does not prove a name is unused and does not keep the old config's settings. +- To edit an existing config, start from its current, complete record and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't complete, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP: have the user make the change in the Console editor, or hand them the drafted change to apply there with their authorization. Never round, renew, extend or drop an expiry, drop a field the schema doesn't list, or re-enable a config as a side effect; change any of them only when the user asks, and say so. + ### 3. Preview -- `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. +- `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. It says nothing about whether the name is in use: check that as in New Names and Replacing a Config. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. ### 4. Apply diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 629327e..9d080da 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -18,11 +18,11 @@ Run what fits the question; for a full briefing, all of it: - **Hot processes:** `fleet_snapshot` with `sort: "cpu_percent"`, then `sort: "used_heap_size"` and `sort: "loop_estimated_lag"` (limit 5 each; add `where: 'app == ""'` for one app). Note anything far from its peers. - **Growing memory:** `query_metric` with `metric: "used_heap_size"`, `groupBy: "app"` and `from: "-24h"`: a series that only climbs needs a look (`ns-memory-spike-analysis`). - **Latency:** `query_metric` with `metric: "http_server"` (the p99) and `groupBy: "app"`. -- **Crashes and restarts:** `list_ended_processes` with `byApp: true`, `limit: 25` and `from: "-24h"`: how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. For one app, pass `app` without `byApp`; tenant-wide `byApp` refuses `app`, `host` and `ending`, so don't add filters to it. One app's list of ended processes is a sample, not a total: for the app's event totals over the whole window, use `event_histogram` filtered to that app with the same absolute `from` and `to`. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. +- **Crashes and restarts:** resolve the window once to absolute times (e.g. the last 24 hours as one `from` and `to`) and pass that same `from` and `to` to every call you compare. `list_ended_processes` with `byApp: true`, `limit: 25` and that window: how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. For one app, pass `app` without `byApp`; tenant-wide `byApp` refuses `app`, `host` and `ending`, so don't add filters to it. One app's list of ended processes is a sample of processes, not a total. `event_histogram` filtered to that app counts `exit` and `lost` events, not ended processes: a lost process can reconnect and drop its ended record, and old events can expire, so the two numbers differ. Report the process sample and the event totals separately, each labeled for what it counts. For an exact count of an app's ended processes, page through an authorized `GET /v1/tenants/{tenant}/tombstones` filtered to the app over the same window; without it, say the count is a sample. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's version, use `find_agents` with `where: 'app == ""'`. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice makes the result partial: keep every version observed and don't claim the whole app was covered. For a confirmed process's advisories, end of life and `upgradeTo`, read `get_agent`'s runtime verdict, or an authorized `GET /v1/tenants/{tenant}/runtimes?app=`; otherwise mark that app's advisory coverage incomplete. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. diff --git a/skills/ns-manage-alerts/SKILL.md b/skills/ns-manage-alerts/SKILL.md index 7132e03..d65f411 100644 --- a/skills/ns-manage-alerts/SKILL.md +++ b/skills/ns-manage-alerts/SKILL.md @@ -7,9 +7,13 @@ description: >- This skill works through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Saving rules, acknowledging or silencing alerts and ending a silence need the `nsolid:manage-alerts` permission; without it those tools are missing, and the skill can still review and draft. ## Partial Rule Lists and Name Checks -- When `list_alert_rules` comes back cut, partial or with an error (a cut notice, fewer rules than it counts, a refused page), the rules it left out are unknown. A name missing from it is unverified, not unused, and coverage conclusions from it are partial: say which. -- Before presenting or saving a name as new, verify that exact tenant and name through a complete source. A complete, successful `list_alert_rules` response for that tenant (not cut, not partial, no error) is itself a complete source: if the exact name is absent from it, the name is free, and no Console or REST check is needed. Only when the list's coverage is incomplete, or a complete record of the named rule can't otherwise be obtained, fall back to an authorized check of that exact name: the Console's alert rules page, or an authorized `GET /v1/tenants/{tenant}/alert-rules/{name}`. A 404 from an authorized request confirms the name is free. A 200, or the exact name appearing in any rule list, means saving would replace that rule: show it, and if the user approved only creating a rule, get their go-ahead to replace it. The user's existing go-ahead for the same action on the same target still counts; ask nothing more. +- `list_alert_rules` returns a bare JSON array of rules: it has no `limit`, count, `truncated` flag or cursor. Its result is incomplete when a content block carries a cut notice, when it comes back as a text-only notice that the result was too large, or when it returns an error. Then the rules it left out are unknown: a name missing from it is unverified, not unused, and coverage conclusions from it are partial: say which. +- A successful JSON list with no cut notice, too-large notice or error is complete for that tenant: if the exact name is absent from it, the name is free, and no Console or REST check is needed. +- A positive match still counts on a partial list: the exact name appearing in any rule list, cut or not, proves that rule exists. +- When the list is incomplete and the name is absent from it, verify that exact tenant and name before presenting or saving it as new: the Console's alert rules page, or an authorized `GET /v1/tenants/{tenant}/alert-rules/{name}`. A 404 from an authorized request confirms the name is free. A 200 means the rule exists. +- An existing name means saving would replace that rule: show it, and if the user approved only creating a rule, get their go-ahead to replace it. The user's existing go-ahead for the same action on the same target still counts; ask nothing more. - If the exact name can't be verified (no Console or REST access, or the request is refused), don't infer that it is free and don't save it as new: hand the user the drafted spec to check and save themselves. +- The REST lookup settles the rule's identity and whether the name exists; it is not `set_alert_rule` input. A REST rule is in the API's own shape (expressions as trees, durations in milliseconds), not the MCP writer's arguments. To edit a rule, start from its complete record in `list_alert_rules` (in `set_alert_rule`'s terms); when that record isn't available through MCP, have the user make the change in the Console's rule editor, or hand them the drafted change. Never translate a REST rule into `set_alert_rule` arguments, and neither drop nor copy fields MCP doesn't name. Keep the user's existing authorization and the selected tenant and rule as they are. ## Start a Newly Connected Service's Baseline @@ -80,13 +84,13 @@ Before saving, show the user what the rule would have done: - `diagnose` makes a firing process capture evidence: `profile` (CPU), `heap_sampling` or `snapshot` (pauses the process). Only rules grouped by `agent` take one: an app's alert is about no one process. A small hourly budget per app (`diagnosePerAppPerHour`, default 2) keeps an incident from profiling the whole fleet; `diagnoseDurationMs` (default 10000) sets how long a profile or sampling runs. Suggest `profile` for CPU and latency rules and `heap_sampling` for memory rules; `snapshot` only when the user asks. A process that profiles continuously isn't asked for a `profile`: the alert keeps its continuous profiles instead. ### 5. Save -- Show the rule as you'll save it (name, kind, condition, scope, channels, diagnostics), and save it with `set_alert_rule` once the user agrees. A name that exists is replaced whole: check `list_alert_rules` first (when it is partial, verify the exact name as in Partial Rule Lists and Name Checks before saving it as new), say so when the user is changing a rule rather than adding one, and pass back every field it shows (they are in `set_alert_rule`'s terms), changing only what the user asked. `enabled` left out keeps the rule's state; a new rule is on. +- Show the rule as you'll save it (name, kind, condition, scope, channels, diagnostics), and save it with `set_alert_rule` once the user agrees. A name that exists is replaced whole: check `list_alert_rules` first (when it is partial, verify the exact name as in Partial Rule Lists and Name Checks before saving it as new), say so when the user is changing a rule rather than adding one, and pass back every field its `list_alert_rules` record shows (they are in `set_alert_rule`'s terms), changing only what the user asked. Without that complete MCP record, use the Console's rule editor or hand off the draft rather than rebuilding the rule from REST. `enabled` left out keeps the rule's state; a new rule is on. - Arguments are checked strictly: a misspelled or unknown key is refused with its name. Fix it rather than dropping it. - A channel that doesn't exist is refused. The saved rule comes back with `warnings`, such as a metric no process reported in the last 7 days (a misspelled name, or an app that hasn't started) or a disabled channel: tell the user. - Names use letters, digits, `.`, `_` and `-`: `checkout-p99-latency`. ## Reviewing and Tuning -- `list_alert_rules` lists the rules (all of them unless it is cut or partial) with their condition, scope, channels, diagnostics and `health`, which tells a quiet rule from a broken one: +- `list_alert_rules` lists the rules (all of them unless a cut notice, too-large notice or error says otherwise) with their condition, scope, channels, diagnostics and `health`, which tells a quiet rule from a broken one: - `ok`, with what the last check saw: the groups with a value and how many held (`groups`, `holding`), or for an event rule when an event last came and last matched (`checkedAt`, `matchedAt`); - `no-data`: no process in scope reports the metric (a misspelled name, an empty scope); - `failing`: its last check failed, and `lastError` says why; diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index e22e8a3..0548aec 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -36,7 +36,8 @@ Otherwise, check in order: ### 2b. Check What Production Runs (when N|Solid is available) If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). -- Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm a known app's version with `find_agents` scoped to it (`where: 'app == ""'`). Production may run a different version than the workspace pins: say so, and plan from what runs. +- Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm what a known app runs now with `find_agents` given the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`). Historical, disconnected or exited processes are not confirmation of what production runs now. A `More agents match` notice means the result is partial: keep every version it shows and don't claim it covers all of the app's processes. Production may run a different version than the workspace pins: say so, and plan from what runs. +- For each confirmed version `list_runtimes` didn't show with its advisories (cut, omitted or missing), recover its end of life, core advisories and `upgradeTo` from `get_agent`'s runtime verdict for a confirmed process, or an authorized `GET /v1/tenants/{tenant}/runtimes?app=`. If neither is available, say advisory coverage for that version is incomplete; don't conclude that no same-line patch is needed. - Core advisories with an `upgradeTo` on the same line call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. - If the server isn't available, continue from the workspace alone. @@ -71,7 +72,8 @@ If the user asks about a specific version mid-conversation, re-run the detection ## Tools - `getPackageVersions` — check direct dependencies for known incompatibilities with target Node version. - `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them. -- `find_agents` (`nsolid` server, when available) — the processes of one app, with the Node.js version each runs. +- `find_agents` (`nsolid` server, when available) — the connected processes of one app (`app`, `status: "connected"`), with the Node.js version each runs. +- `get_agent` (`nsolid` server, when available) — one process's runtime verdict: end of life, core advisories and `upgradeTo`. ## Guardrails - The release schedule table from `fetch-node-releases.cjs` or the injected host table is authoritative. Do not override LTS names, EOL dates, or latest patch versions with training-data values. From 258b71bb184d31447d593ddd9fd1e68ed438225a Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 20:10:33 -0500 Subject: [PATCH 04/14] fix: align fallback guidance with API contracts --- skills/ns-configure-agents/SKILL.md | 8 ++++++-- skills/ns-fleet-health/SKILL.md | 4 ++-- skills/ns-manage-alerts/SKILL.md | 2 +- skills/ns-node-upgrade/SKILL.md | 9 ++++++--- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index 87ef2d7..4c95fa7 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -22,10 +22,14 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - Names use letters, digits, `.`, `_` and `-`: `checkout-tracing-1h`. #### New Names and Replacing a Config -- Saving under a name that exists replaces that config. A name missing from a bounded or partial `list_desired_configs` result does not authorize replacing anything: before saving a config as new, verify the exact tenant and name in a successful, complete `list_desired_configs` result (no cut notice, nothing omitted, no error), the Console, or an authorized `GET /v1/tenants/{tenant}/configs` (the full REST list; there is no route for one config by name). If none of these is available, don't save it: hand the user the drafted spec to check and save themselves. +- Saving under a name that exists replaces that config. A name missing from a bounded or partial `list_desired_configs` result does not authorize replacing anything: before saving a config as new, verify the exact tenant and name in a successful, complete `list_desired_configs` result (no JSON `outputCut`, no cut notice, too-large notice or error in any content block, nothing omitted), the Console, or an authorized `GET /v1/tenants/{tenant}/configs` (the full REST list; there is no route for one config by name). If none of these is available, don't save it: hand the user the drafted spec to check and save themselves. - The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now; if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the same change still counts; ask nothing more. - `preview_desired_config` shows which processes would change; it does not prove a name is unused and does not keep the old config's settings. -- To edit an existing config, start from its current, complete record and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't complete, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP: have the user make the change in the Console editor, or hand them the drafted change to apply there with their authorization. Never round, renew, extend or drop an expiry, drop a field the schema doesn't list, or re-enable a config as a side effect; change any of them only when the user asks, and say so. +- To edit an existing config, start from its current, complete record (a complete `list_desired_configs` result as above, or an authorized `GET /v1/tenants/{tenant}/configs`) and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't complete, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP. + - The Console's Process Settings can't edit arbitrary config names or keep a config's state or absolute expiry: don't send the user there for such an edit. + - With the user's authorization for the REST API, `PUT /v1/tenants/{tenant}/configs/{name}` with the exact requested tenant and name accepts `config`, the target (`where` or a selector), `priority`, `enabled`, and either `expiresAt` (an absolute time in the future; omit it for a permanent config) or `durationMs`, never both. Carry over every unchanged writable value from the record and change only what the user asked. Build the body from those fields; don't pass the read record through, since its metadata isn't input. A kept `expiresAt` that is no longer in the future can't be written as is. + - When neither writer can make the edit faithfully, or the REST write isn't authorized, leave the config unchanged and hand the user the drafted change with its complete current record, naming the fields that must be preserved (target, priority, state, absolute expiry, and any field the MCP schema doesn't list). +- Never round, renew, extend or drop an expiry, drop a field the schema doesn't list, or re-enable a config as a side effect; change any of them only when the user asks, and say so. ### 3. Preview - `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. It says nothing about whether the name is in use: check that as in New Names and Replacing a Config. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 9d080da..7f4176c 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -18,11 +18,11 @@ Run what fits the question; for a full briefing, all of it: - **Hot processes:** `fleet_snapshot` with `sort: "cpu_percent"`, then `sort: "used_heap_size"` and `sort: "loop_estimated_lag"` (limit 5 each; add `where: 'app == ""'` for one app). Note anything far from its peers. - **Growing memory:** `query_metric` with `metric: "used_heap_size"`, `groupBy: "app"` and `from: "-24h"`: a series that only climbs needs a look (`ns-memory-spike-analysis`). - **Latency:** `query_metric` with `metric: "http_server"` (the p99) and `groupBy: "app"`. -- **Crashes and restarts:** resolve the window once to absolute times (e.g. the last 24 hours as one `from` and `to`) and pass that same `from` and `to` to every call you compare. `list_ended_processes` with `byApp: true`, `limit: 25` and that window: how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. For one app, pass `app` without `byApp`; tenant-wide `byApp` refuses `app`, `host` and `ending`, so don't add filters to it. One app's list of ended processes is a sample of processes, not a total. `event_histogram` filtered to that app counts `exit` and `lost` events, not ended processes: a lost process can reconnect and drop its ended record, and old events can expire, so the two numbers differ. Report the process sample and the event totals separately, each labeled for what it counts. For an exact count of an app's ended processes, page through an authorized `GET /v1/tenants/{tenant}/tombstones` filtered to the app over the same window; without it, say the count is a sample. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. +- **Crashes and restarts:** resolve the window once to absolute times (e.g. the last 24 hours as one `from` and `to`) and pass that same `from` and `to` to every call you compare. `list_ended_processes` with `byApp: true`, `limit: 25` and that window: how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. For one app, pass `app` without `byApp`; tenant-wide `byApp` refuses `app`, `host` and `ending`, so don't add filters to it. One app's list of ended processes is a sample of processes, not a total. When the `byApp` result includes the named app, its ended count already aggregates the same retained tombstones: use it without paging; an app missing from a partial `byApp` list is unknown, not zero. Otherwise, paging through an authorized `GET /v1/tenants/{tenant}/tombstones` filtered to the app over the same window counts the ended-process records N|Solid currently retains for that window, not every process that actually ended: records still settling, an ingest backlog, retention, and lost processes that reconnect and drop their record all affect coverage. Report it as recorded tombstones; never present it as an exact count of all ended processes, and don't take zero as proof that none ended. Without either, say the count is a sample. `event_histogram` filtered to that app counts `exit` and `lost` events, not ended processes, so the numbers differ: report the tombstone count and the event totals separately, each labeled for what it counts, over the same absolute window. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice makes the result partial: keep every version observed and don't claim the whole app was covered. For a confirmed process's advisories, end of life and `upgradeTo`, read `get_agent`'s runtime verdict, or an authorized `GET /v1/tenants/{tenant}/runtimes?app=`; otherwise mark that app's advisory coverage incomplete. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. For advisories, end of life and the fixing release (`fixed` in both sources below, `upgradeTo` in `list_runtimes`), prefer an authorized `GET /v1/tenants/{tenant}/runtimes?app=`, or combine `get_agent` runtime verdicts, one per confirmed version and platform pair: one process's verdict covers only its own version and platform. The REST list covers processes seen in the last day, disconnected and exited included, so match it to the versions and platforms observed connected rather than using it alone as the current baseline, and say what it covered. Mark the app's advisory coverage incomplete if any version and platform pair, or any of its processes, is uncovered. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. diff --git a/skills/ns-manage-alerts/SKILL.md b/skills/ns-manage-alerts/SKILL.md index d65f411..c96d78a 100644 --- a/skills/ns-manage-alerts/SKILL.md +++ b/skills/ns-manage-alerts/SKILL.md @@ -84,7 +84,7 @@ Before saving, show the user what the rule would have done: - `diagnose` makes a firing process capture evidence: `profile` (CPU), `heap_sampling` or `snapshot` (pauses the process). Only rules grouped by `agent` take one: an app's alert is about no one process. A small hourly budget per app (`diagnosePerAppPerHour`, default 2) keeps an incident from profiling the whole fleet; `diagnoseDurationMs` (default 10000) sets how long a profile or sampling runs. Suggest `profile` for CPU and latency rules and `heap_sampling` for memory rules; `snapshot` only when the user asks. A process that profiles continuously isn't asked for a `profile`: the alert keeps its continuous profiles instead. ### 5. Save -- Show the rule as you'll save it (name, kind, condition, scope, channels, diagnostics), and save it with `set_alert_rule` once the user agrees. A name that exists is replaced whole: check `list_alert_rules` first (when it is partial, verify the exact name as in Partial Rule Lists and Name Checks before saving it as new), say so when the user is changing a rule rather than adding one, and pass back every field its `list_alert_rules` record shows (they are in `set_alert_rule`'s terms), changing only what the user asked. Without that complete MCP record, use the Console's rule editor or hand off the draft rather than rebuilding the rule from REST. `enabled` left out keeps the rule's state; a new rule is on. +- Show the rule as you'll save it (name, kind, condition, scope, channels, diagnostics), and save it with `set_alert_rule` once the user agrees. A name that exists is replaced whole: check `list_alert_rules` first (when it is partial, verify the exact name as in Partial Rule Lists and Name Checks before saving it as new), say so when the user is changing a rule rather than adding one, and pass back every writable input value its `list_alert_rules` record shows (in `set_alert_rule`'s terms), changing only what the user asked. Not every returned key is input: `health`, `updatedAt`, `updatedBy` and `kind` are output metadata, not `set_alert_rule` arguments, and leaving out such known read-only metadata is correct. Never discard an unknown writable field or setting to get past validation: when the writer can't carry it faithfully, use a writer that can or hand off the draft. Without that complete MCP record, use the Console's rule editor or hand off the draft rather than rebuilding the rule from REST. `enabled` left out keeps the rule's state; a new rule is on. - Arguments are checked strictly: a misspelled or unknown key is refused with its name. Fix it rather than dropping it. - A channel that doesn't exist is refused. The saved rule comes back with `warnings`, such as a metric no process reported in the last 7 days (a misspelled name, or an app that hasn't started) or a disabled channel: tell the user. - Names use letters, digits, `.`, `_` and `-`: `checkout-p99-latency`. diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index 0548aec..e87a267 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -36,8 +36,11 @@ Otherwise, check in order: ### 2b. Check What Production Runs (when N|Solid is available) If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). -- Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm what a known app runs now with `find_agents` given the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`). Historical, disconnected or exited processes are not confirmation of what production runs now. A `More agents match` notice means the result is partial: keep every version it shows and don't claim it covers all of the app's processes. Production may run a different version than the workspace pins: say so, and plan from what runs. -- For each confirmed version `list_runtimes` didn't show with its advisories (cut, omitted or missing), recover its end of life, core advisories and `upgradeTo` from `get_agent`'s runtime verdict for a confirmed process, or an authorized `GET /v1/tenants/{tenant}/runtimes?app=`. If neither is available, say advisory coverage for that version is incomplete; don't conclude that no same-line patch is needed. +- Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm what a known app runs now with `find_agents` given the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`). Historical, disconnected or exited processes are not confirmation of what production runs now. The result is partial on a `More agents match` notice or on a cut, too-large notice or error in any content block; a result that fills the default 50 or the requested limit is not proof of completeness either. Every version it does show stands; don't claim it covers all of the app's processes. Production may run a different version than the workspace pins: say so, and plan from what runs. +- For each confirmed version `list_runtimes` didn't show with its advisories (cut, omitted or missing), prefer an authorized `GET /v1/tenants/{tenant}/runtimes?app=`; otherwise combine `get_agent` runtime verdicts, one per confirmed version and platform pair. Both give end of life, core advisories and the fixing release as `fixed` (what `list_runtimes` calls `upgradeTo`). + - A `get_agent` verdict covers only that process's version and platform: one process doesn't prove other platforms safe. + - The REST list covers processes seen in the last day, disconnected and exited ones included, so it doesn't confirm what runs now: match its entries to the versions and platforms observed connected, and say what it covered. + - If any version and platform pair, or any of the app's processes (a partial `find_agents`), is left uncovered, say advisory coverage is incomplete; don't conclude that no same-line patch is needed. - Core advisories with an `upgradeTo` on the same line call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. - If the server isn't available, continue from the workspace alone. @@ -73,7 +76,7 @@ If the user asks about a specific version mid-conversation, re-run the detection - `getPackageVersions` — check direct dependencies for known incompatibilities with target Node version. - `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them. - `find_agents` (`nsolid` server, when available) — the connected processes of one app (`app`, `status: "connected"`), with the Node.js version each runs. -- `get_agent` (`nsolid` server, when available) — one process's runtime verdict: end of life, core advisories and `upgradeTo`. +- `get_agent` (`nsolid` server, when available) — one process's runtime verdict for its own version and platform: end of life, core advisories and `fixed` (`list_runtimes`'s `upgradeTo`). ## Guardrails - The release schedule table from `fetch-node-releases.cjs` or the injected host table is authoritative. Do not override LTS names, EOL dates, or latest patch versions with training-data values. From cd5b6c7144339405d82f99da533c134e6a34d5dd Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 20:18:13 -0500 Subject: [PATCH 05/14] fix: preserve preview and platform coverage in fallbacks --- skills/ns-configure-agents/SKILL.md | 13 ++++++++----- skills/ns-fleet-health/SKILL.md | 2 +- skills/ns-node-upgrade/SKILL.md | 14 ++++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index 4c95fa7..24a6bfc 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -22,17 +22,20 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - Names use letters, digits, `.`, `_` and `-`: `checkout-tracing-1h`. #### New Names and Replacing a Config -- Saving under a name that exists replaces that config. A name missing from a bounded or partial `list_desired_configs` result does not authorize replacing anything: before saving a config as new, verify the exact tenant and name in a successful, complete `list_desired_configs` result (no JSON `outputCut`, no cut notice, too-large notice or error in any content block, nothing omitted), the Console, or an authorized `GET /v1/tenants/{tenant}/configs` (the full REST list; there is no route for one config by name). If none of these is available, don't save it: hand the user the drafted spec to check and save themselves. +- Saving under a name that exists replaces that config. A name missing from a bounded or partial `list_desired_configs` result does not authorize replacing anything: before saving a config as new, verify the exact tenant and name in a successful, complete, uncut `list_desired_configs` JSON result (no JSON `outputCut`, no cut notice, too-large notice or error in any content block, nothing omitted) or an authorized `GET /v1/tenants/{tenant}/configs` (the full REST list; there is no route for one config by name). The Console doesn't list every arbitrarily named config, so it is not proof that a name is absent. If neither source is available, don't save it: hand the user the drafted spec to check and save themselves. - The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now; if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the same change still counts; ask nothing more. - `preview_desired_config` shows which processes would change; it does not prove a name is unused and does not keep the old config's settings. -- To edit an existing config, start from its current, complete record (a complete `list_desired_configs` result as above, or an authorized `GET /v1/tenants/{tenant}/configs`) and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't complete, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP. +- To edit an existing config, start from its current, intact record and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't intact, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP. + - An intact record is the exact tenant and name's own entry, from a successful `list_desired_configs` result or an authorized `GET /v1/tenants/{tenant}/configs`, with every writable field present and nothing cut in it: no cut notice, `outputCut` or omission touching that record, its target (`where`/`whereText` or selector), its `config`, its priority, its state or its expiry. Such a record can be used even when the list is partial and other, unrelated records were omitted: the name appearing already proves the config exists. A cut that only touches known read-only metadata (such as who saved it or when) leaves the writable values usable. A complete, uncut list is needed only to conclude a name is absent, as above, not to edit a record that is present and intact. - The Console's Process Settings can't edit arbitrary config names or keep a config's state or absolute expiry: don't send the user there for such an edit. - - With the user's authorization for the REST API, `PUT /v1/tenants/{tenant}/configs/{name}` with the exact requested tenant and name accepts `config`, the target (`where` or a selector), `priority`, `enabled`, and either `expiresAt` (an absolute time in the future; omit it for a permanent config) or `durationMs`, never both. Carry over every unchanged writable value from the record and change only what the user asked. Build the body from those fields; don't pass the read record through, since its metadata isn't input. A kept `expiresAt` that is no longer in the future can't be written as is. - - When neither writer can make the edit faithfully, or the REST write isn't authorized, leave the config unchanged and hand the user the drafted change with its complete current record, naming the fields that must be preserved (target, priority, state, absolute expiry, and any field the MCP schema doesn't list). + - With the user's authorization for the REST API, `PUT /v1/tenants/{tenant}/configs/{name}` with the exact requested tenant and name accepts `config`, the target (`where` or a selector, never both), `priority`, `enabled`, and either `expiresAt` (an absolute time in the future; omit it for a permanent config) or `durationMs`, never both. Carry over every unchanged writable value from the record and change only what the user asked. When the target is unchanged, send the record's `where` expression (tree or its exact `whereText`) as is: don't rebuild it as a legacy selector, simplify or widen it. Build the body from those fields; don't pass the read record through, since its metadata isn't input. + - Before the `PUT`, send that exact body to `POST /v1/tenants/{tenant}/configs/{name}/preview` (it takes the same body as the `PUT`), show the user the result, and get their agreement, or reuse their existing go-ahead for the same action on the same tenant, name and target. Don't substitute `preview_desired_config`: it can't carry `enabled` or an absolute `expiresAt`. + - If the kept `expiresAt` is no longer in the future or can't be written exactly, or the REST write or preview isn't authorized, leave the config unchanged and hand the user the drafted change; don't renew, extend or drop the expiry to make it fit. + - When neither writer can make the edit faithfully, leave the config unchanged and hand the user the drafted change with its intact current record, naming the fields that must be preserved (target, priority, state, absolute expiry, and any field the MCP schema doesn't list). - Never round, renew, extend or drop an expiry, drop a field the schema doesn't list, or re-enable a config as a side effect; change any of them only when the user asks, and say so. ### 3. Preview -- `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. It says nothing about whether the name is in use: check that as in New Names and Replacing a Config. +- `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. A REST replacement is previewed with its own exact body through `POST /v1/tenants/{tenant}/configs/{name}/preview`, as in New Names and Replacing a Config. It says nothing about whether the name is in use: check that as in New Names and Replacing a Config. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. ### 4. Apply diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 7f4176c..5df1244 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -22,7 +22,7 @@ Run what fits the question; for a full briefing, all of it: - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. For advisories, end of life and the fixing release (`fixed` in both sources below, `upgradeTo` in `list_runtimes`), prefer an authorized `GET /v1/tenants/{tenant}/runtimes?app=`, or combine `get_agent` runtime verdicts, one per confirmed version and platform pair: one process's verdict covers only its own version and platform. The REST list covers processes seen in the last day, disconnected and exited included, so match it to the versions and platforms observed connected rather than using it alone as the current baseline, and say what it covered. Mark the app's advisory coverage incomplete if any version and platform pair, or any of its processes, is uncovered. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform: read `get_agent` for the connected agents it returned (reuse any detail already read) to get each one's platform and runtime verdict before grouping them into version and platform pairs. For the current app's advisories, end of life and the same-line patch, prefer these connected-agent verdicts; the patch is the verdict's `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for the Node.js runtime sources' `checkedAt`, errors and freshness. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index e87a267..3b8ab9c 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -37,11 +37,12 @@ Otherwise, check in order: ### 2b. Check What Production Runs (when N|Solid is available) If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). - Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm what a known app runs now with `find_agents` given the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`). Historical, disconnected or exited processes are not confirmation of what production runs now. The result is partial on a `More agents match` notice or on a cut, too-large notice or error in any content block; a result that fills the default 50 or the requested limit is not proof of completeness either. Every version it does show stands; don't claim it covers all of the app's processes. Production may run a different version than the workspace pins: say so, and plan from what runs. -- For each confirmed version `list_runtimes` didn't show with its advisories (cut, omitted or missing), prefer an authorized `GET /v1/tenants/{tenant}/runtimes?app=`; otherwise combine `get_agent` runtime verdicts, one per confirmed version and platform pair. Both give end of life, core advisories and the fixing release as `fixed` (what `list_runtimes` calls `upgradeTo`). - - A `get_agent` verdict covers only that process's version and platform: one process doesn't prove other platforms safe. - - The REST list covers processes seen in the last day, disconnected and exited ones included, so it doesn't confirm what runs now: match its entries to the versions and platforms observed connected, and say what it covered. - - If any version and platform pair, or any of the app's processes (a partial `find_agents`), is left uncovered, say advisory coverage is incomplete; don't conclude that no same-line patch is needed. -- Core advisories with an `upgradeTo` on the same line call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. +- `find_agents` gives each process's version but not its platform. Read `get_agent` for the connected agents it returned (reuse any detail already read) to get each one's platform and runtime verdict before grouping them into version and platform pairs. Prefer these connected-agent verdicts for the current same-line patch decision: each gives end of life, core advisories and the fixing release as `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). + - A verdict covers only that agent's own version and platform: one process doesn't prove other platforms safe. An unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. + - Each confirmed version and platform pair needs a verdict; a partial `find_agents` leaves the rest of the app's processes uncovered. + - Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for the Node.js runtime sources' `checkedAt`, errors and freshness. + - If a source is missing, stale or failed, or any pair or process is left without a verdict, say advisory coverage is incomplete, keep the advisories observed, and don't conclude that no same-line patch is needed. +- Core advisories with a fix on the same line (`runtime.fixed`, or `upgradeTo` in `list_runtimes`) call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. - If the server isn't available, continue from the workspace alone. ### 3. Recommend a Target Version @@ -76,7 +77,8 @@ If the user asks about a specific version mid-conversation, re-run the detection - `getPackageVersions` — check direct dependencies for known incompatibilities with target Node version. - `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them. - `find_agents` (`nsolid` server, when available) — the connected processes of one app (`app`, `status: "connected"`), with the Node.js version each runs. -- `get_agent` (`nsolid` server, when available) — one process's runtime verdict for its own version and platform: end of life, core advisories and `fixed` (`list_runtimes`'s `upgradeTo`). +- `get_agent` (`nsolid` server, when available) — one process's platform and runtime verdict for its own version and platform: end of life, core advisories and `runtime.fixed` (`list_runtimes`'s `upgradeTo`). +- `vulnerability_data` (`nsolid` server, when available; takes no `tenant`) — when the advisory sources, Node.js runtime ones included, were last checked, and any sync error. ## Guardrails - The release schedule table from `fetch-node-releases.cjs` or the injected host table is authoritative. Do not override LTS names, EOL dates, or latest patch versions with training-data values. From 12392962f598eba457d37dd72bf8918a8056ac14 Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 20:24:07 -0500 Subject: [PATCH 06/14] fix: retain defaults and reuse exact approvals --- skills/ns-configure-agents/SKILL.md | 16 +++++++++------- skills/ns-fleet-health/SKILL.md | 2 +- skills/ns-node-upgrade/SKILL.md | 6 +++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index 24a6bfc..0981901 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -23,23 +23,25 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres #### New Names and Replacing a Config - Saving under a name that exists replaces that config. A name missing from a bounded or partial `list_desired_configs` result does not authorize replacing anything: before saving a config as new, verify the exact tenant and name in a successful, complete, uncut `list_desired_configs` JSON result (no JSON `outputCut`, no cut notice, too-large notice or error in any content block, nothing omitted) or an authorized `GET /v1/tenants/{tenant}/configs` (the full REST list; there is no route for one config by name). The Console doesn't list every arbitrarily named config, so it is not proof that a name is absent. If neither source is available, don't save it: hand the user the drafted spec to check and save themselves. -- The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now; if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the same change still counts; ask nothing more. +- The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now; if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the exact same change (every writable setting, target, priority, state and expiry) still counts; ask nothing more. A go-ahead for the same tenant, name or target with a different body does not. - `preview_desired_config` shows which processes would change; it does not prove a name is unused and does not keep the old config's settings. - To edit an existing config, start from its current, intact record and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't intact, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP. - - An intact record is the exact tenant and name's own entry, from a successful `list_desired_configs` result or an authorized `GET /v1/tenants/{tenant}/configs`, with every writable field present and nothing cut in it: no cut notice, `outputCut` or omission touching that record, its target (`where`/`whereText` or selector), its `config`, its priority, its state or its expiry. Such a record can be used even when the list is partial and other, unrelated records were omitted: the name appearing already proves the config exists. A cut that only touches known read-only metadata (such as who saved it or when) leaves the writable values usable. A complete, uncut list is needed only to conclude a name is absent, as above, not to edit a record that is present and intact. + - An intact record is the exact tenant and name's own entry, from a successful `list_desired_configs` result or an authorized `GET /v1/tenants/{tenant}/configs`, with every writable field present and nothing cut in it: no cut notice, `outputCut` or omission touching that record, its target (`where`/`whereText` or selector), its `config`, its priority, its state or its expiry. Such a record can be used even when the list is partial and other, unrelated records were omitted: the name appearing already proves the config exists. A cut that only touches known read-only metadata (such as who saved it or when) leaves the writable values usable. Documented compaction is not a cut: MCP results drop null fields, so in an otherwise intact record a missing `expiresAt` means the config is permanent (REST `null`); treat it as present and don't block the edit over it. An unexplained cut path that could cover a writable field still makes the record unusable. A complete, uncut list is needed only to conclude a name is absent, as above, not to edit a record that is present and intact. - The Console's Process Settings can't edit arbitrary config names or keep a config's state or absolute expiry: don't send the user there for such an edit. - - With the user's authorization for the REST API, `PUT /v1/tenants/{tenant}/configs/{name}` with the exact requested tenant and name accepts `config`, the target (`where` or a selector, never both), `priority`, `enabled`, and either `expiresAt` (an absolute time in the future; omit it for a permanent config) or `durationMs`, never both. Carry over every unchanged writable value from the record and change only what the user asked. When the target is unchanged, send the record's `where` expression (tree or its exact `whereText`) as is: don't rebuild it as a legacy selector, simplify or widen it. Build the body from those fields; don't pass the read record through, since its metadata isn't input. - - Before the `PUT`, send that exact body to `POST /v1/tenants/{tenant}/configs/{name}/preview` (it takes the same body as the `PUT`), show the user the result, and get their agreement, or reuse their existing go-ahead for the same action on the same tenant, name and target. Don't substitute `preview_desired_config`: it can't carry `enabled` or an absolute `expiresAt`. - - If the kept `expiresAt` is no longer in the future or can't be written exactly, or the REST write or preview isn't authorized, leave the config unchanged and hand the user the drafted change; don't renew, extend or drop the expiry to make it fit. + - With the user's authorization for the REST API, `PUT /v1/tenants/{tenant}/configs/{name}` with the exact requested tenant and name accepts `config`, the target (`where` or a selector, never both), `priority`, `enabled`, and either `expiresAt` (an absolute time in the future; omit it for a permanent config) or `durationMs`, never both. Carry over every unchanged writable value from the record and change only what the user asked. When the target is unchanged, send the record's `where` tree as is: don't swap in the rendered `whereText` (it can exceed the 4,000-character input limit), rebuild it as a legacy selector, simplify or widen it, and never send `where` and a selector together. Build the body from those fields; don't pass the read record through, since its metadata isn't input. + - Before the `PUT`, send that exact body to `POST /v1/tenants/{tenant}/configs/{name}/preview` (it takes the same body as the `PUT`) and always show the user the result. If the user already authorized this exact body on this tenant and name, that authorization covers it: don't ask again. Otherwise, or if the body changed in any way, get their agreement to the body shown. Don't substitute `preview_desired_config`: it can't carry `enabled` or an absolute `expiresAt`. + - If the kept `expiresAt` is present but no longer in the future (an expired config stays expired; never omit or renew it to force the save) or can't be written exactly, or the REST write or preview isn't authorized, leave the config unchanged and hand the user the drafted change; don't renew, extend or drop the expiry to make it fit. - When neither writer can make the edit faithfully, leave the config unchanged and hand the user the drafted change with its intact current record, naming the fields that must be preserved (target, priority, state, absolute expiry, and any field the MCP schema doesn't list). - Never round, renew, extend or drop an expiry, drop a field the schema doesn't list, or re-enable a config as a side effect; change any of them only when the user asks, and say so. ### 3. Preview - `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. A REST replacement is previewed with its own exact body through `POST /v1/tenants/{tenant}/configs/{name}/preview`, as in New Names and Replacing a Config. It says nothing about whether the name is in use: check that as in New Names and Replacing a Config. +- Preview with the writer that will apply: REST preview then the same-body REST `PUT`, or `preview_desired_config` then `set_desired_config` with the same inputs; don't mix them. +- A preview can be truncated (`truncated`): label its matching and changing processes as partial, don't claim complete counts, and don't widen the approved scope from it. A successful preview doesn't guarantee the save: permissions or a feature gate can still refuse it. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. ### 4. Apply -- With the user's agreement, `set_desired_config`. Connected processes are reconfigured now, new ones when they connect. +- With the user's agreement, apply with the writer previewed: `set_desired_config`, or the REST `PUT` with the exact previewed body. Connected processes are reconfigured now, new ones when they connect. If the save is refused, report it; nothing changed. - Check it took effect: `agent_config` on one of the processes shows whether it converged. - `reconfigure_agent` changes one running process once; N|Solid sets back any key a desired config manages, and a restart forgets it. Use it only when the user wants exactly that. @@ -57,7 +59,7 @@ No MCP tool covers this: it is for the tenant's admins, in the web UI's **Connec - Never paste a token into chat or a file the user didn't ask for: it is a credential. ## Guardrails -- Never save, change or delete a config without showing the preview and getting the user's agreement. +- Never save, change or delete a config without showing the preview and having the user's agreement to that exact change. - Never target every process (`allAgents: true`) unless the user asked for exactly that. - Never turn off `redactSnapshots`, metrics or assets without spelling out the consequence first. - Prefer expiring overrides for diagnostics, so nothing stays switched on by accident. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 5df1244..5cad603 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -22,7 +22,7 @@ Run what fits the question; for a full briefing, all of it: - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform: read `get_agent` for the connected agents it returned (reuse any detail already read) to get each one's platform and runtime verdict before grouping them into version and platform pairs. For the current app's advisories, end of life and the same-line patch, prefer these connected-agent verdicts; the patch is the verdict's `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for the Node.js runtime sources' `checkedAt`, errors and freshness. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform; never infer a platform from it. When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` to get each process's `nodeVersion` and platform, then read `get_agent` once per observed version and platform pair, not for every identical replica. MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps the app's coverage explicitly incomplete instead of reading every agent. Reuse any detail already read. `get_agent` is not a list and has no list cut: don't report one for it. When process detail is unavailable or denied, an authorized `GET /v1/tenants/{tenant}/runtimes?app=` still gives positive advisories for runtimes seen in the last day: keep them, labeled with that window, but they don't prove which platforms run now, and its top-level `fixed` (aggregated over historical platforms) is neither a connected-only patch recommendation nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding findings already known. For the current app's advisories, end of life and the same-line patch, prefer these connected-agent verdicts; the patch is the verdict's `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for the Node.js runtime sources' `checkedAt`, errors and freshness. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index 3b8ab9c..b02f8cb 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -37,7 +37,11 @@ Otherwise, check in order: ### 2b. Check What Production Runs (when N|Solid is available) If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). - Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm what a known app runs now with `find_agents` given the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`). Historical, disconnected or exited processes are not confirmation of what production runs now. The result is partial on a `More agents match` notice or on a cut, too-large notice or error in any content block; a result that fills the default 50 or the requested limit is not proof of completeness either. Every version it does show stands; don't claim it covers all of the app's processes. Production may run a different version than the workspace pins: say so, and plan from what runs. -- `find_agents` gives each process's version but not its platform. Read `get_agent` for the connected agents it returned (reuse any detail already read) to get each one's platform and runtime verdict before grouping them into version and platform pairs. Prefer these connected-agent verdicts for the current same-line patch decision: each gives end of life, core advisories and the fixing release as `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). +- `find_agents` gives each process's version but not its platform; never infer a platform from it. Get each pair's platform before grouping, then one runtime verdict per version and platform pair, reusing any detail already read: + - When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` for each process's `nodeVersion` and platform, then read `get_agent` once per observed pair rather than for every identical replica. + - MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps coverage explicitly incomplete rather than reading every agent. `get_agent` is not a list and has no list cut: don't report one for it. + - When process detail is unavailable or denied, an authorized `GET /v1/tenants/{tenant}/runtimes?app=` still gives positive advisories for runtimes seen in the last day: keep them, labeled with that window, but they don't prove which platforms run now, and its top-level `fixed` (aggregated over historical platforms) is neither a connected-only patch recommendation nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding known findings. +- Prefer these connected-agent verdicts for the current same-line patch decision: each gives end of life, core advisories and the fixing release as `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). - A verdict covers only that agent's own version and platform: one process doesn't prove other platforms safe. An unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. - Each confirmed version and platform pair needs a verdict; a partial `find_agents` leaves the rest of the app's processes uncovered. - Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for the Node.js runtime sources' `checkedAt`, errors and freshness. From 6047a890c9328f522239610c1cfb2820dbd0226b Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 20:32:14 -0500 Subject: [PATCH 07/14] fix: preserve selector semantics and preview coverage --- skills/ns-configure-agents/SKILL.md | 11 ++++++----- skills/ns-fleet-health/SKILL.md | 2 +- skills/ns-node-upgrade/SKILL.md | 9 +++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index 0981901..4952d63 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -26,10 +26,11 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now; if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the exact same change (every writable setting, target, priority, state and expiry) still counts; ask nothing more. A go-ahead for the same tenant, name or target with a different body does not. - `preview_desired_config` shows which processes would change; it does not prove a name is unused and does not keep the old config's settings. - To edit an existing config, start from its current, intact record and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't intact, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP. - - An intact record is the exact tenant and name's own entry, from a successful `list_desired_configs` result or an authorized `GET /v1/tenants/{tenant}/configs`, with every writable field present and nothing cut in it: no cut notice, `outputCut` or omission touching that record, its target (`where`/`whereText` or selector), its `config`, its priority, its state or its expiry. Such a record can be used even when the list is partial and other, unrelated records were omitted: the name appearing already proves the config exists. A cut that only touches known read-only metadata (such as who saved it or when) leaves the writable values usable. Documented compaction is not a cut: MCP results drop null fields, so in an otherwise intact record a missing `expiresAt` means the config is permanent (REST `null`); treat it as present and don't block the edit over it. An unexplained cut path that could cover a writable field still makes the record unusable. A complete, uncut list is needed only to conclude a name is absent, as above, not to edit a record that is present and intact. + - An intact record is the exact tenant and name's own entry, from a successful `list_desired_configs` result or an authorized `GET /v1/tenants/{tenant}/configs`, with every writable field present and nothing cut in it: no cut notice, `outputCut` or omission touching that record, its target (`where`/`whereText` or selector), its `config`, its priority, its state or its expiry. Such a record can be used even when the list is partial and other, unrelated records were omitted: the name appearing already proves the config exists. A cut that only touches known read-only metadata (such as who saved it or when) leaves the writable values usable. Documented compaction is not a cut: MCP results drop null fields, so in an otherwise intact record a missing `expiresAt` means the config is permanent (REST `null`); treat it as present and don't block the edit over it. That compaction also drops nested null operands inside the `where` tree (`hostname != null` loses its `value: null`), so an MCP `where` tree can be malformed with no cut notice at all; the `expiresAt` exception says nothing about the tree. Never replay an MCP `where` tree. An unexplained cut path that could cover a writable field still makes the record unusable. A complete, uncut list is needed only to conclude a name is absent, as above, not to edit a record that is present and intact. - The Console's Process Settings can't edit arbitrary config names or keep a config's state or absolute expiry: don't send the user there for such an edit. - - With the user's authorization for the REST API, `PUT /v1/tenants/{tenant}/configs/{name}` with the exact requested tenant and name accepts `config`, the target (`where` or a selector, never both), `priority`, `enabled`, and either `expiresAt` (an absolute time in the future; omit it for a permanent config) or `durationMs`, never both. Carry over every unchanged writable value from the record and change only what the user asked. When the target is unchanged, send the record's `where` tree as is: don't swap in the rendered `whereText` (it can exceed the 4,000-character input limit), rebuild it as a legacy selector, simplify or widen it, and never send `where` and a selector together. Build the body from those fields; don't pass the read record through, since its metadata isn't input. - - Before the `PUT`, send that exact body to `POST /v1/tenants/{tenant}/configs/{name}/preview` (it takes the same body as the `PUT`) and always show the user the result. If the user already authorized this exact body on this tenant and name, that authorization covers it: don't ask again. Otherwise, or if the body changed in any way, get their agreement to the body shown. Don't substitute `preview_desired_config`: it can't carry `enabled` or an absolute `expiresAt`. + - With the user's authorization for the REST API, `PUT /v1/tenants/{tenant}/configs/{name}` with the exact requested tenant and name accepts `config`, the target (`where` or a selector, never both), `priority`, `enabled`, and either `expiresAt` (an absolute time in the future; omit it for a permanent config) or `durationMs`, never both. Carry over every unchanged writable value from the record and change only what the user asked. Build the body from those fields; don't pass the read record through, since its metadata isn't input. + - An unchanged target must keep its exact semantics, with either writer. Prefer the canonical `where` tree from an authorized `GET /v1/tenants/{tenant}/configs` (raw REST keeps nulls), sent unmodified. An unchanged, uncut `whereText` that fits the writer's input (at most 4,000 characters) may also be used exactly. When the expression is longer or its `whereText` is missing, cut or invalid, recover the tree from that already authorized REST list; if no faithful representation is available, hand the user the drafted change. Never rebuild it as a legacy selector, simplify or widen it, and never send `where` and a selector together. + - Before the `PUT`, send that exact body to `POST /v1/tenants/{tenant}/configs/{name}/preview` (it takes the same body as the `PUT`) and always show the user the result. If the user already authorized this exact body on this tenant and name, that authorization covers it: don't ask again, unless the preview shows material impact outside the scope they authorized, which needs their agreement for that scope. Otherwise, or if the body changed in any way, get their agreement to the body shown. Don't substitute `preview_desired_config`: it can't carry `enabled` or an absolute `expiresAt`. - If the kept `expiresAt` is present but no longer in the future (an expired config stays expired; never omit or renew it to force the save) or can't be written exactly, or the REST write or preview isn't authorized, leave the config unchanged and hand the user the drafted change; don't renew, extend or drop the expiry to make it fit. - When neither writer can make the edit faithfully, leave the config unchanged and hand the user the drafted change with its intact current record, naming the fields that must be preserved (target, priority, state, absolute expiry, and any field the MCP schema doesn't list). - Never round, renew, extend or drop an expiry, drop a field the schema doesn't list, or re-enable a config as a side effect; change any of them only when the user asks, and say so. @@ -37,11 +38,11 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres ### 3. Preview - `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. A REST replacement is previewed with its own exact body through `POST /v1/tenants/{tenant}/configs/{name}/preview`, as in New Names and Replacing a Config. It says nothing about whether the name is in use: check that as in New Names and Replacing a Config. - Preview with the writer that will apply: REST preview then the same-body REST `PUT`, or `preview_desired_config` then `set_desired_config` with the same inputs; don't mix them. -- A preview can be truncated (`truncated`): label its matching and changing processes as partial, don't claim complete counts, and don't widen the approved scope from it. A successful preview doesn't guarantee the save: permissions or a feature gate can still refuse it. +- A preview can be truncated (`truncated`): label its counts as incomplete, and don't widen the approved scope from it. Separately, the processes it names are a sample even without that flag: MCP shows at most 10 agents, REST at most 50. Counts can be complete while the shown agents are a sample; say which is which and never imply every affected process is listed. A successful preview doesn't guarantee the save: permissions or a feature gate can still refuse it. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. ### 4. Apply -- With the user's agreement, apply with the writer previewed: `set_desired_config`, or the REST `PUT` with the exact previewed body. Connected processes are reconfigured now, new ones when they connect. If the save is refused, report it; nothing changed. +- With the user's agreement, apply with the writer previewed: `set_desired_config`, or the REST `PUT` with the exact previewed body. Connected processes are reconfigured now, new ones when they connect. If the save is refused, report it; nothing changed. A timeout, 5xx or other uncertain outcome is not a refusal: don't claim nothing changed and don't retry automatically; re-read the current config to reconcile before any further write. - Check it took effect: `agent_config` on one of the processes shows whether it converged. - `reconfigure_agent` changes one running process once; N|Solid sets back any key a desired config manages, and a restart forgets it. Use it only when the user wants exactly that. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 5cad603..4098217 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -22,7 +22,7 @@ Run what fits the question; for a full briefing, all of it: - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform; never infer a platform from it. When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` to get each process's `nodeVersion` and platform, then read `get_agent` once per observed version and platform pair, not for every identical replica. MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps the app's coverage explicitly incomplete instead of reading every agent. Reuse any detail already read. `get_agent` is not a list and has no list cut: don't report one for it. When process detail is unavailable or denied, an authorized `GET /v1/tenants/{tenant}/runtimes?app=` still gives positive advisories for runtimes seen in the last day: keep them, labeled with that window, but they don't prove which platforms run now, and its top-level `fixed` (aggregated over historical platforms) is neither a connected-only patch recommendation nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding findings already known. For the current app's advisories, end of life and the same-line patch, prefer these connected-agent verdicts; the patch is the verdict's `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for the Node.js runtime sources' `checkedAt`, errors and freshness. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform; never infer a platform from it. When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` to get each process's `nodeVersion` and platform, then read `get_agent` once per observed version and platform pair, not for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. A complete authorized REST connected-agent inventory supersedes an earlier partial `find_agents` sample: completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps the app's coverage explicitly incomplete instead of reading every agent. Reuse any detail already read. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. When process detail is unavailable or denied, an authorized `GET /v1/tenants/{tenant}/runtimes?app=` still gives positive advisories for runtimes seen in the last day: keep them, labeled with that window, but they don't prove which platforms run now, and each version's `runtimes[].fixed` (aggregated over the platforms seen in the last day) is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding findings already known, and the `vulnerability_data` source check below applies to this fallback too. For the current app's advisories, end of life and the same-line patch, prefer these connected-agent verdicts; the patch is the verdict's `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index b02f8cb..54716c4 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -38,13 +38,14 @@ Otherwise, check in order: If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). - Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm what a known app runs now with `find_agents` given the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`). Historical, disconnected or exited processes are not confirmation of what production runs now. The result is partial on a `More agents match` notice or on a cut, too-large notice or error in any content block; a result that fills the default 50 or the requested limit is not proof of completeness either. Every version it does show stands; don't claim it covers all of the app's processes. Production may run a different version than the workspace pins: say so, and plan from what runs. - `find_agents` gives each process's version but not its platform; never infer a platform from it. Get each pair's platform before grouping, then one runtime verdict per version and platform pair, reusing any detail already read: - - When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` for each process's `nodeVersion` and platform, then read `get_agent` once per observed pair rather than for every identical replica. - - MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps coverage explicitly incomplete rather than reading every agent. `get_agent` is not a list and has no list cut: don't report one for it. - - When process detail is unavailable or denied, an authorized `GET /v1/tenants/{tenant}/runtimes?app=` still gives positive advisories for runtimes seen in the last day: keep them, labeled with that window, but they don't prove which platforms run now, and its top-level `fixed` (aggregated over historical platforms) is neither a connected-only patch recommendation nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding known findings. + - When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` for each process's `nodeVersion` and platform, then read `get_agent` once per observed pair rather than for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. + - A complete authorized REST connected-agent inventory supersedes an earlier partial `find_agents` sample: completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. + - MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps coverage explicitly incomplete rather than reading every agent. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. + - When process detail is unavailable or denied, an authorized `GET /v1/tenants/{tenant}/runtimes?app=` still gives positive advisories for runtimes seen in the last day: keep them, labeled with that window, but they don't prove which platforms run now, and each version's `runtimes[].fixed` (aggregated over the platforms seen in the last day) is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding known findings, and the `vulnerability_data` source check below applies to this fallback too. - Prefer these connected-agent verdicts for the current same-line patch decision: each gives end of life, core advisories and the fixing release as `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). - A verdict covers only that agent's own version and platform: one process doesn't prove other platforms safe. An unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. - Each confirmed version and platform pair needs a verdict; a partial `find_agents` leaves the rest of the app's processes uncovered. - - Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for the Node.js runtime sources' `checkedAt`, errors and freshness. + - Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. - If a source is missing, stale or failed, or any pair or process is left without a verdict, say advisory coverage is incomplete, keep the advisories observed, and don't conclude that no same-line patch is needed. - Core advisories with a fix on the same line (`runtime.fixed`, or `upgradeTo` in `list_runtimes`) call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. - If the server isn't available, continue from the workspace alone. From bf42d545f1af94ce2ceb44d5c3cf828345268f2d Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 20:46:16 -0500 Subject: [PATCH 08/14] fix: separate writer inputs and preview observations --- skills/ns-configure-agents/SKILL.md | 20 +++++++++++++------- skills/ns-fleet-health/SKILL.md | 2 +- skills/ns-node-upgrade/SKILL.md | 14 +++++++------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index 4952d63..3353799 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -14,9 +14,14 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - `desired_config_history` with a config's `name`: its 50 newest saves and deletes, each with the settings, target, priority, expiry, who made it and when. Deleted configs keep their history. Read it before changing or deleting a named config, to see who set it and what it held before. No tool restores an earlier version: to go back to one, make it a new change and preview and apply it as below. ### 2. Shape the Change -- `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. `list_desired_configs` names those settings `config`; its `where` is an expression tree, while `whereText` is the expression string the input accepts. Do not pass the entire returned row as tool arguments. Selectors and configuration subtrees are kept whole, but their entire entry can be omitted by the byte cap: a missing name in a partial list is not proof it is absent. +- `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. `list_desired_configs` names those settings `config`; its `where` is an expression tree, while `whereText` is the expression string the input accepts: the MCP `where` input takes a string only, never a JSON tree or a JSON-stringified tree. Do not pass the entire returned row as tool arguments. Selectors and configuration subtrees are kept whole, but their entire entry can be omitted by the byte cap: a missing name in a partial list is not proof it is absent. - Settings: `tracingEnabled`, `traceSampleRate` (0 to 1), `contCpuProfile` (continuous CPU profiling), `interval` (metrics, ms), `blockedLoopThreshold` (ms), `pauseMetrics`, `promiseTracking`, `redactSnapshots`, `assetsEnabled`, `tags`. Turning `contCpuProfile` on needs the tenant's continuous-profiling feature: where N|Solid has it off, the save is refused, and processes stay off whatever a config says. -- Target: `app`, `tags`, one `agentId`, or `where`, an expression over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields; it takes no `tenant`). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. +- Target: `app`, `tags`, one `agentId`, or `where`, an expression string over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields; it takes no `tenant`). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. +- Selector paths must be unambiguous for both writers, for new and edited configs alike: + - `preview_desired_config` and `set_desired_config` take `where` as a string only: an unchanged, valid, uncut `whereText` of at most 4,000 characters, used exactly. Never pass them a JSON tree or a JSON-stringified tree. + - When no faithful supported string is available or it is too long, use the REST writer only if REST writes are already authorized, with the canonical tree; otherwise hand the user the drafted change. Permission to read REST doesn't authorize a REST write, and a REST tree never becomes usable by MCP. + - The REST preview and `PUT` may take the canonical `where` tree, unmodified and with its nulls kept, from an actual `GET /v1/tenants/{tenant}/configs` response, or the faithful supported expression text; never a compacted, possibly malformed MCP tree. + - Never rebuild a kept target as a legacy selector, simplify or widen it, and never send `where` and a selector together. - **Prefer an expiring override** for anything diagnostic: `expiresInMinutes` makes it stop applying, and the processes go back to what they had. Tracing or continuous profiling "for the next hour" is an override of 60 minutes. - `priority`: higher wins where configs disagree. - Names use letters, digits, `.`, `_` and `-`: `checkout-tracing-1h`. @@ -24,19 +29,20 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres #### New Names and Replacing a Config - Saving under a name that exists replaces that config. A name missing from a bounded or partial `list_desired_configs` result does not authorize replacing anything: before saving a config as new, verify the exact tenant and name in a successful, complete, uncut `list_desired_configs` JSON result (no JSON `outputCut`, no cut notice, too-large notice or error in any content block, nothing omitted) or an authorized `GET /v1/tenants/{tenant}/configs` (the full REST list; there is no route for one config by name). The Console doesn't list every arbitrarily named config, so it is not proof that a name is absent. If neither source is available, don't save it: hand the user the drafted spec to check and save themselves. - The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now; if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the exact same change (every writable setting, target, priority, state and expiry) still counts; ask nothing more. A go-ahead for the same tenant, name or target with a different body does not. -- `preview_desired_config` shows which processes would change; it does not prove a name is unused and does not keep the old config's settings. +- `preview_desired_config` shows what the selector matches and which processes would still converge toward the full desired state (see Preview); it does not prove a name is unused and does not keep the old config's settings. - To edit an existing config, start from its current, intact record and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't intact, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP. - - An intact record is the exact tenant and name's own entry, from a successful `list_desired_configs` result or an authorized `GET /v1/tenants/{tenant}/configs`, with every writable field present and nothing cut in it: no cut notice, `outputCut` or omission touching that record, its target (`where`/`whereText` or selector), its `config`, its priority, its state or its expiry. Such a record can be used even when the list is partial and other, unrelated records were omitted: the name appearing already proves the config exists. A cut that only touches known read-only metadata (such as who saved it or when) leaves the writable values usable. Documented compaction is not a cut: MCP results drop null fields, so in an otherwise intact record a missing `expiresAt` means the config is permanent (REST `null`); treat it as present and don't block the edit over it. That compaction also drops nested null operands inside the `where` tree (`hostname != null` loses its `value: null`), so an MCP `where` tree can be malformed with no cut notice at all; the `expiresAt` exception says nothing about the tree. Never replay an MCP `where` tree. An unexplained cut path that could cover a writable field still makes the record unusable. A complete, uncut list is needed only to conclude a name is absent, as above, not to edit a record that is present and intact. + - An intact record is the exact tenant and name's own entry, from a successful `list_desired_configs` result or an authorized `GET /v1/tenants/{tenant}/configs`, with every writable field present and nothing cut in it: no cut notice, `outputCut` or omission touching that record, its target (`where`/`whereText` or selector), its `config`, its priority, its state or its expiry. Such a record can be used even when the list is partial and other, unrelated records were omitted: the name appearing already proves the config exists. A cut that only touches known read-only metadata (such as who saved it or when) leaves the writable values usable. Documented compaction is not a cut: MCP results drop null fields, so in an otherwise intact record a missing `expiresAt` means the config is permanent (REST `null`); treat it as present and don't block the edit over it. That compaction also removes null object properties inside the `where` tree, operands included (`hostname != null` loses its `value: null`), while null array entries stay `null`; so an MCP `where` tree can be malformed with no cut notice at all, and the `expiresAt` exception says nothing about the tree. Never replay an MCP `where` tree with either writer. An unexplained cut path that could cover a writable field still makes the record unusable. A complete, uncut list is needed only to conclude a name is absent, as above, not to edit a record that is present and intact. - The Console's Process Settings can't edit arbitrary config names or keep a config's state or absolute expiry: don't send the user there for such an edit. - With the user's authorization for the REST API, `PUT /v1/tenants/{tenant}/configs/{name}` with the exact requested tenant and name accepts `config`, the target (`where` or a selector, never both), `priority`, `enabled`, and either `expiresAt` (an absolute time in the future; omit it for a permanent config) or `durationMs`, never both. Carry over every unchanged writable value from the record and change only what the user asked. Build the body from those fields; don't pass the read record through, since its metadata isn't input. - - An unchanged target must keep its exact semantics, with either writer. Prefer the canonical `where` tree from an authorized `GET /v1/tenants/{tenant}/configs` (raw REST keeps nulls), sent unmodified. An unchanged, uncut `whereText` that fits the writer's input (at most 4,000 characters) may also be used exactly. When the expression is longer or its `whereText` is missing, cut or invalid, recover the tree from that already authorized REST list; if no faithful representation is available, hand the user the drafted change. Never rebuild it as a legacy selector, simplify or widen it, and never send `where` and a selector together. - - Before the `PUT`, send that exact body to `POST /v1/tenants/{tenant}/configs/{name}/preview` (it takes the same body as the `PUT`) and always show the user the result. If the user already authorized this exact body on this tenant and name, that authorization covers it: don't ask again, unless the preview shows material impact outside the scope they authorized, which needs their agreement for that scope. Otherwise, or if the body changed in any way, get their agreement to the body shown. Don't substitute `preview_desired_config`: it can't carry `enabled` or an absolute `expiresAt`. + - An unchanged target must keep its exact semantics, with either writer, by the selector rules in Shape the Change. + - Before the `PUT`, send that exact body to `POST /v1/tenants/{tenant}/configs/{name}/preview` (it takes the same body as the `PUT`) and always show the user the result. Approval follows the rules in Preview: a go-ahead for this exact body on this tenant and name stays valid; a changed body, or an actual effect outside the authorized scope, needs their agreement to it. Don't substitute `preview_desired_config`: it can't carry `enabled` or an absolute `expiresAt`. - If the kept `expiresAt` is present but no longer in the future (an expired config stays expired; never omit or renew it to force the save) or can't be written exactly, or the REST write or preview isn't authorized, leave the config unchanged and hand the user the drafted change; don't renew, extend or drop the expiry to make it fit. - When neither writer can make the edit faithfully, leave the config unchanged and hand the user the drafted change with its intact current record, naming the fields that must be preserved (target, priority, state, absolute expiry, and any field the MCP schema doesn't list). - Never round, renew, extend or drop an expiry, drop a field the schema doesn't list, or re-enable a config as a side effect; change any of them only when the user asks, and say so. ### 3. Preview -- `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. A REST replacement is previewed with its own exact body through `POST /v1/tenants/{tenant}/configs/{name}/preview`, as in New Names and Replacing a Config. It says nothing about whether the name is in use: check that as in New Names and Replacing a Config. +- `preview_desired_config` with the same inputs, without saving. Show it to the user with each count's meaning: `matching` counts the connected processes the proposed selector matches; `changing` and the listed agents are processes not yet at the full desired state once this is saved, which includes other configs' pending drift, not only the causal effect of this edit. A REST replacement is previewed with its own exact body through `POST /v1/tenants/{tenant}/configs/{name}/preview`, as in New Names and Replacing a Config. A preview says nothing about whether the name is in use: check that as in New Names and Replacing a Config. +- Judge impact from the actual proposed target and settings against the scope the user authorized, not from the aggregate `changing` count or the capped sample. Unrelated drift is neither caused by this edit nor a reason to broaden authorization. For either writer, the user's go-ahead for the same exact change and scope stays valid: don't ask again just because unrelated drift or replica counts moved. A changed proposal, or an actual effect outside the authorized scope, needs their agreement to that. - Preview with the writer that will apply: REST preview then the same-body REST `PUT`, or `preview_desired_config` then `set_desired_config` with the same inputs; don't mix them. - A preview can be truncated (`truncated`): label its counts as incomplete, and don't widen the approved scope from it. Separately, the processes it names are a sample even without that flag: MCP shows at most 10 agents, REST at most 50. Counts can be complete while the shown agents are a sample; say which is which and never imply every affected process is listed. A successful preview doesn't guarantee the save: permissions or a feature gate can still refuse it. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 4098217..9e0dba9 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -22,7 +22,7 @@ Run what fits the question; for a full briefing, all of it: - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform; never infer a platform from it. When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` to get each process's `nodeVersion` and platform, then read `get_agent` once per observed version and platform pair, not for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. A complete authorized REST connected-agent inventory supersedes an earlier partial `find_agents` sample: completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps the app's coverage explicitly incomplete instead of reading every agent. Reuse any detail already read. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. When process detail is unavailable or denied, an authorized `GET /v1/tenants/{tenant}/runtimes?app=` still gives positive advisories for runtimes seen in the last day: keep them, labeled with that window, but they don't prove which platforms run now, and each version's `runtimes[].fixed` (aggregated over the platforms seen in the last day) is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding findings already known, and the `vulnerability_data` source check below applies to this fallback too. For the current app's advisories, end of life and the same-line patch, prefer these connected-agent verdicts; the patch is the verdict's `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Its `upgradeTo` projects REST `runtimes[].fixed`: both aggregate over the versions and platforms seen in the last day. Keep their positive advisories and aggregate fix as last-day observations, labeled so; neither proves the patch target or an all-clear for only the connected platforms when historical platforms differ. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform; never infer a platform from it. When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` to get each process's `nodeVersion` and platform, then read `get_agent` once per observed version and platform pair, not for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. Paging an authorized REST connected-agent inventory successfully through its final page closes the `find_agents` sampling gap, but live pages aren't an atomic snapshot: state when the scan ran and what it observed, and keep and reconcile any version seen earlier that it contradicts rather than deleting it or claiming an instantaneous all-clear for the whole app. A cursor or page error leaves the inventory partial. Completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps the app's coverage explicitly incomplete instead of reading every agent. Reuse any detail already read. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. When process detail is unavailable or denied, keep the last-day aggregate evidence from `list_runtimes` or an authorized `GET /v1/tenants/{tenant}/runtimes?app=` (positive advisories and `upgradeTo`/`runtimes[].fixed`), labeled with that window and with the coverage limit: it doesn't prove which platforms run now and is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding findings already known, and the `vulnerability_data` source check below applies to this fallback too. For the current app's advisories, end of life and the same-line patch, prefer these verified per-platform connected-agent verdicts; the patch is the verdict's `runtime.fixed`, which is for that agent's actual version and platform and can differ from the aggregate `upgradeTo`. A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index 54716c4..d15162b 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -35,19 +35,19 @@ Otherwise, check in order: **At the start of your response**, briefly tell the user which source was used to determine the current version (one sentence). If the project-pinned version and the runtime on PATH disagree, treat the project pin as authoritative and note the mismatch. ### 2b. Check What Production Runs (when N|Solid is available) -If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). +If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). `upgradeTo` projects REST `runtimes[].fixed`: both aggregate over the versions and platforms seen in the last day. Keep their positive advisories and aggregate fix as last-day observations, labeled so; neither proves the patch target or an all-clear for only the connected platforms when historical platforms differ. - Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm what a known app runs now with `find_agents` given the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`). Historical, disconnected or exited processes are not confirmation of what production runs now. The result is partial on a `More agents match` notice or on a cut, too-large notice or error in any content block; a result that fills the default 50 or the requested limit is not proof of completeness either. Every version it does show stands; don't claim it covers all of the app's processes. Production may run a different version than the workspace pins: say so, and plan from what runs. - `find_agents` gives each process's version but not its platform; never infer a platform from it. Get each pair's platform before grouping, then one runtime verdict per version and platform pair, reusing any detail already read: - When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` for each process's `nodeVersion` and platform, then read `get_agent` once per observed pair rather than for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. - - A complete authorized REST connected-agent inventory supersedes an earlier partial `find_agents` sample: completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. + - Paging an authorized REST connected-agent inventory successfully through its final page closes the `find_agents` sampling gap, but live pages aren't an atomic snapshot: state when the scan ran and what it observed, and keep and reconcile any version seen earlier that it contradicts rather than deleting it or claiming an instantaneous all-clear for the whole app. A cursor or page error leaves the inventory partial. Completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. - MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps coverage explicitly incomplete rather than reading every agent. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. - - When process detail is unavailable or denied, an authorized `GET /v1/tenants/{tenant}/runtimes?app=` still gives positive advisories for runtimes seen in the last day: keep them, labeled with that window, but they don't prove which platforms run now, and each version's `runtimes[].fixed` (aggregated over the platforms seen in the last day) is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding known findings, and the `vulnerability_data` source check below applies to this fallback too. -- Prefer these connected-agent verdicts for the current same-line patch decision: each gives end of life, core advisories and the fixing release as `runtime.fixed` (what `list_runtimes` calls `upgradeTo`). + - When process detail is unavailable or denied, keep the last-day aggregate evidence from `list_runtimes` or an authorized `GET /v1/tenants/{tenant}/runtimes?app=` (positive advisories and `upgradeTo`/`runtimes[].fixed`), labeled with that window and with the coverage limit: it doesn't prove which platforms run now and is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding known findings, and the `vulnerability_data` source check below applies to this fallback too. +- Prefer these verified per-platform connected-agent verdicts for the current same-line patch decision: each gives end of life, core advisories and the fixing release as `runtime.fixed`, for that agent's actual version and platform; it can differ from the aggregate `upgradeTo`. - A verdict covers only that agent's own version and platform: one process doesn't prove other platforms safe. An unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. - Each confirmed version and platform pair needs a verdict; a partial `find_agents` leaves the rest of the app's processes uncovered. - Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. - If a source is missing, stale or failed, or any pair or process is left without a verdict, say advisory coverage is incomplete, keep the advisories observed, and don't conclude that no same-line patch is needed. -- Core advisories with a fix on the same line (`runtime.fixed`, or `upgradeTo` in `list_runtimes`) call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. +- Core advisories with a fix on the same line (per-agent `runtime.fixed`, or the last-day aggregate `upgradeTo` in `list_runtimes`) call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. - If the server isn't available, continue from the workspace alone. ### 3. Recommend a Target Version @@ -80,9 +80,9 @@ If the user asks about a specific version mid-conversation, re-run the detection ## Tools - `getPackageVersions` — check direct dependencies for known incompatibilities with target Node version. -- `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them. +- `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them (`upgradeTo`, aggregated over the last day). - `find_agents` (`nsolid` server, when available) — the connected processes of one app (`app`, `status: "connected"`), with the Node.js version each runs. -- `get_agent` (`nsolid` server, when available) — one process's platform and runtime verdict for its own version and platform: end of life, core advisories and `runtime.fixed` (`list_runtimes`'s `upgradeTo`). +- `get_agent` (`nsolid` server, when available) — one process's platform and runtime verdict for its own version and platform: end of life, core advisories and `runtime.fixed` (per agent; can differ from `list_runtimes`'s aggregate `upgradeTo`). - `vulnerability_data` (`nsolid` server, when available; takes no `tenant`) — when the advisory sources, Node.js runtime ones included, were last checked, and any sync error. ## Guardrails From 6c0fd8a8a81c4d7c594853418c0be1b7cc916ce1 Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 20:56:11 -0500 Subject: [PATCH 09/14] fix: scope previews and runtime evidence accurately --- skills/ns-configure-agents/SKILL.md | 15 ++++++++------- skills/ns-fleet-health/SKILL.md | 2 +- skills/ns-node-upgrade/SKILL.md | 8 ++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index 3353799..42f777f 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -17,10 +17,11 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. `list_desired_configs` names those settings `config`; its `where` is an expression tree, while `whereText` is the expression string the input accepts: the MCP `where` input takes a string only, never a JSON tree or a JSON-stringified tree. Do not pass the entire returned row as tool arguments. Selectors and configuration subtrees are kept whole, but their entire entry can be omitted by the byte cap: a missing name in a partial list is not proof it is absent. - Settings: `tracingEnabled`, `traceSampleRate` (0 to 1), `contCpuProfile` (continuous CPU profiling), `interval` (metrics, ms), `blockedLoopThreshold` (ms), `pauseMetrics`, `promiseTracking`, `redactSnapshots`, `assetsEnabled`, `tags`. Turning `contCpuProfile` on needs the tenant's continuous-profiling feature: where N|Solid has it off, the save is refused, and processes stay off whatever a config says. - Target: `app`, `tags`, one `agentId`, or `where`, an expression string over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields; it takes no `tenant`). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. -- Selector paths must be unambiguous for both writers, for new and edited configs alike: - - `preview_desired_config` and `set_desired_config` take `where` as a string only: an unchanged, valid, uncut `whereText` of at most 4,000 characters, used exactly. Never pass them a JSON tree or a JSON-stringified tree. - - When no faithful supported string is available or it is too long, use the REST writer only if REST writes are already authorized, with the canonical tree; otherwise hand the user the drafted change. Permission to read REST doesn't authorize a REST write, and a REST tree never becomes usable by MCP. - - The REST preview and `PUT` may take the canonical `where` tree, unmodified and with its nulls kept, from an actual `GET /v1/tenants/{tenant}/configs` response, or the faithful supported expression text; never a compacted, possibly malformed MCP tree. +- Selector paths must be unambiguous for both writers: + - `preview_desired_config` and `set_desired_config` take `where` as a string of at most 4,000 characters only; never a JSON tree or a JSON-stringified tree. + - **New or intentionally changed target:** author a valid proposed expression string, or use the supported `app`, `tags` or `agentId` shortcuts. It needs no old record. + - **Preserving an existing target:** reuse that config's faithful, uncut `whereText` exactly. "Unchanged `whereText`" applies only to preserving a stored target. + - The REST preview and `PUT` may take the canonical `where` tree, unmodified and with its nulls kept, from an actual `GET /v1/tenants/{tenant}/configs` response, only when preserving that existing selector and REST writes are already authorized; otherwise the faithful supported expression text. Never send a compacted, possibly malformed MCP tree. A new expression has no stored GET tree: if it exceeds 4,000 characters, or a preserved target has no faithful string MCP accepts, and no authorized writer supports a faithful representation, hand the user the drafted change. Permission to read REST doesn't authorize a REST write, and a REST tree never becomes usable by MCP. - Never rebuild a kept target as a legacy selector, simplify or widen it, and never send `where` and a selector together. - **Prefer an expiring override** for anything diagnostic: `expiresInMinutes` makes it stop applying, and the processes go back to what they had. Tracing or continuous profiling "for the next hour" is an override of 60 minutes. - `priority`: higher wins where configs disagree. @@ -28,7 +29,7 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres #### New Names and Replacing a Config - Saving under a name that exists replaces that config. A name missing from a bounded or partial `list_desired_configs` result does not authorize replacing anything: before saving a config as new, verify the exact tenant and name in a successful, complete, uncut `list_desired_configs` JSON result (no JSON `outputCut`, no cut notice, too-large notice or error in any content block, nothing omitted) or an authorized `GET /v1/tenants/{tenant}/configs` (the full REST list; there is no route for one config by name). The Console doesn't list every arbitrarily named config, so it is not proof that a name is absent. If neither source is available, don't save it: hand the user the drafted spec to check and save themselves. -- The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now; if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the exact same change (every writable setting, target, priority, state and expiry) still counts; ask nothing more. A go-ahead for the same tenant, name or target with a different body does not. +- The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now and the old target's effects (see Preview); if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the exact same change (every writable setting, target, priority, state and expiry) still counts; ask nothing more. A go-ahead for the same tenant, name or target with a different body does not. - `preview_desired_config` shows what the selector matches and which processes would still converge toward the full desired state (see Preview); it does not prove a name is unused and does not keep the old config's settings. - To edit an existing config, start from its current, intact record and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't intact, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP. - An intact record is the exact tenant and name's own entry, from a successful `list_desired_configs` result or an authorized `GET /v1/tenants/{tenant}/configs`, with every writable field present and nothing cut in it: no cut notice, `outputCut` or omission touching that record, its target (`where`/`whereText` or selector), its `config`, its priority, its state or its expiry. Such a record can be used even when the list is partial and other, unrelated records were omitted: the name appearing already proves the config exists. A cut that only touches known read-only metadata (such as who saved it or when) leaves the writable values usable. Documented compaction is not a cut: MCP results drop null fields, so in an otherwise intact record a missing `expiresAt` means the config is permanent (REST `null`); treat it as present and don't block the edit over it. That compaction also removes null object properties inside the `where` tree, operands included (`hostname != null` loses its `value: null`), while null array entries stay `null`; so an MCP `where` tree can be malformed with no cut notice at all, and the `expiresAt` exception says nothing about the tree. Never replay an MCP `where` tree with either writer. An unexplained cut path that could cover a writable field still makes the record unusable. A complete, uncut list is needed only to conclude a name is absent, as above, not to edit a record that is present and intact. @@ -41,8 +42,8 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - Never round, renew, extend or drop an expiry, drop a field the schema doesn't list, or re-enable a config as a side effect; change any of them only when the user asks, and say so. ### 3. Preview -- `preview_desired_config` with the same inputs, without saving. Show it to the user with each count's meaning: `matching` counts the connected processes the proposed selector matches; `changing` and the listed agents are processes not yet at the full desired state once this is saved, which includes other configs' pending drift, not only the causal effect of this edit. A REST replacement is previewed with its own exact body through `POST /v1/tenants/{tenant}/configs/{name}/preview`, as in New Names and Replacing a Config. A preview says nothing about whether the name is in use: check that as in New Names and Replacing a Config. -- Judge impact from the actual proposed target and settings against the scope the user authorized, not from the aggregate `changing` count or the capped sample. Unrelated drift is neither caused by this edit nor a reason to broaden authorization. For either writer, the user's go-ahead for the same exact change and scope stays valid: don't ask again just because unrelated drift or replica counts moved. A changed proposal, or an actual effect outside the authorized scope, needs their agreement to that. +- `preview_desired_config` with the same inputs, without saving. Show it to the user with each count's meaning: MCP `matchingConnectedAgents` (REST `matching`) counts the examined connected agents the proposed selector matches. `changing` counts agents with nonempty planned changes toward the full merged desired state once this is saved, which includes other configs' pending convergence, not the causal delta of this edit alone. Agents without an observed config aren't planned yet. The sample agents may include drift-only rows not counted in `changing`; drift there means keys the agent can't achieve, not merely unrelated pending convergence. A REST replacement is previewed with its own exact body through `POST /v1/tenants/{tenant}/configs/{name}/preview`, as in New Names and Replacing a Config. A preview says nothing about whether the name is in use: check that as in New Names and Replacing a Config. +- Judge impact by comparing the current and proposed target and settings against the scope the user authorized, independently of the aggregate `changing` count or the capped sample. Replacing a named config removes its old target: agents matched only by the old selector lose its settings and may revert, so include those effects in the authorization scope. Never call an effect unrelated just because it falls outside the new target. Other configs' pending convergence is neither caused by this edit nor a reason to broaden authorization; report unachievable drift rather than dismissing it. For either writer, the user's go-ahead for the same exact change stays valid when both old and new effects fall within the scope they actually authorized: don't ask again just because pending convergence or replica counts moved, and don't ask for blanket re-approval. A changed proposal, or an actual old or new effect outside the authorized scope, needs their agreement to that. - Preview with the writer that will apply: REST preview then the same-body REST `PUT`, or `preview_desired_config` then `set_desired_config` with the same inputs; don't mix them. - A preview can be truncated (`truncated`): label its counts as incomplete, and don't widen the approved scope from it. Separately, the processes it names are a sample even without that flag: MCP shows at most 10 agents, REST at most 50. Counts can be complete while the shown agents are a sample; say which is which and never imply every affected process is listed. A successful preview doesn't guarantee the save: permissions or a feature gate can still refuse it. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 9e0dba9..bde8815 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -22,7 +22,7 @@ Run what fits the question; for a full briefing, all of it: - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. Its `upgradeTo` projects REST `runtimes[].fixed`: both aggregate over the versions and platforms seen in the last day. Keep their positive advisories and aggregate fix as last-day observations, labeled so; neither proves the patch target or an all-clear for only the connected platforms when historical platforms differ. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform; never infer a platform from it. When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` to get each process's `nodeVersion` and platform, then read `get_agent` once per observed version and platform pair, not for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. Paging an authorized REST connected-agent inventory successfully through its final page closes the `find_agents` sampling gap, but live pages aren't an atomic snapshot: state when the scan ran and what it observed, and keep and reconcile any version seen earlier that it contradicts rather than deleting it or claiming an instantaneous all-clear for the whole app. A cursor or page error leaves the inventory partial. Completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps the app's coverage explicitly incomplete instead of reading every agent. Reuse any detail already read. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. When process detail is unavailable or denied, keep the last-day aggregate evidence from `list_runtimes` or an authorized `GET /v1/tenants/{tenant}/runtimes?app=` (positive advisories and `upgradeTo`/`runtimes[].fixed`), labeled with that window and with the coverage limit: it doesn't prove which platforms run now and is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding findings already known, and the `vulnerability_data` source check below applies to this fallback too. For the current app's advisories, end of life and the same-line patch, prefer these verified per-platform connected-agent verdicts; the patch is the verdict's `runtime.fixed`, which is for that agent's actual version and platform and can differ from the aggregate `upgradeTo`. A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. It is tenant-wide: each row is one Node.js version, combined across the apps and platforms that ran it in the last day. Unfiltered REST `GET /v1/tenants/{tenant}/runtimes` has that same scope; `?app=` aggregates only that app's last-day platforms, so its `runtimes[].fixed` may differ. `upgradeTo` is the MCP name for the corresponding `fixed` field, not a promise of the same value across different scopes. Keep positive advisories and fixes as observations labeled with their actual scope and window: tenant-wide advisories stay tenant context and aren't attributed to the selected app without app and platform evidence, even when the app is named in the row. Neither aggregate proves the current app or platform patch target or an all-clear. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform; never infer a platform from it. When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` to get each process's `nodeVersion` and platform, then read `get_agent` once per observed version and platform pair, not for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. Paging an authorized REST connected-agent inventory successfully through its final page closes the `find_agents` sampling gap, but live pages aren't an atomic snapshot: state when the scan ran and what it observed, and keep and reconcile any version seen earlier that it contradicts rather than deleting it or claiming an instantaneous all-clear for the whole app. A cursor or page error leaves the inventory partial. Completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps the app's coverage explicitly incomplete instead of reading every agent. Reuse any detail already read. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. When process detail is unavailable or denied, keep the last-day aggregate evidence from `list_runtimes` (tenant-wide) or an authorized `GET /v1/tenants/{tenant}/runtimes?app=` (that app only) (positive advisories and `upgradeTo`/`runtimes[].fixed`), labeled with its scope, window and coverage limit: it doesn't prove which platforms run now and is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding findings already known, and the `vulnerability_data` source check below applies to this fallback too. For the current app's advisories, end of life and the same-line patch, prefer these verified per-platform connected-agent verdicts; the patch is the verdict's `runtime.fixed`, which is for that agent's actual version and platform and can differ from the aggregate `upgradeTo`. A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index d15162b..07e4856 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -35,19 +35,19 @@ Otherwise, check in order: **At the start of your response**, briefly tell the user which source was used to determine the current version (one sentence). If the project-pinned version and the runtime on PATH disagree, treat the project pin as authoritative and note the mismatch. ### 2b. Check What Production Runs (when N|Solid is available) -If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). `upgradeTo` projects REST `runtimes[].fixed`: both aggregate over the versions and platforms seen in the last day. Keep their positive advisories and aggregate fix as last-day observations, labeled so; neither proves the patch target or an all-clear for only the connected platforms when historical platforms differ. +If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). It is tenant-wide: each row is one Node.js version, combined across the apps and platforms that ran it in the last day. Unfiltered REST `GET /v1/tenants/{tenant}/runtimes` has that same scope; `?app=` aggregates only that app's last-day platforms, so its `runtimes[].fixed` may differ. `upgradeTo` is the MCP name for the corresponding `fixed` field, not a promise of the same value across different scopes. Keep positive advisories and fixes as observations labeled with their actual scope and window: tenant-wide advisories stay tenant context and aren't attributed to the project's app without app and platform evidence, even when the app is named in the row. Neither aggregate proves the current app or platform patch target or an all-clear. - Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm what a known app runs now with `find_agents` given the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`). Historical, disconnected or exited processes are not confirmation of what production runs now. The result is partial on a `More agents match` notice or on a cut, too-large notice or error in any content block; a result that fills the default 50 or the requested limit is not proof of completeness either. Every version it does show stands; don't claim it covers all of the app's processes. Production may run a different version than the workspace pins: say so, and plan from what runs. - `find_agents` gives each process's version but not its platform; never infer a platform from it. Get each pair's platform before grouping, then one runtime verdict per version and platform pair, reusing any detail already read: - When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` for each process's `nodeVersion` and platform, then read `get_agent` once per observed pair rather than for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. - Paging an authorized REST connected-agent inventory successfully through its final page closes the `find_agents` sampling gap, but live pages aren't an atomic snapshot: state when the scan ran and what it observed, and keep and reconcile any version seen earlier that it contradicts rather than deleting it or claiming an instantaneous all-clear for the whole app. A cursor or page error leaves the inventory partial. Completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. - MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps coverage explicitly incomplete rather than reading every agent. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. - - When process detail is unavailable or denied, keep the last-day aggregate evidence from `list_runtimes` or an authorized `GET /v1/tenants/{tenant}/runtimes?app=` (positive advisories and `upgradeTo`/`runtimes[].fixed`), labeled with that window and with the coverage limit: it doesn't prove which platforms run now and is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding known findings, and the `vulnerability_data` source check below applies to this fallback too. + - When process detail is unavailable or denied, keep the last-day aggregate evidence from `list_runtimes` (tenant-wide) or an authorized `GET /v1/tenants/{tenant}/runtimes?app=` (that app only) (positive advisories and `upgradeTo`/`runtimes[].fixed`), labeled with its scope, window and coverage limit: it doesn't prove which platforms run now and is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding known findings, and the `vulnerability_data` source check below applies to this fallback too. - Prefer these verified per-platform connected-agent verdicts for the current same-line patch decision: each gives end of life, core advisories and the fixing release as `runtime.fixed`, for that agent's actual version and platform; it can differ from the aggregate `upgradeTo`. - A verdict covers only that agent's own version and platform: one process doesn't prove other platforms safe. An unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. - Each confirmed version and platform pair needs a verdict; a partial `find_agents` leaves the rest of the app's processes uncovered. - Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. - If a source is missing, stale or failed, or any pair or process is left without a verdict, say advisory coverage is incomplete, keep the advisories observed, and don't conclude that no same-line patch is needed. -- Core advisories with a fix on the same line (per-agent `runtime.fixed`, or the last-day aggregate `upgradeTo` in `list_runtimes`) call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. +- When a same-line fixing release is shown to apply to the verified target version and platform (per-agent `runtime.fixed`), recommend that patch upgrade first, whatever the line move: it is the smallest change that fixes them. An aggregate `upgradeTo`/`runtimes[].fixed` stays an observation for its own scope; don't prescribe a tenant-wide `upgradeTo` as the project app's patch. - If the server isn't available, continue from the workspace alone. ### 3. Recommend a Target Version @@ -80,7 +80,7 @@ If the user asks about a specific version mid-conversation, re-run the detection ## Tools - `getPackageVersions` — check direct dependencies for known incompatibilities with target Node version. -- `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them (`upgradeTo`, aggregated over the last day). +- `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them (`upgradeTo`, tenant-wide per version over the last day's apps and platforms). - `find_agents` (`nsolid` server, when available) — the connected processes of one app (`app`, `status: "connected"`), with the Node.js version each runs. - `get_agent` (`nsolid` server, when available) — one process's platform and runtime verdict for its own version and platform: end of life, core advisories and `runtime.fixed` (per agent; can differ from `list_runtimes`'s aggregate `upgradeTo`). - `vulnerability_data` (`nsolid` server, when available; takes no `tenant`) — when the advisory sources, Node.js runtime ones included, were last checked, and any sync error. From 58dfaa0c403ab43e22072584401a43ca3d5b6516 Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 21:07:51 -0500 Subject: [PATCH 10/14] fix: keep bounded-list guidance within MCP writer scope --- skills/ns-configure-agents/SKILL.md | 38 ++++++++--------------------- skills/ns-fleet-health/SKILL.md | 6 ++++- skills/ns-node-upgrade/SKILL.md | 18 ++++---------- 3 files changed, 20 insertions(+), 42 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index 42f777f..39185ff 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -14,42 +14,24 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - `desired_config_history` with a config's `name`: its 50 newest saves and deletes, each with the settings, target, priority, expiry, who made it and when. Deleted configs keep their history. Read it before changing or deleting a named config, to see who set it and what it held before. No tool restores an earlier version: to go back to one, make it a new change and preview and apply it as below. ### 2. Shape the Change -- `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. `list_desired_configs` names those settings `config`; its `where` is an expression tree, while `whereText` is the expression string the input accepts: the MCP `where` input takes a string only, never a JSON tree or a JSON-stringified tree. Do not pass the entire returned row as tool arguments. Selectors and configuration subtrees are kept whole, but their entire entry can be omitted by the byte cap: a missing name in a partial list is not proof it is absent. +- `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. `list_desired_configs` names those settings `config`; its `where` is an expression tree, while `whereText` is the expression string the input accepts. Do not pass the entire returned row as tool arguments. Selectors and configuration subtrees are kept whole, but their entire entry can be omitted by the byte cap: a missing name in a partial list is not proof it is absent. - Settings: `tracingEnabled`, `traceSampleRate` (0 to 1), `contCpuProfile` (continuous CPU profiling), `interval` (metrics, ms), `blockedLoopThreshold` (ms), `pauseMetrics`, `promiseTracking`, `redactSnapshots`, `assetsEnabled`, `tags`. Turning `contCpuProfile` on needs the tenant's continuous-profiling feature: where N|Solid has it off, the save is refused, and processes stay off whatever a config says. -- Target: `app`, `tags`, one `agentId`, or `where`, an expression string over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields; it takes no `tenant`). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. -- Selector paths must be unambiguous for both writers: - - `preview_desired_config` and `set_desired_config` take `where` as a string of at most 4,000 characters only; never a JSON tree or a JSON-stringified tree. - - **New or intentionally changed target:** author a valid proposed expression string, or use the supported `app`, `tags` or `agentId` shortcuts. It needs no old record. - - **Preserving an existing target:** reuse that config's faithful, uncut `whereText` exactly. "Unchanged `whereText`" applies only to preserving a stored target. - - The REST preview and `PUT` may take the canonical `where` tree, unmodified and with its nulls kept, from an actual `GET /v1/tenants/{tenant}/configs` response, only when preserving that existing selector and REST writes are already authorized; otherwise the faithful supported expression text. Never send a compacted, possibly malformed MCP tree. A new expression has no stored GET tree: if it exceeds 4,000 characters, or a preserved target has no faithful string MCP accepts, and no authorized writer supports a faithful representation, hand the user the drafted change. Permission to read REST doesn't authorize a REST write, and a REST tree never becomes usable by MCP. - - Never rebuild a kept target as a legacy selector, simplify or widen it, and never send `where` and a selector together. +- Target: `app`, `tags`, one `agentId`, or `where`, an expression over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields; it takes no `tenant`). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. +- MCP `where` takes a valid expression string; new expressions are fine. To keep an existing selector, reuse its valid, uncut `whereText` (at most 4,000 characters), never the compacted JSON tree or a stringified one. The `app`, `tags`, `agentId` and `allAgents` shortcuts stay MCP inputs with their usual authorization. - **Prefer an expiring override** for anything diagnostic: `expiresInMinutes` makes it stop applying, and the processes go back to what they had. Tracing or continuous profiling "for the next hour" is an override of 60 minutes. - `priority`: higher wins where configs disagree. - Names use letters, digits, `.`, `_` and `-`: `checkout-tracing-1h`. - -#### New Names and Replacing a Config -- Saving under a name that exists replaces that config. A name missing from a bounded or partial `list_desired_configs` result does not authorize replacing anything: before saving a config as new, verify the exact tenant and name in a successful, complete, uncut `list_desired_configs` JSON result (no JSON `outputCut`, no cut notice, too-large notice or error in any content block, nothing omitted) or an authorized `GET /v1/tenants/{tenant}/configs` (the full REST list; there is no route for one config by name). The Console doesn't list every arbitrarily named config, so it is not proof that a name is absent. If neither source is available, don't save it: hand the user the drafted spec to check and save themselves. -- The name appearing in any list, partial or not, means saving replaces that config. Show what it holds now and the old target's effects (see Preview); if the user authorized only creating a config, get their go-ahead to replace it. The user's existing go-ahead to replace that config with the exact same change (every writable setting, target, priority, state and expiry) still counts; ask nothing more. A go-ahead for the same tenant, name or target with a different body does not. -- `preview_desired_config` shows what the selector matches and which processes would still converge toward the full desired state (see Preview); it does not prove a name is unused and does not keep the old config's settings. -- To edit an existing config, start from its current, intact record and keep everything the user didn't ask to change: target, settings, priority, state (enabled or not) and expiry (`expiresAt`). `set_desired_config` only takes the fields its schema lists, and expiry only as a whole `expiresInMinutes` (1 to 10080) counted from the save; it cannot carry an absolute `expiresAt`, a state or any other field as it is. When the record isn't intact, or any of these can't be written exactly with the fields `set_desired_config` offers, don't save it through MCP. - - An intact record is the exact tenant and name's own entry, from a successful `list_desired_configs` result or an authorized `GET /v1/tenants/{tenant}/configs`, with every writable field present and nothing cut in it: no cut notice, `outputCut` or omission touching that record, its target (`where`/`whereText` or selector), its `config`, its priority, its state or its expiry. Such a record can be used even when the list is partial and other, unrelated records were omitted: the name appearing already proves the config exists. A cut that only touches known read-only metadata (such as who saved it or when) leaves the writable values usable. Documented compaction is not a cut: MCP results drop null fields, so in an otherwise intact record a missing `expiresAt` means the config is permanent (REST `null`); treat it as present and don't block the edit over it. That compaction also removes null object properties inside the `where` tree, operands included (`hostname != null` loses its `value: null`), while null array entries stay `null`; so an MCP `where` tree can be malformed with no cut notice at all, and the `expiresAt` exception says nothing about the tree. Never replay an MCP `where` tree with either writer. An unexplained cut path that could cover a writable field still makes the record unusable. A complete, uncut list is needed only to conclude a name is absent, as above, not to edit a record that is present and intact. - - The Console's Process Settings can't edit arbitrary config names or keep a config's state or absolute expiry: don't send the user there for such an edit. - - With the user's authorization for the REST API, `PUT /v1/tenants/{tenant}/configs/{name}` with the exact requested tenant and name accepts `config`, the target (`where` or a selector, never both), `priority`, `enabled`, and either `expiresAt` (an absolute time in the future; omit it for a permanent config) or `durationMs`, never both. Carry over every unchanged writable value from the record and change only what the user asked. Build the body from those fields; don't pass the read record through, since its metadata isn't input. - - An unchanged target must keep its exact semantics, with either writer, by the selector rules in Shape the Change. - - Before the `PUT`, send that exact body to `POST /v1/tenants/{tenant}/configs/{name}/preview` (it takes the same body as the `PUT`) and always show the user the result. Approval follows the rules in Preview: a go-ahead for this exact body on this tenant and name stays valid; a changed body, or an actual effect outside the authorized scope, needs their agreement to it. Don't substitute `preview_desired_config`: it can't carry `enabled` or an absolute `expiresAt`. - - If the kept `expiresAt` is present but no longer in the future (an expired config stays expired; never omit or renew it to force the save) or can't be written exactly, or the REST write or preview isn't authorized, leave the config unchanged and hand the user the drafted change; don't renew, extend or drop the expiry to make it fit. - - When neither writer can make the edit faithfully, leave the config unchanged and hand the user the drafted change with its intact current record, naming the fields that must be preserved (target, priority, state, absolute expiry, and any field the MCP schema doesn't list). -- Never round, renew, extend or drop an expiry, drop a field the schema doesn't list, or re-enable a config as a side effect; change any of them only when the user asks, and say so. +- **New name or replacement:** saving under an existing name replaces it. Before saving as new, confirm the exact tenant and name are absent from a successful, complete JSON `list_desired_configs` result (no `outputCut`, no cut, too-large or error notice in any content block) or an already authorized REST read of `GET /v1/tenants/{tenant}/configs`; a preview proves nothing about absence. Otherwise hand the user the drafted spec and don't save it as new. The exact name appearing anywhere means replacement: an existing go-ahead for that exact change and target counts; one only to create doesn't cover overwriting an unknown config. +- **Editing:** the named record needs its writable values intact; omitted unrelated rows don't matter. MCP drops null properties, so a missing `expiresAt` on an otherwise intact row means permanent. Keep the target, config, priority, state and expiry the user didn't ask to change; if `set_desired_config` can't represent them faithfully (disabled state, absolute expiry, too-long `whereText`), hand over the draft and current record instead of dropping or renewing fields. ### 3. Preview -- `preview_desired_config` with the same inputs, without saving. Show it to the user with each count's meaning: MCP `matchingConnectedAgents` (REST `matching`) counts the examined connected agents the proposed selector matches. `changing` counts agents with nonempty planned changes toward the full merged desired state once this is saved, which includes other configs' pending convergence, not the causal delta of this edit alone. Agents without an observed config aren't planned yet. The sample agents may include drift-only rows not counted in `changing`; drift there means keys the agent can't achieve, not merely unrelated pending convergence. A REST replacement is previewed with its own exact body through `POST /v1/tenants/{tenant}/configs/{name}/preview`, as in New Names and Replacing a Config. A preview says nothing about whether the name is in use: check that as in New Names and Replacing a Config. -- Judge impact by comparing the current and proposed target and settings against the scope the user authorized, independently of the aggregate `changing` count or the capped sample. Replacing a named config removes its old target: agents matched only by the old selector lose its settings and may revert, so include those effects in the authorization scope. Never call an effect unrelated just because it falls outside the new target. Other configs' pending convergence is neither caused by this edit nor a reason to broaden authorization; report unachievable drift rather than dismissing it. For either writer, the user's go-ahead for the same exact change stays valid when both old and new effects fall within the scope they actually authorized: don't ask again just because pending convergence or replica counts moved, and don't ask for blanket re-approval. A changed proposal, or an actual old or new effect outside the authorized scope, needs their agreement to that. -- Preview with the writer that will apply: REST preview then the same-body REST `PUT`, or `preview_desired_config` then `set_desired_config` with the same inputs; don't mix them. -- A preview can be truncated (`truncated`): label its counts as incomplete, and don't widen the approved scope from it. Separately, the processes it names are a sample even without that flag: MCP shows at most 10 agents, REST at most 50. Counts can be complete while the shown agents are a sample; say which is which and never imply every affected process is listed. A successful preview doesn't guarantee the save: permissions or a feature gate can still refuse it. +- `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. +- `matchingConnectedAgents` counts agents the proposed selector matches; `changing` counts agents with nonempty planned changes toward the full merged desired state, not this edit's own delta. The sample (at most 10 agents) may include drift-only keys they can't achieve; agents without an observed config aren't planned yet; `truncated` makes counts incomplete. +- A replacement also withdraws the old target's effects where that config is active and effective; an inactive, expired or shadowed one isn't assumed to revert agents. A prior go-ahead for the exact change holds while the actual effects stay within it; moved counts alone need no new approval. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. ### 4. Apply -- With the user's agreement, apply with the writer previewed: `set_desired_config`, or the REST `PUT` with the exact previewed body. Connected processes are reconfigured now, new ones when they connect. If the save is refused, report it; nothing changed. A timeout, 5xx or other uncertain outcome is not a refusal: don't claim nothing changed and don't retry automatically; re-read the current config to reconcile before any further write. +- With the user's agreement, `set_desired_config`. Connected processes are reconfigured now, new ones when they connect. - Check it took effect: `agent_config` on one of the processes shows whether it converged. - `reconfigure_agent` changes one running process once; N|Solid sets back any key a desired config manages, and a restart forgets it. Use it only when the user wants exactly that. @@ -67,7 +49,7 @@ No MCP tool covers this: it is for the tenant's admins, in the web UI's **Connec - Never paste a token into chat or a file the user didn't ask for: it is a credential. ## Guardrails -- Never save, change or delete a config without showing the preview and having the user's agreement to that exact change. +- Never save, change or delete a config without showing the preview and getting the user's agreement. - Never target every process (`allAgents: true`) unless the user asked for exactly that. - Never turn off `redactSnapshots`, metrics or assets without spelling out the consequence first. - Prefer expiring overrides for diagnostics, so nothing stays switched on by accident. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index bde8815..25bd4cb 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -22,7 +22,11 @@ Run what fits the question; for a full briefing, all of it: - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them. It is tenant-wide: each row is one Node.js version, combined across the apps and platforms that ran it in the last day. Unfiltered REST `GET /v1/tenants/{tenant}/runtimes` has that same scope; `?app=` aggregates only that app's last-day platforms, so its `runtimes[].fixed` may differ. `upgradeTo` is the MCP name for the corresponding `fixed` field, not a promise of the same value across different scopes. Keep positive advisories and fixes as observations labeled with their actual scope and window: tenant-wide advisories stay tenant context and aren't attributed to the selected app without app and platform evidence, even when the app is named in the row. Neither aggregate proves the current app or platform patch target or an all-clear. Each version names at most 20 apps; `appsOmitted` counts the rest, and an `outputCut` notice can hide more. For one app's current version, use `find_agents` with the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`); historical, disconnected or exited processes don't confirm what runs now. A `More agents match` notice or a cut, too-large notice or error in any content block makes the result partial, and a result that fills the default 50 or the requested limit isn't proof of completeness: keep every version observed and don't claim the whole app was covered. `find_agents` gives each process's version but not its platform; never infer a platform from it. When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` to get each process's `nodeVersion` and platform, then read `get_agent` once per observed version and platform pair, not for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. Paging an authorized REST connected-agent inventory successfully through its final page closes the `find_agents` sampling gap, but live pages aren't an atomic snapshot: state when the scan ran and what it observed, and keep and reconcile any version seen earlier that it contradicts rather than deleting it or claiming an instantaneous all-clear for the whole app. A cursor or page error leaves the inventory partial. Completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps the app's coverage explicitly incomplete instead of reading every agent. Reuse any detail already read. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. When process detail is unavailable or denied, keep the last-day aggregate evidence from `list_runtimes` (tenant-wide) or an authorized `GET /v1/tenants/{tenant}/runtimes?app=` (that app only) (positive advisories and `upgradeTo`/`runtimes[].fixed`), labeled with its scope, window and coverage limit: it doesn't prove which platforms run now and is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding findings already known, and the `vulnerability_data` source check below applies to this fallback too. For the current app's advisories, end of life and the same-line patch, prefer these verified per-platform connected-agent verdicts; the patch is the verdict's `runtime.fixed`, which is for that agent's actual version and platform and can differ from the aggregate `upgradeTo`. A verdict covers only that agent's own version and platform; an unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. Each confirmed pair needs a verdict, and a partial `find_agents` leaves the rest of the app uncovered. Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. A missing, stale or failed source or verdict makes the app's runtime coverage incomplete: keep the findings observed and don't give it an all-clear. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them (`upgradeTo`). + - Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). An app named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For one app's current version, use `find_agents` with the exact `app` (or a valid, escaped `where`) and `status: "connected"`; a `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. + - Advisories and `upgradeTo` are tenant-and-version context. A platform-independent advisory applies to the app named with that version; a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read, keep reads bounded (not one per replica), and mark unchecked or unknown scope incomplete, never all-clear. + - An already authorized `GET /v1/tenants/{tenant}/runtimes?app=` is optional positive last-day evidence for that app, not current-only; keep its advisories and `fixed` (MCP's `upgradeTo`) labeled by that scope, since different tenant, app or platform scopes can give different values. If it refuses the app filter, don't drop or truncate it. + - A same-line patch backed by verified applicable evidence comes first; an aggregate-only fix is a scoped candidate with its limits, neither a proven app target nor proof no patch is needed. Missing, null, errored or stale data is incomplete. Before claiming complete coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index 07e4856..53d1f9f 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -35,19 +35,11 @@ Otherwise, check in order: **At the start of your response**, briefly tell the user which source was used to determine the current version (one sentence). If the project-pinned version and the runtime on PATH disagree, treat the project pin as authoritative and note the mismatch. ### 2b. Check What Production Runs (when N|Solid is available) -If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). It is tenant-wide: each row is one Node.js version, combined across the apps and platforms that ran it in the last day. Unfiltered REST `GET /v1/tenants/{tenant}/runtimes` has that same scope; `?app=` aggregates only that app's last-day platforms, so its `runtimes[].fixed` may differ. `upgradeTo` is the MCP name for the corresponding `fixed` field, not a promise of the same value across different scopes. Keep positive advisories and fixes as observations labeled with their actual scope and window: tenant-wide advisories stay tenant context and aren't attributed to the project's app without app and platform evidence, even when the app is named in the row. Neither aggregate proves the current app or platform patch target or an all-clear. -- Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Each version names at most 20 apps; `appsOmitted` and an `outputCut` notice mean some are hidden, so an app missing from the list is not proof that it doesn't run. Confirm what a known app runs now with `find_agents` given the exact `app` and `status: "connected"` (or a `where` with the app name escaped in the expression language's string syntax, plus `status: "connected"`). Historical, disconnected or exited processes are not confirmation of what production runs now. The result is partial on a `More agents match` notice or on a cut, too-large notice or error in any content block; a result that fills the default 50 or the requested limit is not proof of completeness either. Every version it does show stands; don't claim it covers all of the app's processes. Production may run a different version than the workspace pins: say so, and plan from what runs. -- `find_agents` gives each process's version but not its platform; never infer a platform from it. Get each pair's platform before grouping, then one runtime verdict per version and platform pair, reusing any detail already read: - - When an app-scoped `GET /v1/tenants/{tenant}/agents` is already authorized, page it with `status=connected` and the exact nonempty `app` for each process's `nodeVersion` and platform, then read `get_agent` once per observed pair rather than for every identical replica. If the REST `app` filter rejects a long name, use a supported `where` with the exact app expression, properly escaped, when it can be represented; don't invent fields or simplify the target; otherwise keep what was observed and mark that scope unavailable. - - Paging an authorized REST connected-agent inventory successfully through its final page closes the `find_agents` sampling gap, but live pages aren't an atomic snapshot: state when the scan ran and what it observed, and keep and reconcile any version seen earlier that it contradicts rather than deleting it or claiming an instantaneous all-clear for the whole app. A cursor or page error leaves the inventory partial. Completeness comes from the inventory actually used, plus known version and platform and verdict and freshness coverage. - - MCP-only, use `find_agents` and a bounded number of `get_agent` checks; any process or platform left unexamined keeps coverage explicitly incomplete rather than reading every agent. `get_agent` has no `list_*` cap, but still honor any actual client truncation, persistence, error or incomplete-response notice on its result. - - When process detail is unavailable or denied, keep the last-day aggregate evidence from `list_runtimes` (tenant-wide) or an authorized `GET /v1/tenants/{tenant}/runtimes?app=` (that app only) (positive advisories and `upgradeTo`/`runtimes[].fixed`), labeled with its scope, window and coverage limit: it doesn't prove which platforms run now and is neither a connected-only patch proof nor an all-clear. A missing or stale `checkedAt` or a reported `error` makes coverage incomplete without discarding known findings, and the `vulnerability_data` source check below applies to this fallback too. -- Prefer these verified per-platform connected-agent verdicts for the current same-line patch decision: each gives end of life, core advisories and the fixing release as `runtime.fixed`, for that agent's actual version and platform; it can differ from the aggregate `upgradeTo`. - - A verdict covers only that agent's own version and platform: one process doesn't prove other platforms safe. An unknown platform or a missing or null `runtime` leaves that agent uncovered, never clean. - - Each confirmed version and platform pair needs a verdict; a partial `find_agents` leaves the rest of the app's processes uncovered. - - Before claiming complete advisory coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness; an aggregate `checkedAt` alone doesn't prove every source fresh. - - If a source is missing, stale or failed, or any pair or process is left without a verdict, say advisory coverage is incomplete, keep the advisories observed, and don't conclude that no same-line patch is needed. -- When a same-line fixing release is shown to apply to the verified target version and platform (per-agent `runtime.fixed`), recommend that patch upgrade first, whatever the line move: it is the smallest change that fixes them. An aggregate `upgradeTo`/`runtimes[].fixed` stays an observation for its own scope; don't prescribe a tenant-wide `upgradeTo` as the project app's patch. +If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). +- Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). The project's app (its `package.json` `name` or `NSOLID_APPNAME`) named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For what runs now, use `find_agents` with the exact `app` (or a valid, escaped `where`) and `status: "connected"`; a `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. If production differs from the workspace pin, say so and plan from what runs. +- Advisories and `upgradeTo` are tenant-and-version context. A platform-independent advisory applies to the app named with that version; a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read, keep reads bounded (not one per replica), and mark unchecked or unknown scope incomplete, never all-clear. +- An already authorized `GET /v1/tenants/{tenant}/runtimes?app=` is optional positive last-day evidence for that app, not current-only; keep its advisories and `fixed` (MCP's `upgradeTo`) labeled by that scope, since different tenant, app or platform scopes can give different values. If it refuses the app filter, don't drop or truncate it. +- Recommend the same-line patch first, whatever the line move, when verified applicable evidence supports it; an aggregate-only fix is a scoped candidate with its limits, neither a proven app target nor proof no patch is needed. Missing, null, errored or stale data is incomplete. Before claiming complete coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness. - If the server isn't available, continue from the workspace alone. ### 3. Recommend a Target Version From dd8d874442ab1e6f300d116990aec8e91a7a928e Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 21:17:49 -0500 Subject: [PATCH 11/14] fix: retain replacement and advisory evidence guards --- skills/ns-configure-agents/SKILL.md | 9 +++++---- skills/ns-fleet-health/SKILL.md | 5 +++-- skills/ns-node-upgrade/SKILL.md | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index 39185ff..e4560b7 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -17,21 +17,22 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. `list_desired_configs` names those settings `config`; its `where` is an expression tree, while `whereText` is the expression string the input accepts. Do not pass the entire returned row as tool arguments. Selectors and configuration subtrees are kept whole, but their entire entry can be omitted by the byte cap: a missing name in a partial list is not proof it is absent. - Settings: `tracingEnabled`, `traceSampleRate` (0 to 1), `contCpuProfile` (continuous CPU profiling), `interval` (metrics, ms), `blockedLoopThreshold` (ms), `pauseMetrics`, `promiseTracking`, `redactSnapshots`, `assetsEnabled`, `tags`. Turning `contCpuProfile` on needs the tenant's continuous-profiling feature: where N|Solid has it off, the save is refused, and processes stay off whatever a config says. - Target: `app`, `tags`, one `agentId`, or `where`, an expression over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields; it takes no `tenant`). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. -- MCP `where` takes a valid expression string; new expressions are fine. To keep an existing selector, reuse its valid, uncut `whereText` (at most 4,000 characters), never the compacted JSON tree or a stringified one. The `app`, `tags`, `agentId` and `allAgents` shortcuts stay MCP inputs with their usual authorization. +- MCP `where` takes a valid expression string; new supported expressions are fine within the same 4,000-character limit. To keep an existing selector, reuse its valid, uncut `whereText` (at most 4,000 characters), never the compacted JSON tree or a stringified one. The `app`, `tags`, `agentId` and `allAgents` shortcuts stay MCP inputs with their usual authorization. - **Prefer an expiring override** for anything diagnostic: `expiresInMinutes` makes it stop applying, and the processes go back to what they had. Tracing or continuous profiling "for the next hour" is an override of 60 minutes. - `priority`: higher wins where configs disagree. - Names use letters, digits, `.`, `_` and `-`: `checkout-tracing-1h`. -- **New name or replacement:** saving under an existing name replaces it. Before saving as new, confirm the exact tenant and name are absent from a successful, complete JSON `list_desired_configs` result (no `outputCut`, no cut, too-large or error notice in any content block) or an already authorized REST read of `GET /v1/tenants/{tenant}/configs`; a preview proves nothing about absence. Otherwise hand the user the drafted spec and don't save it as new. The exact name appearing anywhere means replacement: an existing go-ahead for that exact change and target counts; one only to create doesn't cover overwriting an unknown config. -- **Editing:** the named record needs its writable values intact; omitted unrelated rows don't matter. MCP drops null properties, so a missing `expiresAt` on an otherwise intact row means permanent. Keep the target, config, priority, state and expiry the user didn't ask to change; if `set_desired_config` can't represent them faithfully (disabled state, absolute expiry, too-long `whereText`), hand over the draft and current record instead of dropping or renewing fields. +- **New name or replacement:** saving under an existing name replaces it. This absence check applies only to saving as new: confirm the exact tenant and name are absent from a successful, complete JSON `list_desired_configs` result (no `outputCut`, no cut, too-large or error notice in any content block) or an already authorized REST read of `GET /v1/tenants/{tenant}/configs`; a preview proves nothing about absence. Otherwise hand the user the drafted spec and don't save it as new. Any exact match of the name is a replacement: say so. A go-ahead only to create never covers a replacement, whether or not the existing record was known; an existing go-ahead for that exact replacement and scope counts, with no fresh ask. +- **Editing:** an existing named record with its writable values intact can be edited; omitted unrelated rows don't matter. MCP drops null properties, so a missing `expiresAt` on an otherwise intact row means permanent. Keep the target, config, priority, state and expiry the user didn't ask to change; never round or restart a remaining duration, or revive an expired config, to make the save fit. If `set_desired_config` can't represent them faithfully (disabled state, absolute expiry, too-long `whereText`), hand over the draft and current record instead of dropping or renewing fields. ### 3. Preview - `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. - `matchingConnectedAgents` counts agents the proposed selector matches; `changing` counts agents with nonempty planned changes toward the full merged desired state, not this edit's own delta. The sample (at most 10 agents) may include drift-only keys they can't achieve; agents without an observed config aren't planned yet; `truncated` makes counts incomplete. -- A replacement also withdraws the old target's effects where that config is active and effective; an inactive, expired or shadowed one isn't assumed to revert agents. A prior go-ahead for the exact change holds while the actual effects stay within it; moved counts alone need no new approval. +- Before a replacement, compare the old and proposed effective target and settings and show the effects, including what only the old one set and the replacement withdraws. Only an inactive, expired or fully ineffective old config withdraws nothing; a partially shadowed one may still set other keys, so don't treat the whole config as inert. Get agreement for any actual effect outside the authorized scope; a prior exact agreement holds when both the old and new actual effects stay within it, and moved counts alone need no new approval. Never widen scope from a partial preview, or infer what the change causes from `changing` or the sample alone. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. ### 4. Apply - With the user's agreement, `set_desired_config`. Connected processes are reconfigured now, new ones when they connect. +- On a timeout, 5xx or lost response, claim neither success nor no write, and don't retry: re-read the exact tenant and name from complete authoritative evidence before any further write or report (a name missing from a cut list proves nothing), and if that can't settle it, report it as uncertain and hand off. Report a known refusal as a refusal; don't change other fields to get around it. - Check it took effect: `agent_config` on one of the processes shows whether it converged. - `reconfigure_agent` changes one running process once; N|Solid sets back any key a desired config manages, and a restart forgets it. Use it only when the user wants exactly that. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 25bd4cb..bff43a1 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -23,8 +23,9 @@ Run what fits the question; for a full briefing, all of it: - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. - **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them (`upgradeTo`). - - Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). An app named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For one app's current version, use `find_agents` with the exact `app` (or a valid, escaped `where`) and `status: "connected"`; a `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. - - Advisories and `upgradeTo` are tenant-and-version context. A platform-independent advisory applies to the app named with that version; a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read, keep reads bounded (not one per replica), and mark unchecked or unknown scope incomplete, never all-clear. + - Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). An app named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For one app's current version, use `find_agents` with the exact `app` (or, e.g. for long names, a valid, properly escaped app-equality `where`) and `status: "connected"`; if refused, never truncate the name or drop the scope. A `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. + - Advisories and `upgradeTo` are tenant-and-version context. Call an advisory platform-independent only from intact, complete advisory data or a per-platform verdict: `list_runtimes` text cut at its end can lose a `[platform only]` qualifier, so its absence there proves nothing; otherwise keep it as tenant context, not attributed to the app. A platform-independent advisory applies to the app named with that version as last-day exposure, not proof that version runs now (confirm that as above); a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read and keep reads bounded (not one per replica). Unexamined agents and platforms stay unknown, and the same Node.js version doesn't prove replicas share a platform; mark unchecked or unknown scope incomplete, never all-clear. + - Optionally, an already authorized read of the app-scoped, connected `GET /v1/tenants/{tenant}/agents` inventory, paged successfully through its final page, lists the observed `nodeVersion`/platform pairs for one verdict per pair; it covers the observed scan, not an atomic snapshot. A failed page or cursor leaves it partial, and earlier conflicting observations stand. Read only: no writing credentials or extra access. - An already authorized `GET /v1/tenants/{tenant}/runtimes?app=` is optional positive last-day evidence for that app, not current-only; keep its advisories and `fixed` (MCP's `upgradeTo`) labeled by that scope, since different tenant, app or platform scopes can give different values. If it refuses the app filter, don't drop or truncate it. - A same-line patch backed by verified applicable evidence comes first; an aggregate-only fix is a scoped candidate with its limits, neither a proven app target nor proof no patch is needed. Missing, null, errored or stale data is incomplete. Before claiming complete coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index 53d1f9f..4f2bba7 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -36,8 +36,9 @@ Otherwise, check in order: ### 2b. Check What Production Runs (when N|Solid is available) If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). -- Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). The project's app (its `package.json` `name` or `NSOLID_APPNAME`) named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For what runs now, use `find_agents` with the exact `app` (or a valid, escaped `where`) and `status: "connected"`; a `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. If production differs from the workspace pin, say so and plan from what runs. -- Advisories and `upgradeTo` are tenant-and-version context. A platform-independent advisory applies to the app named with that version; a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read, keep reads bounded (not one per replica), and mark unchecked or unknown scope incomplete, never all-clear. +- Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). The project's app (its `package.json` `name` or `NSOLID_APPNAME`) named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For what runs now, use `find_agents` with the exact `app` (or, e.g. for long names, a valid, properly escaped app-equality `where`) and `status: "connected"`; if refused, never truncate the name or drop the scope. A `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. If production differs from the workspace pin, say so and plan from what runs. +- Advisories and `upgradeTo` are tenant-and-version context. Call an advisory platform-independent only from intact, complete advisory data or a per-platform verdict: `list_runtimes` text cut at its end can lose a `[platform only]` qualifier, so its absence there proves nothing; otherwise keep it as tenant context, not attributed to the app. A platform-independent advisory applies to the app named with that version as last-day exposure, not proof that version runs now (confirm that as above); a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read and keep reads bounded (not one per replica). Unexamined agents and platforms stay unknown, and the same Node.js version doesn't prove replicas share a platform; mark unchecked or unknown scope incomplete, never all-clear. +- Optionally, an already authorized read of the app-scoped, connected `GET /v1/tenants/{tenant}/agents` inventory, paged successfully through its final page, lists the observed `nodeVersion`/platform pairs for one verdict per pair; it covers the observed scan, not an atomic snapshot. A failed page or cursor leaves it partial, and earlier conflicting observations stand. Read only: no writing credentials or extra access. - An already authorized `GET /v1/tenants/{tenant}/runtimes?app=` is optional positive last-day evidence for that app, not current-only; keep its advisories and `fixed` (MCP's `upgradeTo`) labeled by that scope, since different tenant, app or platform scopes can give different values. If it refuses the app filter, don't drop or truncate it. - Recommend the same-line patch first, whatever the line move, when verified applicable evidence supports it; an aggregate-only fix is a scoped candidate with its limits, neither a proven app target nor proof no patch is needed. Missing, null, errored or stale data is incomplete. Before claiming complete coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness. - If the server isn't available, continue from the workspace alone. From c35b29797225ba5167b7cf86a6ab03c8c8c7f89d Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Tue, 3 Nov 2026 21:31:33 -0500 Subject: [PATCH 12/14] docs: keep runtime verdicts and ambiguous saves scoped --- skills/ns-configure-agents/SKILL.md | 10 +++++----- skills/ns-fleet-health/SKILL.md | 4 ++-- skills/ns-node-upgrade/SKILL.md | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index e4560b7..1d67061 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -17,22 +17,22 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. `list_desired_configs` names those settings `config`; its `where` is an expression tree, while `whereText` is the expression string the input accepts. Do not pass the entire returned row as tool arguments. Selectors and configuration subtrees are kept whole, but their entire entry can be omitted by the byte cap: a missing name in a partial list is not proof it is absent. - Settings: `tracingEnabled`, `traceSampleRate` (0 to 1), `contCpuProfile` (continuous CPU profiling), `interval` (metrics, ms), `blockedLoopThreshold` (ms), `pauseMetrics`, `promiseTracking`, `redactSnapshots`, `assetsEnabled`, `tags`. Turning `contCpuProfile` on needs the tenant's continuous-profiling feature: where N|Solid has it off, the save is refused, and processes stay off whatever a config says. - Target: `app`, `tags`, one `agentId`, or `where`, an expression over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields; it takes no `tenant`). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. -- MCP `where` takes a valid expression string; new supported expressions are fine within the same 4,000-character limit. To keep an existing selector, reuse its valid, uncut `whereText` (at most 4,000 characters), never the compacted JSON tree or a stringified one. The `app`, `tags`, `agentId` and `allAgents` shortcuts stay MCP inputs with their usual authorization. +- MCP `where` takes a valid expression string; a new supported expression must satisfy every server limit at once: at most 4,000 characters of text, 256 tree nodes, 32 levels of depth, 500 characters per string literal and 200 values per `in` list. Any shorter valid syntax within those limits is fine. To keep an existing selector, reuse its valid, uncut `whereText` (at most 4,000 characters), never the compacted JSON tree or a stringified one. The `app`, `tags`, `agentId` and `allAgents` shortcuts stay MCP inputs with their usual authorization. - **Prefer an expiring override** for anything diagnostic: `expiresInMinutes` makes it stop applying, and the processes go back to what they had. Tracing or continuous profiling "for the next hour" is an override of 60 minutes. - `priority`: higher wins where configs disagree. - Names use letters, digits, `.`, `_` and `-`: `checkout-tracing-1h`. -- **New name or replacement:** saving under an existing name replaces it. This absence check applies only to saving as new: confirm the exact tenant and name are absent from a successful, complete JSON `list_desired_configs` result (no `outputCut`, no cut, too-large or error notice in any content block) or an already authorized REST read of `GET /v1/tenants/{tenant}/configs`; a preview proves nothing about absence. Otherwise hand the user the drafted spec and don't save it as new. Any exact match of the name is a replacement: say so. A go-ahead only to create never covers a replacement, whether or not the existing record was known; an existing go-ahead for that exact replacement and scope counts, with no fresh ask. -- **Editing:** an existing named record with its writable values intact can be edited; omitted unrelated rows don't matter. MCP drops null properties, so a missing `expiresAt` on an otherwise intact row means permanent. Keep the target, config, priority, state and expiry the user didn't ask to change; never round or restart a remaining duration, or revive an expired config, to make the save fit. If `set_desired_config` can't represent them faithfully (disabled state, absolute expiry, too-long `whereText`), hand over the draft and current record instead of dropping or renewing fields. +- **New name or replacement:** saving under an existing name replaces it. This absence check applies only to saving as new: confirm the exact tenant and name are absent from a successful, complete JSON `list_desired_configs` result (no `outputCut`, no cut, too-large or error notice in any content block) or an already authorized REST read of `GET /v1/tenants/{tenant}/configs`; a preview proves nothing about absence. Otherwise hand the user the drafted spec and don't save it as new. A positive match of the name among the exact tenant's current configs means a replacement: say so. Historical deleted names and other tenants' configs don't. A go-ahead only to create never covers a replacement, whether or not the existing record was known; an existing go-ahead for that exact replacement and scope counts, with no fresh ask. +- **Editing:** requires the existing named record with its writable values intact; without it, hand the user the draft instead of saving. Omitted unrelated rows don't matter. MCP drops null properties, so a missing `expiresAt` on an otherwise intact row means permanent. Keep the target, config, priority, state and expiry the user didn't ask to change; never round or restart a remaining duration, or revive an expired config, to make the save fit. If `set_desired_config` can't represent them faithfully (disabled state, absolute expiry, too-long `whereText`), hand over the draft and current record instead of dropping or renewing fields. ### 3. Preview - `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. - `matchingConnectedAgents` counts agents the proposed selector matches; `changing` counts agents with nonempty planned changes toward the full merged desired state, not this edit's own delta. The sample (at most 10 agents) may include drift-only keys they can't achieve; agents without an observed config aren't planned yet; `truncated` makes counts incomplete. -- Before a replacement, compare the old and proposed effective target and settings and show the effects, including what only the old one set and the replacement withdraws. Only an inactive, expired or fully ineffective old config withdraws nothing; a partially shadowed one may still set other keys, so don't treat the whole config as inert. Get agreement for any actual effect outside the authorized scope; a prior exact agreement holds when both the old and new actual effects stay within it, and moved counts alone need no new approval. Never widen scope from a partial preview, or infer what the change causes from `changing` or the sample alone. +- Before a replacement, compare the actual old and proposed effective target and settings and show the effects. A withdrawal is only an effect present before and absent after; not every active config withdraws something. A partially shadowed old config may still own other keys, so don't treat the whole config as inert; where a temporary override shadows it, account for what each would set once that override expires. Get agreement for any actual effect outside the authorized scope; an existing agreement for that exact scope is reused when both the old and new actual effects stay within it, with no fresh ask solely because counts moved. Never widen scope from a partial preview, or infer what the change causes from `changing` or the sample alone. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. ### 4. Apply - With the user's agreement, `set_desired_config`. Connected processes are reconfigured now, new ones when they connect. -- On a timeout, 5xx or lost response, claim neither success nor no write, and don't retry: re-read the exact tenant and name from complete authoritative evidence before any further write or report (a name missing from a cut list proves nothing), and if that can't settle it, report it as uncertain and hand off. Report a known refusal as a refusal; don't change other fields to get around it. +- On a timeout, 5xx or lost response, claim neither success nor no write, and don't retry automatically. Finding the name, or reading current state, doesn't establish what this write did: report the observed state separately, and compare the intended writable fields and an attributable `desired_config_history` revision if available. Unless that evidence resolves it, report the save as uncertain and hand off. Report a known refusal as a refusal; don't change other fields to get around it. - Check it took effect: `agent_config` on one of the processes shows whether it converged. - `reconfigure_agent` changes one running process once; N|Solid sets back any key a desired config manages, and a restart forgets it. Use it only when the user wants exactly that. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index bff43a1..575d7d1 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -24,8 +24,8 @@ Run what fits the question; for a full briefing, all of it: - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. - **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them (`upgradeTo`). - Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). An app named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For one app's current version, use `find_agents` with the exact `app` (or, e.g. for long names, a valid, properly escaped app-equality `where`) and `status: "connected"`; if refused, never truncate the name or drop the scope. A `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. - - Advisories and `upgradeTo` are tenant-and-version context. Call an advisory platform-independent only from intact, complete advisory data or a per-platform verdict: `list_runtimes` text cut at its end can lose a `[platform only]` qualifier, so its absence there proves nothing; otherwise keep it as tenant context, not attributed to the app. A platform-independent advisory applies to the app named with that version as last-day exposure, not proof that version runs now (confirm that as above); a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read and keep reads bounded (not one per replica). Unexamined agents and platforms stay unknown, and the same Node.js version doesn't prove replicas share a platform; mark unchecked or unknown scope incomplete, never all-clear. - - Optionally, an already authorized read of the app-scoped, connected `GET /v1/tenants/{tenant}/agents` inventory, paged successfully through its final page, lists the observed `nodeVersion`/platform pairs for one verdict per pair; it covers the observed scan, not an atomic snapshot. A failed page or cursor leaves it partial, and earlier conflicting observations stand. Read only: no writing credentials or extra access. + - Advisories and `upgradeTo` are tenant-and-version context. Call an advisory platform-independent only from intact, complete, authoritative advisory metadata that shows no platform restriction: `list_runtimes` text cut at its end can lose a platform qualifier (such as `[linux only]`), so its absence there proves nothing; otherwise keep it as tenant context, not attributed to the app. A `get_agent` verdict supports only its own observed version/platform pair, never independence. A platform-independent advisory applies to the app named with that version as last-day exposure, not proof that version runs now (confirm that as above); a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read and keep reads bounded (not one per replica). Unexamined agents and platforms stay unknown, and the same Node.js version doesn't prove replicas share a platform; mark unchecked or unknown scope incomplete, never all-clear. + - Optionally, an already authorized read of the app-scoped, connected `GET /v1/tenants/{tenant}/agents` inventory enumerates observed `nodeVersion`/platform pairs only; it has no verdict. `get_agent` on one representative relevant agent per pair gives that pair's `runtime.fixed` verdict. Keep the scan within the task's budget, stopping as partial if needed; complete scope needs a successful read through the final page, and covers the observed scan, not an atomic snapshot. A failed page or cursor leaves it partial, and earlier conflicting observations stand. Read only: no writing credentials or extra access; if it refuses the app filter, don't drop or truncate it. - An already authorized `GET /v1/tenants/{tenant}/runtimes?app=` is optional positive last-day evidence for that app, not current-only; keep its advisories and `fixed` (MCP's `upgradeTo`) labeled by that scope, since different tenant, app or platform scopes can give different values. If it refuses the app filter, don't drop or truncate it. - A same-line patch backed by verified applicable evidence comes first; an aggregate-only fix is a scoped candidate with its limits, neither a proven app target nor proof no patch is needed. Missing, null, errored or stale data is incomplete. Before claiming complete coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index 4f2bba7..4525ec2 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -37,8 +37,8 @@ Otherwise, check in order: ### 2b. Check What Production Runs (when N|Solid is available) If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). - Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). The project's app (its `package.json` `name` or `NSOLID_APPNAME`) named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For what runs now, use `find_agents` with the exact `app` (or, e.g. for long names, a valid, properly escaped app-equality `where`) and `status: "connected"`; if refused, never truncate the name or drop the scope. A `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. If production differs from the workspace pin, say so and plan from what runs. -- Advisories and `upgradeTo` are tenant-and-version context. Call an advisory platform-independent only from intact, complete advisory data or a per-platform verdict: `list_runtimes` text cut at its end can lose a `[platform only]` qualifier, so its absence there proves nothing; otherwise keep it as tenant context, not attributed to the app. A platform-independent advisory applies to the app named with that version as last-day exposure, not proof that version runs now (confirm that as above); a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read and keep reads bounded (not one per replica). Unexamined agents and platforms stay unknown, and the same Node.js version doesn't prove replicas share a platform; mark unchecked or unknown scope incomplete, never all-clear. -- Optionally, an already authorized read of the app-scoped, connected `GET /v1/tenants/{tenant}/agents` inventory, paged successfully through its final page, lists the observed `nodeVersion`/platform pairs for one verdict per pair; it covers the observed scan, not an atomic snapshot. A failed page or cursor leaves it partial, and earlier conflicting observations stand. Read only: no writing credentials or extra access. +- Advisories and `upgradeTo` are tenant-and-version context. Call an advisory platform-independent only from intact, complete, authoritative advisory metadata that shows no platform restriction: `list_runtimes` text cut at its end can lose a platform qualifier (such as `[linux only]`), so its absence there proves nothing; otherwise keep it as tenant context, not attributed to the app. A `get_agent` verdict supports only its own observed version/platform pair, never independence. A platform-independent advisory applies to the app named with that version as last-day exposure, not proof that version runs now (confirm that as above); a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read and keep reads bounded (not one per replica). Unexamined agents and platforms stay unknown, and the same Node.js version doesn't prove replicas share a platform; mark unchecked or unknown scope incomplete, never all-clear. +- Optionally, an already authorized read of the app-scoped, connected `GET /v1/tenants/{tenant}/agents` inventory enumerates observed `nodeVersion`/platform pairs only; it has no verdict. `get_agent` on one representative relevant agent per pair gives that pair's `runtime.fixed` verdict. Keep the scan within the task's budget, stopping as partial if needed; complete scope needs a successful read through the final page, and covers the observed scan, not an atomic snapshot. A failed page or cursor leaves it partial, and earlier conflicting observations stand. Read only: no writing credentials or extra access; if it refuses the app filter, don't drop or truncate it. - An already authorized `GET /v1/tenants/{tenant}/runtimes?app=` is optional positive last-day evidence for that app, not current-only; keep its advisories and `fixed` (MCP's `upgradeTo`) labeled by that scope, since different tenant, app or platform scopes can give different values. If it refuses the app filter, don't drop or truncate it. - Recommend the same-line patch first, whatever the line move, when verified applicable evidence supports it; an aggregate-only fix is a scoped candidate with its limits, neither a proven app target nor proof no patch is needed. Missing, null, errored or stale data is incomplete. Before claiming complete coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness. - If the server isn't available, continue from the workspace alone. From f74f3a63949ae989ee8af0d45d717a647a58c4d6 Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Wed, 4 Nov 2026 15:56:35 -0500 Subject: [PATCH 13/14] docs: keep MCP guidance within list contract scope --- skills/ns-analyze-vulnerabilities/SKILL.md | 6 +++--- skills/ns-configure-agents/SKILL.md | 9 ++------- skills/ns-fleet-health/SKILL.md | 13 ++++--------- skills/ns-manage-alerts/SKILL.md | 21 +++++---------------- skills/ns-node-upgrade/SKILL.md | 12 +++--------- 5 files changed, 17 insertions(+), 44 deletions(-) diff --git a/skills/ns-analyze-vulnerabilities/SKILL.md b/skills/ns-analyze-vulnerabilities/SKILL.md index 71fa6c0..af8b6d7 100644 --- a/skills/ns-analyze-vulnerabilities/SKILL.md +++ b/skills/ns-analyze-vulnerabilities/SKILL.md @@ -9,9 +9,9 @@ This skill reads vulnerability findings through the N|Solid MCP server (`nsolid` ### 1. High-Level Overview - Call `list_vulnerabilities`: open findings, most severe first. Each names its advisory (GHSA, a malicious-package report MAL, or a Node.js core advisory NODE-CORE), the app, severity, the vulnerable packages with the version that fixes them, the app's own dependencies that bring each in (`via`), and the runtime evidence: `installed`, `loaded` (required by the app) or `running` (seen in CPU profiles). A finding whose CVE CISA lists as exploited in the wild carries `knownExploited`: the CVE, when CISA listed it, its remediation due date, and whether ransomware campaigns use it. - Narrow large fleets with `app`, `severity` (at least), or `where`, e.g. `severity >= "high" && evidence >= "loaded"`, or `knownExploited` (`expression_help` with context `finding` lists the fields; it takes no `tenant`). -- A result that fills its `limit` or carries a cut notice is partial: the findings it shows stand, but more may exist. Narrow and re-read; don't invent fields or drop known findings to make it fit. -- Summarize which app has the most critical issues. A ranking of apps needs complete results for every app it compares; from partial results, say the ranking is partial. Lead with malicious packages (MAL-…) when there are any, then known exploited findings in loaded or running code: attackers use those now, whatever their severity. -- Before saying an app has nothing to fix, get complete results from `list_vulnerabilities` scoped to that `app`; current advisory data alone isn't enough. `vulnerability_data` (it takes no `tenant`) shows whether the advisory data is current: when the OSV mirror and CISA's catalog were last checked, and any sync error. Say so when it is stale. +- A result that fills its `limit` or carries a cut notice is partial: the findings it shows stand, but more may exist. +- Summarize which app has the most critical issues. Lead with malicious packages (MAL-…) when there are any, then known exploited findings in loaded or running code: attackers use those now, whatever their severity. +- Before saying an app has nothing to fix, `vulnerability_data` (it takes no `tenant`) shows whether the advisory data is current: when the OSV mirror and CISA's catalog were last checked, and any sync error. Say so when it is stale. - For the Node.js runtime itself, `list_runtimes` gives each Node.js version in use: its core advisories, the release that fixes them, and whether its line is at or near end of life. ### 2. Detail Per App diff --git a/skills/ns-configure-agents/SKILL.md b/skills/ns-configure-agents/SKILL.md index 1d67061..9bb2767 100644 --- a/skills/ns-configure-agents/SKILL.md +++ b/skills/ns-configure-agents/SKILL.md @@ -14,25 +14,20 @@ N|Solid keeps agents at **desired configs**: each targets processes by an expres - `desired_config_history` with a config's `name`: its 50 newest saves and deletes, each with the settings, target, priority, expiry, who made it and when. Deleted configs keep their history. Read it before changing or deleting a named config, to see who set it and what it held before. No tool restores an earlier version: to go back to one, make it a new change and preview and apply it as below. ### 2. Shape the Change -- `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. `list_desired_configs` names those settings `config`; its `where` is an expression tree, while `whereText` is the expression string the input accepts. Do not pass the entire returned row as tool arguments. Selectors and configuration subtrees are kept whole, but their entire entry can be omitted by the byte cap: a missing name in a partial list is not proof it is absent. +- `preview_desired_config` and `set_desired_config` accept exactly one of `settings` or its alias `config`, never both. - Settings: `tracingEnabled`, `traceSampleRate` (0 to 1), `contCpuProfile` (continuous CPU profiling), `interval` (metrics, ms), `blockedLoopThreshold` (ms), `pauseMetrics`, `promiseTracking`, `redactSnapshots`, `assetsEnabled`, `tags`. Turning `contCpuProfile` on needs the tenant's continuous-profiling feature: where N|Solid has it off, the save is refused, and processes stay off whatever a config says. - Target: `app`, `tags`, one `agentId`, or `where`, an expression over what a process is (`app == "checkout" && hostname.endsWith(".prod")`, `nodeVersion >= "22"`; `expression_help` with context `selector` lists the fields; it takes no `tenant`). Every process of the tenant needs `allAgents: true` and the user's explicit agreement. -- MCP `where` takes a valid expression string; a new supported expression must satisfy every server limit at once: at most 4,000 characters of text, 256 tree nodes, 32 levels of depth, 500 characters per string literal and 200 values per `in` list. Any shorter valid syntax within those limits is fine. To keep an existing selector, reuse its valid, uncut `whereText` (at most 4,000 characters), never the compacted JSON tree or a stringified one. The `app`, `tags`, `agentId` and `allAgents` shortcuts stay MCP inputs with their usual authorization. +- A `where` expression is limited to 4,000 characters, 256 nodes, 32 levels of depth, 500 characters per string literal and 200 values per `in` list. - **Prefer an expiring override** for anything diagnostic: `expiresInMinutes` makes it stop applying, and the processes go back to what they had. Tracing or continuous profiling "for the next hour" is an override of 60 minutes. - `priority`: higher wins where configs disagree. - Names use letters, digits, `.`, `_` and `-`: `checkout-tracing-1h`. -- **New name or replacement:** saving under an existing name replaces it. This absence check applies only to saving as new: confirm the exact tenant and name are absent from a successful, complete JSON `list_desired_configs` result (no `outputCut`, no cut, too-large or error notice in any content block) or an already authorized REST read of `GET /v1/tenants/{tenant}/configs`; a preview proves nothing about absence. Otherwise hand the user the drafted spec and don't save it as new. A positive match of the name among the exact tenant's current configs means a replacement: say so. Historical deleted names and other tenants' configs don't. A go-ahead only to create never covers a replacement, whether or not the existing record was known; an existing go-ahead for that exact replacement and scope counts, with no fresh ask. -- **Editing:** requires the existing named record with its writable values intact; without it, hand the user the draft instead of saving. Omitted unrelated rows don't matter. MCP drops null properties, so a missing `expiresAt` on an otherwise intact row means permanent. Keep the target, config, priority, state and expiry the user didn't ask to change; never round or restart a remaining duration, or revive an expired config, to make the save fit. If `set_desired_config` can't represent them faithfully (disabled state, absolute expiry, too-long `whereText`), hand over the draft and current record instead of dropping or renewing fields. ### 3. Preview - `preview_desired_config` with the same inputs: how many connected processes match and which would change, without saving. Show it to the user. -- `matchingConnectedAgents` counts agents the proposed selector matches; `changing` counts agents with nonempty planned changes toward the full merged desired state, not this edit's own delta. The sample (at most 10 agents) may include drift-only keys they can't achieve; agents without an observed config aren't planned yet; `truncated` makes counts incomplete. -- Before a replacement, compare the actual old and proposed effective target and settings and show the effects. A withdrawal is only an effect present before and absent after; not every active config withdraws something. A partially shadowed old config may still own other keys, so don't treat the whole config as inert; where a temporary override shadows it, account for what each would set once that override expires. Get agreement for any actual effect outside the authorized scope; an existing agreement for that exact scope is reused when both the old and new actual effects stay within it, with no fresh ask solely because counts moved. Never widen scope from a partial preview, or infer what the change causes from `changing` or the sample alone. - Call out what the change costs or stops: tracing and continuous profiling add overhead; turning off metrics, tracing or assets stops that data; `redactSnapshots: false` makes later heap snapshots carry the application's strings, which can include secrets and personal data. ### 4. Apply - With the user's agreement, `set_desired_config`. Connected processes are reconfigured now, new ones when they connect. -- On a timeout, 5xx or lost response, claim neither success nor no write, and don't retry automatically. Finding the name, or reading current state, doesn't establish what this write did: report the observed state separately, and compare the intended writable fields and an attributable `desired_config_history` revision if available. Unless that evidence resolves it, report the save as uncertain and hand off. Report a known refusal as a refusal; don't change other fields to get around it. - Check it took effect: `agent_config` on one of the processes shows whether it converged. - `reconfigure_agent` changes one running process once; N|Solid sets back any key a desired config manages, and a restart forgets it. Use it only when the user wants exactly that. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index 575d7d1..f4ce2c2 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -7,7 +7,7 @@ description: >- This skill reads N|Solid through its MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and check each tenant the user cares about (production and staging are usually separate tenants). ### 1. Scope -- The whole tenant by default, or the app the user names. `list_apps` with `limit: 50` lists the apps with their processes by status and the app, N|Solid and Node.js versions in use. +- The whole tenant by default, or the app the user names. `list_apps` lists the apps (50 by default) with their processes by status and the app, N|Solid and Node.js versions in use. ### Bounded Lists - Read `total`, `returned`, `truncated`, `more`, `totalAtLeast`, `appsOmitted`, any `outputCut` notice and any cut notice in each content block before drawing conclusions. `more: true` means the entries are a sample, not the whole set; `totalAtLeast` is a lower bound, not a count. List results stay within 20,000 serialized UTF-8 bytes, so fewer entries may fit than the requested limit. An omitted app or process is not proof that it is absent or healthy; say when the briefing covers only part of the tenant. @@ -18,16 +18,11 @@ Run what fits the question; for a full briefing, all of it: - **Hot processes:** `fleet_snapshot` with `sort: "cpu_percent"`, then `sort: "used_heap_size"` and `sort: "loop_estimated_lag"` (limit 5 each; add `where: 'app == ""'` for one app). Note anything far from its peers. - **Growing memory:** `query_metric` with `metric: "used_heap_size"`, `groupBy: "app"` and `from: "-24h"`: a series that only climbs needs a look (`ns-memory-spike-analysis`). - **Latency:** `query_metric` with `metric: "http_server"` (the p99) and `groupBy: "app"`. -- **Crashes and restarts:** resolve the window once to absolute times (e.g. the last 24 hours as one `from` and `to`) and pass that same `from` and `to` to every call you compare. `list_ended_processes` with `byApp: true`, `limit: 25` and that window: how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. For one app, pass `app` without `byApp`; tenant-wide `byApp` refuses `app`, `host` and `ending`, so don't add filters to it. One app's list of ended processes is a sample of processes, not a total. When the `byApp` result includes the named app, its ended count already aggregates the same retained tombstones: use it without paging; an app missing from a partial `byApp` list is unknown, not zero. Otherwise, paging through an authorized `GET /v1/tenants/{tenant}/tombstones` filtered to the app over the same window counts the ended-process records N|Solid currently retains for that window, not every process that actually ended: records still settling, an ingest backlog, retention, and lost processes that reconnect and drop their record all affect coverage. Report it as recorded tombstones; never present it as an exact count of all ended processes, and don't take zero as proof that none ended. Without either, say the count is a sample. `event_histogram` filtered to that app counts `exit` and `lost` events, not ended processes, so the numbers differ: report the tombstone count and the event totals separately, each labeled for what it counts, over the same absolute window. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. +- **Crashes and restarts:** `list_ended_processes` with `byApp: true` and `from: "-24h"` (25 apps by default): how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. `byApp` takes `from`, `to` and `limit` only; for one app, pass `app` without `byApp`. `truncated` or `totalAtLeast` means some apps were left out, so an app missing from the list isn't zero. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. -- **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial. An app is clear only on complete evidence scoped to that app (`app`), and a ranking of apps needs complete results for every app it compares; current advisory data alone doesn't make an app clear. -- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them (`upgradeTo`). - - Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). An app named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For one app's current version, use `find_agents` with the exact `app` (or, e.g. for long names, a valid, properly escaped app-equality `where`) and `status: "connected"`; if refused, never truncate the name or drop the scope. A `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. - - Advisories and `upgradeTo` are tenant-and-version context. Call an advisory platform-independent only from intact, complete, authoritative advisory metadata that shows no platform restriction: `list_runtimes` text cut at its end can lose a platform qualifier (such as `[linux only]`), so its absence there proves nothing; otherwise keep it as tenant context, not attributed to the app. A `get_agent` verdict supports only its own observed version/platform pair, never independence. A platform-independent advisory applies to the app named with that version as last-day exposure, not proof that version runs now (confirm that as above); a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read and keep reads bounded (not one per replica). Unexamined agents and platforms stay unknown, and the same Node.js version doesn't prove replicas share a platform; mark unchecked or unknown scope incomplete, never all-clear. - - Optionally, an already authorized read of the app-scoped, connected `GET /v1/tenants/{tenant}/agents` inventory enumerates observed `nodeVersion`/platform pairs only; it has no verdict. `get_agent` on one representative relevant agent per pair gives that pair's `runtime.fixed` verdict. Keep the scan within the task's budget, stopping as partial if needed; complete scope needs a successful read through the final page, and covers the observed scan, not an atomic snapshot. A failed page or cursor leaves it partial, and earlier conflicting observations stand. Read only: no writing credentials or extra access; if it refuses the app filter, don't drop or truncate it. - - An already authorized `GET /v1/tenants/{tenant}/runtimes?app=` is optional positive last-day evidence for that app, not current-only; keep its advisories and `fixed` (MCP's `upgradeTo`) labeled by that scope, since different tenant, app or platform scopes can give different values. If it refuses the app filter, don't drop or truncate it. - - A same-line patch backed by verified applicable evidence comes first; an aggregate-only fix is a scoped candidate with its limits, neither a proven app target nor proof no patch is needed. Missing, null, errored or stale data is incomplete. Before claiming complete coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness. +- **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial, never an all-clear. +- **Node.js runtimes:** `list_runtimes`: versions at or near end of life, and core advisories with the release that fixes them (`upgradeTo`). Each version names at most 20 apps, and `appsOmitted` counts the rest: an app it doesn't name may still run that version. - **New behaviors:** `list_behaviors`: dependencies doing something new, release regressions and drift. - **Agent credentials** (admins, on request): the web UI's Connect agents page, or `GET /v1/tenants//ingest-tokens`, shows ingest tokens due for rotation (`rotationDueAt`) and rotated ones still in use; rotation is covered in `ns-configure-agents`. - **How the team uses N|Solid** (admins, on request): the web UI's Activity page, or `GET /v1/tenants//activity?by=area|tool|clientName|person`, shows which features, pages and AI assistants and tools people use; `ns-activity-report` writes it as a page to share. It is off where N|Solid hasn't enabled it for the tenant. diff --git a/skills/ns-manage-alerts/SKILL.md b/skills/ns-manage-alerts/SKILL.md index c96d78a..0367f7e 100644 --- a/skills/ns-manage-alerts/SKILL.md +++ b/skills/ns-manage-alerts/SKILL.md @@ -6,14 +6,7 @@ description: >- This skill works through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Saving rules, acknowledging or silencing alerts and ending a silence need the `nsolid:manage-alerts` permission; without it those tools are missing, and the skill can still review and draft. -## Partial Rule Lists and Name Checks -- `list_alert_rules` returns a bare JSON array of rules: it has no `limit`, count, `truncated` flag or cursor. Its result is incomplete when a content block carries a cut notice, when it comes back as a text-only notice that the result was too large, or when it returns an error. Then the rules it left out are unknown: a name missing from it is unverified, not unused, and coverage conclusions from it are partial: say which. -- A successful JSON list with no cut notice, too-large notice or error is complete for that tenant: if the exact name is absent from it, the name is free, and no Console or REST check is needed. -- A positive match still counts on a partial list: the exact name appearing in any rule list, cut or not, proves that rule exists. -- When the list is incomplete and the name is absent from it, verify that exact tenant and name before presenting or saving it as new: the Console's alert rules page, or an authorized `GET /v1/tenants/{tenant}/alert-rules/{name}`. A 404 from an authorized request confirms the name is free. A 200 means the rule exists. -- An existing name means saving would replace that rule: show it, and if the user approved only creating a rule, get their go-ahead to replace it. The user's existing go-ahead for the same action on the same target still counts; ask nothing more. -- If the exact name can't be verified (no Console or REST access, or the request is refused), don't infer that it is free and don't save it as new: hand the user the drafted spec to check and save themselves. -- The REST lookup settles the rule's identity and whether the name exists; it is not `set_alert_rule` input. A REST rule is in the API's own shape (expressions as trees, durations in milliseconds), not the MCP writer's arguments. To edit a rule, start from its complete record in `list_alert_rules` (in `set_alert_rule`'s terms); when that record isn't available through MCP, have the user make the change in the Console's rule editor, or hand them the drafted change. Never translate a REST rule into `set_alert_rule` arguments, and neither drop nor copy fields MCP doesn't name. Keep the user's existing authorization and the selected tenant and rule as they are. +When `list_alert_rules` comes back cut, too large or with an error, a name missing from it is not proof the name is free: don't claim a rule is absent from a partial list. ## Start a Newly Connected Service's Baseline @@ -24,8 +17,7 @@ do not keep copies of starter definitions or reconstruct their thresholds. - Call `list_alert_rules` first. Check both names and coverage: a tenant-wide rule may already cover the service. Offer the useful missing starters and - explain their reasons; do not install the whole list automatically. When - the rule list is partial, say the coverage check is partial too. + explain their reasons; do not install the whole list automatically. - Review each chosen `spec` with the user. Starters are enabled, cover every process or app, have no notification channels, and take no diagnostics. Agree on the intended coverage and whether to record alerts only or use @@ -34,9 +26,7 @@ do not keep copies of starter definitions or reconstruct their thresholds. For finding/behavior rules, combine the original `where` with the app expression using parentheses. Use the expression language's escaped string syntax for the app name. Choose an unused rule name for service-specific - rules; a name already in use replaces the whole rule. If the rule list was - partial, verify the exact name as in Partial Rule Lists and Name Checks - before offering it as new. + rules; a name already in use replaces the whole rule. - Preview what it would have caught using step 3 below, and show the final spec. Once the user authorizes saving, pass that spec with the selected `tenant` to `set_alert_rule`; do not pass the starter's id, title or why. @@ -84,13 +74,13 @@ Before saving, show the user what the rule would have done: - `diagnose` makes a firing process capture evidence: `profile` (CPU), `heap_sampling` or `snapshot` (pauses the process). Only rules grouped by `agent` take one: an app's alert is about no one process. A small hourly budget per app (`diagnosePerAppPerHour`, default 2) keeps an incident from profiling the whole fleet; `diagnoseDurationMs` (default 10000) sets how long a profile or sampling runs. Suggest `profile` for CPU and latency rules and `heap_sampling` for memory rules; `snapshot` only when the user asks. A process that profiles continuously isn't asked for a `profile`: the alert keeps its continuous profiles instead. ### 5. Save -- Show the rule as you'll save it (name, kind, condition, scope, channels, diagnostics), and save it with `set_alert_rule` once the user agrees. A name that exists is replaced whole: check `list_alert_rules` first (when it is partial, verify the exact name as in Partial Rule Lists and Name Checks before saving it as new), say so when the user is changing a rule rather than adding one, and pass back every writable input value its `list_alert_rules` record shows (in `set_alert_rule`'s terms), changing only what the user asked. Not every returned key is input: `health`, `updatedAt`, `updatedBy` and `kind` are output metadata, not `set_alert_rule` arguments, and leaving out such known read-only metadata is correct. Never discard an unknown writable field or setting to get past validation: when the writer can't carry it faithfully, use a writer that can or hand off the draft. Without that complete MCP record, use the Console's rule editor or hand off the draft rather than rebuilding the rule from REST. `enabled` left out keeps the rule's state; a new rule is on. +- Show the rule as you'll save it (name, kind, condition, scope, channels, diagnostics), and save it with `set_alert_rule` once the user agrees. A name that exists is replaced whole: check `list_alert_rules` first, say so when the user is changing a rule rather than adding one, and pass back every field it shows (they are in `set_alert_rule`'s terms), changing only what the user asked. `enabled` left out keeps the rule's state; a new rule is on. - Arguments are checked strictly: a misspelled or unknown key is refused with its name. Fix it rather than dropping it. - A channel that doesn't exist is refused. The saved rule comes back with `warnings`, such as a metric no process reported in the last 7 days (a misspelled name, or an app that hasn't started) or a disabled channel: tell the user. - Names use letters, digits, `.`, `_` and `-`: `checkout-p99-latency`. ## Reviewing and Tuning -- `list_alert_rules` lists the rules (all of them unless a cut notice, too-large notice or error says otherwise) with their condition, scope, channels, diagnostics and `health`, which tells a quiet rule from a broken one: +- `list_alert_rules` lists every rule with its condition, scope, channels, diagnostics and `health`, which tells a quiet rule from a broken one: - `ok`, with what the last check saw: the groups with a value and how many held (`groups`, `holding`), or for an event rule when an event last came and last matched (`checkedAt`, `matchedAt`); - `no-data`: no process in scope reports the metric (a misspelled name, an empty scope); - `failing`: its last check failed, and `lastError` says why; @@ -109,7 +99,6 @@ Before saving, show the user what the rule would have done: ## Present the Result - For a new or changed rule: the rule as saved, what it would have caught (step 3), and where it notifies. -- For coverage reviews: mark the conclusion partial when `list_alert_rules` left rules out. - For triage: what is firing, since when, what the evidence shows, and the next step. ## Guardrails diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index 4525ec2..7c30d3f 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -36,11 +36,8 @@ Otherwise, check in order: ### 2b. Check What Production Runs (when N|Solid is available) If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). -- Each row is one Node.js version, tenant-wide over the last day, naming at most 20 apps (`appsOmitted` or `outputCut` mean more). The project's app (its `package.json` `name` or `NSOLID_APPNAME`) named under a version is last-day evidence it ran that version; an omitted app isn't proof of absence. For what runs now, use `find_agents` with the exact `app` (or, e.g. for long names, a valid, properly escaped app-equality `where`) and `status: "connected"`; if refused, never truncate the name or drop the scope. A `More agents match` notice, cut or error leaves it partial, and every version seen stands. It shows no platform. If production differs from the workspace pin, say so and plan from what runs. -- Advisories and `upgradeTo` are tenant-and-version context. Call an advisory platform-independent only from intact, complete, authoritative advisory metadata that shows no platform restriction: `list_runtimes` text cut at its end can lose a platform qualifier (such as `[linux only]`), so its absence there proves nothing; otherwise keep it as tenant context, not attributed to the app. A `get_agent` verdict supports only its own observed version/platform pair, never independence. A platform-independent advisory applies to the app named with that version as last-day exposure, not proof that version runs now (confirm that as above); a platform-restricted one needs that app's platform evidence first. `get_agent` on relevant confirmed agents gives platform and `runtime.fixed` for that actual version and platform: reuse detail already read and keep reads bounded (not one per replica). Unexamined agents and platforms stay unknown, and the same Node.js version doesn't prove replicas share a platform; mark unchecked or unknown scope incomplete, never all-clear. -- Optionally, an already authorized read of the app-scoped, connected `GET /v1/tenants/{tenant}/agents` inventory enumerates observed `nodeVersion`/platform pairs only; it has no verdict. `get_agent` on one representative relevant agent per pair gives that pair's `runtime.fixed` verdict. Keep the scan within the task's budget, stopping as partial if needed; complete scope needs a successful read through the final page, and covers the observed scan, not an atomic snapshot. A failed page or cursor leaves it partial, and earlier conflicting observations stand. Read only: no writing credentials or extra access; if it refuses the app filter, don't drop or truncate it. -- An already authorized `GET /v1/tenants/{tenant}/runtimes?app=` is optional positive last-day evidence for that app, not current-only; keep its advisories and `fixed` (MCP's `upgradeTo`) labeled by that scope, since different tenant, app or platform scopes can give different values. If it refuses the app filter, don't drop or truncate it. -- Recommend the same-line patch first, whatever the line move, when verified applicable evidence supports it; an aggregate-only fix is a scoped candidate with its limits, neither a proven app target nor proof no patch is needed. Missing, null, errored or stale data is incomplete. Before claiming complete coverage, check `vulnerability_data` (it takes no `tenant`) for each Node.js runtime source's `checkedAt`, errors and freshness. +- Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Production may run a different version than the workspace pins: say so, and plan from what runs. Each version names at most 20 apps, and `appsOmitted` counts the rest: an app it doesn't name may still run that version. +- Core advisories with an `upgradeTo` on the same line call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. - If the server isn't available, continue from the workspace alone. ### 3. Recommend a Target Version @@ -73,10 +70,7 @@ If the user asks about a specific version mid-conversation, re-run the detection ## Tools - `getPackageVersions` — check direct dependencies for known incompatibilities with target Node version. -- `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them (`upgradeTo`, tenant-wide per version over the last day's apps and platforms). -- `find_agents` (`nsolid` server, when available) — the connected processes of one app (`app`, `status: "connected"`), with the Node.js version each runs. -- `get_agent` (`nsolid` server, when available) — one process's platform and runtime verdict for its own version and platform: end of life, core advisories and `runtime.fixed` (per agent; can differ from `list_runtimes`'s aggregate `upgradeTo`). -- `vulnerability_data` (`nsolid` server, when available; takes no `tenant`) — when the advisory sources, Node.js runtime ones included, were last checked, and any sync error. +- `list_runtimes` (`nsolid` server, when available) — the Node.js versions production runs, their end of life, core advisories and the release that fixes them. ## Guardrails - The release schedule table from `fetch-node-releases.cjs` or the injected host table is authoritative. Do not override LTS names, EOL dates, or latest patch versions with training-data values. From 348f2c323a68affacb0903a7036810c37cc2394a Mon Sep 17 00:00:00 2001 From: Wbert Adrian Castro Vera Date: Wed, 4 Nov 2026 16:01:25 -0500 Subject: [PATCH 14/14] docs: clarify tenant and partial-list consequences --- skills/ns-analyze-tracing/SKILL.md | 2 +- skills/ns-analyze-vulnerabilities/SKILL.md | 2 +- skills/ns-fleet-health/SKILL.md | 2 +- skills/ns-generate-sbom/SKILL.md | 2 +- skills/ns-manage-alerts/SKILL.md | 4 ++-- skills/ns-node-upgrade/SKILL.md | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/skills/ns-analyze-tracing/SKILL.md b/skills/ns-analyze-tracing/SKILL.md index 4eb9773..4e50c25 100644 --- a/skills/ns-analyze-tracing/SKILL.md +++ b/skills/ns-analyze-tracing/SKILL.md @@ -13,7 +13,7 @@ This skill reads traces through the N|Solid MCP server (`nsolid`). Most N|Solid ### 2. Discover Services Only When Needed - **Skip this step if sufficient host-provided trace data is present**, or when the user names the service or app. -- Call `list_apps` for the app names. A span's `service` is its app's name. An app missing from an omitted or truncated answer is not proven absent: pass the named app straight to the tools that take it, or ask the user for its exact name. +- Call `list_apps` for the app names. A span's `service` is its app's name. An app missing from an omitted or truncated answer is not proven absent: if the user gave its exact name, pass it straight to the tools that take it; otherwise ask for the exact name rather than guess. - The N|Solid server doesn't collect AWS Lambda functions; if the user asks about a serverless function, say so. ### 3. Find Slow Requests Only If No Trace Was Supplied diff --git a/skills/ns-analyze-vulnerabilities/SKILL.md b/skills/ns-analyze-vulnerabilities/SKILL.md index af8b6d7..bda7a3d 100644 --- a/skills/ns-analyze-vulnerabilities/SKILL.md +++ b/skills/ns-analyze-vulnerabilities/SKILL.md @@ -9,7 +9,7 @@ This skill reads vulnerability findings through the N|Solid MCP server (`nsolid` ### 1. High-Level Overview - Call `list_vulnerabilities`: open findings, most severe first. Each names its advisory (GHSA, a malicious-package report MAL, or a Node.js core advisory NODE-CORE), the app, severity, the vulnerable packages with the version that fixes them, the app's own dependencies that bring each in (`via`), and the runtime evidence: `installed`, `loaded` (required by the app) or `running` (seen in CPU profiles). A finding whose CVE CISA lists as exploited in the wild carries `knownExploited`: the CVE, when CISA listed it, its remediation due date, and whether ransomware campaigns use it. - Narrow large fleets with `app`, `severity` (at least), or `where`, e.g. `severity >= "high" && evidence >= "loaded"`, or `knownExploited` (`expression_help` with context `finding` lists the fields; it takes no `tenant`). -- A result that fills its `limit` or carries a cut notice is partial: the findings it shows stand, but more may exist. +- A result that fills its `limit` or carries a cut notice is partial: the findings it shows stand, but more may exist, so it cannot support an app all-clear or a complete ranking. - Summarize which app has the most critical issues. Lead with malicious packages (MAL-…) when there are any, then known exploited findings in loaded or running code: attackers use those now, whatever their severity. - Before saying an app has nothing to fix, `vulnerability_data` (it takes no `tenant`) shows whether the advisory data is current: when the OSV mirror and CISA's catalog were last checked, and any sync error. Say so when it is stale. - For the Node.js runtime itself, `list_runtimes` gives each Node.js version in use: its core advisories, the release that fixes them, and whether its line is at or near end of life. diff --git a/skills/ns-fleet-health/SKILL.md b/skills/ns-fleet-health/SKILL.md index f4ce2c2..51a5c4c 100644 --- a/skills/ns-fleet-health/SKILL.md +++ b/skills/ns-fleet-health/SKILL.md @@ -18,7 +18,7 @@ Run what fits the question; for a full briefing, all of it: - **Hot processes:** `fleet_snapshot` with `sort: "cpu_percent"`, then `sort: "used_heap_size"` and `sort: "loop_estimated_lag"` (limit 5 each; add `where: 'app == ""'` for one app). Note anything far from its peers. - **Growing memory:** `query_metric` with `metric: "used_heap_size"`, `groupBy: "app"` and `from: "-24h"`: a series that only climbs needs a look (`ns-memory-spike-analysis`). - **Latency:** `query_metric` with `metric: "http_server"` (the p99) and `groupBy: "app"`. -- **Crashes and restarts:** `list_ended_processes` with `byApp: true` and `from: "-24h"` (25 apps by default): how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. `byApp` takes `from`, `to` and `limit` only; for one app, pass `app` without `byApp`. `truncated` or `totalAtLeast` means some apps were left out, so an app missing from the list isn't zero. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. +- **Crashes and restarts:** `list_ended_processes` with `byApp: true` and `from: "-24h"` (25 apps by default): how many processes of each app ended and how the latest did, lost ones (gone without an exit: killed, or the host lost) included. Beyond `tenant` and `byApp`, it takes only `from`, `to` and `limit`; `app`, `host` and `ending` are refused, so for one app pass `app` without `byApp`. `truncated` or `totalAtLeast` means some apps were left out, so an app missing from the list isn't zero. `event_stacks` with `type: "exit"` groups exits by error; `event_histogram` with `types: ["exit", "lost"]` shows when they came. - **Blocked event loops:** `event_stacks` (default `loop_blocked`) over the last day: where loops blocked, how often and how long. - **Alerts:** `list_alerts` with `state: "firing"`. `list_alert_rules` shows each rule's `health`: a rule that is `no-data`, `failing` or `stale` protects nothing, so name it under Needs attention. - **Vulnerabilities:** `list_vulnerabilities` with `severity: "high"` (and `where: 'evidence >= "loaded"'` for the ones whose code is in use); malicious packages first, then findings marked `knownExploited` (CISA lists them as exploited in the wild), whatever their severity: `where: "knownExploited"`. A result that fills its `limit` or carries a cut notice is partial, never an all-clear. diff --git a/skills/ns-generate-sbom/SKILL.md b/skills/ns-generate-sbom/SKILL.md index 1ceae02..97debe9 100644 --- a/skills/ns-generate-sbom/SKILL.md +++ b/skills/ns-generate-sbom/SKILL.md @@ -13,7 +13,7 @@ This skill reads SBOMs through the N|Solid MCP server (`nsolid`) and a bundled s ### 2. Identify Target Application - Determine the specific `app` name you need the SBOM for. -- If the app name is unknown, call `list_apps` for the apps and their process counts. An app missing from an omitted or truncated answer is not proven absent: pass the named app straight to `get_sbom`, or ask the user for its exact name. +- If the app name is unknown, call `list_apps` for the apps and their process counts. An app missing from an omitted or truncated answer is not proven absent: if the user gave its exact name, pass it straight to `get_sbom`; otherwise ask for the exact name rather than guess. - An app's SBOM covers its connected processes together. For one process instead, find its `agentId` with `find_agents` (`app`). ### 3. Know What It Contains diff --git a/skills/ns-manage-alerts/SKILL.md b/skills/ns-manage-alerts/SKILL.md index 0367f7e..d5e6e38 100644 --- a/skills/ns-manage-alerts/SKILL.md +++ b/skills/ns-manage-alerts/SKILL.md @@ -6,7 +6,7 @@ description: >- This skill works through the N|Solid MCP server (`nsolid`). Most N|Solid tools take a `tenant`: when the token can use one tenant it is the default; otherwise call `list_tenants` (it takes no `tenant`) once and use the tenant the user names. Saving rules, acknowledging or silencing alerts and ending a silence need the `nsolid:manage-alerts` permission; without it those tools are missing, and the skill can still review and draft. -When `list_alert_rules` comes back cut, too large or with an error, a name missing from it is not proof the name is free: don't claim a rule is absent from a partial list. +When `list_alert_rules` comes back cut, too large or with an error, a name missing from it is not proof the name is free: don't claim a rule is absent from a partial list, and don't save under that name as a new rule until the exact name is verified unused. ## Start a Newly Connected Service's Baseline @@ -80,7 +80,7 @@ Before saving, show the user what the rule would have done: - Names use letters, digits, `.`, `_` and `-`: `checkout-p99-latency`. ## Reviewing and Tuning -- `list_alert_rules` lists every rule with its condition, scope, channels, diagnostics and `health`, which tells a quiet rule from a broken one: +- `list_alert_rules` lists rules with their condition, scope, channels, diagnostics and `health`, which tells a quiet rule from a broken one: - `ok`, with what the last check saw: the groups with a value and how many held (`groups`, `holding`), or for an event rule when an event last came and last matched (`checkedAt`, `matchedAt`); - `no-data`: no process in scope reports the metric (a misspelled name, an empty scope); - `failing`: its last check failed, and `lastError` says why; diff --git a/skills/ns-node-upgrade/SKILL.md b/skills/ns-node-upgrade/SKILL.md index 7c30d3f..49e52ae 100644 --- a/skills/ns-node-upgrade/SKILL.md +++ b/skills/ns-node-upgrade/SKILL.md @@ -35,7 +35,7 @@ Otherwise, check in order: **At the start of your response**, briefly tell the user which source was used to determine the current version (one sentence). If the project-pinned version and the runtime on PATH disagree, treat the project pin as authoritative and note the mismatch. ### 2b. Check What Production Runs (when N|Solid is available) -If the `nsolid` MCP server is available, call `list_runtimes` (it takes a `tenant` when the token can use several: `list_tenants`). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). +If the `nsolid` MCP server is available, call `list_runtimes` (pass a `tenant` when the token can use several; `list_tenants` takes no `tenant` and lists them). For each Node.js version the tenant's processes run, it gives the apps and process counts, whether its line is at or within 90 days of end of life (and which line to move to), the Node.js security team's core advisories that affect it, and the lowest release of its line that fixes them all (`upgradeTo`). - Find the project's app among them (its `package.json` `name` or `NSOLID_APPNAME`). Production may run a different version than the workspace pins: say so, and plan from what runs. Each version names at most 20 apps, and `appsOmitted` counts the rest: an app it doesn't name may still run that version. - Core advisories with an `upgradeTo` on the same line call for a patch upgrade first, whatever the line move: it is the smallest change that fixes them. - If the server isn't available, continue from the workspace alone.