linkedparticles-core 1.210.2__tar.gz → 1.214.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/PKG-INFO +1 -1
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/config.py +411 -27
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/shacl.py +8 -1
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/schema.py +4 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/mcp_memory.py +31 -1
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/rdf.py +37 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/structure.py +13 -8
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/adapters/openai_compat.py +44 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/render/markdown.py +200 -33
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/url_safety.py +124 -1
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/pyproject.toml +2 -2
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/_client_fixtures.py +11 -4
- linkedparticles_core-1.214.1/tests/test_config_path_guard.py +72 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/LICENSE +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/NOTICE +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/README.md +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/conformance/README.md +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/conformance/profile.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/conformance/similarity_vectors.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/schemas/context.jsonld +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/schemas/interchange.schema.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/schemas/particle.schema.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/schemas/shacl/CorpusSnapshotShape.ttl +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/schemas/shacl/ParticleShape.ttl +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/schemas/shacl/ProvenanceChainShape.ttl +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/schemas/shacl/SubjectShape.ttl +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/schemas/shacl/TrustStatementShape.ttl +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/artifacts/schemas/trust_lens.schema.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/constraints.txt +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/_resources.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/contract.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/fixtures.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/jsonschema.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/profile.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/runner.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/types.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/validator.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/_resources.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/artifact_fold.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/cascade_gate.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/claims.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/closure_measure.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/conflict_resolution.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/conflict_review.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/consolidation_cadence.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/contradiction_disclosure.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/duplicate_key.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/equivalence.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/fingerprint.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/generics.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/granularity.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/held_release.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/jsonld_context.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/ladders.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/modality.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/observer_scope.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/past_value.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/predicate_profile.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/probe_verdict.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/progress.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/provenance.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/reanchor.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/rung_ladder.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/schema_version.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/scoring/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/scoring/confidence.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/scoring/decay.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/scoring/relevance.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/scoring/utility.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/spend_budget.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/stance.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/status.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/validity.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/vocabulary.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/vocabulary_jsonld.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/core/vocabulary_report.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/egress.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/embeddings.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/append_delta.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/calibration.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/components.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/docstrings.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/general.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/github/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/github/_shared.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/github/gist.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/github/pages.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/github/repo.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/hackernews.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/incremental.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/journal.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/mastodon.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/migration_preview.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/modality.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/nomisma.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/numista/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/numista/_shared.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/numista/coin.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/numista/issuer.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/numista/listing.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/polarity.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/property_keys.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/reddit.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/registry.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/scope.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/session_artifacts.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/session_state.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/subject_gate.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/subject_scope.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/taxonomy.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/tool_turns.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/trust_lens.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/vocabulary.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/extraction/wikidata.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/http.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/interchange/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/interchange/codec.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/interchange/jsonl.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/interchange/yaml_ld.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/adapters/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/adapters/anthropic.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/adapters/local.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/batch_budget.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/breaker.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/client.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/errors.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/fencing.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/logprobs.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/pool.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/registry.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/replay.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/spend_budget.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/llm/usage.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/py.typed +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/render/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/secrets.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/url_canonical.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/__init__.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/_upstream.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/MANIFEST.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/hackernews-thread-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/hackernews-thread-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/hackernews-thread-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/journal-entry-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/journal-entry-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/journal-entry-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/mastodon-thread-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/mastodon-thread-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/mastodon-thread-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/mcp-memory-export-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/mcp-memory-export-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/mcp-memory-export-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/nomisma-concept-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/nomisma-concept-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/nomisma-concept-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/numista-coin-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/numista-coin-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/numista-coin-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/numista-issuer-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/numista-issuer-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/numista-issuer-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/numista-listing-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/numista-listing-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/numista-listing-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/rdf-graph-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/rdf-graph-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/rdf-graph-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/reddit-thread-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/reddit-thread-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/reddit-thread-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/taxonomy-coins-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/taxonomy-coins-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/taxonomy-coins-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/trust-lens-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/trust-lens-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/trust-lens-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-article-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-article-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-article-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-article-002/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-article-002/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-article-002/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-essay-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-essay-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-essay-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-interview-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-interview-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/web-interview-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/wikidata-entity-001/content.bin +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/wikidata-entity-001/manifest.yaml +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conformance/fixtures/wikidata-entity-001/snapshot.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/conftest.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/fixtures/scope/normative-rule-file.md +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/fixtures/scope/normative-rule-file.recorded.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/fixtures/scope/rule-file-agents-md.md +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/fixtures/scope/rule-file-agents-md.recorded.json +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_calibration_isotonic.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_confidence.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_config_client_sections.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_config_sample.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_conformance_fixture_gen.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_conformance_similarity_vectors.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_conformance_validator.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_core_contradiction_disclosure.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_core_held_release.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_core_observer_scope.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_core_provenance.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_core_vocabulary.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_egress_policy.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_embeddings.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_extraction_vision.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_interchange_schema.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_llm_batch_budget.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_llm_errors.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_llm_logprobs.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_llm_pool.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_llm_replay.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_llm_usage.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_schema.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_schema_openness.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_secrets.py +0 -0
- {linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/tests/test_url_canonical.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: linkedparticles-core
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.214.1
|
|
4
4
|
Summary: Store-free client library for Particles: define, validate, serialize, and extract provenance-tracked belief particles
|
|
5
5
|
Project-URL: Homepage, https://linkedparticles.org
|
|
6
6
|
Project-URL: Documentation, https://docs.linkedparticles.org
|
|
@@ -16,9 +16,11 @@ from __future__ import annotations
|
|
|
16
16
|
import ipaddress
|
|
17
17
|
import logging
|
|
18
18
|
import os
|
|
19
|
+
import stat
|
|
19
20
|
from collections.abc import Callable, Mapping
|
|
20
21
|
from pathlib import Path
|
|
21
|
-
from typing import Any, Literal, TypeVar
|
|
22
|
+
from typing import Any, Literal, NamedTuple, TypeVar
|
|
23
|
+
from urllib.parse import urlsplit
|
|
22
24
|
|
|
23
25
|
import yaml
|
|
24
26
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
@@ -222,6 +224,25 @@ class ApiConfig(BaseModel):
|
|
|
222
224
|
audit log), and ``/digest`` (the provenance-ranked belief digest) — are
|
|
223
225
|
gated by the bearer **regardless** of this flag; under the ``dev-key``
|
|
224
226
|
loopback skip they stay open for local development.
|
|
227
|
+
|
|
228
|
+
``allowed_origins`` lists browser origins (``scheme://host[:port]``) whose
|
|
229
|
+
cross-site requests the engine serves without a verified bearer. Every gated
|
|
230
|
+
route refuses a cross-site browser request otherwise (security review
|
|
231
|
+
2026-10-23: any web page could POST a multipart deposit to a dev-key
|
|
232
|
+
loopback engine). The engine's own web UI at ``/app`` is same-origin and
|
|
233
|
+
needs no entry. **Default empty**; a wildcard or ``null`` is rejected.
|
|
234
|
+
|
|
235
|
+
``allowed_hosts`` lists the ``Host`` header names (no port) the engine
|
|
236
|
+
answers on, beside the loopback names (``localhost``, ``127.0.0.0/8``,
|
|
237
|
+
``::1``), which are always allowed so a local health probe keeps working.
|
|
238
|
+
A ``*.`` prefix matches any subdomain (``*.tailnet.example``). The check
|
|
239
|
+
covers every path, ``/health`` and ``/app`` included, and is what refuses a
|
|
240
|
+
DNS-rebinding page: such a page reaches the engine's address under its own
|
|
241
|
+
hostname (security review 2026-10-24, N1). Under the dev-key skip only the
|
|
242
|
+
loopback names and these entries are served, whatever this list holds.
|
|
243
|
+
With a real ``PARTICLES_API_KEY`` and an empty list (the default) no
|
|
244
|
+
``Host`` is refused; set it on a non-loopback deployment whose reads are
|
|
245
|
+
open, and include the name your health probe sends.
|
|
225
246
|
"""
|
|
226
247
|
|
|
227
248
|
max_request_body_bytes: int = 25 * 1024 * 1024 # 25 MiB
|
|
@@ -229,6 +250,57 @@ class ApiConfig(BaseModel):
|
|
|
229
250
|
trusted_proxies: list[str] = Field(default_factory=list)
|
|
230
251
|
rate_limit_per_minute: int = 60
|
|
231
252
|
require_auth_for_reads: bool = False
|
|
253
|
+
allowed_origins: list[str] = Field(default_factory=list)
|
|
254
|
+
allowed_hosts: list[str] = Field(default_factory=list)
|
|
255
|
+
|
|
256
|
+
@field_validator("allowed_hosts")
|
|
257
|
+
@classmethod
|
|
258
|
+
def _validate_allowed_hosts(cls, value: list[str]) -> list[str]:
|
|
259
|
+
"""Accept bare host names, optionally with a leading ``*.`` wildcard.
|
|
260
|
+
|
|
261
|
+
A ``Host`` header is compared without its port, so an entry carrying a
|
|
262
|
+
scheme, a path or a port could never match. A bare ``*`` would match
|
|
263
|
+
every rebinding page, which is the attack the list exists to refuse.
|
|
264
|
+
"""
|
|
265
|
+
normalized: list[str] = []
|
|
266
|
+
for entry in value:
|
|
267
|
+
host = entry.strip().lower()
|
|
268
|
+
bare = host.removeprefix("*.")
|
|
269
|
+
# A bracketed IPv6 literal carries colons of its own.
|
|
270
|
+
forbidden = "/@ " if bare.startswith("[") and bare.endswith("]") else "/:@ "
|
|
271
|
+
if not bare or "*" in bare or any(c in bare for c in forbidden):
|
|
272
|
+
raise ValueError(
|
|
273
|
+
f"api.allowed_hosts entry {entry!r} is not a host name; give a "
|
|
274
|
+
"bare name with no scheme or port, e.g. engine.example.org or "
|
|
275
|
+
"*.tailnet.example"
|
|
276
|
+
)
|
|
277
|
+
normalized.append(host)
|
|
278
|
+
return normalized
|
|
279
|
+
|
|
280
|
+
@field_validator("allowed_origins")
|
|
281
|
+
@classmethod
|
|
282
|
+
def _validate_allowed_origins(cls, value: list[str]) -> list[str]:
|
|
283
|
+
"""Accept only exact ``scheme://host[:port]`` origins.
|
|
284
|
+
|
|
285
|
+
A browser sends exactly that form, so anything else (a path, a
|
|
286
|
+
wildcard, the opaque ``null`` origin a sandboxed frame or a ``file:``
|
|
287
|
+
page sends) could never match a real origin, or would match every page.
|
|
288
|
+
"""
|
|
289
|
+
for entry in value:
|
|
290
|
+
parts = urlsplit(entry.strip())
|
|
291
|
+
if (
|
|
292
|
+
not parts.scheme
|
|
293
|
+
or not parts.netloc
|
|
294
|
+
or "*" in entry
|
|
295
|
+
or parts.path not in ("", "/")
|
|
296
|
+
or parts.query
|
|
297
|
+
or parts.fragment
|
|
298
|
+
):
|
|
299
|
+
raise ValueError(
|
|
300
|
+
f"api.allowed_origins entry {entry!r} is not an origin; give "
|
|
301
|
+
"scheme://host[:port], e.g. https://notes.example.org"
|
|
302
|
+
)
|
|
303
|
+
return value
|
|
232
304
|
|
|
233
305
|
@field_validator("trusted_proxies")
|
|
234
306
|
@classmethod
|
|
@@ -319,10 +391,18 @@ class EngineConfig(BaseModel):
|
|
|
319
391
|
bearer token the client presents is a *secret* and is read via
|
|
320
392
|
``particles.secrets.get_engine_token_optional`` (``PARTICLES_ENGINE_TOKEN``)
|
|
321
393
|
— never a field here, never in ``config.yaml``.
|
|
394
|
+
|
|
395
|
+
``allow_plaintext_token`` lets the bearer ride a non-loopback ``http://``
|
|
396
|
+
URL, with a warning. Off by default: without it the client refuses to send
|
|
397
|
+
the token there at all (completing the June review's F18), so
|
|
398
|
+
a token reaches a remote engine only over ``https://`` or a loopback hop
|
|
399
|
+
(an SSH or Tailscale forward). Set it only for an engine on a network you
|
|
400
|
+
trust end to end.
|
|
322
401
|
"""
|
|
323
402
|
|
|
324
403
|
base_url: str | None = None
|
|
325
404
|
timeout_seconds: float = 60.0
|
|
405
|
+
allow_plaintext_token: bool = False
|
|
326
406
|
|
|
327
407
|
|
|
328
408
|
class ObservabilityConfig(BaseModel):
|
|
@@ -359,6 +439,11 @@ class ObservabilityConfig(BaseModel):
|
|
|
359
439
|
# local collector, or a SaaS ingest URL). Non-secret; the credential is the
|
|
360
440
|
# PARTICLES_OTEL_EXPORTER_HEADERS secret. Unset ⇒ the OTLP SDK default endpoint.
|
|
361
441
|
endpoint: str | None = None
|
|
442
|
+
# Exporter headers (PARTICLES_OTEL_EXPORTER_HEADERS, OTEL_EXPORTER_OTLP_*HEADERS)
|
|
443
|
+
# are refused over a non-loopback http:// endpoint: no OTLP exporter is
|
|
444
|
+
# installed and one WARNING names why (N12). True sends them anyway, with
|
|
445
|
+
# the warning, for a collector on a network you trust end to end.
|
|
446
|
+
allow_plaintext_headers: bool = False
|
|
362
447
|
# Per-signal enables — turn off a signal without disabling the whole layer.
|
|
363
448
|
traces: bool = True
|
|
364
449
|
metrics: bool = True
|
|
@@ -428,6 +513,10 @@ class OpenAICompatProviderConfig(BaseModel):
|
|
|
428
513
|
# OpenAI-compatible base URL; the adapter appends ``/chat/completions``.
|
|
429
514
|
# Default targets a local Ollama server (the case).
|
|
430
515
|
base_url: str = "http://localhost:11434/v1"
|
|
516
|
+
# The entry's API key is refused over a non-loopback http:// base_url, before
|
|
517
|
+
# any request (N12; the engine token's rule). True sends it
|
|
518
|
+
# anyway, with a warning, for an endpoint on a network you trust end to end.
|
|
519
|
+
allow_plaintext_key: bool = False
|
|
431
520
|
# Per-request wall-clock timeout (seconds). Local models on modest hardware
|
|
432
521
|
# can be slow, so the default is generous.
|
|
433
522
|
timeout_seconds: float = 120.0
|
|
@@ -1490,6 +1579,14 @@ class MigrationConfig(BaseModel):
|
|
|
1490
1579
|
# Deliberately low. A migrated belief is second-hand: this store never saw
|
|
1491
1580
|
# the claim made, cannot check it, and did not calibrate the number.
|
|
1492
1581
|
import_confidence: float = Field(default=0.35, ge=0.0, le=1.0)
|
|
1582
|
+
# Bounds on one export (security review N23). An export larger than
|
|
1583
|
+
# ``max_bytes`` is not parsed, and the mapping stops at ``max_candidates``
|
|
1584
|
+
# with a quality note naming what it left out. Each candidate is reconciled
|
|
1585
|
+
# with an embedding and the §6.6 ladder, and an HTTP text deposit typed
|
|
1586
|
+
# ``MCP_MEMORY_EXPORT`` reaches this extractor too, so the export is not
|
|
1587
|
+
# trusted to be small.
|
|
1588
|
+
max_bytes: int = Field(default=32 * 1024 * 1024, ge=1)
|
|
1589
|
+
max_candidates: int = Field(default=10_000, ge=1)
|
|
1493
1590
|
|
|
1494
1591
|
|
|
1495
1592
|
class WebClipperConfig(BaseModel):
|
|
@@ -2872,6 +2969,31 @@ class LocalRefreshConfig(BaseModel):
|
|
|
2872
2969
|
# *identity* rather than of content, which an unattended pass should decline
|
|
2873
2970
|
# to follow rather than silently ingest.
|
|
2874
2971
|
follow_symlinks: bool = False
|
|
2972
|
+
# Extra directories (or files) the refresh may read, beyond the derived
|
|
2973
|
+
# roots. The tier reads a ``file://`` entry only when its resolved path is
|
|
2974
|
+
# under one of these or a derived root (security review D2): the rule-file
|
|
2975
|
+
# roots (``rule_sources.paths``, the roots ``rules sync`` and
|
|
2976
|
+
# ``init claude-code`` recorded, the main checkout of a recorded worktree,
|
|
2977
|
+
# and ``~/.claude``; never the working directory), admitting only
|
|
2978
|
+
# files named in ``rule_sources.filenames`` there, and ``*.md`` under
|
|
2979
|
+
# ``~/.claude/projects`` (the Claude Code memory files). A file deposited
|
|
2980
|
+
# with ``--fetch-policy LAZY`` anywhere else needs its directory named here.
|
|
2981
|
+
# ``~`` and ``$VAR`` are expanded. An entry outside every root is skipped
|
|
2982
|
+
# like a missing file and logged.
|
|
2983
|
+
allowed_roots: list[str] = Field(default_factory=list)
|
|
2984
|
+
|
|
2985
|
+
|
|
2986
|
+
class LocalFilesConfig(BaseModel):
|
|
2987
|
+
"""Bounds on the local files the corpus reads itself (security review C9).
|
|
2988
|
+
|
|
2989
|
+
Every local read the corpus makes goes through one descriptor-checked reader
|
|
2990
|
+
(``particles.corpus.local_read``): a file deposit, a vault, web-clipper or
|
|
2991
|
+
project walk, a rule-file intake and the local refresh tier. ``max_bytes``
|
|
2992
|
+
caps the file it will read; a larger file is refused before a byte is read
|
|
2993
|
+
and reported like any unreadable file.
|
|
2994
|
+
"""
|
|
2995
|
+
|
|
2996
|
+
max_bytes: int = Field(default=256 * 1024 * 1024, ge=1)
|
|
2875
2997
|
|
|
2876
2998
|
|
|
2877
2999
|
class RuleSourcesConfig(BaseModel):
|
|
@@ -2938,7 +3060,8 @@ class McpWriteConfig(BaseModel):
|
|
|
2938
3060
|
enabled_stores: list[str] = Field(default_factory=list)
|
|
2939
3061
|
# Seeded AUTHOR-scoped trust_rank for a new asserter identity (§6/§6a).
|
|
2940
3062
|
agent_trust_rank: float = Field(default=0.8, ge=0.0, le=1.0)
|
|
2941
|
-
# Whether retract/supersede may target another
|
|
3063
|
+
# Whether retract / supersede / tag / untag / link may target another
|
|
3064
|
+
# identity's particles, and link_remove another writer's link (§6).
|
|
2942
3065
|
allow_cross_asserter: bool = False
|
|
2943
3066
|
# The server-bound asserting principal stamped onto asserted_by / author_id
|
|
2944
3067
|
# / event actor — NOT a per-call client argument (§4a/§6, M3).
|
|
@@ -3012,6 +3135,18 @@ class McpMemoryCompatConfig(BaseModel):
|
|
|
3012
3135
|
|
|
3013
3136
|
|
|
3014
3137
|
class McpConfig(BaseModel):
|
|
3138
|
+
"""MCP server surfaces.
|
|
3139
|
+
|
|
3140
|
+
``rate_limit_per_minute`` caps, per server process, the tool calls that can
|
|
3141
|
+
drive a paid model call: a ``query`` with a question (the NL response), a
|
|
3142
|
+
``particle_assert`` / ``particle_supersede`` (the §6.6 contradiction
|
|
3143
|
+
probe), and every write of the memory-server façade. One token bucket per
|
|
3144
|
+
process, refilling over a minute, so an agent looping on a tool (a prompt
|
|
3145
|
+
injection, a runaway plan) spends at most this many such calls a minute
|
|
3146
|
+
(security review D30). ``0`` disables it. Other reads are not counted.
|
|
3147
|
+
"""
|
|
3148
|
+
|
|
3149
|
+
rate_limit_per_minute: int = Field(default=60, ge=0)
|
|
3015
3150
|
write: McpWriteConfig = Field(default_factory=McpWriteConfig)
|
|
3016
3151
|
recall: McpRecallConfig = Field(default_factory=McpRecallConfig)
|
|
3017
3152
|
memory_compat: McpMemoryCompatConfig = Field(default_factory=McpMemoryCompatConfig)
|
|
@@ -4299,9 +4434,16 @@ class CliConfig(BaseModel):
|
|
|
4299
4434
|
config rather than behind a flag; set to 0 to disable entirely. The
|
|
4300
4435
|
heartbeat is already suppressed whenever stderr is not a TTY, so this knob
|
|
4301
4436
|
only matters for interactive use.
|
|
4437
|
+
|
|
4438
|
+
``warn_unused_config`` controls the one warning a process prints when a
|
|
4439
|
+
Particles ``config.yaml`` sits in its working directory or an ancestor and
|
|
4440
|
+
is not read (settings come only from ``PARTICLES_CONFIG`` or
|
|
4441
|
+
``~/.particles/config.yaml``). Only a file the loader reads can turn it
|
|
4442
|
+
off, so a file it ignores never can.
|
|
4302
4443
|
"""
|
|
4303
4444
|
|
|
4304
4445
|
heartbeat_seconds: float = Field(default=20.0, ge=0)
|
|
4446
|
+
warn_unused_config: bool = True
|
|
4305
4447
|
|
|
4306
4448
|
|
|
4307
4449
|
class SourcePassageConfig(BaseModel):
|
|
@@ -4407,6 +4549,7 @@ class ParticlesConfig(BaseModel):
|
|
|
4407
4549
|
benchmark: BenchmarkConfig = Field(default_factory=BenchmarkConfig)
|
|
4408
4550
|
refetch_floors: dict[str, int] = Field(default_factory=lambda: dict(_DEFAULT_REFETCH_FLOORS))
|
|
4409
4551
|
local_refresh: LocalRefreshConfig = Field(default_factory=LocalRefreshConfig)
|
|
4552
|
+
local_files: LocalFilesConfig = Field(default_factory=LocalFilesConfig)
|
|
4410
4553
|
rule_sources: RuleSourcesConfig = Field(default_factory=RuleSourcesConfig)
|
|
4411
4554
|
|
|
4412
4555
|
def reconciliation_mode_for(self, store: str | None) -> str:
|
|
@@ -4488,6 +4631,7 @@ class ParticlesConfig(BaseModel):
|
|
|
4488
4631
|
#: carve itself is reserved, not taken.
|
|
4489
4632
|
CLIENT_SECTIONS: frozenset[str] = frozenset(
|
|
4490
4633
|
{
|
|
4634
|
+
"cli",
|
|
4491
4635
|
"confidence",
|
|
4492
4636
|
"content_age_decay",
|
|
4493
4637
|
"contestedness",
|
|
@@ -4587,49 +4731,265 @@ def env_override_names() -> frozenset[str]:
|
|
|
4587
4731
|
return frozenset(name for name, _section, _field in _ENV_OVERRIDES)
|
|
4588
4732
|
|
|
4589
4733
|
|
|
4734
|
+
#: Environment variables that choose where a secret, a prompt or telemetry is
|
|
4735
|
+
#: sent, and beat the config file a process pins: the two Particles overrides
|
|
4736
|
+
#: above, plus the ones the Anthropic and OpenTelemetry SDKs read themselves.
|
|
4737
|
+
ENDPOINT_ENV_VARS: tuple[str, ...] = (
|
|
4738
|
+
"PARTICLES_ENGINE_BASE_URL",
|
|
4739
|
+
"PARTICLES_OBSERVABILITY_ENDPOINT",
|
|
4740
|
+
"ANTHROPIC_BASE_URL",
|
|
4741
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
4742
|
+
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
|
|
4743
|
+
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
|
|
4744
|
+
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
|
|
4745
|
+
)
|
|
4746
|
+
|
|
4747
|
+
|
|
4748
|
+
def _endpoint_origin(value: str) -> str:
|
|
4749
|
+
"""``scheme://host[:port]`` of an endpoint value, without any userinfo or path."""
|
|
4750
|
+
try:
|
|
4751
|
+
parts = urlsplit(value)
|
|
4752
|
+
host = parts.hostname or ""
|
|
4753
|
+
port = f":{parts.port}" if parts.port is not None else ""
|
|
4754
|
+
except ValueError:
|
|
4755
|
+
return "(unparseable)"
|
|
4756
|
+
return f"{parts.scheme}://{host}{port}" if parts.scheme and host else "(unparseable)"
|
|
4757
|
+
|
|
4758
|
+
|
|
4759
|
+
def environment_notes() -> list[str]:
|
|
4760
|
+
"""What this process's environment changes about its config, for the diagnostics.
|
|
4761
|
+
|
|
4762
|
+
``config validate`` and ``hook doctor`` print these: a relative
|
|
4763
|
+
``PARTICLES_CONFIG`` (read against the working directory), every set
|
|
4764
|
+
override from :data:`_ENV_OVERRIDES`, and each endpoint variable that
|
|
4765
|
+
sends a secret or the prompts somewhere other than the pinned file says
|
|
4766
|
+
(security review N15, N16). An entry starting with ``⚠`` deserves a look.
|
|
4767
|
+
"""
|
|
4768
|
+
notes: list[str] = []
|
|
4769
|
+
explicit = os.environ.get("PARTICLES_CONFIG")
|
|
4770
|
+
if explicit and not Path(explicit).is_absolute():
|
|
4771
|
+
notes.append(
|
|
4772
|
+
f"⚠ PARTICLES_CONFIG is relative ({explicit}) and is read against the working "
|
|
4773
|
+
"directory; set an absolute path"
|
|
4774
|
+
)
|
|
4775
|
+
overrides = sorted(name for name in env_override_names() if name in os.environ)
|
|
4776
|
+
if overrides:
|
|
4777
|
+
notes.append("overrides set: " + ", ".join(overrides))
|
|
4778
|
+
for name in ENDPOINT_ENV_VARS:
|
|
4779
|
+
value = os.environ.get(name)
|
|
4780
|
+
if value:
|
|
4781
|
+
notes.append(f"⚠ {name} sends to {_endpoint_origin(value)}, overriding the config file")
|
|
4782
|
+
return notes
|
|
4783
|
+
|
|
4784
|
+
|
|
4590
4785
|
# ---------------------------------------------------------------------------
|
|
4591
4786
|
# Loader
|
|
4592
4787
|
# ---------------------------------------------------------------------------
|
|
4593
4788
|
|
|
4594
4789
|
|
|
4790
|
+
def user_config_path() -> Path:
|
|
4791
|
+
"""The per-user config file, ``~/.particles/config.yaml``.
|
|
4792
|
+
|
|
4793
|
+
Fixed under the home directory, beside the per-user state Particles already
|
|
4794
|
+
keeps there, and never configurable from a config. Computed per call so a
|
|
4795
|
+
test that swaps ``HOME`` sees its own.
|
|
4796
|
+
"""
|
|
4797
|
+
return Path.home() / ".particles" / "config.yaml"
|
|
4798
|
+
|
|
4799
|
+
|
|
4595
4800
|
def _find_config_file() -> Path | None:
|
|
4596
|
-
"""Resolve the ``config.yaml`` this process
|
|
4597
|
-
|
|
4598
|
-
Order: ``PARTICLES_CONFIG
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
every project beneath it: a config at a repo root is a deliberate project
|
|
4612
|
-
setting, one above it is an accident waiting to be inherited.
|
|
4801
|
+
"""Resolve the ``config.yaml`` this process loads.
|
|
4802
|
+
|
|
4803
|
+
Order: ``PARTICLES_CONFIG``, then the user file
|
|
4804
|
+
(:func:`user_config_path`), then nothing (compiled defaults).
|
|
4805
|
+
``PARTICLES_CONFIG`` is absolute authority: a named file that does not
|
|
4806
|
+
exist resolves to ``None`` rather than falling through to the user file,
|
|
4807
|
+
and :func:`_load_config` reports it.
|
|
4808
|
+
|
|
4809
|
+
Nothing is read from the working directory or its ancestors. The loader
|
|
4810
|
+
used to walk up from the working directory, which let a cloned
|
|
4811
|
+
repository that ships a ``config.yaml`` configure every unpinned process run
|
|
4812
|
+
inside it: rebind the engine, provider and telemetry endpoints the secrets
|
|
4813
|
+
are sent to, or turn on the MCP write tools (security review 2026-10-23,
|
|
4814
|
+
findings D1, D6, D8 and D15). A nearby file that is no longer read is
|
|
4815
|
+
reported instead (:func:`nearby_unused_config`).
|
|
4613
4816
|
"""
|
|
4614
4817
|
explicit = os.environ.get("PARTICLES_CONFIG")
|
|
4615
4818
|
if explicit:
|
|
4616
4819
|
p = Path(explicit)
|
|
4617
4820
|
return p if p.exists() else None
|
|
4821
|
+
user = user_config_path()
|
|
4822
|
+
return user if user.exists() else None
|
|
4823
|
+
|
|
4824
|
+
|
|
4825
|
+
#: Largest nearby file :func:`nearby_unused_config` will parse to see whether it
|
|
4826
|
+
#: is a Particles config. A larger file is not reported; it is read for no
|
|
4827
|
+
#: other purpose.
|
|
4828
|
+
_NEARBY_CONFIG_MAX_BYTES = 1024 * 1024
|
|
4829
|
+
|
|
4830
|
+
|
|
4831
|
+
def _read_bounded_regular_file(path: Path, limit: int) -> bytes | None:
|
|
4832
|
+
"""At most ``limit`` bytes of ``path``, or ``None`` when it is not a small regular file.
|
|
4618
4833
|
|
|
4619
|
-
|
|
4834
|
+
The file is opened non-blocking and checked with ``fstat`` on the open
|
|
4835
|
+
descriptor, so a FIFO or device swapped in after an ``is_file()`` check
|
|
4836
|
+
can neither hang the read nor feed it without end (security review N14).
|
|
4837
|
+
"""
|
|
4838
|
+
try:
|
|
4839
|
+
fd = os.open(path, os.O_RDONLY | getattr(os, "O_NONBLOCK", 0))
|
|
4840
|
+
except OSError:
|
|
4841
|
+
return None
|
|
4842
|
+
try:
|
|
4843
|
+
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
4844
|
+
return None
|
|
4845
|
+
chunks: list[bytes] = []
|
|
4846
|
+
remaining = limit + 1
|
|
4847
|
+
while remaining > 0:
|
|
4848
|
+
chunk = os.read(fd, remaining)
|
|
4849
|
+
if not chunk:
|
|
4850
|
+
break
|
|
4851
|
+
chunks.append(chunk)
|
|
4852
|
+
remaining -= len(chunk)
|
|
4853
|
+
except OSError:
|
|
4854
|
+
return None
|
|
4855
|
+
finally:
|
|
4856
|
+
os.close(fd)
|
|
4857
|
+
data = b"".join(chunks)
|
|
4858
|
+
return None if len(data) > limit else data
|
|
4859
|
+
|
|
4860
|
+
|
|
4861
|
+
def _names_a_particles_section(path: Path) -> bool:
|
|
4862
|
+
"""Whether ``path`` is a YAML mapping naming at least one Particles section.
|
|
4863
|
+
|
|
4864
|
+
``yaml.safe_load`` builds plain values only, so parsing a file nobody
|
|
4865
|
+
vouched for applies nothing. It can still fail in ways a YAML error does
|
|
4866
|
+
not cover (a deeply nested document raises ``RecursionError``), and this
|
|
4867
|
+
only decides whether to print a warning, so any failure means "no"
|
|
4868
|
+
(security review N14).
|
|
4869
|
+
"""
|
|
4870
|
+
data = _read_bounded_regular_file(path, _NEARBY_CONFIG_MAX_BYTES)
|
|
4871
|
+
if data is None:
|
|
4872
|
+
return False
|
|
4873
|
+
try:
|
|
4874
|
+
loaded = yaml.safe_load(data.decode("utf-8"))
|
|
4875
|
+
except Exception: # noqa: BLE001 - a warning path must never fail the load
|
|
4876
|
+
return False
|
|
4877
|
+
return isinstance(loaded, dict) and any(key in ParticlesConfig.model_fields for key in loaded)
|
|
4878
|
+
|
|
4879
|
+
|
|
4880
|
+
def nearby_unused_config(loaded: Path | None = None) -> Path | None:
|
|
4881
|
+
"""The nearest Particles ``config.yaml`` around the working directory, if unread.
|
|
4882
|
+
|
|
4883
|
+
Looks in the working directory and its ancestors, stopping after the
|
|
4884
|
+
directory that holds a ``.git`` entry (a file in a git worktree), the
|
|
4885
|
+
bounds walk used. Returns the nearest ``config.yaml`` when it
|
|
4886
|
+
names a Particles section and is not the file the loader read (``loaded``);
|
|
4887
|
+
another tool's ``config.yaml`` returns ``None``. Never applies anything.
|
|
4888
|
+
"""
|
|
4889
|
+
loaded_real = loaded.resolve() if loaded is not None else None
|
|
4890
|
+
try:
|
|
4891
|
+
start = Path.cwd()
|
|
4892
|
+
except OSError: # the working directory was removed
|
|
4893
|
+
return None
|
|
4620
4894
|
for directory in (start, *start.parents):
|
|
4621
4895
|
candidate = directory / "config.yaml"
|
|
4622
|
-
if candidate.
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
# the case this bound has to catch, since worktrees are where the
|
|
4627
|
-
# subdirectory-launch problem showed up.
|
|
4896
|
+
if candidate.is_file():
|
|
4897
|
+
if loaded_real is not None and candidate.resolve() == loaded_real:
|
|
4898
|
+
return None
|
|
4899
|
+
return candidate if _names_a_particles_section(candidate) else None
|
|
4628
4900
|
if (directory / ".git").exists():
|
|
4629
4901
|
return None
|
|
4630
4902
|
return None
|
|
4631
4903
|
|
|
4632
4904
|
|
|
4905
|
+
class ConfigResolution(NamedTuple):
|
|
4906
|
+
"""Where this process's settings come from, for ``config validate`` and ``hook doctor``.
|
|
4907
|
+
|
|
4908
|
+
``origin`` is ``"PARTICLES_CONFIG"``, ``"user file"`` or ``"none"``
|
|
4909
|
+
(compiled defaults). ``missing`` is the path ``PARTICLES_CONFIG`` names when
|
|
4910
|
+
that file does not exist; ``ignored`` is a nearby Particles config the
|
|
4911
|
+
loader does not read (:func:`nearby_unused_config`).
|
|
4912
|
+
"""
|
|
4913
|
+
|
|
4914
|
+
path: Path | None
|
|
4915
|
+
origin: str
|
|
4916
|
+
missing: Path | None
|
|
4917
|
+
ignored: Path | None
|
|
4918
|
+
|
|
4919
|
+
|
|
4920
|
+
def describe_config_resolution() -> ConfigResolution:
|
|
4921
|
+
"""Resolve the config origin without loading or caching it."""
|
|
4922
|
+
explicit = os.environ.get("PARTICLES_CONFIG")
|
|
4923
|
+
path = _find_config_file()
|
|
4924
|
+
if explicit:
|
|
4925
|
+
origin = "PARTICLES_CONFIG" if path is not None else "none"
|
|
4926
|
+
missing = None if path is not None else Path(explicit)
|
|
4927
|
+
return ConfigResolution(path, origin, missing, None)
|
|
4928
|
+
origin = "user file" if path is not None else "none"
|
|
4929
|
+
return ConfigResolution(path, origin, None, nearby_unused_config(path))
|
|
4930
|
+
|
|
4931
|
+
|
|
4932
|
+
_warned_resolution: set[str] = set()
|
|
4933
|
+
_resolution_warnings: list[str] = []
|
|
4934
|
+
|
|
4935
|
+
|
|
4936
|
+
def _warn_resolution_once(key: str, message: str) -> None:
|
|
4937
|
+
"""Log one config-resolution warning per process, and keep it for the hook log."""
|
|
4938
|
+
if key in _warned_resolution:
|
|
4939
|
+
return
|
|
4940
|
+
_warned_resolution.add(key)
|
|
4941
|
+
_resolution_warnings.append(message)
|
|
4942
|
+
log.warning("%s", message)
|
|
4943
|
+
|
|
4944
|
+
|
|
4945
|
+
def config_resolution_warnings() -> list[str]:
|
|
4946
|
+
"""The config-resolution warnings this process has emitted (§3).
|
|
4947
|
+
|
|
4948
|
+
The lifecycle hooks print nothing but their own output, so they copy these
|
|
4949
|
+
into the hook log instead.
|
|
4950
|
+
"""
|
|
4951
|
+
return list(_resolution_warnings)
|
|
4952
|
+
|
|
4953
|
+
|
|
4954
|
+
def _report_resolution(config_path: Path | None, cfg: ParticlesConfig) -> None:
|
|
4955
|
+
"""Warn about a missing or relative ``PARTICLES_CONFIG``, or a nearby config left unread."""
|
|
4956
|
+
explicit = os.environ.get("PARTICLES_CONFIG")
|
|
4957
|
+
if explicit:
|
|
4958
|
+
if config_path is not None and not Path(explicit).is_absolute():
|
|
4959
|
+
_warn_resolution_once(
|
|
4960
|
+
f"relative:{explicit}",
|
|
4961
|
+
f"PARTICLES_CONFIG names a relative path ({explicit}), so it is read against "
|
|
4962
|
+
f"the working directory as {config_path.resolve()}; a cloned repository with "
|
|
4963
|
+
"a file at that path would configure this process. Set an absolute path.",
|
|
4964
|
+
)
|
|
4965
|
+
if config_path is None:
|
|
4966
|
+
hint = (
|
|
4967
|
+
" The shell does not expand `~` inside quotes; write $HOME/... instead."
|
|
4968
|
+
if explicit.startswith("~")
|
|
4969
|
+
else ""
|
|
4970
|
+
)
|
|
4971
|
+
_warn_resolution_once(
|
|
4972
|
+
f"missing:{explicit}",
|
|
4973
|
+
f"PARTICLES_CONFIG names {explicit}, which does not exist; "
|
|
4974
|
+
f"running on compiled defaults.{hint}",
|
|
4975
|
+
)
|
|
4976
|
+
return
|
|
4977
|
+
if not cfg.cli.warn_unused_config:
|
|
4978
|
+
return
|
|
4979
|
+
try:
|
|
4980
|
+
nearby = nearby_unused_config(config_path)
|
|
4981
|
+
except Exception: # noqa: BLE001 - a warning path must never fail the load (N14)
|
|
4982
|
+
log.debug("nearby config check failed", exc_info=True)
|
|
4983
|
+
return
|
|
4984
|
+
if nearby is not None:
|
|
4985
|
+
_warn_resolution_once(
|
|
4986
|
+
f"ignored:{nearby}",
|
|
4987
|
+
f"Ignoring {nearby}: Particles no longer reads config.yaml from the working "
|
|
4988
|
+
f"directory. To use it, set PARTICLES_CONFIG={nearby}, or move it to "
|
|
4989
|
+
f"{user_config_path()}.",
|
|
4990
|
+
)
|
|
4991
|
+
|
|
4992
|
+
|
|
4633
4993
|
def sqlite_file_path(url: str) -> str | None:
|
|
4634
4994
|
"""Return a file-based SQLite URL's on-disk path, or ``None`` for memory/non-SQLite.
|
|
4635
4995
|
|
|
@@ -4647,6 +5007,28 @@ def sqlite_file_path(url: str) -> str | None:
|
|
|
4647
5007
|
return path
|
|
4648
5008
|
|
|
4649
5009
|
|
|
5010
|
+
def describe_default_store() -> str:
|
|
5011
|
+
"""Where the ``default`` store is, for a startup or diagnostic line.
|
|
5012
|
+
|
|
5013
|
+
A file-backed SQLite store reads as its absolute path, marked when the
|
|
5014
|
+
configured path is relative and so names a different file in each working
|
|
5015
|
+
directory. Any other DSN reads as its scheme only, so no credential in it
|
|
5016
|
+
is printed.
|
|
5017
|
+
"""
|
|
5018
|
+
url = get_config().storage.database_url
|
|
5019
|
+
db_file = sqlite_file_path(url)
|
|
5020
|
+
if db_file is None:
|
|
5021
|
+
return f"{url.split(':', 1)[0]} database"
|
|
5022
|
+
path = Path(db_file).expanduser()
|
|
5023
|
+
if path.is_absolute():
|
|
5024
|
+
return str(path)
|
|
5025
|
+
try:
|
|
5026
|
+
here = Path.cwd() / path
|
|
5027
|
+
except OSError:
|
|
5028
|
+
here = path
|
|
5029
|
+
return f"{os.path.normpath(here)} (relative to the working directory)"
|
|
5030
|
+
|
|
5031
|
+
|
|
4650
5032
|
def resolve_store_adjacent_path(value: str) -> Path:
|
|
4651
5033
|
"""Resolve a store-adjacent path, anchoring relative values to the store.
|
|
4652
5034
|
|
|
@@ -4714,7 +5096,9 @@ def _load_config() -> ParticlesConfig:
|
|
|
4714
5096
|
else:
|
|
4715
5097
|
raw[section][field] = value
|
|
4716
5098
|
|
|
4717
|
-
|
|
5099
|
+
cfg = ParticlesConfig.model_validate(raw)
|
|
5100
|
+
_report_resolution(config_path, cfg)
|
|
5101
|
+
return cfg
|
|
4718
5102
|
|
|
4719
5103
|
|
|
4720
5104
|
def validate_config() -> tuple[Path | None, ParticlesConfig]:
|
{linkedparticles_core-1.210.2 → linkedparticles_core-1.214.1}/particles/conformance/shacl.py
RENAMED
|
@@ -120,12 +120,19 @@ def _validate(doc: dict[str, Any], shape_name: str) -> ValidationResult:
|
|
|
120
120
|
import pyshacl # type: ignore[import-untyped,unused-ignore]
|
|
121
121
|
import rdflib
|
|
122
122
|
|
|
123
|
+
from particles.extraction.rdf import no_remote_retrieval
|
|
124
|
+
|
|
123
125
|
# Use Graph (not Dataset) for the data side: our particle documents are
|
|
124
126
|
# flat JSON-LD with no @graph array, so a single default graph is enough.
|
|
125
127
|
# Dataset triggers rdflib's own DeprecationWarnings for default_context
|
|
126
128
|
# and identifier in current rdflib releases.
|
|
127
129
|
data_graph = rdflib.Graph()
|
|
128
|
-
|
|
130
|
+
# The document is caller-supplied, so a string @context or an @import
|
|
131
|
+
# in it would make rdflib fetch that URL, with no SSRF check (D43,
|
|
132
|
+
# SECURITY_REVIEW_20261023-1). The serializers below embed the context
|
|
133
|
+
# inline; a document that names one remotely is refused, not fetched.
|
|
134
|
+
with no_remote_retrieval():
|
|
135
|
+
data_graph.parse(data=json.dumps(doc), format="json-ld")
|
|
129
136
|
shapes_graph = rdflib.Graph().parse(str(shape_path), format="turtle")
|
|
130
137
|
|
|
131
138
|
conforms, report_graph, report_text = pyshacl.validate(
|
|
@@ -331,6 +331,10 @@ class RelationCreatedBy(StrEnum):
|
|
|
331
331
|
# Written on a CONTRADICTS edge; the probe verdict it records was already
|
|
332
332
|
# paid for on the write path, so lint reports it without a second probe.
|
|
333
333
|
OBSERVER_DIVERGENCE = "OBSERVER_DIVERGENCE"
|
|
334
|
+
# An MCP agent's `link_add`. Kept distinct from MANUAL_CLI, the
|
|
335
|
+
# operator's `links add`, so an agent may withdraw only the links an agent
|
|
336
|
+
# wrote and the event log credits the agent, not the operator CLI.
|
|
337
|
+
AGENT_ASSERTED = "AGENT_ASSERTED"
|
|
334
338
|
|
|
335
339
|
|
|
336
340
|
class SuggestMode(StrEnum):
|