xrefkit 0.4.4__tar.gz → 0.4.5__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.
Files changed (109) hide show
  1. {xrefkit-0.4.4 → xrefkit-0.4.5}/PKG-INFO +45 -1
  2. {xrefkit-0.4.4 → xrefkit-0.4.5}/README.md +44 -0
  3. {xrefkit-0.4.4 → xrefkit-0.4.5}/pyproject.toml +1 -1
  4. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_mcp_setup.py +35 -0
  5. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/__init__.py +1 -1
  6. xrefkit-0.4.5/xrefkit/mcp/context_token.py +108 -0
  7. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/server.py +137 -17
  8. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/setup.py +13 -6
  9. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit.egg-info/PKG-INFO +45 -1
  10. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit.egg-info/SOURCES.txt +1 -0
  11. {xrefkit-0.4.4 → xrefkit-0.4.5}/LICENSE +0 -0
  12. {xrefkit-0.4.4 → xrefkit-0.4.5}/setup.cfg +0 -0
  13. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_base_sync_ownership.py +0 -0
  14. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_boundary_analysis.py +0 -0
  15. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_calibration_lint.py +0 -0
  16. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_check_skill_knowledge_xids.py +0 -0
  17. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_cli.py +0 -0
  18. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_collect_analyzer_sarif.py +0 -0
  19. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_convert_to_xrefkit_skill.py +0 -0
  20. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_cs_scope_probe.py +0 -0
  21. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_csharp_commonality.py +0 -0
  22. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_csharp_naming_profile.py +0 -0
  23. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_ctx.py +0 -0
  24. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_cutover_readiness.py +0 -0
  25. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_dashboard.py +0 -0
  26. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_error_policy_audit.py +0 -0
  27. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_error_policy_locator.py +0 -0
  28. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_fm_multiroot.py +0 -0
  29. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_gate.py +0 -0
  30. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_goal_desired_state.py +0 -0
  31. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_instruction_workflow.py +0 -0
  32. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_knowledge_relations_validator.py +0 -0
  33. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_ownership.py +0 -0
  34. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_packmeta.py +0 -0
  35. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_project_quality_baseline.py +0 -0
  36. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_resource_provider.py +0 -0
  37. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_runtime_contracts.py +0 -0
  38. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_sarif_to_locator.py +0 -0
  39. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_skill_runtime_audit.py +0 -0
  40. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_skillmeta.py +0 -0
  41. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_structure_catalog.py +0 -0
  42. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_xref.py +0 -0
  43. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_xrefkit_instance.py +0 -0
  44. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_xrefkit_tools.py +0 -0
  45. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_xrefkit_v2_discovery.py +0 -0
  46. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_xrefkit_v2_models.py +0 -0
  47. {xrefkit-0.4.4 → xrefkit-0.4.5}/tests/test_xrefkit_v2_pipeline.py +0 -0
  48. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/__main__.py +0 -0
  49. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/boundary_analysis.py +0 -0
  50. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/catalog_cli.py +0 -0
  51. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/cli.py +0 -0
  52. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/contracts.py +0 -0
  53. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/ctx.py +0 -0
  54. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/dashboard.py +0 -0
  55. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/discovery.py +0 -0
  56. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/gate.py +0 -0
  57. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/goalstate.py +0 -0
  58. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/hashing.py +0 -0
  59. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/import_skill.py +0 -0
  60. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/instance.py +0 -0
  61. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/loaders.py +0 -0
  62. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/__init__.py +0 -0
  63. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/audit.py +0 -0
  64. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/bootstrap.py +0 -0
  65. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/catalog.py +0 -0
  66. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/cli.py +0 -0
  67. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/client_cache.py +0 -0
  68. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/context_registry.py +0 -0
  69. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/contracts.py +0 -0
  70. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/dist.py +0 -0
  71. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/ownership.py +0 -0
  72. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/repository.py +0 -0
  73. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/schemas.py +0 -0
  74. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp/startup_contract_pack.py +0 -0
  75. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/mcp_tools.py +0 -0
  76. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/models/__init__.py +0 -0
  77. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/models/common.py +0 -0
  78. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/models/effective_bundle.py +0 -0
  79. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/models/local_manifest.py +0 -0
  80. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/models/package_manifest.py +0 -0
  81. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/models/run_log.py +0 -0
  82. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/models/server_config.py +0 -0
  83. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/models/skill_definition.py +0 -0
  84. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/operations_cli.py +0 -0
  85. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/ownership.py +0 -0
  86. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/packmeta.py +0 -0
  87. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/registry.py +0 -0
  88. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/resolver.py +0 -0
  89. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/resource_provider.py +0 -0
  90. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/resources/base/contracts.json +0 -0
  91. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/resources/base/current.json +0 -0
  92. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/resources/base/generations/7a682a5272907354/contracts.json +0 -0
  93. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/resources/base/generations/7a682a5272907354/model_body.md +0 -0
  94. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +0 -0
  95. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +0 -0
  96. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/resources/base/model_body.md +0 -0
  97. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/runlog.py +0 -0
  98. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/skillmeta.py +0 -0
  99. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/skillrun.py +0 -0
  100. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/structure_catalog.py +0 -0
  101. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/tools/__init__.py +0 -0
  102. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/tools/__main__.py +0 -0
  103. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/v2_cli.py +0 -0
  104. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/workspace.py +0 -0
  105. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit/xref.py +0 -0
  106. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit.egg-info/dependency_links.txt +0 -0
  107. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit.egg-info/entry_points.txt +0 -0
  108. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit.egg-info/requires.txt +0 -0
  109. {xrefkit-0.4.4 → xrefkit-0.4.5}/xrefkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xrefkit
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit
5
5
  Author: synthaicode
