delimit-cli 4.1.44 → 4.1.48
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.
- package/CHANGELOG.md +22 -0
- package/bin/delimit-cli.js +365 -30
- package/bin/delimit-setup.js +115 -81
- package/gateway/ai/activate_helpers.py +253 -7
- package/gateway/ai/backends/gateway_core.py +236 -13
- package/gateway/ai/backends/repo_bridge.py +80 -16
- package/gateway/ai/backends/tools_infra.py +49 -32
- package/gateway/ai/checksums.sha256 +6 -0
- package/gateway/ai/continuity.py +462 -0
- package/gateway/ai/deliberation.pyi +53 -0
- package/gateway/ai/governance.pyi +32 -0
- package/gateway/ai/governance_hardening.py +569 -0
- package/gateway/ai/hot_reload.py +445 -0
- package/gateway/ai/inbox_daemon_runner.py +217 -0
- package/gateway/ai/ledger_manager.py +40 -0
- package/gateway/ai/license.py +104 -3
- package/gateway/ai/license_core.py +177 -36
- package/gateway/ai/license_core.pyi +50 -0
- package/gateway/ai/loop_engine.py +786 -22
- package/gateway/ai/reddit_scanner.py +150 -5
- package/gateway/ai/server.py +301 -19
- package/gateway/ai/swarm.py +87 -0
- package/gateway/ai/swarm_infra.py +656 -0
- package/gateway/ai/tweet_corpus_schema.sql +76 -0
- package/gateway/core/diff_engine_v2.py +6 -2
- package/gateway/core/generator_drift.py +242 -0
- package/gateway/core/json_schema_diff.py +375 -0
- package/gateway/core/openapi_version.py +124 -0
- package/gateway/core/spec_detector.py +47 -7
- package/gateway/core/spec_health.py +5 -2
- package/lib/cross-model-hooks.js +31 -17
- package/lib/delimit-template.js +19 -85
- package/package.json +9 -2
- package/scripts/sync-gateway.sh +13 -1
|
@@ -23,7 +23,12 @@ if str(GATEWAY_ROOT) not in sys.path:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def _load_specs(spec_path: str) -> Dict[str, Any]:
|
|
26
|
-
"""Load an
|
|
26
|
+
"""Load an API spec (OpenAPI or JSON Schema) from a file path.
|
|
27
|
+
|
|
28
|
+
Performs a non-fatal version compatibility check (LED-290) so that
|
|
29
|
+
unknown OpenAPI versions log a warning instead of silently parsing.
|
|
30
|
+
JSON Schema documents skip the OpenAPI version assert.
|
|
31
|
+
"""
|
|
27
32
|
import yaml
|
|
28
33
|
|
|
29
34
|
p = Path(spec_path)
|
|
@@ -32,8 +37,149 @@ def _load_specs(spec_path: str) -> Dict[str, Any]:
|
|
|
32
37
|
|
|
33
38
|
content = p.read_text(encoding="utf-8")
|
|
34
39
|
if p.suffix in (".yaml", ".yml"):
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
spec = yaml.safe_load(content)
|
|
41
|
+
else:
|
|
42
|
+
spec = json.loads(content)
|
|
43
|
+
|
|
44
|
+
# LED-290: warn (non-fatal) if version is outside the validated set.
|
|
45
|
+
# Only applies to OpenAPI/Swagger documents — bare JSON Schema files
|
|
46
|
+
# have no "openapi"/"swagger" key and would otherwise trip the assert.
|
|
47
|
+
try:
|
|
48
|
+
if isinstance(spec, dict) and ("openapi" in spec or "swagger" in spec):
|
|
49
|
+
from core.openapi_version import assert_supported
|
|
50
|
+
assert_supported(spec, strict=False)
|
|
51
|
+
except Exception as exc: # pragma: no cover -- defensive only
|
|
52
|
+
logger.debug("openapi version check skipped: %s", exc)
|
|
53
|
+
|
|
54
|
+
return spec
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# LED-713: JSON Schema spec-type dispatch helpers
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _spec_type(doc: Any) -> str:
|
|
63
|
+
"""Classify a loaded spec doc. 'openapi' or 'json_schema'."""
|
|
64
|
+
from core.spec_detector import detect_spec_type
|
|
65
|
+
t = detect_spec_type(doc)
|
|
66
|
+
# Fallback to openapi for unknown so we never break existing flows.
|
|
67
|
+
return "json_schema" if t == "json_schema" else "openapi"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _json_schema_changes_to_dicts(changes: List[Any]) -> List[Dict[str, Any]]:
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
"type": c.type.value,
|
|
74
|
+
"path": c.path,
|
|
75
|
+
"message": c.message,
|
|
76
|
+
"is_breaking": c.is_breaking,
|
|
77
|
+
"details": c.details,
|
|
78
|
+
}
|
|
79
|
+
for c in changes
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _json_schema_semver(changes: List[Any]) -> Dict[str, Any]:
|
|
84
|
+
"""Build an OpenAPI-compatible semver result from JSON Schema changes.
|
|
85
|
+
|
|
86
|
+
Mirrors core.semver_classifier.classify_detailed shape so downstream
|
|
87
|
+
consumers (PR comment, CI formatter, ledger) don't need to branch.
|
|
88
|
+
"""
|
|
89
|
+
breaking = [c for c in changes if c.is_breaking]
|
|
90
|
+
non_breaking = [c for c in changes if not c.is_breaking]
|
|
91
|
+
if breaking:
|
|
92
|
+
bump = "major"
|
|
93
|
+
elif non_breaking:
|
|
94
|
+
bump = "minor"
|
|
95
|
+
else:
|
|
96
|
+
bump = "none"
|
|
97
|
+
return {
|
|
98
|
+
"bump": bump,
|
|
99
|
+
"is_breaking": bool(breaking),
|
|
100
|
+
"counts": {
|
|
101
|
+
"breaking": len(breaking),
|
|
102
|
+
"non_breaking": len(non_breaking),
|
|
103
|
+
"total": len(changes),
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _bump_semver_version(current: str, bump: str) -> Optional[str]:
|
|
109
|
+
"""Minimal semver bump for JSON Schema path (core.semver_classifier
|
|
110
|
+
only understands OpenAPI ChangeType enums)."""
|
|
111
|
+
if not current:
|
|
112
|
+
return None
|
|
113
|
+
try:
|
|
114
|
+
parts = current.lstrip("v").split(".")
|
|
115
|
+
major, minor, patch = (int(parts[0]), int(parts[1]), int(parts[2]))
|
|
116
|
+
except Exception:
|
|
117
|
+
return None
|
|
118
|
+
if bump == "major":
|
|
119
|
+
return f"{major + 1}.0.0"
|
|
120
|
+
if bump == "minor":
|
|
121
|
+
return f"{major}.{minor + 1}.0"
|
|
122
|
+
if bump == "patch":
|
|
123
|
+
return f"{major}.{minor}.{patch + 1}"
|
|
124
|
+
return current
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _run_json_schema_lint(
|
|
128
|
+
old_doc: Dict[str, Any],
|
|
129
|
+
new_doc: Dict[str, Any],
|
|
130
|
+
current_version: Optional[str] = None,
|
|
131
|
+
api_name: Optional[str] = None,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
"""Build an evaluate_with_policy-compatible result for JSON Schema.
|
|
134
|
+
|
|
135
|
+
Policy rules in Delimit are defined against OpenAPI ChangeType values,
|
|
136
|
+
so they do not apply here. We return zero violations and rely on the
|
|
137
|
+
breaking-change count + semver bump to drive the governance gate.
|
|
138
|
+
"""
|
|
139
|
+
from core.json_schema_diff import JSONSchemaDiffEngine
|
|
140
|
+
|
|
141
|
+
engine = JSONSchemaDiffEngine()
|
|
142
|
+
changes = engine.compare(old_doc, new_doc)
|
|
143
|
+
semver = _json_schema_semver(changes)
|
|
144
|
+
|
|
145
|
+
if current_version:
|
|
146
|
+
semver["current_version"] = current_version
|
|
147
|
+
semver["next_version"] = _bump_semver_version(current_version, semver["bump"])
|
|
148
|
+
|
|
149
|
+
breaking_count = semver["counts"]["breaking"]
|
|
150
|
+
total = semver["counts"]["total"]
|
|
151
|
+
|
|
152
|
+
decision = "pass"
|
|
153
|
+
exit_code = 0
|
|
154
|
+
# No policy rules apply to JSON Schema, but breaking changes still
|
|
155
|
+
# flag MAJOR semver and the downstream gate uses that to block.
|
|
156
|
+
# Mirror the shape of evaluate_with_policy so the action/CLI renderers
|
|
157
|
+
# need no JSON Schema-specific branch.
|
|
158
|
+
result: Dict[str, Any] = {
|
|
159
|
+
"spec_type": "json_schema",
|
|
160
|
+
"api_name": api_name or new_doc.get("title") or old_doc.get("title") or "JSON Schema",
|
|
161
|
+
"decision": decision,
|
|
162
|
+
"exit_code": exit_code,
|
|
163
|
+
"violations": [],
|
|
164
|
+
"summary": {
|
|
165
|
+
"total_changes": total,
|
|
166
|
+
"breaking_changes": breaking_count,
|
|
167
|
+
"violations": 0,
|
|
168
|
+
"errors": 0,
|
|
169
|
+
"warnings": 0,
|
|
170
|
+
},
|
|
171
|
+
"all_changes": [
|
|
172
|
+
{
|
|
173
|
+
"type": c.type.value,
|
|
174
|
+
"path": c.path,
|
|
175
|
+
"message": c.message,
|
|
176
|
+
"is_breaking": c.is_breaking,
|
|
177
|
+
}
|
|
178
|
+
for c in changes
|
|
179
|
+
],
|
|
180
|
+
"semver": semver,
|
|
181
|
+
}
|
|
182
|
+
return result
|
|
37
183
|
|
|
38
184
|
|
|
39
185
|
def _read_jsonl(path: Path) -> List[Dict[str, Any]]:
|
|
@@ -101,29 +247,51 @@ def run_lint(old_spec: str, new_spec: str, policy_file: Optional[str] = None) ->
|
|
|
101
247
|
"""Run the full lint pipeline: diff + policy evaluation.
|
|
102
248
|
|
|
103
249
|
This is the Tier 1 primary tool — combines diff detection with
|
|
104
|
-
policy enforcement into a single pass/fail decision.
|
|
250
|
+
policy enforcement into a single pass/fail decision. Auto-detects
|
|
251
|
+
spec type (OpenAPI vs JSON Schema, LED-713) and dispatches to the
|
|
252
|
+
matching engine.
|
|
105
253
|
"""
|
|
106
254
|
from core.policy_engine import evaluate_with_policy
|
|
107
255
|
|
|
108
256
|
old = _load_specs(old_spec)
|
|
109
257
|
new = _load_specs(new_spec)
|
|
110
258
|
|
|
259
|
+
# LED-713: JSON Schema dispatch. Policy rules are OpenAPI-specific,
|
|
260
|
+
# so JSON Schema takes the no-policy (breaking-count + semver) path.
|
|
261
|
+
if _spec_type(new) == "json_schema" or _spec_type(old) == "json_schema":
|
|
262
|
+
return _run_json_schema_lint(old, new)
|
|
263
|
+
|
|
111
264
|
return evaluate_with_policy(old, new, policy_file)
|
|
112
265
|
|
|
113
266
|
|
|
114
267
|
def run_diff(old_spec: str, new_spec: str) -> Dict[str, Any]:
|
|
115
|
-
"""Run diff engine only — no policy evaluation.
|
|
116
|
-
from core.diff_engine_v2 import OpenAPIDiffEngine
|
|
268
|
+
"""Run diff engine only — no policy evaluation.
|
|
117
269
|
|
|
270
|
+
Auto-detects OpenAPI vs JSON Schema and dispatches (LED-713).
|
|
271
|
+
"""
|
|
118
272
|
old = _load_specs(old_spec)
|
|
119
273
|
new = _load_specs(new_spec)
|
|
120
274
|
|
|
275
|
+
if _spec_type(new) == "json_schema" or _spec_type(old) == "json_schema":
|
|
276
|
+
from core.json_schema_diff import JSONSchemaDiffEngine
|
|
277
|
+
engine = JSONSchemaDiffEngine()
|
|
278
|
+
changes = engine.compare(old, new)
|
|
279
|
+
breaking = [c for c in changes if c.is_breaking]
|
|
280
|
+
return {
|
|
281
|
+
"spec_type": "json_schema",
|
|
282
|
+
"total_changes": len(changes),
|
|
283
|
+
"breaking_changes": len(breaking),
|
|
284
|
+
"changes": _json_schema_changes_to_dicts(changes),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
from core.diff_engine_v2 import OpenAPIDiffEngine
|
|
121
288
|
engine = OpenAPIDiffEngine()
|
|
122
289
|
changes = engine.compare(old, new)
|
|
123
290
|
|
|
124
291
|
breaking = [c for c in changes if c.is_breaking]
|
|
125
292
|
|
|
126
293
|
return {
|
|
294
|
+
"spec_type": "openapi",
|
|
127
295
|
"total_changes": len(changes),
|
|
128
296
|
"breaking_changes": len(breaking),
|
|
129
297
|
"changes": [
|
|
@@ -150,13 +318,20 @@ def run_changelog(
|
|
|
150
318
|
Uses the diff engine to detect changes, then formats them into
|
|
151
319
|
a human-readable changelog grouped by category.
|
|
152
320
|
"""
|
|
153
|
-
from core.diff_engine_v2 import OpenAPIDiffEngine
|
|
154
321
|
from datetime import datetime, timezone
|
|
155
322
|
|
|
156
323
|
old = _load_specs(old_spec)
|
|
157
324
|
new = _load_specs(new_spec)
|
|
158
325
|
|
|
159
|
-
|
|
326
|
+
# LED-713: dispatch on spec type. JSONSchemaChange / Change share the
|
|
327
|
+
# (.type.value, .path, .message, .is_breaking) duck type.
|
|
328
|
+
if _spec_type(new) == "json_schema" or _spec_type(old) == "json_schema":
|
|
329
|
+
from core.json_schema_diff import JSONSchemaDiffEngine
|
|
330
|
+
engine = JSONSchemaDiffEngine()
|
|
331
|
+
else:
|
|
332
|
+
from core.diff_engine_v2 import OpenAPIDiffEngine
|
|
333
|
+
engine = OpenAPIDiffEngine()
|
|
334
|
+
|
|
160
335
|
changes = engine.compare(old, new)
|
|
161
336
|
|
|
162
337
|
# Categorize changes
|
|
@@ -794,14 +969,26 @@ def run_semver(
|
|
|
794
969
|
"""Classify the semver bump for a spec change.
|
|
795
970
|
|
|
796
971
|
Returns detailed breakdown: bump level, per-category counts,
|
|
797
|
-
and optionally the bumped version string.
|
|
972
|
+
and optionally the bumped version string. Auto-detects OpenAPI vs
|
|
973
|
+
JSON Schema (LED-713).
|
|
798
974
|
"""
|
|
799
|
-
from core.diff_engine_v2 import OpenAPIDiffEngine
|
|
800
|
-
from core.semver_classifier import classify_detailed, bump_version, classify
|
|
801
|
-
|
|
802
975
|
old = _load_specs(old_spec)
|
|
803
976
|
new = _load_specs(new_spec)
|
|
804
977
|
|
|
978
|
+
# LED-713: JSON Schema path
|
|
979
|
+
if _spec_type(new) == "json_schema" or _spec_type(old) == "json_schema":
|
|
980
|
+
from core.json_schema_diff import JSONSchemaDiffEngine
|
|
981
|
+
engine = JSONSchemaDiffEngine()
|
|
982
|
+
changes = engine.compare(old, new)
|
|
983
|
+
result = _json_schema_semver(changes)
|
|
984
|
+
if current_version:
|
|
985
|
+
result["current_version"] = current_version
|
|
986
|
+
result["next_version"] = _bump_semver_version(current_version, result["bump"])
|
|
987
|
+
return result
|
|
988
|
+
|
|
989
|
+
from core.diff_engine_v2 import OpenAPIDiffEngine
|
|
990
|
+
from core.semver_classifier import classify_detailed, bump_version, classify
|
|
991
|
+
|
|
805
992
|
engine = OpenAPIDiffEngine()
|
|
806
993
|
changes = engine.compare(old, new)
|
|
807
994
|
result = classify_detailed(changes)
|
|
@@ -932,7 +1119,6 @@ def run_diff_report(
|
|
|
932
1119
|
"""
|
|
933
1120
|
from datetime import datetime, timezone
|
|
934
1121
|
|
|
935
|
-
from core.diff_engine_v2 import OpenAPIDiffEngine
|
|
936
1122
|
from core.policy_engine import PolicyEngine
|
|
937
1123
|
from core.semver_classifier import classify_detailed, classify
|
|
938
1124
|
from core.spec_health import score_spec
|
|
@@ -941,6 +1127,43 @@ def run_diff_report(
|
|
|
941
1127
|
old = _load_specs(old_spec)
|
|
942
1128
|
new = _load_specs(new_spec)
|
|
943
1129
|
|
|
1130
|
+
# LED-713: JSON Schema dispatch — short-circuit to a minimal report
|
|
1131
|
+
# shape compatible with the JSON renderer (HTML renderer remains
|
|
1132
|
+
# OpenAPI-only; JSON Schema callers should use fmt="json").
|
|
1133
|
+
if _spec_type(new) == "json_schema" or _spec_type(old) == "json_schema":
|
|
1134
|
+
from core.json_schema_diff import JSONSchemaDiffEngine
|
|
1135
|
+
js_engine = JSONSchemaDiffEngine()
|
|
1136
|
+
js_changes = js_engine.compare(old, new)
|
|
1137
|
+
js_breaking = [c for c in js_changes if c.is_breaking]
|
|
1138
|
+
js_semver = _json_schema_semver(js_changes)
|
|
1139
|
+
now_js = datetime.now(timezone.utc)
|
|
1140
|
+
return {
|
|
1141
|
+
"format": fmt,
|
|
1142
|
+
"spec_type": "json_schema",
|
|
1143
|
+
"generated_at": now_js.isoformat(),
|
|
1144
|
+
"old_spec": old_spec,
|
|
1145
|
+
"new_spec": new_spec,
|
|
1146
|
+
"old_title": old.get("title", "") if isinstance(old, dict) else "",
|
|
1147
|
+
"new_title": new.get("title", "") if isinstance(new, dict) else "",
|
|
1148
|
+
"semver": js_semver,
|
|
1149
|
+
"changes": _json_schema_changes_to_dicts(js_changes),
|
|
1150
|
+
"breaking_count": len(js_breaking),
|
|
1151
|
+
"non_breaking_count": len(js_changes) - len(js_breaking),
|
|
1152
|
+
"total_changes": len(js_changes),
|
|
1153
|
+
"policy": {
|
|
1154
|
+
"decision": "pass",
|
|
1155
|
+
"violations": [],
|
|
1156
|
+
"errors": 0,
|
|
1157
|
+
"warnings": 0,
|
|
1158
|
+
},
|
|
1159
|
+
"health": None,
|
|
1160
|
+
"migration": "",
|
|
1161
|
+
"output_file": output_file,
|
|
1162
|
+
"note": "JSON Schema report (policy rules and HTML report are OpenAPI-only in v1)",
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
from core.diff_engine_v2 import OpenAPIDiffEngine
|
|
1166
|
+
|
|
944
1167
|
# -- Diff --
|
|
945
1168
|
engine = OpenAPIDiffEngine()
|
|
946
1169
|
changes = engine.compare(old, new)
|
|
@@ -158,21 +158,80 @@ def config_audit(target: str = ".", options: Optional[Dict] = None) -> Dict[str,
|
|
|
158
158
|
# ─── EvidencePack ───────────────────────────────────────────────────────
|
|
159
159
|
|
|
160
160
|
def evidence_collect(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
161
|
-
"""Collect project evidence: git log, test files, configs, governance data.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
161
|
+
"""Collect project evidence: git log, test files, configs, governance data.
|
|
162
|
+
|
|
163
|
+
Accepts either a local filesystem path (repo directory) or a remote
|
|
164
|
+
reference (GitHub URL, owner/repo#N, or any non-filesystem string).
|
|
165
|
+
Remote targets skip the filesystem walk and store reference metadata.
|
|
166
|
+
"""
|
|
167
|
+
import re
|
|
168
|
+
import subprocess
|
|
169
|
+
import time as _time
|
|
170
|
+
|
|
171
|
+
opts = options or {}
|
|
172
|
+
evidence_type = opts.get("evidence_type", "")
|
|
173
|
+
|
|
174
|
+
# Detect non-filesystem targets: URLs, owner/repo#N, bare issue refs, etc.
|
|
175
|
+
is_remote = (
|
|
176
|
+
"://" in target
|
|
177
|
+
or target.startswith("http")
|
|
178
|
+
or re.match(r"^[\w.-]+/[\w.-]+#\d+$", target) is not None
|
|
179
|
+
or "#" in target
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
evidence: Dict[str, Any] = {"collected_at": _time.time(), "target": target}
|
|
183
|
+
if evidence_type:
|
|
184
|
+
evidence["evidence_type"] = evidence_type
|
|
185
|
+
|
|
186
|
+
if is_remote:
|
|
187
|
+
# Remote/reference target — no filesystem walk, just record metadata.
|
|
188
|
+
evidence["target_type"] = "remote"
|
|
170
189
|
evidence["git_log"] = []
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
evidence["test_directories"] = []
|
|
191
|
+
evidence["configs"] = []
|
|
192
|
+
m = re.match(r"^([\w.-]+)/([\w.-]+)#(\d+)$", target)
|
|
193
|
+
if m:
|
|
194
|
+
evidence["repo"] = f"{m.group(1)}/{m.group(2)}"
|
|
195
|
+
evidence["issue_number"] = int(m.group(3))
|
|
196
|
+
else:
|
|
197
|
+
root = Path(target).resolve()
|
|
198
|
+
evidence["target"] = str(root)
|
|
199
|
+
evidence["target_type"] = "local"
|
|
200
|
+
|
|
201
|
+
if not root.exists():
|
|
202
|
+
return {
|
|
203
|
+
"tool": "evidence.collect",
|
|
204
|
+
"status": "error",
|
|
205
|
+
"error": "target_not_found",
|
|
206
|
+
"message": f"Path {root} does not exist. For remote targets, pass a URL or owner/repo#N.",
|
|
207
|
+
"target": target,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Git log (safe for non-git dirs)
|
|
211
|
+
try:
|
|
212
|
+
r = subprocess.run(
|
|
213
|
+
["git", "-C", str(root), "log", "--oneline", "-10"],
|
|
214
|
+
capture_output=True, text=True, timeout=10,
|
|
215
|
+
)
|
|
216
|
+
evidence["git_log"] = r.stdout.strip().splitlines() if r.returncode == 0 else []
|
|
217
|
+
except Exception:
|
|
218
|
+
evidence["git_log"] = []
|
|
219
|
+
|
|
220
|
+
# Test dirs + configs (only if target is a directory)
|
|
221
|
+
if root.is_dir():
|
|
222
|
+
test_dirs = [d for d in ["tests", "test", "__tests__", "spec"] if (root / d).exists()]
|
|
223
|
+
evidence["test_directories"] = test_dirs
|
|
224
|
+
try:
|
|
225
|
+
evidence["configs"] = [
|
|
226
|
+
f.name for f in root.iterdir()
|
|
227
|
+
if f.is_file() and (f.suffix in [".json", ".yaml", ".yml", ".toml"] or f.name.startswith("."))
|
|
228
|
+
]
|
|
229
|
+
except (PermissionError, OSError):
|
|
230
|
+
evidence["configs"] = []
|
|
231
|
+
else:
|
|
232
|
+
evidence["test_directories"] = []
|
|
233
|
+
evidence["configs"] = []
|
|
234
|
+
|
|
176
235
|
# Save bundle
|
|
177
236
|
ev_dir = Path(os.environ.get("DELIMIT_HOME", str(Path.home() / ".delimit"))) / "evidence"
|
|
178
237
|
ev_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -180,8 +239,13 @@ def evidence_collect(target: str = ".", options: Optional[Dict] = None) -> Dict[
|
|
|
180
239
|
bundle_path = ev_dir / f"{bundle_id}.json"
|
|
181
240
|
evidence["bundle_id"] = bundle_id
|
|
182
241
|
bundle_path.write_text(json.dumps(evidence, indent=2))
|
|
183
|
-
return {
|
|
184
|
-
|
|
242
|
+
return {
|
|
243
|
+
"tool": "evidence.collect",
|
|
244
|
+
"status": "ok",
|
|
245
|
+
"bundle_id": bundle_id,
|
|
246
|
+
"bundle_path": str(bundle_path),
|
|
247
|
+
"summary": {k: len(v) if isinstance(v, list) else v for k, v in evidence.items()},
|
|
248
|
+
}
|
|
185
249
|
|
|
186
250
|
|
|
187
251
|
def evidence_verify(bundle_id: Optional[str] = None, bundle_path: Optional[str] = None, options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
@@ -36,12 +36,37 @@ SECRET_PATTERNS = {
|
|
|
36
36
|
"aws_secret_key": r"(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['\"]?[A-Za-z0-9/+=]{40}",
|
|
37
37
|
"generic_api_key": r"\b(?:api[_-]?key|apikey)\b\s*[=:]\s*['\"]?[A-Za-z0-9_\-]{20,}",
|
|
38
38
|
"generic_secret": r"\b(?:secret|password|passwd|token)\b\s*[=:]\s*['\"]?[^\s'\"]{8,}",
|
|
39
|
+
# Catches dict/JSON-style credentials where a key like password or api_key
|
|
40
|
+
# is followed by a quoted literal value (>=4 chars). Example shape omitted
|
|
41
|
+
# intentionally so the scanner does not flag this comment as a finding.
|
|
42
|
+
"dict_credential": r"""['\"](?:password|passwd|secret|api_key|apikey|token|auth_token|access_token|private_key)['\"][\s]*:[\s]*['\"][^'\"]{4,}['\"]""",
|
|
39
43
|
"private_key_header": r"-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----",
|
|
40
44
|
"github_token": r"gh[pousr]_[A-Za-z0-9_]{36,}",
|
|
41
45
|
"slack_token": r"xox[baprs]-[0-9A-Za-z\-]{10,}",
|
|
42
46
|
"jwt_token": r"eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}",
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
# False-positive exclusions for generic credential patterns — values that are
|
|
50
|
+
# clearly not real secrets (placeholders, env-var lookups, test fixtures,
|
|
51
|
+
# function-call RHS, demo literals, local variable assignments from parsers).
|
|
52
|
+
_CREDENTIAL_FALSE_POSITIVES = re.compile(
|
|
53
|
+
r"(?:environ|getenv|process\.env|os\.environ|"
|
|
54
|
+
r"<configured|example|placeholder|REDACTED|"
|
|
55
|
+
r"your[_-]?(?:password|secret|token|key)|"
|
|
56
|
+
r"change[_-]?me|TODO|FIXME|xxx+|\.{4,}|"
|
|
57
|
+
r"\$\{|%\(|None|null|undefined|"
|
|
58
|
+
r"test[_-]?(?:password|secret|token|key)|"
|
|
59
|
+
# Demo/sample literal values used in docs, recordings, fixtures
|
|
60
|
+
r"sk-ant-demo|sk-demo|AIza-demo|xai-demo|demo[_-]?(?:key|secret|token)|"
|
|
61
|
+
r"-demo['\"]|"
|
|
62
|
+
# Function-call RHS (reading from parsed JSON, env, getters, slicing strings)
|
|
63
|
+
r"json\.loads|\.read_text\(|\.slice\(|"
|
|
64
|
+
r"tokens\.get\(|token\s*=\s*_make_token|"
|
|
65
|
+
# RHS that is a parameter reference like token=tokens.get("access_token"...
|
|
66
|
+
r"=\s*tokens\.get\()",
|
|
67
|
+
re.IGNORECASE,
|
|
68
|
+
)
|
|
69
|
+
|
|
45
70
|
# Dangerous code patterns: name -> (regex, description, severity)
|
|
46
71
|
ANTI_PATTERNS = {
|
|
47
72
|
"eval_usage": (r"\beval\s*\(", "Use of eval() — potential code injection", "high"),
|
|
@@ -258,15 +283,23 @@ def security_audit(target: str = ".") -> Dict[str, Any]:
|
|
|
258
283
|
rel = str(fpath.relative_to(Path(target).resolve())) if Path(target).resolve() in fpath.parents or fpath == Path(target).resolve() else str(fpath)
|
|
259
284
|
|
|
260
285
|
# Secret detection
|
|
286
|
+
# Patterns where false-positive filtering applies (generic/dict patterns only)
|
|
287
|
+
_FP_FILTERED = {"generic_secret", "dict_credential", "generic_api_key"}
|
|
261
288
|
for secret_name, pattern in SECRET_PATTERNS.items():
|
|
262
289
|
for match in re.finditer(pattern, content):
|
|
290
|
+
matched_text = match.group(0)
|
|
291
|
+
# Skip false positives only for generic patterns (not specific token formats)
|
|
292
|
+
if secret_name in _FP_FILTERED and _CREDENTIAL_FALSE_POSITIVES.search(matched_text):
|
|
293
|
+
continue
|
|
263
294
|
line_num = content[:match.start()].count("\n") + 1
|
|
295
|
+
# Redact actual secret values in snippet output
|
|
296
|
+
snippet_raw = content[max(0, match.start() - 10):match.end() + 10].strip()[:80]
|
|
264
297
|
secrets_found.append({
|
|
265
298
|
"file": rel,
|
|
266
299
|
"line": line_num,
|
|
267
300
|
"type": secret_name,
|
|
268
301
|
"severity": "critical",
|
|
269
|
-
"snippet":
|
|
302
|
+
"snippet": snippet_raw,
|
|
270
303
|
})
|
|
271
304
|
severity_counts["critical"] += 1
|
|
272
305
|
|
|
@@ -1151,41 +1184,25 @@ def deploy_npm(project_path: str = ".", bump: str = "patch", tag: str = "latest"
|
|
|
1151
1184
|
results["status"] = "dry_run_complete"
|
|
1152
1185
|
return results
|
|
1153
1186
|
|
|
1154
|
-
# 4. Version bump
|
|
1187
|
+
# 4. Version bump (dry_run already returned above, so this is always a real bump)
|
|
1155
1188
|
if bump in ("patch", "minor", "major"):
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1189
|
+
try:
|
|
1190
|
+
bump_cmd = ["npm", "version", bump, "--no-git-tag-version"]
|
|
1191
|
+
result = subprocess.run(
|
|
1192
|
+
bump_cmd, capture_output=True, text=True, timeout=10, cwd=str(p)
|
|
1193
|
+
)
|
|
1194
|
+
if result.returncode == 0:
|
|
1195
|
+
new_version = result.stdout.strip().lstrip("v")
|
|
1159
1196
|
results["new_version"] = new_version
|
|
1160
|
-
results["steps"].append({
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
"from": current_version,
|
|
1164
|
-
"to": new_version,
|
|
1165
|
-
"bump": bump,
|
|
1166
|
-
})
|
|
1167
|
-
except Exception as e:
|
|
1168
|
-
results["steps"].append({"step": "version_bump", "status": "error", "detail": str(e)})
|
|
1169
|
-
results["status"] = "bump_failed"
|
|
1170
|
-
return results
|
|
1171
|
-
else:
|
|
1172
|
-
try:
|
|
1173
|
-
bump_cmd = ["npm", "version", bump, "--no-git-tag-version"]
|
|
1174
|
-
result = subprocess.run(
|
|
1175
|
-
bump_cmd, capture_output=True, text=True, timeout=10, cwd=str(p)
|
|
1176
|
-
)
|
|
1177
|
-
if result.returncode == 0:
|
|
1178
|
-
new_version = result.stdout.strip().lstrip("v")
|
|
1179
|
-
results["new_version"] = new_version
|
|
1180
|
-
results["steps"].append({"step": "version_bump", "status": "ok", "from": current_version, "to": new_version, "bump": bump})
|
|
1181
|
-
else:
|
|
1182
|
-
results["steps"].append({"step": "version_bump", "status": "error", "detail": result.stderr.strip()[:200]})
|
|
1183
|
-
results["status"] = "bump_failed"
|
|
1184
|
-
return results
|
|
1185
|
-
except Exception as e:
|
|
1186
|
-
results["steps"].append({"step": "version_bump", "status": "error", "detail": str(e)})
|
|
1197
|
+
results["steps"].append({"step": "version_bump", "status": "ok", "from": current_version, "to": new_version, "bump": bump})
|
|
1198
|
+
else:
|
|
1199
|
+
results["steps"].append({"step": "version_bump", "status": "error", "detail": result.stderr.strip()[:200]})
|
|
1187
1200
|
results["status"] = "bump_failed"
|
|
1188
1201
|
return results
|
|
1202
|
+
except Exception as e:
|
|
1203
|
+
results["steps"].append({"step": "version_bump", "status": "error", "detail": str(e)})
|
|
1204
|
+
results["status"] = "bump_failed"
|
|
1205
|
+
return results
|
|
1189
1206
|
else:
|
|
1190
1207
|
results["new_version"] = current_version
|
|
1191
1208
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
26becca967c64620439da0980fc7f4c2fd261eef687176a0a25907d07eb69be7 deliberation.cpython-310-x86_64-linux-gnu.so
|
|
2
|
+
0c9a30560849a410b0d48c2f4463f2bad430e25992c007e0a9a383bfacc9f35b deliberation.pyi
|
|
3
|
+
ac902ff9e9d2e699091fa090c16f10e8b7b8a85e03e2e825357ffb1c24110ca8 governance.cpython-310-x86_64-linux-gnu.so
|
|
4
|
+
6f3759a35d443451dc70bc38cd3198bb9d95f5f63f1d8c0d31b131fbd3a6edad governance.pyi
|
|
5
|
+
75a1b15f2e010bec84da4de9e655aa87da51736cf5fab34ddb66ffe3354f487f license_core.cpython-310-x86_64-linux-gnu.so
|
|
6
|
+
fd19a8623486ec0dc1fe4f31b863f18177731b3853fad85aa1964f1adbd2e902 license_core.pyi
|