delimit-cli 4.6.0 → 4.6.2

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.
@@ -26,17 +26,42 @@ class SemverBump(Enum):
26
26
 
27
27
  # ── Change-type buckets ──────────────────────────────────────────────
28
28
 
29
+ # LED-1600: this bucket MUST stay aligned with diff_engine_v2.Change.is_breaking.
30
+ # Previously it listed only 10 of the engine's breaking types, so a
31
+ # PARAM_REQUIRED_CHANGED, RESPONSE_TYPE_CHANGED, SECURITY_REMOVED,
32
+ # SECURITY_SCOPE_REMOVED, MAX_LENGTH_DECREASED, MIN_LENGTH_INCREASED or
33
+ # PARAM_TYPE_CHANGED — all of which the engine flags `is_breaking=True` — was
34
+ # silently classified MINOR instead of MAJOR. That is the exact "silent semver
35
+ # minor->major leak" this LED closes: a breaking change slipping through as
36
+ # non-breaking is the worst failure mode for a merge gate. The context-
37
+ # sensitive field types (FIELD_REMOVED / REQUIRED_FIELD_ADDED /
38
+ # FIELD_REQUIREMENT_RELAXED) are NOT listed here as unconditional — their
39
+ # breaking-ness depends on request/response direction and is read per-Change
40
+ # via `is_breaking` in classify() below.
29
41
  BREAKING_TYPES = frozenset({
30
42
  ChangeType.ENDPOINT_REMOVED,
31
43
  ChangeType.METHOD_REMOVED,
32
44
  ChangeType.REQUIRED_PARAM_ADDED,
33
45
  ChangeType.PARAM_REMOVED,
34
46
  ChangeType.RESPONSE_REMOVED,
35
- ChangeType.REQUIRED_FIELD_ADDED,
36
- ChangeType.FIELD_REMOVED,
37
47
  ChangeType.TYPE_CHANGED,
38
48
  ChangeType.FORMAT_CHANGED,
39
49
  ChangeType.ENUM_VALUE_REMOVED,
50
+ ChangeType.PARAM_TYPE_CHANGED,
51
+ ChangeType.PARAM_REQUIRED_CHANGED,
52
+ ChangeType.RESPONSE_TYPE_CHANGED,
53
+ ChangeType.SECURITY_REMOVED,
54
+ ChangeType.SECURITY_SCOPE_REMOVED,
55
+ ChangeType.MAX_LENGTH_DECREASED,
56
+ ChangeType.MIN_LENGTH_INCREASED,
57
+ })
58
+
59
+ # Context-sensitive types whose breaking-ness is decided per-Change by the
60
+ # engine's direction-aware is_breaking, not by membership in BREAKING_TYPES.
61
+ CONTEXT_SENSITIVE_TYPES = frozenset({
62
+ ChangeType.FIELD_REMOVED,
63
+ ChangeType.REQUIRED_FIELD_ADDED,
64
+ ChangeType.FIELD_REQUIREMENT_RELAXED,
40
65
  })
41
66
 
