mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
2d75952476f2e489838e3dc4a5fa88b690ee5c43
279
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2d75952476 |
fix: close remaining review gaps in vocabulary/deterministic-IRI PR
serialize_to_rdfxml still defaulted entity_type to the bare string "semantica:Entity" written into an rdf:resource attribute, which isn't namespace-expanded the way a Turtle angle-bracket or XML element name is - the same #1101 failure mode, just on the path the original tests didn't cover. Now uses the full-IRI DEFAULT_ENTITY_TYPE like the Turtle path. json_exporter.py emits semantica:format and @type: "semantica:KnowledgeGraph", neither of which was declared in the vocabulary or included in EMITTED_TERMS, so the "undeclared terms fail the build" guarantee didn't actually cover them. Both are now declared with rdfs:label/comment and added to the guard set. MANIFEST.in didn't mirror the pyproject.toml package-data addition, so a source-distribution install could ship without the vocabulary file. The cross-process minting-stability test replaced the subprocess's entire environment with a POSIX-only PATH, breaking it on Windows and any host needing other inherited env vars; now overrides only PYTHONHASHSEED on top of the inherited environment. Also folds mint_entity_iri/mint_relationship_iri's hand-rolled hashlib.sha256(...).hexdigest() into the existing hash_data() helper this file already imports alongside. 229 export and ontology tests pass, including a new regression test for the RDF/XML default-type fix. Co-Authored-By: fabio-rovai <fabio@thetesseractacademy.com> |
||
|
|
49707729ad |
fix(security): address Qodo review findings on the disclosure-fix PR
- export_table_data() re-raises ValidationError instead of masking it as ProcessingError via the blanket except Exception. - _apply_connection_pin() restores the session's original Host header state on an unpinned hop instead of unconditionally clearing it, which was dropping a caller-supplied session's own Host override. - SQL fragment blocklist now masks quoted string/identifier literal contents before matching, so legitimate data containing a blocked keyword (e.g. status = 'union') no longer false-positives; a malformed/unterminated quote stays unmasked and still scrutinized. |
||
|
|
430020c7c4 |
docs(changelog): add Security entry for the disclosure fixes in this PR
Documents the tarball path traversal, latent SQLi, DNS-rebinding TOCTOU, stored XSS, and SPARQL injection fixes, plus the follow-up hardening found in review, under [Unreleased] > Security. |
||
|
|
04602a0e0e |
fix(security): prevent Authorization header leakage across redirects (#947) (#1067)
* fix(security): prevent auth header leakage across redirects * fix(security): harden redirect credential handling Address Copilot and Qodo review findings for #947. - Remove unused variables, imports, and unnecessary pass statements from tests. - Harden cross-origin redirect handling for per-request auth credentials. - Strip session-level auth handlers before cross-origin redirect hops. - Prevent session.auth from regenerating Authorization headers. - Disable trust_env during cross-origin hops to prevent .netrc credential injection. - Restore session auth and trust_env state reliably with try/finally. - Add regression coverage for auth=, session.auth, trust_env, and multi-hop redirects. - Preserve existing security behavior and same-origin authentication semantics. Validated with 189/189 security and affected tests passing. * fix(security): scope allow_private_ips to same-host redirects, fix error handling gaps Follow-up to review findings on #1067: - MCPClient hardcoded allow_private_ips=True for every redirect hop, not just its operator-configured host, so a compromised/malicious MCP server could 302 into private address space (e.g. cloud metadata) unchecked. request_with_ssrf_guard() gains allow_private_ips_on_redirect: a redirect target inherits the original host's private-IP trust only when it matches that host; MCPClient now pins it to False. - detect_public_api() only caught requests.exceptions.RequestException, but the SSRF guard raises ValidationError for blocked hosts/redirects, unlike its sibling ingest_public_api(). Now catches and re-raises it the same way. - detect_public_api()/ingest_public_api() forwarded session/allow_private_ips through **options into request_with_ssrf_guard(), which already passes both explicitly -- a caller supplying either would hit a duplicate-kwarg TypeError. Both are now popped from request_options first. New regression coverage for all three in tests/ingest/, plus a CHANGELOG entry under Unreleased/Security. --------- Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
a8194dfc60 |
fix(split): catch broken-runtime spaCy failures in SemanticChunker
SemanticChunker.__init__ only caught OSError around load_spacy_model(), while NERExtractor's identical call (fixed earlier in this PR) also catches generic Exception for a model that is installed but fails at runtime. Bring SemanticChunker in line so a broken spaCy config degrades to fallback chunking instead of crashing __init__. Adds a regression test mirroring the existing NERExtractor case, and a CHANGELOG entry for #998/#1042. |
||
|
|
d94d8f6ab8 |
Feat/crewai integration (#988)
* feat(crewai): add first-class CrewAI integration (#962) Add native CrewAI support so Crew agents can share a ContextGraph and AgentContext via BaseTool subclasses and a BaseKnowledgeSource, matching the existing agno integration pattern. - SemanticaKGTool: 5 KG actions (extract_entities, extract_relations, add_to_graph, query_graph, find_related) with sync run()/async arun() - SemanticaDecisionTool: 5 decision-intelligence actions (record_decision, find_precedents, trace_causal_chain, analyze_impact, check_policy) over AgentContext - SemanticaKnowledgeSource: serializes a ContextGraph into crew knowledge storage; bridges legacy load_content() and current validate_content()/aadd() contracts for crewai>=0.80.0 - All classes degrade gracefully when crewai is absent - New pip extra crewai=... included in the all bundle - 70 new tests (stub-based present-case + subprocess degradation path) - Docs: integrations/crewai.md, docs.json nav, README matrix updates * fix(crewai): harden tools against real Semantica dataclass shapes (#962) Bugs found during live testing with crewai 1.15.16: - SemanticaKGTool.add_to_graph crashed on real Entity/Relation dataclasses ('str' object has no attribute 'end_char'): string names were passed to extract_relations(entities=...), which requires Entity objects, and the tool read .name/.source/.target instead of Entity's .text/.label and Relation's .subject/.object. Add shape-agnostic field helpers. - SemanticaDecisionTool() created an AgentContext without a knowledge_graph, so _decision_backend was never set and record_decision raised 'Decision tracking is not enabled'. Wire in a ContextGraph. - record_decision hard-failed when the agent omitted optional fields; fall back to category='general', reasoning='agent decision', outcome='recorded'. Add tests covering real Entity/Relation dataclass shapes and the live auto-created AgentContext path (now 77 crewai tests, 212 total). * fix(crewai): make find_related traverse edges undirected (#962) ContextGraph.get_neighbors only follows outgoing edges, so a node whose only edge is incoming (A -> B) reported no related concepts. Rebuild a bidirectional adjacency from find_edges() in SemanticaKGTool._find_related so 'related' honors both directions. * fix(crewai): harden tools for checkpoint serialization and correct action semantics (#962) - Exclude live graph/context/extractor state from JSON serialization (model_dump(mode="json")) so CrewAI checkpointing no longer raises PydanticSerializationError; model_post_init self-heals defaults on restore - query_graph now searches node content via graph.query() plus id/type - trace_causal_chain returns an explicit error when causal tracing is unavailable instead of substituting similarity precedents; call trace_decision_causality(..., max_depth=...) with the correct kwarg name - find_precedents propagates max_precedents/limit to the backend instead of being silently capped at 10 - Serialize add_to_graph batches under a module lock to prevent concurrent double-counting; skip nameless entities instead of creating repr()-junk nodes - aadd() runs CPU-bound serialization in a thread executor - Mirror crewai args_schema serialize/restore in the conftest stub and add serialization regression tests (crewai: 92 tests) * fix(crewai): correct check_policy coercion, guard causal tracing, and harden concurrency (#962) - _eval_rule now coerces rule values type-aware: bool("false") was truthy, so 'enabled == false' reported a violation for enabled=false, and string datums like "0.90" were compared lexicographically instead of numerically - _trace_causal_chain no longer raises AttributeError (which escaped _run) when the decision context lacks knowledge_graph; returns honest error JSON - SemanticaKnowledgeSource storage failures log an actionable ERROR; without a configured crew embedder agents previously retrieved nothing silently - add_to_graph uses a per-graph re-entrant lock (WeakKeyDictionary) instead of a process-global one: independent graphs no longer serialize each other and re-entrant extractor callbacks cannot deadlock - entity/relation confidence=None normalizes to 1.0 instead of failing the whole extraction with float(None) - add subprocess integration test against real crewai covering Crew-level serialization round-trip and checkpoint restore (stub tests cannot see it) - docs: embedder requirement for SemanticaKnowledgeSource; resume contract note * fix(crewai): surface knowledge-source save failures at ERROR when storage is wired (#962) Re-verification against real crewai showed the embedder-missing failure raises ValueError even though storage IS wired, so the old except-ValueError branch mislabeled it as 'storage not wired' and logged DEBUG — hiding the failure. Distinguish by storage presence instead of exception type: storage is None -> DEBUG keep-in-memory (legitimate standalone use); storage wired but save() raises -> actionable ERROR. Add regression test mirroring real crewai's ValueError-on-missing-embedder behavior. * fix(crewai): expose run()/arun() entry points in degraded mode (#962) The public crewai contract is run()/arun(); without crewai installed they were missing (only the private _run existed), so the documented 'usable without crewai' path raised AttributeError at the entry point. Define them in degraded mode only, leaving crewai's BaseTool implementations untouched when present. Extend the degradation subprocess test to exercise run() and arun(). * fix(crewai): standardize query shape, field-name rules, and restore-state flag - _query_graph: id/type matches now return the same schema as content matches (id/type/label/content/score) instead of a bare list - _eval_rule: non-greedy field capture so hyphen/dot/space JSON keys (e.g. "risk-score >= 0.9") are addressable in policy rules - add had_live_state/reconstructed_state so checkpoint-restored tools and knowledge sources signal that their live graph/context was lost and an empty one reconstructed; knowledge source no longer hides the loss by eagerly rebuilding its graph inside __init__ (pydantic calls __init__ during model_validate) * fix(crewai): address Qodo review — confidence errors, string trim, holistic availability - record_decision: stop calling float() in _run, so malformed confidence values surface as JSON errors (via _record_decision's handling) instead of crashing the tool - _coerce_value: return the stripped string for non-numeric literals so whitespace-padded decision_data fields match policy rules - centralize crewai availability in _availability.py so the exported CREWAI_AVAILABLE flag is holistic across tools and knowledge source (previously each module probed crewai independently and the package flag came from decision_tool only) * ci: regenerate requirements-ci.txt for the crewai extra The crewai extra in pyproject.toml brings in crewai, crewai-tools and transitive deps (chromadb, lancedb, ...). Recompile with uv==0.12.1 per CONTRIBUTING.md so the CI staleness check passes. * ci: keep crewai out of the locked CI dependency set crewai (all versions) hard-requires chromadb~=1.1.0, which carries a pre-authentication code-injection advisory (CVE-2026-45829 / GHSA-f4j7-r4q5-qw2c) with NO fixed release — even the latest 1.5.9 is affected. Keeping crewai in the 'all' extra failed pip-audit and the safety check on requirements-ci.txt. - drop crewai from the 'all' aggregate (standalone semantica[crewai] extra is unchanged and still installs crewai) - stop listing crewai-tools in the extra: the integration only uses crewai core (BaseTool, BaseKnowledgeSource) and crewai-tools pulled extra transitive deps - regenerate requirements-ci.txt: OSV/pip-audit 0 vulnerabilities, safety 0 vulnerabilities, staleness check matches * docs(crewai): document crewai extra scope and chromadb CVE-2026-45829 - CHANGELOG: extra is crewai>=0.80.0 only (no crewai-tools) and is not part of the 'all' bundle, with the chromadb CVE-2026-45829 reason - integrations/crewai/README.md: add a security warning that installing the extra pulls chromadb~=1.1.0, which is affected by the unpatched pre-auth code-injection CVE-2026-45829 --------- |
||
|
|
5579851208 |
fix(export): harden YAML export input handling (#958)
* refactor(export): centralize graph-payload key normalization
Graph payloads circulate under two vocabularies, entities/relationships and nodes/edges, and consumers each reconciled them locally with competing idioms. The same payload could be exported, silently dropped, or rejected depending on which consumer read it.
Add normalize_graph_payload() to utils.helpers as the single place that decision is made. Both spellings present with one empty resolves to the populated one, which is the shape JSONExporter emits; both non-empty and different is refused, since there is no basis to prefer either and picking one would silently discard the other; a non-empty mapping with no recognized key raises rather than returning empty collections, with require_recognized=False for callers that should degrade.
Adopt it in the three exporters that genuinely alias. LPGExporter read nodes with entities as the default, so it dropped every entity when nodes was present but empty, losing everything on a JSON round-trip. ArangoAQLExporter had the same idiom plus a manual fallback. Neo4jCSVExporter routes its mapping branch through the shared resolver so the reference implementation cannot drift; its attribute branch stays local, since objects are not mappings.
Also feed LPGExporter._generate_indexes the resolved entities. It read entities directly, so a nodes/edges payload produced no indexes even once node generation was fixed.
CSVExporter and JSONExporter are deliberately excluded: they write entities, relationships, nodes and edges as separate outputs by design rather than reconciling two spellings of one collection, so normalizing there would rename output files.
* fix(export): reject non-mapping input to the YAML exporters
export_yaml declared Union[Dict[str, Any], List[Dict[str, Any]]], but both
YAML exporters read their payload by key, so a list reached .get() and
surfaced as a bare AttributeError from inside the exporter, naming neither
the offending argument nor the shape expected.
Reject rather than wrap. These formats distinguish entities from
relationships from triplets, so inferring which collection a bare list
represents would silently mislabel the records, and wrapping it under an
unrecognised key would write a structurally valid file with every
collection empty - trading a loud failure for silent data loss.
Validate in the exporters, matching the existing precedent in
Neo4jCSVExporter._normalize_graph, so direct users of the classes get the
same contract as callers of the convenience wrapper. Narrow the wrapper
type hint to Dict[str, Any] to match.
* fix(export): address YAML exporter review findings
- semantica/export/yaml_exporter.py — import Sequence from typing
instead of collections.abc. `Sequence[str]` in _require_mapping's
annotation is evaluated at function-definition time; collections.abc.Sequence
only became subscriptable in Python 3.9, so on the 3.8 this project
declares support for, importing this module raised TypeError.
typing.Sequence has supported subscripting since 3.5.3. Mapping stays
imported from collections.abc since it's only used for isinstance.
- tests/export/test_yaml_exporter_input_validation.py — clean up each
test's tempfile.mkdtemp() dir via addCleanup instead of leaking it,
and read exported YAML through a context manager instead of an
unclosed yaml.safe_load(open(...)).
* fix(export): reject YAML export payloads with no recognized key
Both YAML exporters built their output from a fixed set of `.get(key, [])`
lookups, so a mapping keyed by anything else serialized to a structurally
valid file with every collection empty. Nothing signalled the loss: no
exception, no warning, and the progress log reported a completed export.
The only way to notice was to open the file. The realistic trigger is
re-exporting an `export_json` payload, whose `{"data", "count", "metadata"}`
envelope drops every record.
- SemanticNetworkYAMLExporter.export_semantic_network now resolves its
collections through normalize_graph_payload(), which raises rather than
returning empty collections for an unrecognized mapping. Adopting the
shared resolver rather than repeating the check locally also brings the
'nodes'/'edges' aliases, so ContextGraph.to_dict() — the most direct path
from this library's own graph type to YAML, used in
examples/capability_gap_context_graphs_example.py — exports its records
instead of an empty file.
- export_for_pipeline built its nested semantic network from the same
defaulted lookups and had the same defect; it goes through the resolver
too.
- YAMLSchemaExporter.export_ontology_schema gets the equivalent check over
its own key set. Schemas are a separate vocabulary with no aliasing, so
_require_recognized_keys lives in this module rather than in the shared
graph resolver.
- 'metadata' is deliberately not sufficient to make a payload recognized.
An export_json envelope carries one, so accepting it would readmit the
case this fix is most likely to be needed for.
- An empty mapping is still exported: an empty graph is legitimate and has
no records to lose.
- SemanticNetworkYAMLExporter.export() serializes before creating the
output directory, so a rejected export leaves nothing behind.
The two rejections keep distinct exception types, following what the
codebase already does: a payload of the wrong *type* cannot be exported at
all and raises ProcessingError, matching Neo4jCSVExporter._normalize_graph;
a mapping whose *contents* are unusable raises ValidationError, matching
normalize_graph_payload. _require_mapping therefore runs first at every
entry point, so a non-mapping never reaches the resolver.
Docstring Raises sections, export_usage.md and docs/reference/export.md
record the accepted input shapes and both failures.
Closes #953.
* fix(export): reject payloads whose records resolve to nothing
Addresses the Qodo findings on #958.
Presence-only recognition (finding 1): checking that a recognized key is
present answered "did the caller use our vocabulary" when the question that
matters is "did anything the caller supplied survive". A payload like
{"entities": [], "data": [...records...]} cleared the check, resolved to
empty, and dropped every record under 'data' -- the silent-empty export by a
narrower route.
- utils/helpers.py — split the check in two. _require_recognized_keys keeps
the presence rule; _require_nothing_dropped runs after resolution and
refuses a payload that resolved to nothing while an unread key still holds
records. Only a non-empty list counts as evidence: ContextGraph.to_dict()
always carries a populated 'statistics' dict, and an empty graph must stay
exportable, so 'metadata', 'statistics' and 'count' are named as context
rather than records.
- export/yaml_exporter.py — the schema path had the same hole and now runs
both checks through the shared helpers rather than its own copy, so the
two vocabularies cannot drift apart in what counts as a silent-empty
export.
Progress reported success on a failed write (finding 3): export_semantic_
network stops its tracking as completed once serialization returns, but
export() then creates the directory and writes the file. A failure there
left the tracker showing a completed export with no output.
- export/yaml_exporter.py — the serialization span now says it serialized,
not that it exported, and export() opens its own span around the
filesystem work that stops as failed on error. Nothing reports a completed
export until the bytes are on disk.
Finding 2 (export_yaml no longer accepts List[Dict]) is the intended
resolution of #952 rather than a regression: wrapping a bare list under a
guessed key is what would mislabel the records. The signature, docstring and
PR description already record the narrowed contract.
Tests cover both directions of each fix, including that an empty
ContextGraph still exports and that a failing write is not reported as
completed.
* fix(export): validate collection values and make Neo4j mappings strict
Two gaps at the boundary the shared normalizer is supposed to own.
_resolve_collection() resolved on truthiness alone, so a recognized key
could still hold something that is not a collection of records:
{"entities": "abc"} normalized to three single-character "records", and
{"entities": 42} surfaced as a raw TypeError from list() inside whichever
exporter happened to read it, naming the exporter rather than the payload
key at fault. Collection values are now validated before conversion --
strings, bytes, mappings, and non-iterable scalars are rejected by key
name, and each element must be a mapping or an attribute-carrying object,
the two record shapes the exporters actually read. None stays legal as an
absent collection, the spelling a JSON round-trip produces for []; it
cannot hide dropped records, since _require_nothing_dropped() still runs.
Every spelling present is validated, not just the one that wins, so a
malformed alias is not excused by a well-formed canonical key.
Neo4jCSVExporter._normalize_graph() opted out of the recognized-key check
for mappings, which left it able to turn {"data": [...]} into header-only
CSVs indistinguishable from a genuinely empty graph -- the exact failure
the rest of the change exists to prevent. Mapping payloads now go through
normalize_graph_payload() on its default terms. The attribute path for
graph objects is untouched. With no caller left opting out, the
require_recognized flag is removed rather than kept as a way back into
the silent-empty export.
Regression tests cover the malformed values end to end through every
export path that reads the normalizer, and assert the rejected Neo4j
export writes no CSV files.
* fix(export): close YAML schema and record validation gaps
Fix 1 -- _require_usable_schema silent data loss (P1):
_require_usable_schema() passed all values from _SCHEMA_KEYS into
_require_nothing_dropped() as evidence that records survived. Scalar
metadata fields such as version='1.0' and uri='http://...' are truthy
strings, so any one of them caused _require_nothing_dropped() to return
early and silently discard records stored under an unread key alongside
them (e.g. {'version': '1.0', 'nodes': [{'id': 'c1'}]}). Fixed by
building the resolved list from only non-empty list/tuple values of
recognised schema keys.
Fix 2 -- _is_record accepts modules and type objects (P2):
_is_record() accepted any object with __dict__, which includes Python
modules and class objects. Elements that passed _coerce_records then
reached exporters and raised AttributeError (e.g. module 'math' has no
attribute 'get') rather than a ValidationError at the validation
boundary. Fixed by excluding types.ModuleType and type from the
__dict__ branch while preserving support for all user-defined
attribute-bearing record objects.
Tests: 101 tests pass across
tests/utils/test_normalize_graph_payload.py
tests/export/test_yaml_exporter_key_recognition.py
tests/export/test_yaml_exporter_input_validation.py
tests/export/test_neo4j_csv_exporter.py
* fix(export): close exception-type and record-shape gaps in normalize_graph_payload
LPGExporter and ArangoAQLExporter called normalize_graph_payload() with no
type guard, so non-mapping input raised ValidationError from inside the
resolver while the YAML and Neo4j exporters raised ProcessingError for the
identical mistake -- inconsistent with the exception-type contract this PR
establishes. Both now use the shared _require_mapping() guard (moved from
yaml_exporter.py into utils/helpers.py so all three can use it).
Neo4jCSVExporter._normalize_graph checked isinstance(graph, dict), so a
non-dict Mapping (MappingProxyType, ChainMap) fell through to the
object-attribute branch and was rejected, even though the identical payload
exported fine via the other three exporters. Now checks isinstance(graph,
Mapping).
normalize_graph_payload() accepts dataclass/attribute-bearing object
records, but LPGExporter/ArangoAQLExporter call .get(...) directly on
resolved entities -- an object-shaped record passed validation only to
crash with a raw AttributeError once used, the exact failure this
boundary exists to prevent. Records are now converted to plain dicts at
the boundary (_coerce_records -> new _record_to_dict), so every consumer
gets a uniform shape regardless of which reading the caller used.
Two non-empty spellings of the same collection holding identical records
in a different order were rejected as conflicting, since the check used
plain list equality. Comparison is now an order-independent multiset of
each record's canonical JSON form.
* docs(changelog): add entry for #958 YAML export input hardening
Documents the full arc of #958 -- the normalize_graph_payload()
centralization, YAML input validation, both review rounds from
@Sameer6305, and the exception-type/record-shape follow-up fixes -- plus
closes #956, #952, #953.
---------
Co-authored-by: Pravit Ampapathini <pravitampapathini@Pravits-MacBook-Air-3.local>
Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com>
|
||
|
|
f1e7e64ad1 |
feat(context): add retraction and purge to ContextGraph (#957)
* feat(context): add retraction and purge to ContextGraph ContextGraph had 56 public methods and none that removed anything: the only option was clear(), which discards the whole graph. Removing one entity meant exporting to a dict, filtering by hand and rebuilding, losing provenance. Add two operations with deliberately different contracts. retract_node/retract_edge close the entity's validity window. The entity stops being active going forward, but state_at() before the retraction still returns it, so decisions recorded against it remain explainable. This reuses the valid_from/valid_until machinery already present rather than adding a new subsystem. purge_node/purge_edge remove the entity outright, from history as well as from the active view, leaving a tombstone that records that a purge happened and why but never the purged content. Scope is this graph only; copies in AgentMemory or a bound vector store are not reached, so it is one step of an erasure workflow rather than the whole of it. Both record themselves through the existing mutation_callback path. MutationRecord already documented REMOVE_NODE/REMOVE_EDGE in its operation vocabulary, so retraction emits UPDATE_NODE and purge emits REMOVE_NODE with no changes required to change_management. Incident-edge lookup scans self.edges rather than _adjacency, which is keyed by source only and would otherwise leave inbound edges pointing at a removed node. Purge updates edges, edge_type_index and _adjacency together so the indexes cannot drift, and clear() now resets the retraction and tombstone records. * fix(context): address review findings on retraction and purge * fix(context): close every duplicate when retracting/purging by edge_id edge_id is content-derived and not yet guaranteed unique (#922, fix pending in #926): two identical add_edge() calls produce two edge objects sharing one id. retract_edge()/purge_edge() resolved "the edge" via the first matching object only, so a duplicate was silently left untouched (still live, still active) while the call returned True and recorded a tombstone/retraction claiming it was fully handled. Repeat purge_edge() calls also silently overwrote the tombstone's reason/purged_at on each partial attempt instead of no-op'ing once nothing remained to purge. retract_node()'s cascade had the same root cause from the other direction: it checked the live _retractions dict mid-loop, so the first duplicate's just-written record made the second look already handled and it was skipped outright, left permanently active. retract_edge()/purge_edge() now act on every edge matching the id under a single record; the cascade's dedup check is snapshotted before the loop starts so within-call duplicates are still closed rather than skipped. Adds TestDuplicateEdgeId (5 tests) reproducing all three paths. * docs(changelog): document retraction/purge feature Adds an Unreleased/Added entry for #955/#957 covering retract_node, retract_edge, purge_node, purge_edge and the get/list accessors, plus the duplicate-edge_id fix caught and applied during review. --------- Co-authored-by: Pravit Ampapathini <pravitampapathini@Pravits-MacBook-Air-3.local> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
84ce3c5155 |
fix(context): make ContextGraph.add_edge idempotent by deduping on edge_id (#926)
* fix(context): make ContextGraph.add_edge idempotent by deduping on edge_id (#922) * docs(changelog): document add_edge dedupe fix Adds an Unreleased/Fixed entry for #922/#926 so the ContextGraph edge-dedupe bug and its fix are recorded per Keep a Changelog format. --------- Co-authored-by: Pravit Ampapathini <pravitampapathini@Pravits-MacBook-Air-3.local> Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
557e29ee14 |
fix(explorer): repair /api/enrich/extract (always 503) and the /api/decisions routes (always 500) (#886)
* fix(explorer): repair /api/enrich/extract and the /api/decisions routes Two Explorer API endpoints fail on every install. /api/enrich/extract imported extract_entities and extract_relations from semantic_extract.methods, where neither name is defined — that module ships only the per-strategy variants (extract_entities_ml, extract_relations_regex, ...), and nothing re-exports a plain facade. The resulting ImportError was caught and reported as "semantic_extract module not available. Ensure spacy and transformers are installed.", so a wiring bug looked like a missing dependency. The route now calls NamedEntityRecognizer and RelationExtractor directly, the classes the README documents, and feeds the extracted entities into relation extraction rather than re-deriving them. The 503 branch stays for a genuinely absent module. Every /api/decisions* route returned 500 once the graph held a decision: record_decision() stores timestamp as datetime.now().timestamp(), a float, while DecisionResponse types the field as str, so pydantic rejected the value the library itself wrote. A before-mode field validator on DecisionResponse normalizes float, int and datetime inputs to ISO-8601, covering every route that builds the model instead of only the list endpoint. The existing tests missed both: test_extract accepted 503 as a pass, and the decision fixtures are hand-built nodes carrying no timestamp at all. Both are tightened, and a TestRecordedDecisions class exercises the routes against decisions created through record_decision(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(semantic_extract): cache spaCy models instead of loading one per call extract_entities_ml(), extract_relations_similarity() and extract_relations_dependency() called spacy.load() on every invocation, so the model was re-read from disk and re-initialized per call. On a short sentence that is ~120 ms of loading around ~2 ms of work, and successive calls never got cheaper. The path is reachable from the CLI, the MCP extract_entities tool, the pipeline ner_extract step and POST /api/enrich/extract, and process_batch() multiplies it by the number of documents. The module already had a cached loader for one code path — get_nlp_model() and its _nlp_cache global — but the extraction functions bypassed it. Adds load_spacy_model(), a process-level cache keyed by model name behind a lock so concurrent callers do not each start a load, and routes the five call sites through it. Errors are left uncached and propagate unchanged, so the existing OSError fallbacks to pattern extraction still fire. get_nlp_model() keeps its own entry: it loads with disable=["parser", "ner", "lemmatizer"] for similarity work, so its model is not interchangeable with the NER one. Cache entries record the spacy module object they came from. Several tests patch methods.spacy with a mock and assert on load calls; without that guard a name-keyed cache would hand a previous test's mock to a later one. Measured on the same sentence, Python 3.12.13 / spacy 3.8.15 / en_core_web_sm: extract_entities_ml() median 132 ms before, 2.1 ms after, identical entities. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(explorer): harden extraction and timestamp handling * fix(explorer): catch OverflowError/OSError in decision timestamp validator DecisionResponse._normalize_timestamp only guarded against NaN/inf via math.isfinite(), but datetime.fromtimestamp() raises OverflowError or OSError for finite epoch values outside the platform's representable range (e.g. milliseconds stored where seconds were expected). Those exceptions escaped the pydantic validator unhandled, reintroducing an unhandled 500 on /api/decisions* for exactly the bug class this PR closes. Also exclude bool from the numeric branch, since bool is an int subclass and was being silently coerced to epoch 0/1. * docs: add changelog entry for PR #886 (explorer extract/decisions fixes) Documents the extraction 503, decisions timestamp 500, and folded-in spaCy caching fixes, plus the review-round hardening from Sameer6305 and the timestamp overflow/bool fix from this follow-up commit. --------- Co-authored-by: joseedson18jc <joseedson18jc@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Sameer Kadam <sskadam6305@gmail.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> |
||
|
|
75f88b1c40 |
Fix/explorer backend failure states (#980)
* fix(explorer): show a retryable error when the graph fails to load The dependency-pre-bundle overlay had no failure path: on a fetch error it kept rendering the last progress frame forever with no retry. Route isError/error out of the load query, surface a real error card with the underlying message, and let retry re-fetch without a full page reload. * fix(explorer): reflect real backend connectivity on the landing page The status dot and 'System Online' text were static, so a dead backend still looked healthy. Track checking/online/offline explicitly and drive both off the same state so they can't disagree. * feat(explorer): let search results be dismissed, round relevance scores The results strip had no close affordance and stayed pinned until the next search. Add a header row with a dismiss button, and round scores to whole numbers instead of showing three decimals of a raw relevance value nobody can act on. * feat(explorer): add typeahead suggestions to graph search Typing in the search box now debounces a query against the existing search endpoint and shows a combobox dropdown, with arrow-key navigation, Enter/click to jump straight to a node, and Escape to dismiss. Previously nothing happened until the full form was submitted. * fix(explorer): abort stale typeahead requests and clear suggestions on error Clearing the search box while a suggestion fetch was in flight never aborted it, so a late response could reopen the dropdown with results for a query that was no longer typed. A non-OK response also left whatever suggestions were already on screen untouched instead of clearing them. Abort on every effect cleanup (not just unmount) and clear suggestions on any non-abort failure. * docs(changelog): add entry for Explorer backend failure states fix Documents the (#980, closes #977) fix in the Unreleased/Fixed section. --------- Co-authored-by: Sameer Kadam <sskadam6305@gmail.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
1c0cebb1c3 |
security(context): harden Markdown import against TOCTOU symlink races (#932)
* security(context): harden Markdown import against TOCTOU symlink races Closes #856 * fix(context): harden markdown import security tests * docs(changelog): add entry for Markdown import TOCTOU symlink hardening Documents the (#932, closes #856) fix in the Unreleased/Fixed section. --------- Co-authored-by: Sameer Kadam <sskadam6305@gmail.com> Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
611874e63e |
security: apply SSRF guard to feed ingestion requests (#928)
* security: apply SSRF guard to feed ingestion requests FeedIngestor and FeedMonitor fetched feed and website URLs with plain requests.get/head calls, bypassing the SSRF validation already used by web_ingestor.py and api_ingestor.py. This allowed feed URLs pointing at loopback, link-local, or other private network addresses to be fetched directly. Route all outbound requests in feed_ingestor.py through request_with_ssrf_guard, gated by the same allow_private_ips config option the other ingestors expose. * test: mock the correct request boundary in test_discover_feeds_empty The test still patched requests.get after discover_feeds() moved to request_with_ssrf_guard(), which calls requests.request and performs real DNS resolution. That left the test hitting live network/DNS. * docs(changelog): document FeedIngestor SSRF guard fix (#928, closes #927) Records the SSRF guard applied to all 5 feed-ingestion request sites, the Qodo-flagged test-mock fix, independent PoC verification, and the carried-over exception-swallowing behavior in discover_feeds(). --------- Co-authored-by: Sameer Kadam <sskadam6305@gmail.com> Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
43bac6170c |
fix(vector_store): make VectorManager methods work on persistent backends (#855) (#914)
* fix(vector_store): make VectorManager methods work on persistent backends (#855) maintain_store() and collect_statistics() reached into VectorStore internals (.vectors/.metadata), which only exist for the inmemory backend — any persistent backend (FAISS, Qdrant, Pinecone, Milvus, ...) crashed with AttributeError. Add a public backend-agnostic VectorStore.count() accessor following the get_vector()/get_metadata() precedent (#843) and the NotImplementedError-on-unsupported-capability precedent of _filter_by_metadata() (#848): inmemory counts its dict, persistent backends delegate to count() when available, and raise NotImplementedError otherwise. VectorManager methods now go through count(); maintain_store() keeps the exact inmemory semantics (separate vector/metadata dict counts) and reports a 1:1 count for persistent backends, where metadata is stored alongside each vector. Tests: 10 hermetic unit tests covering inmemory, delegation and the NotImplementedError path. Core vector_store suite: 40 passed. * fix(vector_store): raise NotImplementedError when count() unavailable Address Qodo review findings on #914: - Persistent backend with no wrapped store no longer silently returns 0 (which masked a missing initialization as an empty, healthy store); it now raises NotImplementedError like get_vector()/get_metadata(). - A mis-shaped adapter exposing a non-callable 'count' attribute now surfaces a clean NotImplementedError instead of a TypeError, via a getattr + callable() capability check. Adds regression tests for both cases. * fix(vector_store): implement count() on FAISS/SQLite/PgVector backends (#914) - FAISSStore.count(): returns len(index.vector_ids); 0 when no index exists yet - SQLiteVecStore.count(): delegates to get_stats()[vector_count] (SELECT COUNT(*)) - PgVectorStore.count(): delegates to get_stats()[vector_count] (SELECT COUNT(*)) - VectorStore.count(): fix misleading NotImplementedError message; now describes how to add count() support to a backend adapter rather than claiming only the inmemory backend can ever support counting - VectorManager.maintain_store(): split inmemory and persistent paths: * inmemory: independently reads len(vectors) and len(metadata) and compares them as an integrity check (original semantics preserved) * persistent: calls store.count(); returns metadata_count=None because metadata is co-located with vectors in the backend and cannot be counted independently; never manufactures metadata_count=vector_count as a vacuous tautology (#914 Qodo review) - Tests: rewrite test_vector_manager_persistent.py with 31 tests covering dispatch logic, inmemory divergence detection, persistent metadata_count=None invariant, FAISSStore/PgVectorStore via mocks, and SQLiteVecStore via real in-memory SQLite (skipped when sqlite-vec absent) * docs(changelog): document VectorManager persistent-backend count fix (#914, closes #855) Records the VectorStore.count() accessor, the FAISS/SQLite/PgVector implementations added during review, and the maintain_store() metadata_count fix (no longer fabricates equality for persistent backends). --------- Co-authored-by: Sameer6305 <sskadam6305@gmail.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> |
||
|
|
91d02a0f29 |
fix(ingest): harden RepoIngestor GitPython clone surface (#868) (#905)
* fix(ingest): harden RepoIngestor against GitPython URL and option injection
Bump GitPython to >=3.1.58, allowlist clone kwargs, and validate repo URLs
before clone_from to close env-var exfiltration and option-injection paths.
* fix(ingest): accept scp-like SSH remotes in RepoIngestor URL validation
* fix(ingest): resolve repo hostnames to block SSRF via private IPs
* fix(ingest): map malformed repo URL parse errors to ValidationError
* fix(ingest): bound and prune repo host resolve cache
Cap the repository host DNS cache, prune expired entries on access, and evict the oldest entries so long-running processes cannot accumulate unbounded host lookups from user-supplied repo URLs.
* fix(ingest): cap host resolve cache and tighten env-var token checks
Bound the repo host DNS cache with pruning and oldest-entry eviction, and narrow URL env-var blocking to actual $VAR/${VAR} tokens so literal dollar signs are not rejected.
* fix(ingest): preserve repo path compatibility and NAT64 support
* docs(changelog): document RepoIngestor GitPython hardening (#905, closes #868)
Records the clone-surface hardening (GitPython floor, clone-option
allowlist, URL/SSRF validation), the two fixes made during review
(NAT64 false-positive, local-path regression), and a known residual
gap: the SSRF host check doesn't classify RFC 6598 CGNAT space
(100.64.0.0/10) as blocked since ipaddress.is_private doesn't cover it.
---------
Co-authored-by: Pravit Ampapathini <pravitampapathini@wifi-10-43-175-99.wifi.berkeley.edu>
Co-authored-by: Pravit Ampapathini <pravitampapathini@Pravits-MacBook-Air-3.local>
Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com>
|
||
|
|
2cfb5de43d |
feat(export): add opt-in metric_errors column to DistanceExporter (#960)
* feat(export): add opt-in metric_errors column to DistanceExporter
Add a 'metric_errors' field to compute_pairs() output that lets
downstream consumers programmatically distinguish legitimate 'no path'
(None) from computation failures (None + error name).
Usage:
rows = exporter.compute_pairs(include=[..., 'metric_errors'])
# row['metric_errors'] == '' → all metrics succeeded
# row['metric_errors'] == 'hop_count,weighted_distance' → those failed
Design decisions:
- Opt-in: column only appears when explicitly requested via include=
- Default export schema unchanged (backward compatible)
- Comma-separated metric names (not exception messages) — stable for
programmatic filtering without exposing internal error details
- Helpers now return (value, error_name | None) tuples internally
Follow-up to #879, as discussed in its review thread.
* fix: address Qodo findings — track betweenness errors and remove unused constant
1. _betweenness() now returns (dict, error) tuple like the other helpers,
so betweenness computation failures appear in metric_errors.
2. Removed unused _ERROR_COLUMNS constant (dead code).
All 77 tests in tests/export/ pass.
* docs(changelog): add entry for opt-in metric_errors column (#960)
---------
Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com>
Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com>
|
||
|
|
0fa3483b96 |
fix(context): clarify get_node_property not-found contract (#877) (#882)
* fix(context): clarify get_node_property not-found contract (#877) Add default= param to get_node_property and get_node_attributes so callers can distinguish node-missing from property-missing using a sentinel. Fix add_node_attribute calling mutation_callback outside the lock. Tests added for all cases. * fix(context): address Qodo review findings (#877) * fix(context): wrap add_node_attribute mutation_callback in try/except (#877) The PR claimed to move the callback back inside `with self._lock`, but the diff only dropped a stray blank line -- the call stayed outside the lock, unchanged. That's actually correct: self._lock is an RLock, and _add_internal_node/_add_internal_edge deliberately release the lock before invoking the callback too, so a slow/misbehaving callback never holds up other threads. The real gap was that, unlike those two siblings, this call site didn't catch exceptions from the callback. Wrapped it the same way, with a regression test. --------- Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
18f1d55d77 |
test(normalize): make optional tests deterministic (#881)
* test(normalize): make optional tests deterministic Signed-off-by: aoright <102943475+aoright@users.noreply.github.com> * docs(changelog): add entry for #881 / #860 normalize test determinism fixes --------- Signed-off-by: aoright <102943475+aoright@users.noreply.github.com> Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
b0080c3602 |
fix(export): log DistanceExporter metric computation failures instead of swallowing them (#879)
* fix(export): log DistanceExporter metric computation failures instead of swallowing them
The four private metric helpers in DistanceExporter (_betweenness,
_hop_distance, _weighted_distance, _semantic_similarity) each catch a bare
Exception and return None/{} with no signal. That makes an exported None
indistinguishable from a legitimate "no path exists" result, corrupting
downstream CSV/JSONL/DataFrame exports with no way to tell a real gap from a
swallowed error.
Log each caught exception at warning level with the offending source/target
before returning the existing sentinel. The exported row shape and values are
unchanged; only the observability of the failure changes.
Fixes #874
* fix(export): route DistanceExporter warnings through the semantica logger tree
get_logger(__name__) doubled the semantica. prefix (__name__ is already
semantica.export.distance_exporter), so the warnings this PR adds landed on
semantica.semantica.export.distance_exporter, a branch setup_logging() never
configures and does not reach the app's log handler. Also reworded the three
except-Exception log messages: they said "recording as no path", which
overclaims what a generic exception means.
Addresses review feedback from @KaifAhmad1 on #879.
* docs(changelog): add DistanceExporter logging fix entry
---------
Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com>
Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com>
|
||
|
|
1ee3f2f214 |
fix(kg): align GraphBuilder raw-text extraction defaults with the documented contract (#941)
* fix(kg): align GraphBuilder raw-text extraction defaults with the documented contract
_extract_from_text() defaulted ner_method, relation_method and
triplet_method to "llm" and ran relation extraction unconditionally,
contradicting the build() docstring ("ml"/"pattern"/False) and the
standalone extractor defaults. Any raw-text build() therefore required a
provider, an API key, and network access without saying so.
Defaults are now ml/pattern/pattern with extract_relations=False. LLM
extraction is unchanged and now opt-in via explicit kwargs.
Also documents relation_method and extract_triplets, which the docstring
never listed, and drops the stale "Default to LLM methods as per
requirement" comment.
Closes #930
* perf(kg): reuse extractors across texts instead of rebuilding per source
Addresses review feedback on #941. NERExtractor.__init__ loads its spaCy
model eagerly when the method includes "ml", so switching the default
from "llm" to "ml" made _extract_from_text() reload the model once per
source in a multi-document build.
Extractors are now cached per (kind, method) on the builder. Adds tests
asserting single construction across repeated texts, that distinct
methods still get distinct extractors, and that the default path runs
end to end without any provider call.
* fix(kg): keep fallback method lists working with the extractor cache
The extractor cache keyed directly on `method`, but all three extractors
accept a list for fallback ordering (e.g. ner_method=["pattern", "ml"]),
so a list argument raised TypeError: unhashable type: 'list' before
extraction started. Lists are now converted to tuples for the cache key
only; the extractor still receives the original value.
Also seeds _extraction_stats in __init__. It was previously created only
in build(), so calling _extract_from_text() directly — as the report's
repro does — raised an AttributeError that the broad except swallowed and
logged as "Entity extraction failed".
Adds coverage for list methods on all three extractors, cache reuse for
equal lists, and distinct entries for different orderings.
* fix(kg): forward extracted relations into triplet extraction
_extract_from_text() passed only entities= to extract_triplets(), so
TripletExtractor re-derived relations itself whenever relations is None,
using a method taken from triplet_method rather than relation_method.
That duplicated work and could yield triplets inconsistent with the
relations already extracted.
relations is now initialized to None, holds the extracted list when
extract_relations=True succeeds, and is forwarded to extract_triplets().
When extraction is disabled or fails, None is passed and
TripletExtractor's existing self-derivation is unchanged.
Folded in at maintainer request rather than tracked as #944.
* docs(changelog): note that #878 documented the LLM defaults before this landed
#878 merged while this was in review and resolved the same code/docstring
mismatch in the opposite direction. Records that #930's decision makes
the code the side that changes, and that #878's docstring formatting is
retained.
|
||
|
|
1a3dd5038a |
docs(kg): document GraphBuilder public methods (#878)
* docs(kg): document GraphBuilder public methods * test(kg): skip module-level doctest to fix suite run * docs(kg): restore GraphBuilder option documentation * docs(kg): document default values for build() extraction options extract_relations, extract_triplets, ner_method, relation_method, and triplet_method all have concrete defaults in _extract_from_text(), but the build() docstring only stated a default for extract, inconsistent with CONTRIBUTING.md's docstring convention of noting parameter defaults. * docs: add changelog entry for GraphBuilder docstrings (#878, #876) --------- Co-authored-by: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com> Co-authored-by: Sameer Kadam <sskadam6305@gmail.com> Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
c1154b6ed6 |
fix(security): header injection, link-prediction DoS, import ID sanitization (#912)
* fix(security): sanitize node_id in Content-Disposition to prevent header injection (CWE-113) * fix(security): cap link prediction at 10k nodes with semaphore to prevent OOM DoS (CWE-770) * fix(security): sanitize imported node IDs to prevent stored header injection chain (CWE-20) * test(security): add self-contained PoC runner with real measured output * test(security): add regression tests for header injection, DoS cap, import sanitization * fix(security): comprehensive fix for header injection, DoS, and import ID sanitization * fix: move semaphore to wrap entire data-load+scoring region, use node-specific edge queries (Qodo #2, #3) * fix: sanitize edge source/target IDs to match sanitized node IDs (Qodo #4) * fix: scope 999_999 check to predict_links function via AST (Qodo #1) * fix: add explicit None guard to _sanitize_import_node_id * fix(security): close import-sanitizer bypass, enforce link-prediction cap before the expensive scan Follow-up to the fixes in this PR, found in review: - export_import.py's "properties" in raw_node fast path stored the id verbatim, completely skipping _sanitize_import_node_id() -- a node payload of {"id": "<crlf>", "properties": {}} (the shape this app's own /api/export produces) bypassed the VULN-3 fix entirely. That branch now sanitizes id before storing. - The link-prediction 10k-node cap checked `total` only after calling session.get_nodes()/get_edges(), which normalize the graph's entire matching set before applying `limit` -- so the DoS guard ran after the expensive work it exists to prevent had already happened, on every request regardless of graph size. Added GraphSession.get_raw_counts(), an O(1) check against the raw len(graph.nodes)/len(graph.edges), and moved the size check ahead of the normalizing calls (also added an edge-count cap). - 5 of the existing regression tests asserted that literal words like "Set-Cookie"/"Content-Type" disappear from the sanitized value -- the sanitizer strips \r\n\x00"\ , not letters, so those assertions failed against this PR's own fix as submitted. Corrected to assert on the actual security property (no \r/\n survives), and added end-to-end tests that exercise the real /api/import -> /api/provenance/report route chain so the properties-key bypass has regression coverage. Full explorer suite: 241 passed. tests/test_security_regression_pr2.py: 30 passed. --------- Co-authored-by: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com> Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
9ec7959899 |
Bump fastapi minimum version to fix PYSEC-2024-38 (starlette DoS) (#871)
* security(deps): bump fastapi floor to >=0.109.1 (PYSEC-2024-38) The [explorer] extra declared fastapi>=0.100.0, which allows the vulnerable 0.109.0 (PYSEC-2024-38, HTTP response splitting). Raise the floor to 0.109.1, the patched release. One-line change, no functional impact -- the 0.109.x API is stable and backward-compatible. Fixes #869 * fix(deps): bump fastapi to >=0.109.2 and python-multipart to >=0.0.7 for PYSEC-2024-38 PYSEC-2024-38 (CVE-2024-24762 / GHSA-2jv5-9r88-3w3p) is a ReDoS in python-multipart < 0.0.7: an attacker sends a crafted Content-Type header that causes catastrophic backtracking in the multipart regex, stalling the event loop and causing a DoS on any endpoint that parses form data. The original PR bumped fastapi to >=0.109.1, but that version pins starlette<0.36.0,>=0.35.0 and cannot install starlette 0.36.2+ (which contains the fix via python-multipart>=0.0.7). FastAPI 0.109.2 is the first version that pins starlette>=0.36.3 (verified against PyPI metadata). Two changes are necessary: 1. fastapi>=0.109.1 -> fastapi>=0.109.2: ensures starlette>=0.36.3 is installed as a transitive dependency, which in turn pulls the fixed python-multipart>=0.0.7. 2. python-multipart>=0.0.6 -> python-multipart>=0.0.7: closes the direct dependency path. python-multipart is listed explicitly in the explorer extra, so without this floor a resolver could still install 0.0.6 and leave the vulnerability present even with the fastapi bump. The fix targets only the 'explorer' optional dependency group, which is the only code surface where FastAPI and form-data parsing are used. No functional API changes between 0.109.1 and 0.109.2; 239 Explorer tests pass without modification. * ci(security): gate pip-audit on explorer-extra dependency PRs, add changelog entry for PYSEC-2024-38 The Security workflow's pip-audit job ran weekly against a bare Python env with none of Semantica's optional extras installed, and always continue-on-error'd -- it would never have flagged the vulnerable fastapi/python-multipart floors this PR fixes, or the first attempt at the fix that left python-multipart>=0.0.6 in place. security-scan.yml's Safety check has the same blind spot (only installs [llm-litellm]). pip-audit now also runs on pull_request when pyproject.toml changes, installs semantica[all] so it can actually see extras like [explorer], and fails the build on findings for that trigger. Scheduled/dispatch runs stay non-blocking pending a full pass over the [all] tree. Also documents the fix (#871, closes #869) in CHANGELOG.md, including the correction made during review after the original fastapi-only bump turned out not to close the vulnerability. * fix(deps): raise setuptools floor to >=83.0.0 (CVE-2026-59890), harden audit env The new pull_request pip-audit gate (previous commit) caught this on its first run: pip install -e ".[all]" resolved setuptools==79.0.1, vulnerable to CVE-2026-59890 / GHSA-h35f-9h28-mq5c / PYSEC-2026-3447 (Unicode normalization lets a MANIFEST.in exclude/prune pattern be bypassed on macOS APFS/HFS+, leaking excluded files into a built sdist). Fixed in setuptools 83.0.0. [build-system] requires had the same too-permissive floor this whole PR is about (setuptools>=61.0). Raised to >=83.0.0. Also upgrade pip/ setuptools explicitly in the Security workflow before running pip-audit, since [build-system] requires only governs isolated build environments, not the ambient one actions/setup-python provisions and pip-audit scans. --------- Co-authored-by: Sameer Kadam <sskadam6305@gmail.com> Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
6328bfe52d | docs(changelog): add entry for MCP server version fix (#870, closes #863) | ||
|
|
cab995dc97 |
fix: address code review findings in backend metadata filtering
- pinecone_store: call self.index.describe_index_stats() instead of the nonexistent self.describe_index_stats(), and use a unit query vector instead of an all-zero vector so filter_by_metadata() works on cosine-metric indexes (the library's own default) - pgvector_store: apply the existing lowercase true/false bool handling to the list-filter branch too, and use the jsonb ?| operator so list-valued metadata fields match on intersection instead of being compared as a single JSON-text blob - sqlite_vec_store: use json_each() with a json_type guard so list-valued metadata fields match on intersection, mirroring the in-memory backend's set-intersection semantics - faiss_store: filter_by_metadata(limit=0) now returns [] instead of one result - milvus_store: reject NaN/Infinity filter values up front with a clear ValidationError instead of building an invalid expression that gets silently swallowed - update the #848 FAISS NotImplementedError test to reflect that FAISS now implements real filter_by_metadata() (this PR's whole point) - add regression tests for each fix; sqlite tests run against the real sqlite-vec extension |
||
|
|
918830a821 |
fix(pipeline): resolve broken import and missing run() in PipelineWithProvenance (#862)
* fix(pipeline): resolve broken import and missing run() in PipelineWithProvenance Fix two bugs in pipeline_provenance.py: 1. Wrong import path: `from .pipeline import Pipeline` fails because `semantica/pipeline/pipeline.py` does not exist. Pipeline lives in `pipeline_builder.py`. Fixed to `from .pipeline_builder import Pipeline`. 2. Pipeline dataclass has no run() method. PipelineWithProvenance.run() now delegates to ExecutionEngine.execute_pipeline(), which is the intended execution path for built pipelines. Additional changes: - Constructor now accepts a built Pipeline instance (breaking the previous unusable API that tried to instantiate a dataclass with **config). - Replace deprecated datetime.utcnow() with datetime.now(timezone.utc). - Add test suite covering import, instantiation, execution, attribute delegation, and provenance graceful degradation. Fixes #858 * test: address Qodo review findings - Remove redundant test_import_succeeds (module-level import already guards against import regression at collection time). - Fix test_provenance_disabled_when_import_fails to deterministically simulate ImportError via sys.modules patch and assert provenance is actually toggled off (runner.provenance is False). * fix(pipeline): update provenance callers for Pipeline API --------- Co-authored-by: Sameer Kadam <sskadam6305@gmail.com> Co-authored-by: Russell Jurney <russell.jurney@gmail.com> |
||
|
|
5b319560fb |
chore: bump version to 0.6.5 (#918)
Security release bundling fixes for GHSA-j4mq (missing auth), GHSA-8c7v (SSRF via redirect bypass), GHSA-482h (Cypher injection), GHSA-8vgg (SPARQL injection), GHSA-4643 (WebSocket Origin validation), and a CodeQL-flagged ReDoS in the SPARQL route validator. |
||
|
|
f2f1d6787d | docs(changelog): add PR #916 (DNS pinning + object-IRI gap) entry | ||
|
|
a8330874d3 |
Merge remote-tracking branch 'origin/main' into security/sparql-injection
# Conflicts: # CHANGELOG.md |
||
|
|
b846ff88d4 |
security: sanitize Cypher labels/relationship types/property keys (GHSA-482h) (#910)
* security: sanitize Cypher labels/relationship types/property keys (GHSA-482h-hw99-h62p) Node labels and property keys passed to create_node/create_relationship were interpolated directly into Cypher strings in the Neptune, Neo4j, and FalkorDB graph stores. Property values are parameterized, but labels and keys can't be bound as parameters, and nothing validated them, so a document-derived entity type or property name could close the current Cypher token early and append arbitrary statements (e.g. DETACH DELETE), running with the application's database credentials. - New shared semantica/graph_store/query_sanitize.py: sanitize_identifier() generalizes age_store.py's existing _sanitize_label/_sanitize_rel_type (the only backend that already validated this) into a helper the other backends can import without an import cycle with graph_store.py/methods.py. - Applied at every label/relationship-type/property-key interpolation site in amazon_neptune.py, neo4j_store.py, falkordb_store.py, graph_store.py (degree_centrality's own query builder), and methods.py (update_relationship's own query builder) — create_node, create_nodes, create_relationship, get_nodes, get_relationships, get_neighbors, shortest_path, update_node, create_index, and all relationship-type filters. - depth/max_depth path-length parameters are also cast to int before interpolation as defense-in-depth (they're already typed int, but Python doesn't enforce that at runtime). Added tests/graph_store/test_cypher_injection.py (12 tests covering the sanitizer directly and reproducing the advisory's injection payload against Neptune/Neo4j/FalkorDB create_node/create_relationship — asserts the malicious query is never built or sent), plus regression tests for graph_store.py's degree_centrality and methods.py's update_relationship. Full graph_store test suite (224 tests) passes with no regressions. * fix(graph-store): prevent depth-based Cypher injection * test(graph-store): tighten injection regression assertions * docs(changelog): add PR #910 (GHSA-482h Cypher injection) entry --------- Co-authored-by: Sameer6305 <sskadam6305@gmail.com> |
||
|
|
69b79e3d67 | docs(changelog): add PR #911 (GHSA-8vgg SPARQL injection) entry | ||
|
|
6002965c55 | docs(changelog): document PR #898's full scope, including the maintainer follow-up fixes | ||
|
|
03ed4b94e9 |
fix(vector-store): preserve ranking for unbounded scores in Pinecone/Qdrant
The 1.0 / (1.0 + max(0.0, 1.0 - score)) normalization added in the last commit clamped every raw score >= 1.0 to an identical 1.0, collapsing result ranking for dot-product-metric indexes (unbounded), which cosine (bounded to [-1, 1]) never exercised. Replaced with x/(1+|x|) rescaled to (0, 1), which is strictly monotonic for any real score. Also adds regression tests for scores >= 1 and a CHANGELOG entry. |
||
|
|
721a2f0e9c |
Fix candidate-embeddings loop dropping matches when pool exhausted
_get_candidate_embeddings()'s expand-and-retry loop widens the search pool (up to limit*10) when post-filtering leaves too few candidates. If the backend keeps returning a full page and filtered matches never reach `limit`, the loop exited via the while condition instead of the break branch, so the pre-loop empty embeddings/metadata/scores lists were returned instead of the matches actually found in the final iteration. This silently returned [] for filtered queries against large persistent-backend stores even when matches existed - exactly the scenario this PR adds support for. Falls back to the last collected batch instead of discarding it. Also documents this PR and #839 in the changelog. |
||
|
|
b4b10a4928 |
docs(changelog): document Qdrant metadata key normalization
Adds an Unreleased/Fixed entry for #841 (closes #840) — QdrantStore search results were keyed "payload" instead of "metadata", breaking HybridSearch.filter_by_metadata() for Qdrant results. |
||
|
|
49f458e927 | Merge branch 'main' into feat/embedded-triplet-store | ||
|
|
c77184bd77 |
docs(changelog): document embedded Oxigraph backend and ImportError fix
Adds an Unreleased/Added entry for #838 (closes #834), including the follow-up fix that preserves ImportError for a missing pyoxigraph install instead of masking it as a generic ProcessingError. |
||
|
|
1850cdd617 |
Merge remote-tracking branch 'origin/main' into fix-830-followup
# Conflicts: # CHANGELOG.md |
||
|
|
cb716cec61 | Merge semantica-agi/main into fix/833-hybridsearch-attributeerror-non-inmemory-backends | ||
|
|
d0e018a1c9 |
fix(vector_store): stop dropping metadata for add_vectors-only backends (#835)
* fix(vector_store): stop dropping metadata for add_vectors-only backends VectorStore.store_vectors() previously discarded the metadata argument whenever the backend only exposed add_vectors() (e.g. FAISSStore), even though add_vectors() supports it. Now metadata is forwarded, and is only passed when the backend's add_vectors() signature actually accepts it (checked via inspect.signature), avoiding a TypeError for stricter backend signatures. Fixes #832 * fix(vector_store): guard signature introspection in store_vectors inspect.signature() can raise ValueError/TypeError for some callables (e.g. certain C-implemented or dynamically built methods). Wrap the add_vectors() signature probe in try/except, consistent with the same pattern already used in ProvenanceManager.trace_lineage(), defaulting to attempting to pass metadata when introspection fails. * docs(changelog): document VectorStore metadata-drop fix (#832, #835) * test(vector_store): add regression coverage for metadata forwarding --------- Co-authored-by: Sameer6305 <sskadam6305@gmail.com> |
||
|
|
bd8d6c5913 | docs: add #830 Explorer Temporal panel fix to CHANGELOG.md | ||
|
|
9c7dd16126 | docs: add CHANGELOG entry for HybridSearch AttributeError fix (#833, #837) | ||
|
|
26a5c4a1fb | Merge branch 'main' into fix/785-provenance-storage-failure-tests | ||
|
|
db4361ad46 |
feat(provenance): close PROV-O compliance gaps and high-stakes trust blockers (closes #825)
Part A - high-stakes trust blockers: - Invalidation tombstones via ProvenanceManager.invalidate() (archive-then-append, never mutates or deletes) instead of hard delete - Hash-chained integrity: sequence_id/previous_checksum chain every entry to its predecessor; new verify_chain() detects wholesale row deletion that a lone per-row checksum cannot - Typed Agent (AgentRecord: agent_type/is_automated) and Activity (ActivityRecord: start/end timing), wired through all 18 *_provenance.py wrappers - Split parent_entity_id into previous_version_id (correction) vs derived_from_id (cross-source derivation), additive alongside the legacy combined field - Downstream/descendant lineage traversal (get_descendants/trace_descendants, reverse BFS) closing the dead direction="downstream" code path in the Explorer's provenance route - Qualified Association+hadRole and Invalidation in export_prov() - New CLI: provenance invalidate|verify-chain|descendants Part B - general PROV-O spec completeness: - Qualified Generation/Usage/Derivation in export_prov() - wasAssociatedWith, actedOnBehalfOf, wasInformedBy relations - Bitemporal fields (valid_from/valid_until/revision_type/supersedes) plus revision_history()/query_recorded_between(), closing the deprecated kg.ProvenanceTracker's "no direct equivalent yet" migration gaps - prov:Bundle/hadMember membership via bundle_id - Configurable base_uri (--base-uri CLI flag), shared by RDFExporter's NamespaceManager and OWLExporter's default ontology_uri so KG/OWL/PROV exports co-resolve under one namespace instead of three hardcoded ones Bugs fixed along the way: - agent_id was a dead field: no track_* method read it from kwargs - track_entities_batch silently absorbed typed kwargs into the metadata blob - compute_checksum() had to exclude entity_id itself: hashing it made track_entity's versioning-archive relabel permanently orphan any entry already chained from the pre-relabel checksum, a false-positive "broken chain" for a legitimate rename - InMemoryStorage.get_chain_head() ignored the committed head whenever the current transaction had staged entries, corrupting the next chain link - several new ProvenanceEntry fields were wired into the dataclass and export_prov() but not into SQLiteStorage's DDL/INSERT/row-mapping; InMemoryStorage masked the gap. Added a permanent round-trip regression test to catch this class of bug for future field additions Flagged, not fixed (separate pre-existing issues, out of scope for #825): - pipeline/pipeline_provenance.py imports a nonexistent module and wraps a Pipeline dataclass with no run() method - most *_provenance.py wrappers' backing classes are themselves missing or incomplete (context_manager, deduplicator, normalizer, etc.) - kg_provenance.py passes entity_type inside metadata={} instead of as a top-level track_entity() kwarg across most of its call sites |
||
|
|
aae4c946ea | test(provenance): expand storage failure regression coverage (#785) | ||
|
|
b59211ea7f |
security: SHA-pin all Actions, harden release pipeline, add pin verification (#824)
* security: SHA-pin all Actions, harden release pipeline, add pin verification
Hardens the CI/CD supply chain against the LiteLLM/Trivy-style attack (a
compromised third-party Action with a mutable tag stealing a long-lived
publishing token) and closes several related gaps found in an audit of the
actual repository state.
- Pin every third-party GitHub Action across all workflows to a full commit
SHA (tag kept as a trailing comment); add verify-action-pins.yml, a CI
check that confirms via the GitHub API that each pin still matches its
tag, on every workflow change, push to main, and weekly.
- Scope release.yml permissions to the job level (workflow defaults to
contents: read); add a concurrency group so simultaneous tag pushes can't
race the publish job.
- Add SLSA build provenance attestation (actions/attest-build-provenance)
for every released wheel.
- Fix a latent bug in security-scan.yml: the PR-comment step was missing
pull-requests: write and silently failing; add bounded artifact retention
for uploaded scan reports.
- Group github-actions Dependabot updates to cut review noise.
- Document the resulting posture in SECURITY.md for auditors/regulated
adopters, including what's enforced and what a fork needs to reconfigure
for itself (environment/branch protection, Trusted Publishing trust).
Also (via GitHub API, not in this diff): created a protected `pypi`
environment with a required reviewer restricted to v* tags, and enabled
branch protection on main (required review, required status checks, no
force-push/deletion).
* fix: harden verify-action-pins per PR #824 bot review
Addresses real findings from the automated review on #824:
- The script previously only matched uses: lines that already contained a
40-hex SHA, so a newly added mutable-tag action (e.g. some/action@v1)
would never be scanned at all and the check would pass silently. It now
matches every uses: line and hard-fails on any ref that isn't a full
commit SHA.
- A tag that fails to resolve via the GitHub API (rate limit, deleted tag)
previously only logged a warning and continued; that's now a hard
failure too, since an unverifiable pin is exactly the failure mode this
check exists to catch.
- verify-action-pins.yml only triggered on .github/workflows/** changes,
so an edit to the verifier script itself wouldn't run the check that
verifies it. Added the script path to both trigger filters.
The reviewer's claim that slash-containing tag comments (release/v1) break
the API lookup did not reproduce - tested directly against
pypa/gh-action-pypi-publish@release/v1 and GitHub's commits API resolves
multi-segment refs natively - so no change was needed there.
Verified with a synthetic test workflow containing a mutable-tag action,
a correctly-pinned SHA, and a deliberately mismatched SHA: the updated
script now catches the first and third cases and passes the second. Also
re-ran against the real workflow tree (40/40 pins still verify clean).
* fix: repair broken Safety scan and PR comment formatting
The "Comment PR with Security Results" step was producing garbled output
(literal \n characters instead of newlines, "undefined:" labels) because:
- Every line in the JS comment builder used \n (escaped backslash-n)
inside template literals, which JS renders as the literal two-character
string \n, not a newline.
- The Semgrep section read issue.rule_id, but Semgrep's JSON field is
check_id - hence "undefined: <path>" for every entry.
Rewrote the comment builder to construct each section as an array of
lines joined with a real '\n', with correct field names, and collapsed
long finding lists into a <details> block instead of a flat list.
Verified by extracting the exact script and running it under node against
synthetic fixtures matching each tool's real JSON schema (found/clean/
missing-report paths all render correctly).
While tracing the "undefined" and always-empty Safety section, found the
Safety step itself was silently broken:
- `safety check --json --output safety-report.json` is invalid in
Safety 3.x: --output now selects a console format (json/text/screen),
not a file path. The command errored on every run (swallowed by
`|| true`), so safety-report.json was never created and the PR comment
always fell back to a generic "scan completed" message. Switched to
`--save-json`, which is the correct flag for writing a JSON report to
disk, and confirmed against the real safety 3.8.1 CLI locally.
- Even with a report, the code read vuln.package - the real field is
package_name.
- The job never installed Semantica's own dependencies before scanning,
so `safety check` (which defaults to scanning the environment) was
auditing the scanner tools' own dependencies, not Semantica's. Added
`pip install -e ".[llm-litellm]"` so the project's actual dependency
tree - including the LiteLLM extra this whole hardening effort is
about - is what gets scanned.
Also updated the corresponding SECURITY.md bullet to describe what Safety
actually covers now.
* fix: remove unused pypdf2 dependency (CVE-2023-36464)
Now that the Safety scan step actually runs (see previous commit), it
correctly failed this PR's checks on CVE-2023-36464 in pypdf2==3.0.1 - a
real, pre-existing vulnerability that was invisible until the scan was
fixed.
PyPDF2 is not a patchable dependency here: the project is discontinued
(merged into `pypdf`), 3.0.1 is its final release, and there is no fixed
version to upgrade to. Grepping the repo for `import PyPDF2` / `from
PyPDF2` turns up nothing - it was never actually imported anywhere. Its
only presence outside pyproject.toml was in docstrings describing a
"PyPDF2.PdfReader() fallback" for PDF parsing that was never implemented
in code; pdfplumber is the library actually used. Removed the dependency
and corrected the stale docstrings in parse/__init__.py, parse/methods.py,
parse/pdf_parser.py, and ingest/email_ingestor.py accordingly.
* fix: suppress Bandit B324 false positives on non-cryptographic MD5 use
Same pattern as the previous pypdf2 commit: fixing the Safety scan
surfaced this PR's own Bandit HIGH-severity gate actually blocking on 10
pre-existing findings, all Bandit B324 ("Use of weak MD5 hash for
security").
Checked each of the 10 call sites: every one uses hashlib.md5() to build
a short deterministic cache key, entity ID, or IRI suffix from already-
non-secret input (query text, entity text/type, class/property names) -
none are used for passwords, tokens, or integrity verification of
untrusted data. This is exactly the case Bandit's own message points at
("Consider usedforsecurity=False").
Did not use usedforsecurity=False itself: that keyword argument was
added to hashlib in Python 3.9, and pyproject.toml declares
`requires-python = ">=3.8"` - adding it unconditionally risks a TypeError
on 3.8. Used a targeted `# nosec B324` comment with a one-line
justification instead, which suppresses only this specific check and
carries no runtime behavior change on any supported Python version.
Verified locally: bandit -r semantica/ -ll now reports 0 HIGH-severity
findings (was 10).
* docs: add CHANGELOG entry for #824 CI/CD supply-chain hardening
Covers the SHA-pinning + verify-action-pins.yml enforcement, release.yml
hardening (job-scoped permissions, concurrency, SLSA provenance), the
pypi environment/branch protection GitHub-side config, the
security-scan.yml Safety/comment-formatting fixes, and the two
vulnerabilities those fixes surfaced (pypdf2 CVE-2023-36464 removal,
Bandit B324 suppression).
* fix: close two remaining gaps missed by upstream bot-review fixes
verify-action-pins.sh:
- Quoted uses: lines (e.g. uses: owner/action@SHA) were not matched
by the existing regex, so a SHA-pinned action written with quotes would
silently skip verification. Updated the main ERE to accept an optional
leading/trailing single or double quote around the owner/action@ref
value, and excluded quote chars from the inner character classes so the
ref is still extracted cleanly.
- The grep input glob only covered *.yml. GitHub also treats *.yaml as a
valid workflow extension. Added *.yaml to the glob and a 2>/dev/null
guard so the command doesn't fail when no *.yaml files exist.
security-scan.yml (on top of Kaif's --save-json fix in
|
||
|
|
21365cb0e6 | Merge branch 'main' into fix/775-ontology-atomic-writes | ||
|
|
76edaeb1c0 |
fix(agno): make _eval_rule raise instead of silently returning compliant=True on unevaluable rules (closes #778) (#822)
* fix(agno): surface unevaluable policy rules (#778) * fix(agno): fixed qodo reviews check_policy previously let unevaluable policy rules silently return compliant=True with no signal (issue #778): a rule referencing a field missing from decision_data, or a rule string not matching the expected <field> <op> <value> format, both fell through _eval_rule's `return True` and were treated as passed. Both now raise ValueError, which routes through check_policy's existing exception handler and surfaces as a `warnings` entry instead. compliant/ violations semantics are unchanged for every case that previously worked correctly; an unevaluable rule is not counted as a violation since it's genuinely unknown whether it would have passed. Follow-up fixes from code review: - policy_rules decoded via json.loads without checking it was a list; a JSON-encoded bare string decoded to a Python str, so iterating it evaluated one "rule" per character, amplifying a single input-shape mistake into a wall of per-character warnings. A decoded string is now treated as a single rule; any other non-list shape or non-string list element produces exactly one warning instead. - _eval_rule used `data.get(field) is None` to detect a missing field, which can't distinguish an absent key from a key present with JSON null - both produced the same "undefined field" warning. Field presence is now checked with `field not in data` first, and a present-but-null value gets its own distinct message. Added regression tests for all of the above in tests/integrations/agno/test_decision_kit.py (38 tests in the file, 128 passing across tests/integrations/agno/). * fix(agno): reject non-object decision_data in check_policy check_policy only validated that decision_data was well-formed JSON, not that it decoded to an object. When it decoded to a list, `field not in data` in _eval_rule silently became list-membership testing of values instead of a dict key check - e.g. "confidence" not in ["confidence", 0.95] evaluates to False - so a matching rule fell through to data["confidence"], raising a raw internal TypeError ("list indices must be integers or slices, not str") instead of any meaningful diagnostic. Numbers, strings, and bools produced similarly opaque TypeErrors deep inside _eval_rule. check_policy now checks isinstance(data, dict) right after decoding and rejects any other shape with a single clear violations entry, the same way it already rejects malformed JSON. Added 5 regression tests in tests/integrations/agno/test_decision_kit.py covering list/number/string/bool/null decision_data shapes (43 tests in the file, 133 passing across tests/integrations/agno/). Addresses Copilot PR review comment on the #778 fix branch. * docs(changelog): reference PR #822 in the check_policy changelog entry --------- Co-authored-by: KaifAhmad1 <kaifahmad087@gmail.com> |
||
|
|
46dcbbe731 |
Merge remote-tracking branch 'origin/main' into fix/779-record-decision-logging
# Conflicts: # CHANGELOG.md |
||
|
|
5094235ce1 |
Merge branch 'main' into fix/783-tracking-methods-honest-failures
Resolves CHANGELOG.md conflict with #819's SKOS cycle-detection entry by keeping both entries. |