Skip to content
Merged
Show file tree
Hide file tree
Changes from 159 commits
Commits
Show all changes
160 commits
Select commit Hold shift + click to select a range
630ed86
fix: follow in-root symlinks and report case variants at the confinem…
claude Nov 10, 2026
1ab0d90
fix: widen A2A card discovery, bound the scope filter and record card…
claude Nov 10, 2026
b6213cc
fix: bind RAG source configuration and never export inline documents
claude Nov 10, 2026
9f28b08
feat: inventory rule directories, skill bundles and @path imports wit…
claude Nov 10, 2026
346ce28
fix: treat a missing file below an in-root linked directory as absent
claude Nov 10, 2026
c50fde8
feat: bind Claude Code settings and score risky IDE agent declarations
claude Nov 10, 2026
7f3e989
feat: version and confine the native AgentBOM format
claude Nov 10, 2026
82af29f
fix: gate on locally corroborated evidence and accept findings narrowly
claude Nov 10, 2026
938c322
docs: add a policy reference and state the new gate guarantees
claude Nov 10, 2026
9665136
fix(graph): bind agent.yaml to collector records and link packages fo…
claude Nov 10, 2026
c7597f7
fix: serialize compactly only when needed and show an unverified prod…
claude Nov 10, 2026
d911cbd
fix(diff): alert on removals and coverage regressions by name, rate r…
claude Nov 10, 2026
ee9c8f4
fix(shadowscan): refuse ambiguous report locations and record unrecog…
claude Nov 10, 2026
6acad9e
refactor(lineage): build and check lineage in linear time without rec…
claude Nov 10, 2026
ada60da
docs: point readers at the drift gate guide and the agent manifest
claude Nov 10, 2026
35532a3
fix: bind every model weight file and verify it only under operator p…
claude Nov 10, 2026
05182a3
docs: document model integrity binding and the signer policy kinds
claude Nov 10, 2026
5ab26b9
fix: record GGUF chat template names only in a plain form
claude Nov 10, 2026
d6cdb00
feat: make collection limits operator settings with named failures
claude Nov 10, 2026
6703356
fix(registry): classify, resolve and bound what the registry collecto…
claude Nov 10, 2026
cbf88cf
fix: give remote MCP servers a drift path and bind server declarations
claude Nov 10, 2026
26e4a22
fix: make advisories a gate with honest coverage and a stated disclosure
claude Nov 10, 2026
b4ad568
test: generate offline-guard tests from the guard tables
claude Nov 10, 2026
904116a
fix: retire superseded runtime snapshots and give every denial a reason
claude Nov 10, 2026
ff36cc1
docs: explain how to operate the runtime gateway safely
claude Nov 10, 2026
49186e7
build: generate the hash locks from pyproject.toml and test the decla…
claude Nov 10, 2026
0d1b141
test: isolate the suite, bound each test, account for skips and fuzz …
claude Nov 10, 2026
f19cd59
ci: gate on the dependency floors and audit every locked pin; check w…
claude Nov 10, 2026
193549c
docs: describe the collectors' new coverage, bindings and remaining l…
claude Nov 10, 2026
0213e9f
fix: hold PyPI publication for a soak, a signed tag and protected set…
claude Nov 10, 2026
5be31c6
fix: make the benchmark controls falsifiable and their evidence check…
claude Nov 10, 2026
f0d6627
fix: keep live observations usable and reviewable
claude Nov 10, 2026
833794a
docs: state the review policy, continuity plan and security response
claude Nov 10, 2026
7dbd99e
fix: report @path imports of excluded paths as coverage gaps
claude Nov 10, 2026
d2781e7
fix: scrub only secret-like values once per source and widen redaction
claude Nov 10, 2026
7dfe264
fix: report Python MCP registrations the collector cannot resolve
claude Nov 10, 2026
c81a64d
fix: classify MCP launchers, sources and endpoints and fail closed on…
claude Nov 10, 2026
18e0d80
docs: describe MCP launcher classification, dialects and binding cove…
claude Nov 10, 2026
91d1e31
test: keep the README collector table in step with the collected paths
claude Nov 10, 2026
f8d08a7
docs: state exactly when spec_version and upgraded inventories alert
claude Nov 10, 2026
9e02de8
fix(exports): conformant, versioned standard exports with linear vali…
claude Nov 10, 2026
06364f5
docs(exports): map standard export fields, projections, SARIF rules a…
claude Nov 10, 2026
57dcd97
test: spell invisible and bidirectional characters as escapes in the …
claude Nov 10, 2026
0332c78
fix: keep the tool package as MCP launcher identity when uvx adds dep…
claude Nov 10, 2026
02b5ab5
ci: name the publication preflight step after everything it checks
claude Nov 10, 2026
4ef0e03
build: say how to move the upload cut-off when lock regeneration cann…
claude Nov 10, 2026
896afd8
fix: parse only MCP SDK candidates and keep TypeScript analysis linear
claude Nov 10, 2026
00741e3
docs: describe the release candidate's advisory recheck and coverage …
claude Nov 10, 2026
4f6fdb2
fix: keep checkout text inert in CLI and Action output and explain ev…
claude Nov 10, 2026
714017e
docs: say that a pull request owns its workflow file and how to prote…
claude Nov 10, 2026
cbf81ae
fix: name the target of an in-root symlink on A2A, RAG and IDE records
claude Nov 10, 2026
8721547
docs: state that the Claude settings digest can confirm guessed values
claude Nov 10, 2026
92e8960
fix(diff): alert when a claimless baseline hides the new snapshot's gaps
claude Nov 10, 2026
27cac05
fix(graph): name agent.yaml entries by locator and list manifest surf…
claude Nov 10, 2026
9a087a0
fix(shadowscan): record a rating without a level as an unrecognized v…
claude Nov 10, 2026
255f877
Merge work package drift: drift gate, repository graph, agent manifes…
claude Nov 10, 2026
6f932cc
fix(exports): tighten recovery, VEX identity and SARIF text after review
claude Nov 10, 2026
57550d4
fix: inspect each model artifact once and charge signature verificati…
claude Nov 10, 2026
b557937
fix: refuse verified supplier claims on local components and label fa…
claude Nov 10, 2026
26bd894
fix: say why an inventory cannot be opened and document no-follow reads
claude Nov 10, 2026
ed3d2a7
Merge work package exports: conformant, versioned standard exports wi…
claude Nov 10, 2026
06e2bd7
fix: keep Click usage errors readable and say where unlisted scan gap…
claude Nov 10, 2026
aeb9726
docs: scope the CI-log guarantee and name organization rulesets
claude Nov 10, 2026
4d9ea2d
fix: read each model file once per validation and enforce report pins
claude Nov 10, 2026
4b81fdf
docs: state how validation bounds model reads and checks report entries
claude Nov 10, 2026
a0350cb
fix: keep advisory lookups linear and bounded on untrusted OSV replies
claude Nov 10, 2026
6731627
fix: refuse costly served schemas at the first error and bind server …
claude Nov 10, 2026
d8296a8
test: cover resolver timeouts and the cap on outstanding lookups
claude Nov 10, 2026
241bc06
docs: state the extended tool fingerprint contract in the migration g…
claude Nov 10, 2026
45ee708
fix(registry): close review findings on Compose aliases, tags, TOML k…
claude Nov 10, 2026
14ac930
fix: close fail-open gaps found in review of the runtime gateway
claude Nov 10, 2026
6ddd600
test: cover validation of shard indexes and unreadable bound files
claude Nov 10, 2026
c616812
fix: fail closed on MCP runners started outside the launcher grammar
claude Nov 10, 2026
11529e7
Merge origin/main (#127, #128, #129) into the audit remediation
claude Nov 10, 2026
c2ea67a
fix: read MCP project resolver settings as npm reads them and bind th…
claude Nov 10, 2026
8644df2
fix: reject JSONC line ends that MCP clients read differently
claude Nov 10, 2026
07198f7
fix: redact long Bearer, Token and ApiKey values whatever their letters
claude Nov 10, 2026
cea1e0c
docs: describe MCP launcher fail-closed rules and resolver reading
claude Nov 10, 2026
626fd3c
fix: record collection messages in constant time
claude Nov 10, 2026
637546d
fix: read Python MCP sources as CPython decodes them
claude Nov 10, 2026
696ea91
fix: treat a release-tag bypass list the workflow token cannot read a…
claude Nov 10, 2026
d3d7d63
ci: bound write scopes, keep weekly runs alive, measure scripts, test…
claude Nov 10, 2026
cbdd06b
build: regenerate the locks atomically and never lose the recorded cu…
claude Nov 10, 2026
d7ce31c
fix: compare a benchmark installation without the environment's boots…
claude Nov 10, 2026
88011ba
test: read requirement lines as pip does and crash-test every export …
claude Nov 10, 2026
e4c0694
fix: record PowerShell and WSL working directories for MCP resolver f…
claude Nov 10, 2026
b569839
fix: list MCP client sources without a scan per candidate
claude Nov 10, 2026
3879098
fix: show the unchecked ruleset bypass list on the run page as well
claude Nov 10, 2026
6e1fbeb
fix: report case variants below in-root links and bound @path import …
claude Nov 10, 2026
f2f1988
fix: digest and bind every RAG configuration value
claude Nov 10, 2026
079a96c
fix: check both A2A card endpoint fields and skip generic non-card files
claude Nov 10, 2026
9b7aa17
fix: report Gemini CLI MCP servers and bind Cursor hook commands
claude Nov 10, 2026
e731988
docs: describe the review fixes to variants, imports, RAG and IDE set…
claude Nov 10, 2026
6db6d19
Merge work package gates: corroborated gates, native format 1.1.0 and…
claude Nov 10, 2026
eaf45cd
test: check heredoc parsing for linear growth instead of a wall-clock…
claude Nov 10, 2026
5fb60dd
Merge work package cli: inert CLI and Action output with one gate report
claude Nov 10, 2026
bda207e
fix(registry): close second review findings on Compose builds and .env
claude Nov 10, 2026
9dbeb9f
refactor(reporting): keep upstream's name for the failure-listing limit
claude Nov 10, 2026
debfc95
fix: refuse runtime schemas that the validator would read differently
claude Nov 10, 2026
9f6ff87
fix: bound bindings of shared model files and read data_location as p…
claude Nov 10, 2026
37c328b
Merge work package live: remote MCP drift path, bound server declarat…
claude Nov 10, 2026
47164c0
fix(advisories): describe private-origin exclusion with Python lockfiles
claude Nov 10, 2026
539ef18
fix: close link, spelling and import gaps and bound hostile scan work
claude Nov 10, 2026
6aeccdb
fix: decode scripts as tsc does and keep large trees linear and loadable
claude Nov 10, 2026
99ac7d2
fix: close MCP launcher and resolver bypasses found in second review
claude Nov 10, 2026
7ffe538
Merge work package registry: normalised AI catalog, checked lockfiles…
claude Nov 10, 2026
b022820
test: run the Action lock closure check only against the CI lock
claude Nov 10, 2026
d25fe98
Merge work package model: bind every weight file and verify it only u…
claude Nov 10, 2026
59bb947
Merge work package runtime: retired snapshots, scoped signers and fai…
claude Nov 10, 2026
21a9eab
Merge work package collectors: in-root links, case variants, imports …
claude Nov 10, 2026
f9b3d63
Merge work package limits: operator collection limits on upstream's s…
claude Nov 10, 2026
864715b
Merge work package mcp: classified launchers and client dialects on u…
claude Nov 10, 2026
63e9eb3
Merge work package ci: floor-tested locks, an isolated suite and chec…
claude Nov 10, 2026
1738929
test: pin that every collector records the same inventory at any chec…
claude Nov 10, 2026
419c0a1
test: simulate macOS only inside the schema worker module
claude Nov 10, 2026
8e261d5
docs: move the known-limitations catalogue out of SECURITY.md
claude Nov 10, 2026
63871f6
test: restore two assertions that the merges weakened
claude Nov 10, 2026
9d1320e
docs: add architecture, threat model, finding catalog and glossary
claude Nov 10, 2026
f67bdd1
docs: restructure the README for a first-time reader
claude Nov 10, 2026
e302a18
docs: write the Unreleased changelog for the audit remediation
claude Nov 10, 2026
0c371cb
Merge docs/readme: restructure the README for a first-time reader
claude Nov 10, 2026
3227ff7
Merge docs/reference: architecture, threat model, finding catalog and…
claude Nov 10, 2026
008f8a2
Merge docs/changelog: the Unreleased changelog for the audit remediation
claude Nov 10, 2026
261ef91
build: drop the unused Rich runtime dependency
claude Nov 10, 2026
d620fa3
docs: make the merged pages agree with each other and the code
claude Nov 10, 2026
a58a0b8
docs: say which binding key the Action collects with
claude Nov 10, 2026
6cbf965
Merge fix/drop-rich: drop the unused Rich runtime dependency
claude Nov 10, 2026
9942656
fix(registry): read each pnpm lockfile key once
claude Nov 10, 2026
9fabc69
fix(runtime): validate and scope the arguments a canonical gateway ex…
claude Nov 10, 2026
16c5ecb
fix(yaml): read base-60 integers as text in every project YAML loader
claude Nov 10, 2026
fa01526
Merge fix/registry-pnpm-index: read each pnpm lockfile key once
claude Nov 10, 2026
e39a50e
Merge fix/runtime-schemas: validate and scope the arguments a canonic…
claude Nov 10, 2026
72172de
Merge fix/yaml-int-linear: read base-60 integers as text in every YAM…
claude Nov 10, 2026
ded104c
fix(validate): reconcile the coverage claim, verification fields and …
claude Nov 10, 2026
7488de4
fix(gates): truthful validate basis, digest-aware drift ratings, vers…
claude Nov 10, 2026
b193818
fix(release,ci): fail-closed soak and ruleset checks, enforced gate s…
claude Nov 10, 2026
f54ba7c
Merge fix/gates-reconcile: reconcile the coverage claim, verification…
claude Nov 10, 2026
1fb2644
Merge fix/gates-report: truthful validate basis, digest-aware drift r…
claude Nov 10, 2026
2f7e7db
Merge fix/ci-release-controls: fail-closed soak and ruleset checks, e…
claude Nov 10, 2026
b1f4481
fix(a2a): stream the code-card search under the source budget and rea…
claude Nov 10, 2026
233a37f
fix(prompt): follow nested memory-file imports and bound import bookk…
claude Nov 10, 2026
8d89c67
fix(confined): bound every collection failure and refuse undecodable …
claude Nov 10, 2026
5eeae0e
Merge fix/confined-bounds: bound every collection failure and refuse …
claude Nov 10, 2026
f10095e
Merge fix/a2a-code-cards: stream the code-card search under the sourc…
claude Nov 10, 2026
0e8bc0a
Merge fix/prompt-imports: follow nested memory-file imports and bound…
claude Nov 10, 2026
5988528
test: align two fix tests with the merged suite rules and bounded gap…
claude Nov 10, 2026
40271a7
fix(mcp): classify the environment that moves or replaces a pinned ru…
claude Nov 10, 2026
974b860
fix(mcp): withhold token-shaped configuration names from inventories …
claude Nov 10, 2026
91c7800
Merge fix/mcp-config-sources: classify relocating environment, withho…
claude Nov 10, 2026
25692ad
fix(mcp): fail closed on launch text a shell or cmd rewrites, and cha…
claude Nov 11, 2026
0425fb5
fix(mcp): treat remote uv scripts, Windows trailing dots and package …
claude Nov 11, 2026
c20cddb
Merge fix/mcp-launch-grammar: fail closed on rewritten launch text, r…
claude Nov 11, 2026
ad23f51
docs: record the final-review fixes and the launcher limits that remain
claude Nov 11, 2026
9b09af1
fix(ci): bound runtime JSON nesting explicitly and remove two environ…
claude Nov 11, 2026
dc6c9f9
fix(confinement,scanners): enforce case-folding confinement, MCP laun…
aisecnomad Nov 12, 2026
5b84fb3
Merge main into fix/audit-pr2-confinement-and-scanners and reconcile …
claude Nov 12, 2026
bc85820
Merge main into fix/audit-pr2-confinement-and-scanners after #139
claude Nov 12, 2026
d441f10
fix(tests,confinement): resolve the CodeQL alerts on the long-path te…
claude Nov 12, 2026
d8c93b3
fix(confinement): close the descriptor when the exact-spelling check …
claude Nov 12, 2026
78efaff
refactor(confinement): close the descriptor where it is opened
claude Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions src/nexus_symbiote/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,35 @@ def find_policy_path(project_root: Path) -> Path | None:
belongs to whoever controls that directory, not to the project. Both file names
at once are refused: either could silently shadow the other.
"""
from nexus_symbiote.utils.confined import fold_name

current = project_root.resolve()
if current.is_file():
current = current.parent
try:
entries = sorted(os.listdir(current))
except OSError:
return None
folded_targets = {fold_name(name): name for name in POLICY_FILE_NAMES}
found: list[Path] = []
for name in POLICY_FILE_NAMES:
candidate = current / name
variants: list[tuple[str, str]] = []
for entry in entries:
canonical = folded_targets.get(fold_name(entry))
if canonical is None:
continue
candidate = current / entry
if candidate.is_symlink():
raise PolicyError(f"policy must not be a symlink: {candidate}")
if candidate.exists():
found.append(candidate)
if entry in POLICY_FILE_NAMES:
if candidate.exists():
found.append(candidate)
else:
variants.append((entry, canonical))
if variants:
variant_name, canonical = variants[0]
raise PolicyError(
f"case-variant policy file {variant_name} in {current}; rename it to {canonical}"
)
if len(found) > 1:
raise PolicyError(
f"both {POLICY_FILE_NAMES[0]} and {POLICY_FILE_NAMES[1]} exist in {current}; "
Expand Down
59 changes: 48 additions & 11 deletions src/nexus_symbiote/scanners/live_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,23 +545,60 @@ def request(
def _read_sse(self, response: http.client.HTTPResponse, body: bytes, maximum: int) -> bytes:
"""Read event lines until the requested response arrives.

HTTPResponse.readline decodes chunked transfer coding. A single line may
use the whole response byte limit, since SDK servers put an entire result
on one data line.
HTTPResponse.read1 / readline decodes chunked transfer coding. Lines may
terminate in CRLF, LF or bare CR (WHATWG SSE), and a single line may use
the whole response byte limit when SDK servers emit one large data line.
"""
request_id = _json(body).get("id")
decoder = _SSEDecoder(self._max_sse_events)
payload = bytearray()
buffer = bytearray()
saw_bare_cr = False
use_read1 = hasattr(response, "read1")
while True:
line = response.readline(maximum + 1 - len(payload))
if not line:
break
payload.extend(line)
if len(payload) > maximum:
remaining = maximum + 1 - len(payload) - len(buffer)
if remaining <= 0:
raise DiscoveryError("SSE response byte limit exceeded")
for message in decoder.feed(line):
if message.get("id") == request_id and ("result" in message or "error" in message):
return bytes(payload)
chunk = (
response.read1(min(4096, remaining)) if use_read1 else response.readline(remaining)
)
eof = not chunk
buffer.extend(chunk)
while buffer:
cr = buffer.find(b"\r")
lf = buffer.find(b"\n")
if cr < 0 and lf < 0:
if len(payload) + len(buffer) > maximum:
raise DiscoveryError("SSE response byte limit exceeded")
if not eof:
break
end = len(buffer)
elif lf >= 0 and (cr < 0 or lf < cr):
end = lf + 1
elif cr + 1 < len(buffer):
if buffer[cr + 1] == 0x0A:
end = cr + 2
else:
saw_bare_cr = True
end = cr + 1
elif eof or saw_bare_cr:
end = cr + 1
else:
if len(payload) + len(buffer) > maximum:
raise DiscoveryError("SSE response byte limit exceeded")
break
line = bytes(buffer[:end])
del buffer[:end]
payload.extend(line)
if len(payload) > maximum:
raise DiscoveryError("SSE response byte limit exceeded")
for message in decoder.feed(line):
if message.get("id") == request_id and (
"result" in message or "error" in message
):
return bytes(payload)
if eof:
break
raise DiscoveryError("SSE ended without matching response")


Expand Down
93 changes: 82 additions & 11 deletions src/nexus_symbiote/scanners/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,12 +750,17 @@ def _purl(package: Package) -> str | None:
" unsafeHttpWhitelist checksumBehavior enableOfflineMode injectEnvironmentFiles"
" packageExtensions"
)
_BUN_IDENTITY_SETTINGS = frozenset({"preload", "run.preload"})
_BUN_CODE_SETTINGS = frozenset({"run.shell"})
_BUN_SOURCE_SETTINGS = frozenset({"install.ca", "install.cafile"})
_DENO_SOURCE_SETTINGS = frozenset({"importMap", "imports", "scopes"})
_RESOLVER_FILES = {
"npm": (".npmrc",),
"deno": (".npmrc",),
"deno": (".npmrc", "deno.json", "deno.jsonc"),
"bun": (".npmrc", "bunfig.toml"),
"yarn": (".yarnrc.yml", ".yarnrc", ".env.yarn"),
"uv-project": ("uv.toml", "pyproject.toml"),
"uv-tool": ("uv.toml", "pyproject.toml"),
}
# Setting names exported in clear: fixed vocabularies, never a name taken from input.
_KNOWN_SETTINGS = (
Expand All @@ -765,6 +770,10 @@ def _purl(package: Package) -> str | None:
| _NPM_CODE_SETTINGS
| _YARN_CODE_SETTINGS
| _YARN_SOURCE_SETTINGS
| _BUN_IDENTITY_SETTINGS
| _BUN_CODE_SETTINGS
| _BUN_SOURCE_SETTINGS
| _DENO_SOURCE_SETTINGS
| frozenset(_UV_SETTINGS)
| {"registry", "npmRegistryServer", "offline", "prefer-offline", "strict-ssl", "environment"}
)
Expand Down Expand Up @@ -974,6 +983,14 @@ def _file_setting(path: str, family: str, key: str, value: str, launched: set[st
"enableOfflineMode": "false",
}
return "inert" if relaxed.get(key) == value else "source"
if path == "bunfig.toml":
if key in _BUN_IDENTITY_SETTINGS:
return "identity"
if key in _BUN_CODE_SETTINGS:
return "code"
return "source" if key in _BUN_SOURCE_SETTINGS else "inert"
if path in {"deno.json", "deno.jsonc"}:
return "source" if key in _DENO_SOURCE_SETTINGS else "inert"
if path in {"uv.toml", "pyproject.toml"}:
return "source" if not key.startswith("sources.") or key[8:] in launched else "inert"
return "unresolved" if path == ".env.yarn" else "inert"
Expand Down Expand Up @@ -1135,14 +1152,67 @@ def _read(self, path: str) -> _ResolverFile:
if key in _YARN_CODE_SETTINGS or key in _YARN_SOURCE_SETTINGS
]
return _ResolverFile(tuple(entries))
if path in {"deno.json", "deno.jsonc"}:
text = self._text(path)
if text is None:
return _ResolverFile()
try:
data = _load_jsonc(text)
except (ValueError, RecursionError):
self._result.scan_failures.append(f"{path}: invalid JSON or duplicate keys")
return _ResolverFile()
if not isinstance(data, dict):
return _ResolverFile()
deno_entries: list[tuple[str, str]] = []
import_map = data.get("importMap")
if isinstance(import_map, str) and import_map:
deno_entries.append(("importMap", import_map))
imports = data.get("imports")
if isinstance(imports, dict):
for target in imports.values():
if isinstance(target, str):
deno_entries.append(("imports", target))
scopes = data.get("scopes")
if isinstance(scopes, dict) and scopes:
deno_entries.append(("scopes", json.dumps(scopes, sort_keys=True, default=str)))
return _ResolverFile(tuple(deno_entries))
settings = self._toml(path)
if path == "bunfig.toml":
install = settings.get("install")
return _ResolverFile(
tuple(
_registries(install if isinstance(install, dict) else {}, "registry", "scopes")
install_map = install if isinstance(install, dict) else {}
bun_entries = _registries(install_map, "registry", "scopes")
for ca_key in ("ca", "cafile"):
if ca_key in install_map and install_map[ca_key]:
bun_entries.append(
(
f"install.{ca_key}",
install_map[ca_key]
if isinstance(install_map[ca_key], str)
else json.dumps(install_map[ca_key], default=str),
)
)
if settings.get("preload"):
bun_entries.append(
(
"preload",
settings["preload"]
if isinstance(settings["preload"], str)
else json.dumps(settings["preload"], default=str),
)
)
)
run_cfg = settings.get("run")
if isinstance(run_cfg, dict):
for run_key in ("preload", "shell"):
if run_cfg.get(run_key):
bun_entries.append(
(
f"run.{run_key}",
run_cfg[run_key]
if isinstance(run_cfg[run_key], str)
else json.dumps(run_cfg[run_key], default=str),
)
)
return _ResolverFile(tuple(bun_entries))
if path == "pyproject.toml":
tool = settings.get("tool")
uv = tool.get("uv") if isinstance(tool, dict) else None
Expand Down Expand Up @@ -1860,18 +1930,19 @@ def _resolver_view(
view.names.append(label)
kind = _file_setting(path, family, key, value, launched)
if kind == "identity":
view.identity.append(
f"npm {key} setting from {path}"
if path == ".npmrc"
else f"{path} {key} replaces or extends yarn"
)
if path == ".npmrc":
view.identity.append(f"npm {key} setting from {path}")
elif path == "bunfig.toml":
view.identity.append(f"{path} {key} preloads local code")
else:
view.identity.append(f"{path} {key} replaces or extends yarn")
elif kind == "code":
view.code += 1
elif kind == "source":
view.sources.append((label, value))
elif kind == "unresolved":
view.unresolved.append(label)
if path in {"uv.toml", "pyproject.toml", ".env.yarn"}:
if path in {"uv.toml", "pyproject.toml", ".env.yarn", "deno.json", "deno.jsonc"}:
continue
default = "npmRegistryServer" if path == ".yarnrc.yml" else "registry"
scope_label, scope = (
Expand Down
48 changes: 40 additions & 8 deletions src/nexus_symbiote/scanners/mcp_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from __future__ import annotations

import fnmatch
import posixpath
import re
import shlex
from collections.abc import Callable, Iterator
Expand Down Expand Up @@ -1740,12 +1741,18 @@ def _program_reason(name: str, rest: list[str], launch: Launch) -> str | None:
return _embedded_reason(rest)


def _start(words: list[str], launch: Launch) -> None:
def _start(words: list[str], launch: Launch, *, package_entry: bool = False) -> None:
"""A program a runner starts by name: record it and check what it starts in turn."""
launch.executable = normalize_command(words[0])
family = _interpreter_family(launch.executable)
reason = _interpreter_reason(family, words[1:], launch) if family else None
# What the words start in turn is the more specific reason, so it is named first.
reason = reason or _embedded_reason(words)
if reason is None and package_entry:
if re.search(r"[\\/]", words[0]) or words[0].startswith("."):
reason = "runner command is a path, not a package entry point"
elif family is not None:
reason = f"runner starts {family} instead of a package entry point"
if reason is not None:
launch.fail(reason)

Expand Down Expand Up @@ -2181,7 +2188,7 @@ def _package_runner(
launch.packages.append(package)
return
if position < len(rest):
_start(rest[position:], launch)
_start(rest[position:], launch, package_entry=True)
launch.packages.extend(npm_spec(spec) for spec in specs)


Expand Down Expand Up @@ -2232,6 +2239,15 @@ def _npm(name: str, rest: list[str], launch: Launch) -> None:
return
if subcommand in {"exec", "x"}:
launch.launcher = "npm exec"
scanned = _scan(remainder, _NPM_EXEC)
if scanned is not None:
_, pos = scanned
if pos < len(remainder) and (pos == 0 or remainder[pos - 1] != "--"):
after = remainder[pos + 1 :]
unquoted = after[: after.index("--")] if "--" in after else after
if any(word.startswith("-") and word != "-" for word in unquoted):
launch.fail("npm exec options after the package without '--'")
return
_package_runner(remainder, launch, _NPM_EXEC, {"-p", "--package"}, options)
elif subcommand in _NPM_LOCAL:
launch.launcher = f"npm {subcommand}"
Expand Down Expand Up @@ -2342,7 +2358,7 @@ def _python_runner(
if declared is not None:
launch.packages.insert(0, python_spec(declared))
if position < len(rest):
_start(rest[position:], launch)
_start(rest[position:], launch, package_entry=True)
else:
package = python_spec(rest[position], shorthand=shorthand)
launch.packages.insert(0, package)
Expand Down Expand Up @@ -2492,12 +2508,18 @@ def _host_source(spec: str, *, mount: bool) -> str | None:
if fields.get("type", "volume") != "bind":
return None
return fields.get("source") or fields.get("src") or None
if ":" not in spec:
return None
source = spec.split(":", 1)[0]
if re.match(r"[A-Za-z]:[\\/]", spec):
if ":" not in spec[2:]:
return None
source = spec[:2] + spec[2:].split(":", 1)[0]
return (
source
if source.startswith(("/", "~", "$", "%", "\\")) or re.match(r"[A-Za-z]:", source)
if source in {".", ".."}
or source.startswith(("/", "./", "../", ".\\", "..\\", "~", "$", "%", "\\"))
or re.match(r"[A-Za-z]:", source)
else None
)

Expand Down Expand Up @@ -2525,12 +2547,22 @@ def _container_context(options: list[tuple[str, str | None]], launch: Launch) ->
if source is None:
continue
launch.mark("host_mounts")
normalized = source.rstrip("/\\") or "/"
if normalized in {"/", "\\"} or re.fullmatch(r"[A-Za-z]:[\\/]?", source):
normalized = source.rstrip("/\\") or ("/" if source.startswith("/") else "\\")
canonical = (
posixpath.normpath(normalized)
if normalized.startswith("/") and "\\" not in normalized
else normalized
)
if canonical in {"/", "\\"} or re.fullmatch(r"[A-Za-z]:[\\/]?", canonical):
launch.mark("host_root_mount")
elif source.lower().endswith(_SOCKETS):
elif canonical.lower().endswith(_SOCKETS):
launch.mark("runtime_socket_mount")
elif _BROAD.fullmatch(source):
elif (
_BROAD.fullmatch(canonical)
or canonical == ".."
or canonical.startswith(("../", "..\\"))
or posixpath.normpath(normalized.replace("\\", "/")).startswith("..")
):
launch.mark("broad_host_mount")
elif option == "--entrypoint":
launch.mark("entrypoint_override")
Expand Down
Loading
Loading