42
67
  ADDITIVE_TYPES = frozenset({
@@ -65,7 +90,14 @@ def classify(changes: List[Change]) -> SemverBump:
65
90
  has_additive = False
66
91
 
67
92
  for change in changes:
68
- if change.type in BREAKING_TYPES:
93
+ # LED-1600: the engine's is_breaking is the single source of truth.
94
+ # For context-sensitive types it already encodes request/response
95
+ # direction; for everything else it matches BREAKING_TYPES membership.
96
+ # Using it here closes the gap where a breaking change (e.g. an
97
+ # optional->required param, or a response field removal) was bucketed
98
+ # MINOR. `getattr` keeps the function working for duck-typed Change-
99
+ # likes that may not carry the property.
100
+ if _is_breaking(change):
69
101
  has_breaking = True
70
102
  break # short-circuit — can't go higher than MAJOR
71
103
  if change.type in ADDITIVE_TYPES:
@@ -78,6 +110,20 @@ def classify(changes: List[Change]) -> SemverBump:
78
110
  return SemverBump.PATCH
79
111
 
80
112
 
113
+ def _is_breaking(change) -> bool:
114
+ """Authoritative breaking check for a Change.
115
+
116
+ Prefers the engine's direction-aware ``is_breaking`` property. Falls back
117
+ to BREAKING_TYPES membership for objects that don't expose it (e.g. test
118
+ doubles). Context-sensitive types are only breaking when ``is_breaking``
119
+ says so; never inferred from the type alone.
120
+ """
121
+ val = getattr(change, "is_breaking", None)
122
+ if isinstance(val, bool):
123
+ return val
124
+ return getattr(change, "type", None) in BREAKING_TYPES
125
+
126
+
81
127
  def classify_detailed(changes: List[Change]) -> Dict[str, Any]:
82
128
  """Return a detailed classification with per-category breakdowns.
83
129
 
@@ -85,9 +131,9 @@ def classify_detailed(changes: List[Change]) -> Dict[str, Any]:
85
131
  """
86
132
  bump = classify(changes)
87
133
 
88
- breaking = [c for c in changes if c.type in BREAKING_TYPES]
89
- additive = [c for c in changes if c.type in ADDITIVE_TYPES]
90
- patch = [c for c in changes if c.type in PATCH_TYPES]
134
+ breaking = [c for c in changes if _is_breaking(c)]
135
+ additive = [c for c in changes if not _is_breaking(c) and c.type in ADDITIVE_TYPES]
136
+ patch = [c for c in changes if not _is_breaking(c) and c.type in PATCH_TYPES]
91
137
 
92
138
  return {
93
139
  "bump": bump.value,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "4.6.0",
4
+ "version": "4.6.2",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -42,6 +42,9 @@
42
42
  "!scripts/demo-v420-clean.sh",
43
43
  "!scripts/demo-v420-deliberation.sh",
44
44
  "!scripts/sync-gateway.sh",
45
+ "!scripts/build-license-core.sh",
46
+ "!scripts/security-check.sh",
47
+ "!scripts/test-license-core-so.sh",
45
48
  "!gateway/ai/continuity.py",
46
49
  "server.json",
47
50
  "README.md",
@@ -1,85 +0,0 @@
1
- #!/bin/bash
2
- # LED-1259: Compile gateway/ai/license_core.py to a native .so via Nuitka,
3
- # then strip the plaintext .py from the bundle so customers cannot grep
4
- # the validation logic for bypass identifiers.
5
- #
6
- # Linux-only first ship. Mac/Windows expansion is filed as a follow-up
7
- # ledger item — non-linux customers will hit the Python fallback in
8
- # license.py (degraded Pro features) until we ship per-platform binaries.
9
- #
10
- # Idempotent: safe to re-run; will rebuild on every invocation.
11
-
12
- set -euo pipefail
13
-
14
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
- NPM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
16
- AI_DIR="$NPM_ROOT/gateway/ai"
17
- SRC="$AI_DIR/license_core.py"
18
-
19
- # ── Platform gate ────────────────────────────────────────────────────
20
- UNAME_S="$(uname -s)"
21
- UNAME_M="$(uname -m)"
22
- if [ "$UNAME_S" != "Linux" ]; then
23
- echo "⚠️ build-license-core: non-Linux host ($UNAME_S) — skipping compile."
24
- echo " First ship is linux-only. The bundle will fall back to .py."
25
- exit 0
26
- fi
27
-
28
- if [ ! -f "$SRC" ]; then
29
- echo "❌ Source not found: $SRC"
30
- exit 1
31
- fi
32
-
33
- # ── Toolchain check ──────────────────────────────────────────────────
34
- PY="${PYTHON:-python3}"
35
- if ! command -v "$PY" >/dev/null 2>&1; then
36
- echo "❌ python3 not found"
37
- exit 1
38
- fi
39
-
40
- PY_VER="$($PY -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
41
- echo "🔧 build-license-core: python=$PY ($PY_VER), arch=$UNAME_M"
42
-
43
- if ! "$PY" -m nuitka --version >/dev/null 2>&1; then
44
- echo "📦 nuitka not installed — installing via pip..."
45
- "$PY" -m pip install --quiet --user nuitka
46
- fi
47
-
48
- NUITKA_VER="$($PY -m nuitka --version 2>&1 | head -1)"
49
- echo " nuitka=$NUITKA_VER"
50
-
51
- # ── Compile ──────────────────────────────────────────────────────────
52
- echo "🔨 Compiling license_core.py → .so (this takes ~30s)..."
53
- cd "$AI_DIR"
54
- "$PY" -m nuitka --module --quiet --remove-output --output-dir=. license_core.py
55
-
56
- # ── Verify output ────────────────────────────────────────────────────
57
- SO_FILE="$(ls -1 license_core.cpython-*-*.so 2>/dev/null | head -1 || true)"
58
- if [ -z "$SO_FILE" ] || [ ! -f "$SO_FILE" ]; then
59
- echo "❌ Compile failed — no .so produced in $AI_DIR"
60
- ls -la "$AI_DIR"/license_core* 2>&1 || true
61
- exit 1
62
- fi
63
-
64
- SO_SIZE="$(stat -c%s "$SO_FILE")"
65
- echo " ✅ produced: $SO_FILE ($SO_SIZE bytes)"
66
-
67
- # ── Bypass-identifier scan ───────────────────────────────────────────
68
- # Customers must not be able to `strings | grep` the .so for known
69
- # bypass class names. Fail the build if any leak through.
70
- BYPASS_HITS="$(strings "$SO_FILE" | grep -iE 'DELIMIT_TEST_MODE|DELIMIT_INTERNAL_LICENSE_KEY|JAMSONS' || true)"
71
- if [ -n "$BYPASS_HITS" ]; then
72
- echo "❌ Bypass identifiers found in compiled .so:"
73
- echo "$BYPASS_HITS"
74
- exit 1
75
- fi
76
- echo " ✅ strings-grep clean (no bypass identifiers)"
77
-
78
- # ── Drop the plaintext source from the bundle ────────────────────────
79
- # .npmignore + package.json will also exclude it, but removing here is
80
- # belt-and-suspenders so dev/test inspection of the bundle dir matches
81
- # what gets packed.
82
- rm -f "$AI_DIR/license_core.py"
83
- echo " ✅ removed plaintext license_core.py from bundle"
84
-
85
- echo "✅ build-license-core complete: $SO_FILE"
@@ -1,66 +0,0 @@
1
- #!/bin/bash
2
- # Pre-publish security check — blocks npm publish if secrets are found
3
- # Run: bash scripts/security-check.sh
4
-
5
- set -euo pipefail
6
-
7
- echo "🔍 Delimit pre-publish security scan..."
8
-
9
- FAIL=0
10
-
11
- # Pack to temp and scan the actual tarball contents
12
- TMPDIR=$(mktemp -d)
13
- npm pack --pack-destination "$TMPDIR" --quiet 2>/dev/null
14
- TARBALL=$(ls "$TMPDIR"/*.tgz)
15
- tar -xzf "$TARBALL" -C "$TMPDIR"
16
-
17
- # 1. Credential patterns
18
- echo -n " Credentials... "
19
- if grep -rEi '(password|passwd|secret|api_key|apikey)\s*[:=]\s*["\x27][^"\x27]{4,}' "$TMPDIR/package/" --include="*.py" --include="*.js" --include="*.json" 2>/dev/null | grep -v 'environ\|getenv\|process\.env\|os\.environ\|<configured\|example\|placeholder\|REDACTED\|\${credentials\|credentials\.\|security-scan-ignore'; then
20
- echo "❌ FOUND CREDENTIALS"
21
- FAIL=1
22
- else
23
- echo "✅ clean"
24
- fi
25
-
26
- # 2. Blocklist terms
27
- echo -n " Blocklist... "
28
- BLOCKLIST="jamsonsholdings|Bladabah|Domainvested26|Delimit26|home/jamsons|infracore|crypttrx|\.wr_env"
29
- if grep -rEi "$BLOCKLIST" "$TMPDIR/package/" --include="*.py" --include="*.js" --include="*.json" 2>/dev/null; then
30
- echo "❌ BLOCKED TERMS FOUND"
31
- FAIL=1
32
- else
33
- echo "✅ clean"
34
- fi
35
-
36
- # 3. PII (email addresses that aren't examples)
37
- echo -n " PII... "
38
- if grep -rEi '[a-z0-9._%+-]+@(gmail|yahoo|hotmail|outlook|proton|jamsons|wire\.report|domainvested)' "$TMPDIR/package/" --include="*.py" --include="*.js" --include="*.json" 2>/dev/null | grep -v "example\|placeholder\|<configured\|noreply\|e\.g\.\|docstring\|Args:\|Credential resolution"; then
39
- echo "❌ PII FOUND"
40
- FAIL=1
41
- else
42
- echo "✅ clean"
43
- fi
44
-
45
- # 4. Proprietary files that shouldn't ship
46
- echo -n " Proprietary files... "
47
- PROPRIETARY="social_target\.py|social\.py|founding_users\.py|inbox_daemon\.py|deliberation\.py"
48
- if find "$TMPDIR/package/" -name "*.py" | grep -Ei "$PROPRIETARY" 2>/dev/null; then
49
- echo "❌ PROPRIETARY FILES IN PACKAGE"
50
- FAIL=1
51
- else
52
- echo "✅ clean"
53
- fi
54
-
55
- # Cleanup
56
- rm -rf "$TMPDIR"
57
-
58
- if [ $FAIL -ne 0 ]; then
59
- echo ""
60
- echo "❌ SECURITY CHECK FAILED — do not publish"
61
- exit 1
62
- fi
63
-
64
- echo ""
65
- echo "✅ All security checks passed"
66
- exit 0
@@ -1,107 +0,0 @@
1
- #!/bin/bash
2
- # LED-1259: Verify that the compiled license_core .so:
3
- # 1. Compiles cleanly from gateway/ai/license_core.py
4
- # 2. Imports successfully when the .py is absent
5
- # 3. Exposes all public functions/constants the shim relies on
6
- # 4. Returns correct validation verdicts for known-valid / known-expired
7
- # license dicts
8
- # 5. Contains zero bypass identifiers in `strings` output
9
- #
10
- # Runs in an isolated tmp dir so it doesn't pollute the bundle layout.
11
-
12
- set -euo pipefail
13
-
14
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
- NPM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
16
- SRC="$NPM_ROOT/gateway/ai/license_core.py"
17
-
18
- if [ "$(uname -s)" != "Linux" ]; then
19
- echo "⏭️ test-license-core-so: non-Linux host — skipping (Linux-only first ship)"
20
- exit 0
21
- fi
22
-
23
- if [ ! -f "$SRC" ]; then
24
- echo "ℹ️ license_core.py not in bundle (already compiled or pre-build) — copying from gateway src for test"
25
- GW_SRC="${GATEWAY_OVERRIDE:-/home/delimit/delimit-gateway}/ai/license_core.py"
26
- if [ ! -f "$GW_SRC" ]; then
27
- echo "❌ No license_core.py source found (bundle or gateway). Cannot test."
28
- exit 1
29
- fi
30
- SRC="$GW_SRC"
31
- fi
32
-
33
- PY="${PYTHON:-python3}"
34
- TMP="$(mktemp -d)"
35
- trap 'rm -rf "$TMP"' EXIT
36
-
37
- echo "🧪 test-license-core-so: building in $TMP"
38
- mkdir -p "$TMP/ai"
39
- touch "$TMP/ai/__init__.py"
40
- cp "$SRC" "$TMP/license_core.py"
41
-
42
- cd "$TMP"
43
- "$PY" -m nuitka --module --quiet --remove-output --output-dir=. license_core.py 2>&1 | tail -3
44
-
45
- SO_FILE="$(ls -1 license_core.cpython-*-*.so 2>/dev/null | head -1)"
46
- if [ -z "$SO_FILE" ]; then
47
- echo "❌ Compile failed — no .so produced"
48
- exit 1
49
- fi
50
- echo " ✅ compiled: $SO_FILE ($(stat -c%s "$SO_FILE") bytes)"
51
-
52
- # Strings-grep for bypass identifiers
53
- HITS="$(strings "$SO_FILE" | grep -iE 'DELIMIT_TEST_MODE|DELIMIT_INTERNAL_LICENSE_KEY|JAMSONS' || true)"
54
- if [ -n "$HITS" ]; then
55
- echo "❌ Bypass identifiers leaked into .so:"
56
- echo "$HITS"
57
- exit 1
58
- fi
59
- echo " ✅ strings-grep clean"
60
-
61
- # Move .so under ai/, drop the .py, run import + behaviour checks
62
- mv "$SO_FILE" "ai/$SO_FILE"
63
- rm -f license_core.py
64
-
65
- "$PY" - <<'PY'
66
- import os, sys, time
67
- sys.path.insert(0, ".")
68
-
69
- # Import via the compiled .so only — no .py present
70
- from ai.license_core import (
71
- is_license_valid, revalidate_license, needs_revalidation,
72
- PRO_TOOLS, LICENSE_FILE, FREE_TRIAL_LIMITS, activate,
73
- load_license, check_premium, gate_tool,
74
- )
75
-
76
- assert isinstance(PRO_TOOLS, frozenset) and len(PRO_TOOLS) > 0, "PRO_TOOLS must be a non-empty frozenset"
77
- assert FREE_TRIAL_LIMITS.get("delimit_deliberate") == 3, "FREE_TRIAL_LIMITS missing delimit_deliberate=3"
78
- assert "delimit_deliberate" in PRO_TOOLS, "PRO_TOOLS must include delimit_deliberate"
79
- assert callable(activate), "activate must be callable"
80
- assert callable(revalidate_license), "revalidate_license must be callable"
81
-
82
- # Known-valid: pro tier, recent last_validated_at
83
- valid_recent = {"tier": "pro", "valid": True, "last_validated_at": time.time()}
84
- assert is_license_valid(valid_recent) is True, "recent pro license should be valid"
85
- assert needs_revalidation(valid_recent) is False, "recent pro license should not need revalidation"
86
-
87
- # Known-invalid: pro tier but last_validated_at > 44 days ago (beyond hard cutoff)
88
- expired = {"tier": "pro", "valid": True, "last_validated_at": time.time() - 60 * 86400}
89
- assert is_license_valid(expired) is False, "expired (60d) pro license must be invalid"
90
- assert needs_revalidation(expired) is True, "expired (60d) pro license must need revalidation"
91
-
92
- # Free tier never valid for Pro
93
- free = {"tier": "free", "valid": True}
94
- assert is_license_valid(free) is False, "free tier must not pass is_license_valid"
95
-
96
- # valid=False forces invalid even when timestamp is recent
97
- revoked = {"tier": "pro", "valid": False, "last_validated_at": time.time()}
98
- assert is_license_valid(revoked) is False, "revoked license must be invalid"
99
-
100
- # Legacy file with no timestamps — should signal needs_revalidation=True
101
- legacy = {"tier": "pro", "valid": True}
102
- assert needs_revalidation(legacy) is True, "legacy license without timestamps must need revalidation"
103
-
104
- print("ALL_OK")
105
- PY
106
-
107
- echo "✅ test-license-core-so: all checks passed"