delimit-cli 3.14.15 → 3.14.17
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/bin/delimit-cli.js +3 -0
- package/bin/delimit-setup.js +56 -2
- package/gateway/ai/agent_dispatch.py +458 -0
- package/gateway/ai/agent_policy.py +230 -0
- package/gateway/ai/drift_monitor.py +246 -0
- package/gateway/ai/server.py +791 -38
- package/lib/cross-model-hooks.js +95 -3
- package/package.json +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Per-model policy scoping — agent-level governance.
|
|
2
|
+
|
|
3
|
+
Allows setting different permissions per AI model:
|
|
4
|
+
- Which tools each model can call
|
|
5
|
+
- Read-only vs read-write access to ledger/memory
|
|
6
|
+
- Deploy permissions per model
|
|
7
|
+
- Custom constraints per agent identity
|
|
8
|
+
|
|
9
|
+
Storage: ~/.delimit/agents/policies.json
|
|
10
|
+
|
|
11
|
+
Feedback origin: Accurate_Mistake_398 on r/ClaudeAI (2026-03-28)
|
|
12
|
+
identified that governance was session-level, not agent-level.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
AGENTS_DIR = Path.home() / ".delimit" / "agents"
|
|
21
|
+
POLICIES_FILE = AGENTS_DIR / "policies.json"
|
|
22
|
+
|
|
23
|
+
# Default permissions — what each model gets if no policy is set
|
|
24
|
+
DEFAULT_PERMISSIONS = {
|
|
25
|
+
"ledger": "read-write",
|
|
26
|
+
"memory": "read-write",
|
|
27
|
+
"deploy": False,
|
|
28
|
+
"lint": True,
|
|
29
|
+
"deliberate": True,
|
|
30
|
+
"security_audit": True,
|
|
31
|
+
"evidence": "read-write",
|
|
32
|
+
"secrets": False,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
VALID_MODELS = {"claude", "codex", "gemini", "cursor", "any"}
|
|
36
|
+
VALID_ACCESS = {"read-only", "read-write", "none"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_dir():
|
|
40
|
+
AGENTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_policies() -> Dict[str, Any]:
|
|
44
|
+
if not POLICIES_FILE.exists():
|
|
45
|
+
return {}
|
|
46
|
+
try:
|
|
47
|
+
return json.loads(POLICIES_FILE.read_text())
|
|
48
|
+
except (json.JSONDecodeError, OSError):
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _save_policies(policies: Dict[str, Any]):
|
|
53
|
+
_ensure_dir()
|
|
54
|
+
POLICIES_FILE.write_text(json.dumps(policies, indent=2))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def set_agent_policy(
|
|
58
|
+
model: str,
|
|
59
|
+
ledger: str = "",
|
|
60
|
+
memory: str = "",
|
|
61
|
+
deploy: Optional[bool] = None,
|
|
62
|
+
lint: Optional[bool] = None,
|
|
63
|
+
deliberate: Optional[bool] = None,
|
|
64
|
+
security_audit: Optional[bool] = None,
|
|
65
|
+
evidence: str = "",
|
|
66
|
+
secrets: Optional[bool] = None,
|
|
67
|
+
custom_constraints: Optional[List[str]] = None,
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
"""Set permissions for a specific AI model.
|
|
70
|
+
|
|
71
|
+
Example: set_agent_policy("codex", ledger="read-only", deploy=False)
|
|
72
|
+
means Codex can read the ledger but not write, and cannot deploy.
|
|
73
|
+
"""
|
|
74
|
+
model = model.lower().strip()
|
|
75
|
+
if model not in VALID_MODELS:
|
|
76
|
+
return {"error": f"model must be one of: {', '.join(sorted(VALID_MODELS))}"}
|
|
77
|
+
|
|
78
|
+
policies = _load_policies()
|
|
79
|
+
existing = policies.get(model, dict(DEFAULT_PERMISSIONS))
|
|
80
|
+
|
|
81
|
+
if ledger and ledger in VALID_ACCESS:
|
|
82
|
+
existing["ledger"] = ledger
|
|
83
|
+
if memory and memory in VALID_ACCESS:
|
|
84
|
+
existing["memory"] = memory
|
|
85
|
+
if evidence and evidence in VALID_ACCESS:
|
|
86
|
+
existing["evidence"] = evidence
|
|
87
|
+
if deploy is not None:
|
|
88
|
+
existing["deploy"] = deploy
|
|
89
|
+
if lint is not None:
|
|
90
|
+
existing["lint"] = lint
|
|
91
|
+
if deliberate is not None:
|
|
92
|
+
existing["deliberate"] = deliberate
|
|
93
|
+
if security_audit is not None:
|
|
94
|
+
existing["security_audit"] = security_audit
|
|
95
|
+
if secrets is not None:
|
|
96
|
+
existing["secrets"] = secrets
|
|
97
|
+
if custom_constraints is not None:
|
|
98
|
+
existing["custom_constraints"] = custom_constraints
|
|
99
|
+
|
|
100
|
+
existing["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
101
|
+
policies[model] = existing
|
|
102
|
+
_save_policies(policies)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"status": "updated",
|
|
106
|
+
"model": model,
|
|
107
|
+
"policy": existing,
|
|
108
|
+
"message": f"Policy updated for {model}",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_agent_policy(model: str = "") -> Dict[str, Any]:
|
|
113
|
+
"""Get permissions for a specific model, or all models."""
|
|
114
|
+
policies = _load_policies()
|
|
115
|
+
|
|
116
|
+
if not model or not model.strip():
|
|
117
|
+
# Return all policies with defaults filled in
|
|
118
|
+
all_policies = {}
|
|
119
|
+
for m in VALID_MODELS - {"any"}:
|
|
120
|
+
all_policies[m] = policies.get(m, dict(DEFAULT_PERMISSIONS))
|
|
121
|
+
return {
|
|
122
|
+
"status": "ok",
|
|
123
|
+
"policies": all_policies,
|
|
124
|
+
"default": DEFAULT_PERMISSIONS,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
model = model.lower().strip()
|
|
128
|
+
if model not in VALID_MODELS:
|
|
129
|
+
return {"error": f"model must be one of: {', '.join(sorted(VALID_MODELS))}"}
|
|
130
|
+
|
|
131
|
+
policy = policies.get(model, dict(DEFAULT_PERMISSIONS))
|
|
132
|
+
return {
|
|
133
|
+
"status": "ok",
|
|
134
|
+
"model": model,
|
|
135
|
+
"policy": policy,
|
|
136
|
+
"is_default": model not in policies,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def check_agent_permission(
|
|
141
|
+
model: str,
|
|
142
|
+
action: str,
|
|
143
|
+
resource: str = "",
|
|
144
|
+
) -> Dict[str, Any]:
|
|
145
|
+
"""Check if a model is allowed to perform an action.
|
|
146
|
+
|
|
147
|
+
Actions: ledger_write, ledger_read, memory_write, memory_read,
|
|
148
|
+
deploy, lint, deliberate, security_audit, evidence_write,
|
|
149
|
+
evidence_read, secrets_read, secrets_write.
|
|
150
|
+
|
|
151
|
+
Returns: {"allowed": bool, "reason": str}
|
|
152
|
+
"""
|
|
153
|
+
model = model.lower().strip() if model else "any"
|
|
154
|
+
policies = _load_policies()
|
|
155
|
+
policy = policies.get(model, dict(DEFAULT_PERMISSIONS))
|
|
156
|
+
|
|
157
|
+
action = action.lower().strip()
|
|
158
|
+
|
|
159
|
+
# Parse action into category + operation
|
|
160
|
+
if "_" in action:
|
|
161
|
+
parts = action.split("_", 1)
|
|
162
|
+
category = parts[0]
|
|
163
|
+
operation = parts[1] if len(parts) > 1 else "read"
|
|
164
|
+
else:
|
|
165
|
+
category = action
|
|
166
|
+
operation = "read"
|
|
167
|
+
|
|
168
|
+
# Check access-level permissions (ledger, memory, evidence)
|
|
169
|
+
if category in ("ledger", "memory", "evidence"):
|
|
170
|
+
access = policy.get(category, "read-write")
|
|
171
|
+
if access == "none":
|
|
172
|
+
return {
|
|
173
|
+
"allowed": False,
|
|
174
|
+
"model": model,
|
|
175
|
+
"action": action,
|
|
176
|
+
"reason": f"{model} has no access to {category}",
|
|
177
|
+
}
|
|
178
|
+
if operation == "write" and access == "read-only":
|
|
179
|
+
return {
|
|
180
|
+
"allowed": False,
|
|
181
|
+
"model": model,
|
|
182
|
+
"action": action,
|
|
183
|
+
"reason": f"{model} has read-only access to {category}",
|
|
184
|
+
}
|
|
185
|
+
return {"allowed": True, "model": model, "action": action, "reason": "permitted"}
|
|
186
|
+
|
|
187
|
+
# Check boolean permissions (deploy, lint, etc.)
|
|
188
|
+
if category in ("deploy", "lint", "deliberate", "security_audit", "secrets"):
|
|
189
|
+
key = category.replace("security_", "security_")
|
|
190
|
+
allowed = policy.get(key, DEFAULT_PERMISSIONS.get(key, True))
|
|
191
|
+
if not allowed:
|
|
192
|
+
return {
|
|
193
|
+
"allowed": False,
|
|
194
|
+
"model": model,
|
|
195
|
+
"action": action,
|
|
196
|
+
"reason": f"{model} is not permitted to {category}",
|
|
197
|
+
}
|
|
198
|
+
return {"allowed": True, "model": model, "action": action, "reason": "permitted"}
|
|
199
|
+
|
|
200
|
+
# Check custom constraints
|
|
201
|
+
constraints = policy.get("custom_constraints", [])
|
|
202
|
+
for c in constraints:
|
|
203
|
+
c_lower = c.lower().strip()
|
|
204
|
+
if c_lower.startswith("no-") and c_lower[3:] in action:
|
|
205
|
+
return {
|
|
206
|
+
"allowed": False,
|
|
207
|
+
"model": model,
|
|
208
|
+
"action": action,
|
|
209
|
+
"reason": f"Custom constraint: {c}",
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {"allowed": True, "model": model, "action": action, "reason": "no restrictions"}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def remove_agent_policy(model: str) -> Dict[str, Any]:
|
|
216
|
+
"""Remove custom policy for a model, reverting to defaults."""
|
|
217
|
+
model = model.lower().strip()
|
|
218
|
+
policies = _load_policies()
|
|
219
|
+
|
|
220
|
+
if model not in policies:
|
|
221
|
+
return {"status": "ok", "message": f"No custom policy for {model} (already using defaults)"}
|
|
222
|
+
|
|
223
|
+
del policies[model]
|
|
224
|
+
_save_policies(policies)
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
"status": "removed",
|
|
228
|
+
"model": model,
|
|
229
|
+
"message": f"Custom policy removed for {model}. Now using defaults.",
|
|
230
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Continuous drift/compliance monitoring — detects API spec changes without governance review.
|
|
2
|
+
|
|
3
|
+
Compares the current spec against the last known baseline and flags:
|
|
4
|
+
- Spec changed without a governance lint run
|
|
5
|
+
- Policy violations that accumulated since last review
|
|
6
|
+
- New endpoints or schemas without documentation
|
|
7
|
+
- Baseline staleness (hasn't been updated in N days)
|
|
8
|
+
|
|
9
|
+
Storage: ~/.delimit/drift/
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import time
|
|
14
|
+
import hashlib
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
DRIFT_DIR = Path.home() / ".delimit" / "drift"
|
|
19
|
+
HISTORY_FILE = DRIFT_DIR / "history.jsonl"
|
|
20
|
+
BASELINE_FILE = DRIFT_DIR / "baseline_hash.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ensure_dir():
|
|
24
|
+
DRIFT_DIR.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _hash_file(path: str) -> str:
|
|
28
|
+
"""SHA256 of a file's contents."""
|
|
29
|
+
try:
|
|
30
|
+
content = Path(path).read_bytes()
|
|
31
|
+
return hashlib.sha256(content).hexdigest()
|
|
32
|
+
except (OSError, FileNotFoundError):
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_baselines() -> Dict[str, Any]:
|
|
37
|
+
if not BASELINE_FILE.exists():
|
|
38
|
+
return {}
|
|
39
|
+
try:
|
|
40
|
+
return json.loads(BASELINE_FILE.read_text())
|
|
41
|
+
except (json.JSONDecodeError, OSError):
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _save_baselines(baselines: Dict[str, Any]):
|
|
46
|
+
_ensure_dir()
|
|
47
|
+
BASELINE_FILE.write_text(json.dumps(baselines, indent=2))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _append_history(entry: Dict[str, Any]):
|
|
51
|
+
_ensure_dir()
|
|
52
|
+
entry["ts"] = time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
53
|
+
with open(HISTORY_FILE, "a") as f:
|
|
54
|
+
f.write(json.dumps(entry) + "\n")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_drift(
|
|
58
|
+
spec_path: str = "",
|
|
59
|
+
project_path: str = ".",
|
|
60
|
+
staleness_days: int = 7,
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
"""Check for API spec drift against the last governance baseline.
|
|
63
|
+
|
|
64
|
+
Returns drift findings and compliance status.
|
|
65
|
+
"""
|
|
66
|
+
from pathlib import Path as P
|
|
67
|
+
|
|
68
|
+
project = P(project_path).resolve()
|
|
69
|
+
findings: List[Dict[str, str]] = []
|
|
70
|
+
baselines = _load_baselines()
|
|
71
|
+
|
|
72
|
+
# Auto-detect spec if not provided
|
|
73
|
+
if not spec_path:
|
|
74
|
+
spec_patterns = [
|
|
75
|
+
"openapi.yaml", "openapi.yml", "openapi.json",
|
|
76
|
+
"swagger.yaml", "swagger.yml", "swagger.json",
|
|
77
|
+
"api.yaml", "api.yml", "api.json",
|
|
78
|
+
]
|
|
79
|
+
for pattern in spec_patterns:
|
|
80
|
+
candidate = project / pattern
|
|
81
|
+
if candidate.exists():
|
|
82
|
+
spec_path = str(candidate)
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
# Check .delimit/baseline.yaml (zero-spec)
|
|
86
|
+
if not spec_path:
|
|
87
|
+
baseline_candidate = project / ".delimit" / "baseline.yaml"
|
|
88
|
+
if baseline_candidate.exists():
|
|
89
|
+
spec_path = str(baseline_candidate)
|
|
90
|
+
|
|
91
|
+
if not spec_path:
|
|
92
|
+
return {
|
|
93
|
+
"status": "no_spec",
|
|
94
|
+
"message": "No OpenAPI spec found. Run `delimit init` to set up governance.",
|
|
95
|
+
"drift_detected": False,
|
|
96
|
+
"findings": [],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Check if spec has changed since last baseline
|
|
100
|
+
current_hash = _hash_file(spec_path)
|
|
101
|
+
spec_key = str(P(spec_path).resolve())
|
|
102
|
+
baseline = baselines.get(spec_key, {})
|
|
103
|
+
last_hash = baseline.get("hash", "")
|
|
104
|
+
last_reviewed = baseline.get("last_reviewed", "")
|
|
105
|
+
last_lint_pass = baseline.get("last_lint_pass", False)
|
|
106
|
+
|
|
107
|
+
drift_detected = False
|
|
108
|
+
|
|
109
|
+
if not last_hash:
|
|
110
|
+
# First run — save baseline
|
|
111
|
+
findings.append({
|
|
112
|
+
"type": "new_baseline",
|
|
113
|
+
"severity": "info",
|
|
114
|
+
"message": "First drift check — baseline recorded.",
|
|
115
|
+
})
|
|
116
|
+
baselines[spec_key] = {
|
|
117
|
+
"hash": current_hash,
|
|
118
|
+
"last_reviewed": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
119
|
+
"last_lint_pass": True,
|
|
120
|
+
"spec_path": spec_path,
|
|
121
|
+
}
|
|
122
|
+
_save_baselines(baselines)
|
|
123
|
+
elif current_hash != last_hash:
|
|
124
|
+
drift_detected = True
|
|
125
|
+
findings.append({
|
|
126
|
+
"type": "spec_changed",
|
|
127
|
+
"severity": "warning",
|
|
128
|
+
"message": f"API spec changed since last governance review ({last_reviewed or 'unknown'})",
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
# Check staleness
|
|
132
|
+
if last_reviewed:
|
|
133
|
+
try:
|
|
134
|
+
reviewed_ts = time.mktime(time.strptime(last_reviewed, "%Y-%m-%dT%H:%M:%SZ"))
|
|
135
|
+
age_days = (time.time() - reviewed_ts) / 86400
|
|
136
|
+
if age_days > staleness_days:
|
|
137
|
+
drift_detected = True
|
|
138
|
+
findings.append({
|
|
139
|
+
"type": "stale_baseline",
|
|
140
|
+
"severity": "warning",
|
|
141
|
+
"message": f"Baseline is {int(age_days)} days old (threshold: {staleness_days} days)",
|
|
142
|
+
})
|
|
143
|
+
except (ValueError, OverflowError):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# Check for .delimit/policies.yml existence
|
|
147
|
+
policy_file = project / ".delimit" / "policies.yml"
|
|
148
|
+
if not policy_file.exists():
|
|
149
|
+
findings.append({
|
|
150
|
+
"type": "no_policy",
|
|
151
|
+
"severity": "warning",
|
|
152
|
+
"message": "No policy file found. Run `delimit init` to configure governance.",
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
# Check for evidence directory
|
|
156
|
+
evidence_dir = project / ".delimit" / "evidence"
|
|
157
|
+
if not evidence_dir.exists():
|
|
158
|
+
findings.append({
|
|
159
|
+
"type": "no_evidence",
|
|
160
|
+
"severity": "info",
|
|
161
|
+
"message": "No evidence directory. First governance run will create it.",
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
# Record drift check in history
|
|
165
|
+
_append_history({
|
|
166
|
+
"action": "drift_check",
|
|
167
|
+
"spec_path": spec_path,
|
|
168
|
+
"drift_detected": drift_detected,
|
|
169
|
+
"findings_count": len(findings),
|
|
170
|
+
"current_hash": current_hash[:16],
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"status": "drift" if drift_detected else "clean",
|
|
175
|
+
"drift_detected": drift_detected,
|
|
176
|
+
"spec_path": spec_path,
|
|
177
|
+
"findings": findings,
|
|
178
|
+
"baseline": {
|
|
179
|
+
"hash": last_hash[:16] if last_hash else None,
|
|
180
|
+
"last_reviewed": last_reviewed,
|
|
181
|
+
"current_hash": current_hash[:16],
|
|
182
|
+
},
|
|
183
|
+
"recommendation": (
|
|
184
|
+
"Run `delimit lint` to review spec changes and update the governance baseline."
|
|
185
|
+
if drift_detected else
|
|
186
|
+
"No drift detected. Governance baseline is current."
|
|
187
|
+
),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def update_baseline(spec_path: str, lint_passed: bool = True) -> Dict[str, Any]:
|
|
192
|
+
"""Update the drift baseline after a successful governance review."""
|
|
193
|
+
from pathlib import Path as P
|
|
194
|
+
|
|
195
|
+
if not spec_path:
|
|
196
|
+
return {"error": "spec_path is required"}
|
|
197
|
+
|
|
198
|
+
current_hash = _hash_file(spec_path)
|
|
199
|
+
if not current_hash:
|
|
200
|
+
return {"error": f"Cannot read spec at {spec_path}"}
|
|
201
|
+
|
|
202
|
+
spec_key = str(P(spec_path).resolve())
|
|
203
|
+
baselines = _load_baselines()
|
|
204
|
+
|
|
205
|
+
baselines[spec_key] = {
|
|
206
|
+
"hash": current_hash,
|
|
207
|
+
"last_reviewed": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
208
|
+
"last_lint_pass": lint_passed,
|
|
209
|
+
"spec_path": spec_path,
|
|
210
|
+
}
|
|
211
|
+
_save_baselines(baselines)
|
|
212
|
+
|
|
213
|
+
_append_history({
|
|
214
|
+
"action": "baseline_update",
|
|
215
|
+
"spec_path": spec_path,
|
|
216
|
+
"hash": current_hash[:16],
|
|
217
|
+
"lint_passed": lint_passed,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
"status": "updated",
|
|
222
|
+
"spec_path": spec_path,
|
|
223
|
+
"hash": current_hash[:16],
|
|
224
|
+
"message": "Drift baseline updated.",
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_drift_history(limit: int = 20) -> Dict[str, Any]:
|
|
229
|
+
"""Return recent drift check history."""
|
|
230
|
+
entries: List[Dict] = []
|
|
231
|
+
if HISTORY_FILE.exists():
|
|
232
|
+
try:
|
|
233
|
+
lines = HISTORY_FILE.read_text().strip().split("\n")
|
|
234
|
+
for line in lines[-limit:]:
|
|
235
|
+
try:
|
|
236
|
+
entries.append(json.loads(line))
|
|
237
|
+
except json.JSONDecodeError:
|
|
238
|
+
pass
|
|
239
|
+
except OSError:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"status": "ok",
|
|
244
|
+
"entries": entries,
|
|
245
|
+
"total": len(entries),
|
|
246
|
+
}
|