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.
- package/CHANGELOG.md +71 -8
- package/bin/delimit-cli.js +59 -9
- package/bin/delimit-setup.js +7 -3
- package/gateway/ai/agent_dispatch.py +5 -0
- package/gateway/ai/backends/gateway_core.py +6 -0
- package/gateway/ai/backends/git_health.py +175 -0
- package/gateway/ai/backends/memory_bridge.py +210 -53
- package/gateway/ai/backends/tools_infra.py +93 -0
- package/gateway/ai/backends/tools_real.py +53 -7
- package/gateway/ai/cli_contract.py +185 -0
- package/gateway/ai/governance.py +181 -0
- package/gateway/ai/heartbeat.py +290 -0
- package/gateway/ai/ledger_manager.py +81 -4
- package/gateway/ai/ledger_proof.py +127 -0
- package/gateway/ai/license.py +132 -47
- package/gateway/ai/license_core.cpython-310-x86_64-linux-gnu.so +0 -0
- package/gateway/ai/license_core.pyi +1 -1
- package/gateway/ai/outreach_loop_daemon.py +349 -0
- package/gateway/ai/outreach_substantive.py +768 -7
- package/gateway/ai/pro_tools.yaml +167 -0
- package/gateway/ai/reddit_scanner.py +7 -1
- package/gateway/ai/server.py +295 -116
- package/gateway/ai/session_phoenix.py +121 -0
- package/gateway/ai/social_queue.py +166 -10
- package/gateway/ai/tenant_auth.py +329 -0
- package/gateway/ai/tenant_data.py +339 -0
- package/gateway/ai/tenant_paths.py +150 -0
- package/gateway/core/diff_engine_v2.py +517 -54
- package/gateway/core/semver_classifier.py +52 -6
- package/package.json +4 -1
- package/scripts/build-license-core.sh +0 -85
- package/scripts/security-check.sh +0 -66
- package/scripts/test-license-core-so.sh +0 -107
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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"
|