6
6
  License: MIT License
@@ -101,6 +101,38 @@ and handoff records from collapsing into one opaque instruction block.
101
101
 
102
102
  ![XRefKit repository snapshot](human-docs/en/assets/xrefkit_repository_snapshot/xrefkit_repository_snapshot.png)
103
103
 
104
+ ## Skills: what you can ask XRefKit to do
105
+
106
+ XRefKit includes Skills for common stages of software and business work. For
107
+ example:
108
+
109
+ - **Business discovery and planning**: `business_learning_interview`,
110
+ `business_intake_scoping`, `requirements_flow`, `planning_flow`, and
111
+ `estimation_flow`
112
+ - **Investigation, design, and implementation**: `investigation_flow`,
113
+ `source_structure_overview`, `db_current_state_analysis`, `db_design`,
114
+ `design_flow`, and `implementation_flow`
115
+ - **Constraint and change analysis**: `constraint_derivation_index`,
116
+ `dotnet_change_analysis`, `code_constraint_derivation`,
117
+ `cross_constraint_derivation`, and `integration_scenario_derivation`
118
+ - **Review and quality**: `csharp_review`, `python_review`, `security_review`,
119
+ `test_flow`, `qa_gate_review`, and `review_report_composition`
120
+ - **Knowledge and Skill operations**: `import_skill`, `skill_flow_authoring`,
121
+ `knowledge_ontology_management`, `judgment_log`, `doc_ship`, and
122
+ `skill_calibration_evaluation`
123
+ - **Documents and communication**: `pptx_spec_traceability`,
124
+ `xlsx_spec_traceability`, `presentation_flow_review`, `editorial_intake`,
125
+ `draft_authoring`, and `marketing_slide_png`
126
+
127
+ Each Skill is a reusable procedure with a capability/tuning/responsibility
128
+ identity. Its procedure, routing metadata, domain Knowledge, and run evidence
129
+ remain separate. Describe your goal in natural language and XRefKit routes it
130
+ to the relevant Skill; the [Skill catalog](skills/_index.md#xid-8D91F66DDBB7)
131
+ contains the complete current list and summaries.
132
+
133
+ To install and enable Skills distributed as Python Packages, follow the
134
+ [package-first registration guide](docs/guides/089_xrefkit_package_first_registration.md#xid-4F8C2A7D1E90).
135
+
104
136
  ## How It Works
105
137
 
106
138
  1. Original materials are kept in `sources/`.
@@ -196,6 +228,18 @@ searches and XID resolutions then share the same `run_id` as the client Skill
196
228
  Run. The client separately records actual model-context loading and judgment
197
229
  application with `xrefkit skill knowledge --action load|apply`.
198
230
 
231
+ For a network deployment without MCP protocol sessions, provide a shared HMAC
232
+ secret and enable stateless HTTP. The client must return the `context_id` from
233
+ `get_startup_context` in `_meta.io.xrefkit/context_id` on later requests:
234
+
235
+ ```powershell
236
+ $env:XREFKIT_CONTEXT_SECRET = 'replace-with-a-managed-secret'
237
+ python -m xrefkit.mcp.server --repo . --transport streamable-http --stateless-http
238
+ ```
239
+
240
+ The context token is signed, repository-bound, and short-lived. It is an
241
+ execution-context token, not a canonical XID or an authorization grant.
242
+
199
243
  ## Skill Run Observation Dashboard
200
244
 
201
245
  The local dashboard lets a human inspect Skill run status, closure and quality
@@ -62,6 +62,38 @@ and handoff records from collapsing into one opaque instruction block.
62
62
 
63
63
  ![XRefKit repository snapshot](human-docs/en/assets/xrefkit_repository_snapshot/xrefkit_repository_snapshot.png)
64
64
 
65
+ ## Skills: what you can ask XRefKit to do
66
+
67
+ XRefKit includes Skills for common stages of software and business work. For
68
+ example:
69
+
70
+ - **Business discovery and planning**: `business_learning_interview`,
71
+ `business_intake_scoping`, `requirements_flow`, `planning_flow`, and
72
+ `estimation_flow`
73
+ - **Investigation, design, and implementation**: `investigation_flow`,
74
+ `source_structure_overview`, `db_current_state_analysis`, `db_design`,
75
+ `design_flow`, and `implementation_flow`
76
+ - **Constraint and change analysis**: `constraint_derivation_index`,
77
+ `dotnet_change_analysis`, `code_constraint_derivation`,
78
+ `cross_constraint_derivation`, and `integration_scenario_derivation`
79
+ - **Review and quality**: `csharp_review`, `python_review`, `security_review`,
80
+ `test_flow`, `qa_gate_review`, and `review_report_composition`
81
+ - **Knowledge and Skill operations**: `import_skill`, `skill_flow_authoring`,
82
+ `knowledge_ontology_management`, `judgment_log`, `doc_ship`, and
83
+ `skill_calibration_evaluation`
84
+ - **Documents and communication**: `pptx_spec_traceability`,
85
+ `xlsx_spec_traceability`, `presentation_flow_review`, `editorial_intake`,
86
+ `draft_authoring`, and `marketing_slide_png`
87
+
88
+ Each Skill is a reusable procedure with a capability/tuning/responsibility
89
+ identity. Its procedure, routing metadata, domain Knowledge, and run evidence
90
+ remain separate. Describe your goal in natural language and XRefKit routes it
91
+ to the relevant Skill; the [Skill catalog](skills/_index.md#xid-8D91F66DDBB7)
92
+ contains the complete current list and summaries.
93
+
94
+ To install and enable Skills distributed as Python Packages, follow the
95
+ [package-first registration guide](docs/guides/089_xrefkit_package_first_registration.md#xid-4F8C2A7D1E90).
96
+
65
97
  ## How It Works
66
98
 
67
99
  1. Original materials are kept in `sources/`.
@@ -157,6 +189,18 @@ searches and XID resolutions then share the same `run_id` as the client Skill
157
189
  Run. The client separately records actual model-context loading and judgment
158
190
  application with `xrefkit skill knowledge --action load|apply`.
159
191
 
192
+ For a network deployment without MCP protocol sessions, provide a shared HMAC
193
+ secret and enable stateless HTTP. The client must return the `context_id` from
194
+ `get_startup_context` in `_meta.io.xrefkit/context_id` on later requests:
195
+
196
+ ```powershell
197
+ $env:XREFKIT_CONTEXT_SECRET = 'replace-with-a-managed-secret'
198
+ python -m xrefkit.mcp.server --repo . --transport streamable-http --stateless-http
199
+ ```
200
+
201
+ The context token is signed, repository-bound, and short-lived. It is an
202
+ execution-context token, not a canonical XID or an authorization grant.
203
+
160
204
  ## Skill Run Observation Dashboard
161
205
 
162
206
  The local dashboard lets a human inspect Skill run status, closure and quality
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xrefkit"
7
- version = "0.4.4"
7
+ version = "0.4.5"
8
8
  description = "Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -55,6 +55,41 @@ def test_mcp_setup_imports_existing_batch_skill_before_writing_report(tmp_path:
55
55
  assert payload["output"] == str(output.resolve())
56
56
 
57
57
 
58
+ def test_mcp_setup_infers_single_skill_id_without_explicit_option(tmp_path: Path, capsys) -> None:
59
+ source = tmp_path / "existing-skill"
60
+ source.mkdir()
61
+ (source / "SKILL.md").write_text("# Existing Skill\n\nUse the supplied workflow.\n", encoding="utf-8")
62
+ root = tmp_path / "repo"
63
+ output = tmp_path / "setup-output"
64
+
65
+ exit_code = main(
66
+ [
67
+ "mcp",
68
+ "setup",
69
+ "--repo",
70
+ str(root),
71
+ "--import",
72
+ str(source),
73
+ "--output",
74
+ str(output),
75
+ "--json",
76
+ ]
77
+ )
78
+ payload = json.loads(capsys.readouterr().out)
79
+ assert exit_code in {0, 1}
80
+ assert (root / "skills" / "existing-skill" / "SKILL.md").is_file()
81
+ report = json.loads((output / "import-report.json").read_text(encoding="utf-8"))
82
+ assert report["import"]["skill_id"] == "existing-skill"
83
+ assert payload["output"] == str(output.resolve())
84
+
85
+
86
+ def test_mcp_setup_defaults_repo_to_current_directory(tmp_path: Path, monkeypatch, capsys) -> None:
87
+ monkeypatch.chdir(tmp_path)
88
+ assert main(["mcp", "setup", "--json"]) == 0
89
+ payload = json.loads(capsys.readouterr().out)
90
+ assert payload["ok"] is True
91
+
92
+
58
93
  def test_mcp_setup_apply_copies_config_and_appends_instructions(tmp_path: Path, capsys) -> None:
59
94
  output = tmp_path / "setup-output"
60
95
  root = tmp_path / "repo"
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.4.4"
5
+ __version__ = "0.4.5"
@@ -0,0 +1,108 @@
1
+ """Signed, client-carried context for stateless MCP requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import hmac
8
+ import json
9
+ import time
10
+ import uuid
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+
15
+ CONTEXT_META_KEY = "io.xrefkit/context_id"
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ContextClaims:
20
+ context_id: str
21
+ repository_fingerprint: str
22
+ startup_loaded: bool = False
23
+ client_tools_unlocked: bool = False
24
+ run_id: str | None = None
25
+ skill_id: str | None = None
26
+ mcp_session_id: str | None = None
27
+ expires_at: int = 0
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return {
31
+ "context_id": self.context_id,
32
+ "repository_fingerprint": self.repository_fingerprint,
33
+ "startup_loaded": self.startup_loaded,
34
+ "client_tools_unlocked": self.client_tools_unlocked,
35
+ "run_id": self.run_id,
36
+ "skill_id": self.skill_id,
37
+ "mcp_session_id": self.mcp_session_id,
38
+ "expires_at": self.expires_at,
39
+ }
40
+
41
+
42
+ class ContextTokenCodec:
43
+ def __init__(self, secret: str, repository_fingerprint: str, *, ttl_seconds: int = 3600) -> None:
44
+ if not secret:
45
+ raise ValueError("context token secret is required")
46
+ self._secret = secret.encode("utf-8")
47
+ self.repository_fingerprint = repository_fingerprint
48
+ self.ttl_seconds = ttl_seconds
49
+
50
+ def issue(
51
+ self,
52
+ *,
53
+ startup_loaded: bool = False,
54
+ client_tools_unlocked: bool = False,
55
+ run_id: str | None = None,
56
+ skill_id: str | None = None,
57
+ mcp_session_id: str | None = None,
58
+ context_id: str | None = None,
59
+ ) -> str:
60
+ claims = ContextClaims(
61
+ context_id=context_id or f"xc_{uuid.uuid4().hex}",
62
+ repository_fingerprint=self.repository_fingerprint,
63
+ startup_loaded=startup_loaded,
64
+ client_tools_unlocked=client_tools_unlocked,
65
+ run_id=run_id,
66
+ skill_id=skill_id,
67
+ mcp_session_id=mcp_session_id,
68
+ expires_at=int(time.time()) + self.ttl_seconds,
69
+ )
70
+ body = _encode(claims.to_dict())
71
+ return f"v1.{body}.{self._signature(body)}"
72
+
73
+ def verify(self, token: str) -> ContextClaims:
74
+ try:
75
+ version, body, signature = str(token).split(".", 2)
76
+ if version != "v1" or not hmac.compare_digest(signature, self._signature(body)):
77
+ raise ValueError("invalid context token signature")
78
+ payload = json.loads(_decode(body))
79
+ if payload["repository_fingerprint"] != self.repository_fingerprint:
80
+ raise ValueError("context token repository mismatch")
81
+ if int(payload["expires_at"]) < int(time.time()):
82
+ raise ValueError("context token expired")
83
+ return ContextClaims(**payload)
84
+ except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
85
+ raise ValueError("invalid context token") from exc
86
+
87
+ def refresh(self, claims: ContextClaims, **updates: Any) -> str:
88
+ values = claims.to_dict()
89
+ values.update(updates)
90
+ values.pop("context_id", None)
91
+ return self.issue(context_id=claims.context_id, **{k: values[k] for k in (
92
+ "startup_loaded", "client_tools_unlocked", "run_id", "skill_id", "mcp_session_id"
93
+ )})
94
+
95
+ def _signature(self, body: str) -> str:
96
+ return _b64(hmac.new(self._secret, body.encode("ascii"), hashlib.sha256).digest())
97
+
98
+
99
+ def _b64(value: bytes) -> str:
100
+ return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=")
101
+
102
+
103
+ def _encode(value: dict[str, Any]) -> str:
104
+ return _b64(json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"))
105
+
106
+
107
+ def _decode(value: str) -> str:
108
+ return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)).decode("utf-8")
@@ -3,7 +3,9 @@ from __future__ import annotations
3
3
  import argparse
4
4
  import asyncio
5
5
  import logging
6
+ import os
6
7
  import sys
8
+ import uuid
7
9
  import weakref
8
10
  from pathlib import Path
9
11
  from typing import Any
@@ -11,6 +13,7 @@ from typing import Any
11
13
  from . import __version__
12
14
  from .audit import McpAuditLog, SessionRunBinding, SessionRunRegistry
13
15
  from .catalog import XRefCatalog
16
+ from .context_token import CONTEXT_META_KEY, ContextClaims, ContextTokenCodec
14
17
  from .dist import DIST_ROUTE_PATH, ArtifactDistribution, add_dist_routes
15
18
  from xrefkit.structure_catalog import get_entry as get_structure_entry
16
19
  from xrefkit.structure_catalog import list_findings as list_structure_findings
@@ -49,6 +52,11 @@ def _mark_startup_loaded(ctx: Any) -> None:
49
52
 
50
53
 
51
54
  def _require_startup_loaded(ctx: Any, tool_name: str) -> None:
55
+ claims = _context_claims(ctx)
56
+ if claims is not None:
57
+ if not claims.startup_loaded:
58
+ raise RuntimeError(f"XREFKIT_STARTUP_REQUIRED: call get_startup_context before {tool_name}")
59
+ return
52
60
  session = _session_of(ctx)
53
61
  if session is not None and session not in _STARTUP_LOADED_SESSIONS:
54
62
  raise RuntimeError(
@@ -72,6 +80,7 @@ def _with_control_reminder(result: dict[str, Any]) -> dict[str, Any]:
72
80
  # per-Skill flag would make distribution unreachable for Skills that don't
73
81
  # declare required_tools even though the general tool catalog still applies.
74
82
  _CLIENT_TOOLS_UNLOCKED_SESSIONS: "weakref.WeakSet[Any]" = weakref.WeakSet()
83
+ _CONTEXT_CODEC: ContextTokenCodec | None = None
75
84
 
76
85
 
77
86
  def _unlock_client_tools(ctx: Any) -> None:
@@ -80,7 +89,46 @@ def _unlock_client_tools(ctx: Any) -> None:
80
89
  _CLIENT_TOOLS_UNLOCKED_SESSIONS.add(session)
81
90
 
82
91
 
92
+ def _context_claims(ctx: Any) -> ContextClaims | None:
93
+ if _CONTEXT_CODEC is None:
94
+ return None
95
+ meta = getattr(getattr(ctx, "request_context", None), "meta", None)
96
+ extra = getattr(meta, "model_extra", None) or (meta if isinstance(meta, dict) else {})
97
+ token = extra.get(CONTEXT_META_KEY)
98
+ if token is None:
99
+ return None
100
+ try:
101
+ return _CONTEXT_CODEC.verify(str(token))
102
+ except ValueError as exc:
103
+ raise RuntimeError("XREFKIT_CONTEXT_INVALID: context_id could not be verified") from exc
104
+
105
+
106
+ def _context_token(claims: ContextClaims | None) -> str | None:
107
+ if _CONTEXT_CODEC is None or claims is None:
108
+ return None
109
+ return _CONTEXT_CODEC.refresh(claims)
110
+
111
+
112
+ def _binding_for(ctx: Any, run_registry: SessionRunRegistry) -> SessionRunBinding | None:
113
+ claims = _context_claims(ctx)
114
+ if claims is not None and claims.run_id and claims.skill_id and claims.mcp_session_id:
115
+ return SessionRunBinding(
116
+ run_id=claims.run_id,
117
+ mcp_session_id=claims.mcp_session_id,
118
+ repository_fingerprint=claims.repository_fingerprint,
119
+ skill_id=claims.skill_id,
120
+ )
121
+ return run_registry.current(_session_of(ctx))
122
+
123
+
83
124
  def _require_client_tools_unlocked(ctx: Any, tool_name: str) -> None:
125
+ claims = _context_claims(ctx)
126
+ if claims is not None:
127
+ if not claims.client_tools_unlocked:
128
+ raise RuntimeError(
129
+ f"XREFKIT_SKILL_SELECTION_REQUIRED: call get_skill or get_skill_requirements before {tool_name}"
130
+ )
131
+ return
84
132
  session = _session_of(ctx)
85
133
  if session is not None and session not in _CLIENT_TOOLS_UNLOCKED_SESSIONS:
86
134
  raise RuntimeError(
@@ -139,6 +187,15 @@ def main(argv: list[str] | None = None) -> int:
139
187
  action="store_true",
140
188
  help="Enable /dist executable artifacts after deployment trust is configured.",
141
189
  )
190
+ parser.add_argument(
191
+ "--stateless-http",
192
+ action="store_true",
193
+ help="Serve Streamable HTTP without MCP protocol sessions; requires XREFKIT_CONTEXT_SECRET.",
194
+ )
195
+ parser.add_argument(
196
+ "--context-secret",
197
+ help="HMAC secret for client-carried XRefKit context tokens; defaults to XREFKIT_CONTEXT_SECRET.",
198
+ )
142
199
  parser.add_argument(
143
200
  "--distribution-trust-id",
144
201
  help="Out-of-band pinned release manifest or signing-key identity.",
@@ -156,6 +213,11 @@ def main(argv: list[str] | None = None) -> int:
156
213
  help="Structured MCP audit JSONL path. Defaults to <repo>/work/mcp/xid_audit.jsonl.",
157
214
  )
158
215
  args = parser.parse_args(argv)
216
+ if args.stateless_http and args.transport != "streamable-http":
217
+ parser.error("--stateless-http requires --transport streamable-http")
218
+ context_secret = args.context_secret or os.environ.get("XREFKIT_CONTEXT_SECRET")
219
+ if args.stateless_http and not context_secret:
220
+ parser.error("--stateless-http requires --context-secret or XREFKIT_CONTEXT_SECRET")
159
221
  try:
160
222
  _validate_tls_configuration(
161
223
  args.transport,
@@ -177,6 +239,11 @@ def main(argv: list[str] | None = None) -> int:
177
239
  parser.error(str(exc))
178
240
 
179
241
  catalog = XRefCatalog.build(Path(args.repo), args.domain_knowledge_root)
242
+ global _CONTEXT_CODEC
243
+ _CONTEXT_CODEC = ContextTokenCodec(
244
+ context_secret or os.urandom(32).hex(),
245
+ catalog.repository_fingerprint,
246
+ )
180
247
  audit_log = McpAuditLog(args.audit_log or (Path(args.repo) / "work" / "mcp" / "xid_audit.jsonl"))
181
248
  run_registry = SessionRunRegistry()
182
249
 
@@ -212,6 +279,7 @@ def main(argv: list[str] | None = None) -> int:
212
279
  port=args.port,
213
280
  streamable_http_path=args.http_path,
214
281
  log_level=args.log_level.upper(),
282
+ stateless_http=args.stateless_http,
215
283
  )
216
284
 
217
285
  @app.tool()
@@ -221,14 +289,29 @@ def main(argv: list[str] | None = None) -> int:
221
289
  @app.tool()
222
290
  def bind_skill_run(ctx: Context, run_id: str, skill_id: str) -> dict[str, Any]:
223
291
  _require_startup_loaded(ctx, "bind_skill_run")
224
- binding = run_registry.bind(
225
- _session_of(ctx),
226
- run_id=run_id,
227
- repository_fingerprint=catalog.repository_fingerprint,
228
- skill_id=skill_id,
229
- )
292
+ claims = _context_claims(ctx)
293
+ if claims is not None:
294
+ try:
295
+ normalized_run_id = str(uuid.UUID(str(run_id)))
296
+ except ValueError as exc:
297
+ raise ValueError(f"run_id must be a UUID: {run_id}") from exc
298
+ if claims.run_id and claims.run_id != normalized_run_id:
299
+ raise ValueError("context_id is already bound to a different Skill Run")
300
+ binding = SessionRunBinding(
301
+ run_id=normalized_run_id,
302
+ mcp_session_id=claims.mcp_session_id or claims.context_id,
303
+ repository_fingerprint=catalog.repository_fingerprint,
304
+ skill_id=skill_id,
305
+ )
306
+ else:
307
+ binding = run_registry.bind(
308
+ _session_of(ctx),
309
+ run_id=run_id,
310
+ repository_fingerprint=catalog.repository_fingerprint,
311
+ skill_id=skill_id,
312
+ )
230
313
  audit_log.append("run.bound", binding=binding, tool="bind_skill_run")
231
- return {
314
+ result = {
232
315
  **binding.to_dict(),
233
316
  "audit_enabled": True,
234
317
  "client_record_command": (
@@ -237,11 +320,26 @@ def main(argv: list[str] | None = None) -> int:
237
320
  f"--repository-fingerprint {binding.repository_fingerprint}"
238
321
  ),
239
322
  }
323
+ if claims is not None:
324
+ result["context_id"] = _CONTEXT_CODEC.refresh(
325
+ claims,
326
+ startup_loaded=True,
327
+ run_id=binding.run_id,
328
+ skill_id=binding.skill_id,
329
+ mcp_session_id=binding.mcp_session_id,
330
+ )
331
+ return result
240
332
 
241
333
  @app.tool()
242
334
  def end_skill_run(ctx: Context, run_id: str) -> dict[str, str]:
243
335
  _require_startup_loaded(ctx, "end_skill_run")
244
- binding = run_registry.end(_session_of(ctx), run_id=run_id)
336
+ claims = _context_claims(ctx)
337
+ if claims is not None:
338
+ binding = _binding_for(ctx, run_registry)
339
+ if binding is None or binding.run_id != run_id:
340
+ raise ValueError("context_id does not match the active MCP Skill Run binding")
341
+ else:
342
+ binding = run_registry.end(_session_of(ctx), run_id=run_id)
245
343
  audit_log.append("run.ended", binding=binding, tool="end_skill_run")
246
344
  return binding.to_dict()
247
345
 
@@ -254,6 +352,12 @@ def main(argv: list[str] | None = None) -> int:
254
352
  for xid in result.get("load_order", []):
255
353
  _log_xid_query("get_startup_context", xid)
256
354
  _mark_startup_loaded(ctx)
355
+ if _CONTEXT_CODEC is not None:
356
+ claims = _context_claims(ctx)
357
+ if claims is None:
358
+ result["context_id"] = _CONTEXT_CODEC.issue(startup_loaded=True)
359
+ else:
360
+ result["context_id"] = _CONTEXT_CODEC.refresh(claims, startup_loaded=True)
257
361
  if dist is not None:
258
362
  result = _with_artifact_distribution(result, dist, dist_base_url)
259
363
  return result
@@ -293,7 +397,7 @@ def main(argv: list[str] | None = None) -> int:
293
397
 
294
398
  @app.tool()
295
399
  def search_knowledge_catalog(ctx: Context, query: str, limit: int = 10) -> list[dict[str, Any]]:
296
- binding = run_registry.current(_session_of(ctx))
400
+ binding = _binding_for(ctx, run_registry)
297
401
  result = catalog.search_knowledge_catalog(query, limit)
298
402
  if binding is not None:
299
403
  audit_log.append(
@@ -309,7 +413,7 @@ def main(argv: list[str] | None = None) -> int:
309
413
  @app.tool()
310
414
  def get_knowledge_summary(ctx: Context, xid: str) -> dict[str, Any]:
311
415
  _require_startup_loaded(ctx, "get_knowledge_summary")
312
- binding = run_registry.current(_session_of(ctx))
416
+ binding = _binding_for(ctx, run_registry)
313
417
  result = catalog.expand_knowledge(xid)["entry"]
314
418
  _log_xid_query(
315
419
  "get_knowledge_summary",
@@ -323,7 +427,7 @@ def main(argv: list[str] | None = None) -> int:
323
427
  @app.tool()
324
428
  def expand_knowledge(ctx: Context, xid: str) -> dict[str, Any]:
325
429
  _require_startup_loaded(ctx, "expand_knowledge")
326
- binding = run_registry.current(_session_of(ctx))
430
+ binding = _binding_for(ctx, run_registry)
327
431
  result = catalog.expand_knowledge(xid)
328
432
  _log_xid_query(
329
433
  "expand_knowledge",
@@ -341,7 +445,7 @@ def main(argv: list[str] | None = None) -> int:
341
445
  known_version: str | None = None,
342
446
  ) -> dict[str, Any]:
343
447
  _require_startup_loaded(ctx, "get_document_by_xid")
344
- binding = run_registry.current(_session_of(ctx))
448
+ binding = _binding_for(ctx, run_registry)
345
449
  result = catalog.get_document_by_xid(xid, known_version)
346
450
  _log_xid_query(
347
451
  "get_document_by_xid",
@@ -357,7 +461,7 @@ def main(argv: list[str] | None = None) -> int:
357
461
  @app.tool()
358
462
  def build_knowledge_context(ctx: Context, query: str, limit: int = 5) -> dict[str, Any]:
359
463
  _require_startup_loaded(ctx, "build_knowledge_context")
360
- binding = run_registry.current(_session_of(ctx))
464
+ binding = _binding_for(ctx, run_registry)
361
465
  result = catalog.build_knowledge_context(query, limit)
362
466
  if binding is not None:
363
467
  audit_log.append(
@@ -403,7 +507,7 @@ def main(argv: list[str] | None = None) -> int:
403
507
  known_document_versions: dict[str, str] | None = None,
404
508
  ) -> dict[str, Any]:
405
509
  _require_startup_loaded(ctx, "get_skill")
406
- binding = run_registry.current(_session_of(ctx))
510
+ binding = _binding_for(ctx, run_registry)
407
511
  if binding is not None and binding.skill_id != skill_id:
408
512
  raise ValueError(
409
513
  f"get_skill requested {skill_id!r}, but the active Skill Run is bound to {binding.skill_id!r}"
@@ -417,14 +521,30 @@ def main(argv: list[str] | None = None) -> int:
417
521
  selected_skill=skill_id,
418
522
  )
419
523
  _unlock_client_tools(ctx)
420
- return _with_control_reminder(result)
524
+ response = _with_control_reminder(result)
525
+ claims = _context_claims(ctx)
526
+ if claims is not None:
527
+ response["context_id"] = _CONTEXT_CODEC.refresh(
528
+ claims,
529
+ startup_loaded=True,
530
+ client_tools_unlocked=True,
531
+ )
532
+ return response
421
533
 
422
534
  @app.tool()
423
535
  def get_skill_requirements(ctx: Context, skill_id: str) -> dict[str, Any]:
424
536
  _require_startup_loaded(ctx, "get_skill_requirements")
425
537
  result = catalog.get_skill_requirements(skill_id)
426
538
  _unlock_client_tools(ctx)
427
- return _with_control_reminder(result)
539
+ response = _with_control_reminder(result)
540
+ claims = _context_claims(ctx)
541
+ if claims is not None:
542
+ response["context_id"] = _CONTEXT_CODEC.refresh(
543
+ claims,
544
+ startup_loaded=True,
545
+ client_tools_unlocked=True,
546
+ )
547
+ return response
428
548
 
429
549
  @app.tool()
430
550
  def resolve_skill_knowledge(ctx: Context, skill_id: str) -> dict[str, Any]:
@@ -433,7 +553,7 @@ def main(argv: list[str] | None = None) -> int:
433
553
 
434
554
  @app.tool()
435
555
  def rank_skills_for_purpose(ctx: Context, purpose: str, limit: int = 5) -> list[dict[str, Any]]:
436
- binding = run_registry.current(_session_of(ctx))
556
+ binding = _binding_for(ctx, run_registry)
437
557
  result = catalog.rank_skills_for_purpose(purpose, limit)
438
558
  if binding is not None:
439
559
  audit_log.append(
@@ -135,7 +135,7 @@ def setup(args: argparse.Namespace) -> int:
135
135
  metas: list[Path] = []
136
136
 
137
137
  if args.import_source:
138
- from xrefkit.import_skill import convert_skill, convert_skill_tree
138
+ from xrefkit.import_skill import _find_skill_doc, _slug, convert_skill, convert_skill_tree
139
139
 
140
140
  source = Path(args.import_source).resolve()
141
141
  if (source / "skills").is_dir() or args.batch:
@@ -148,14 +148,20 @@ def setup(args: argparse.Namespace) -> int:
148
148
  )
149
149
  import_report = result.to_dict()
150
150
  else:
151
- if not args.skill_id:
152
- raise SystemExit("--skill-id is required when --import is a single Skill directory")
151
+ try:
152
+ _find_skill_doc(source, None)
153
+ except FileNotFoundError as exc:
154
+ raise SystemExit(
155
+ "--import must point to a Skill directory containing SKILL.md or README.md, "
156
+ "or a directory containing skills/"
157
+ ) from exc
158
+ skill_id = args.skill_id or _slug(source.name)
153
159
  result = convert_skill(
154
160
  source_dir=source,
155
161
  source_root=source.parent,
156
162
  repo_root=root,
157
- skill_id=args.skill_id,
158
- target_skill_dir=root / "skills" / args.skill_id,
163
+ skill_id=skill_id,
164
+ target_skill_dir=root / "skills" / skill_id,
159
165
  dry_run=False,
160
166
  )
161
167
  import_report = result.to_dict()
@@ -211,7 +217,7 @@ def main(argv: list[str] | None = None) -> int:
211
217
  parser = argparse.ArgumentParser(prog="xrefkit mcp")
212
218
  sub = parser.add_subparsers(dest="command", required=True)
213
219
  setup_parser = sub.add_parser("setup", help="import Skills and generate reviewable MCP setup artifacts")
214
- setup_parser.add_argument("--repo", required=True)
220
+ setup_parser.add_argument("--repo", default=".", help="Target XRefKit repository; defaults to the current directory")
215
221
  setup_parser.add_argument("--import", dest="import_source", default=None, help="Skill directory or batch root")
216
222
  setup_parser.add_argument("--batch", action="store_true")
217
223
  setup_parser.add_argument("--skill-id-prefix", default="imported")
@@ -232,3 +238,4 @@ def main(argv: list[str] | None = None) -> int:
232
238
 
233
239
  if __name__ == "__main__":
234
240
  raise SystemExit(main())
241
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xrefkit
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit
5
5
  Author: synthaicode
6
6
  License: MIT License
@@ -101,6 +101,38 @@ and handoff records from collapsing into one opaque instruction block.
101
101
 
102
102
  ![XRefKit repository snapshot](human-docs/en/assets/xrefkit_repository_snapshot/xrefkit_repository_snapshot.png)
103
103
 
104
+ ## Skills: what you can ask XRefKit to do
105
+
106
+ XRefKit includes Skills for common stages of software and business work. For
107
+ example:
108
+
109
+ - **Business discovery and planning**: `business_learning_interview`,
110
+ `business_intake_scoping`, `requirements_flow`, `planning_flow`, and
111
+ `estimation_flow`
112
+ - **Investigation, design, and implementation**: `investigation_flow`,
113
+ `source_structure_overview`, `db_current_state_analysis`, `db_design`,
114
+ `design_flow`, and `implementation_flow`
115
+ - **Constraint and change analysis**: `constraint_derivation_index`,
116
+ `dotnet_change_analysis`, `code_constraint_derivation`,
117
+ `cross_constraint_derivation`, and `integration_scenario_derivation`
118
+ - **Review and quality**: `csharp_review`, `python_review`, `security_review`,
119
+ `test_flow`, `qa_gate_review`, and `review_report_composition`
120
+ - **Knowledge and Skill operations**: `import_skill`, `skill_flow_authoring`,
121
+ `knowledge_ontology_management`, `judgment_log`, `doc_ship`, and
122
+ `skill_calibration_evaluation`
123
+ - **Documents and communication**: `pptx_spec_traceability`,
124
+ `xlsx_spec_traceability`, `presentation_flow_review`, `editorial_intake`,
125
+ `draft_authoring`, and `marketing_slide_png`
126
+
127
+ Each Skill is a reusable procedure with a capability/tuning/responsibility
128
+ identity. Its procedure, routing metadata, domain Knowledge, and run evidence
129
+ remain separate. Describe your goal in natural language and XRefKit routes it
130
+ to the relevant Skill; the [Skill catalog](skills/_index.md#xid-8D91F66DDBB7)
131
+ contains the complete current list and summaries.
132
+
133
+ To install and enable Skills distributed as Python Packages, follow the
134
+ [package-first registration guide](docs/guides/089_xrefkit_package_first_registration.md#xid-4F8C2A7D1E90).
135
+
104
136
  ## How It Works
105
137
 
106
138
  1. Original materials are kept in `sources/`.
@@ -196,6 +228,18 @@ searches and XID resolutions then share the same `run_id` as the client Skill
196
228
  Run. The client separately records actual model-context loading and judgment
197
229
  application with `xrefkit skill knowledge --action load|apply`.
198
230
 
231
+ For a network deployment without MCP protocol sessions, provide a shared HMAC
232
+ secret and enable stateless HTTP. The client must return the `context_id` from
233
+ `get_startup_context` in `_meta.io.xrefkit/context_id` on later requests:
234
+
235
+ ```powershell
236
+ $env:XREFKIT_CONTEXT_SECRET = 'replace-with-a-managed-secret'
237
+ python -m xrefkit.mcp.server --repo . --transport streamable-http --stateless-http
238
+ ```
239
+
240
+ The context token is signed, repository-bound, and short-lived. It is an
241
+ execution-context token, not a canonical XID or an authorization grant.
242
+
199
243
  ## Skill Run Observation Dashboard
200
244
 
201
245
  The local dashboard lets a human inspect Skill run status, closure and quality
@@ -79,6 +79,7 @@ xrefkit/mcp/catalog.py
79
79
  xrefkit/mcp/cli.py
80
80
  xrefkit/mcp/client_cache.py
81
81
  xrefkit/mcp/context_registry.py
82
+ xrefkit/mcp/context_token.py
82
83
  xrefkit/mcp/contracts.py
83
84
  xrefkit/mcp/dist.py
84
85
  xrefkit/mcp/ownership.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes