delimit-cli 4.5.0 → 4.5.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +2 -2
  3. package/adapters/cursor-rules.js +17 -4
  4. package/bin/delimit-cli.js +109 -24
  5. package/gateway/ai/content_engine.py +3 -4
  6. package/gateway/ai/inbox_classifier.py +215 -0
  7. package/gateway/ai/integrations/opensage_wrapper.py +4 -1
  8. package/gateway/ai/ledger_manager.py +218 -38
  9. package/gateway/ai/license.py +26 -0
  10. package/gateway/ai/notify.py +68 -3
  11. package/gateway/ai/reddit_proxy.py +93 -15
  12. package/gateway/ai/reddit_scanner.py +36 -18
  13. package/gateway/ai/server.py +128 -6
  14. package/gateway/ai/social_capability/__init__.py +6 -0
  15. package/gateway/ai/social_capability/capability_validator.py +273 -0
  16. package/gateway/ai/social_capability/current_capabilities.yaml +95 -0
  17. package/gateway/ai/social_queue.py +307 -0
  18. package/gateway/ai/supabase_sync.py +14 -2
  19. package/gateway/ai/swarm.py +29 -11
  20. package/gateway/ai/tui.py +6 -2
  21. package/gateway/ai/x_ranker.py +276 -0
  22. package/lib/attest-mcp.js +487 -0
  23. package/lib/attest-telemetry.js +48 -0
  24. package/lib/delimit-home.js +35 -0
  25. package/lib/delimit-template.js +14 -0
  26. package/lib/managed-section.js +92 -0
  27. package/lib/trust-page-engine.js +6 -2
  28. package/lib/wrap-engine.js +21 -4
  29. package/package.json +8 -2
  30. package/scripts/postinstall.js +89 -40
  31. package/gateway/ai/content_grounding/__init__.py +0 -98
  32. package/gateway/ai/content_grounding/build.py +0 -350
  33. package/gateway/ai/content_grounding/consume.py +0 -280
  34. package/gateway/ai/content_grounding/features.py +0 -218
  35. package/gateway/ai/content_grounding/fixtures/fail/01_missing_evidence.json +0 -9
  36. package/gateway/ai/content_grounding/fixtures/fail/02_unknown_evidence_prefix.json +0 -9
  37. package/gateway/ai/content_grounding/fixtures/fail/03_banned_comparative.json +0 -17
  38. package/gateway/ai/content_grounding/fixtures/fail/04_banned_adoption.json +0 -17
  39. package/gateway/ai/content_grounding/fixtures/fail/05_aggregate_no_numeric.json +0 -17
  40. package/gateway/ai/content_grounding/fixtures/fail/06_unversioned_inference_rule.json +0 -18
  41. package/gateway/ai/content_grounding/fixtures/pass/01_feature_shipped.json +0 -18
  42. package/gateway/ai/content_grounding/fixtures/pass/02_aggregate_claim.json +0 -23
  43. package/gateway/ai/content_grounding/fixtures/pass/03_attestation.json +0 -16
  44. package/gateway/ai/content_grounding/schemas/claim.schema.json +0 -40
  45. package/gateway/ai/content_grounding/schemas/event.schema.json +0 -23
  46. package/gateway/ai/content_grounding/schemas.py +0 -276
  47. package/gateway/ai/content_grounding/telemetry.py +0 -221
  48. package/gateway/ai/inbox_drafts/__init__.py +0 -61
  49. package/gateway/ai/inbox_drafts/registry.py +0 -412
  50. package/gateway/ai/inbox_drafts/schema.py +0 -374
  51. package/gateway/ai/inbox_executor.py +0 -565
@@ -1,374 +0,0 @@
1
- """Draft schema, canonicalization, and HMAC primitives — LED-1129 Phase 1.
2
-
3
- This is the foundation layer. Other layers (registry, executor) build on top.
4
-
5
- Key invariants:
6
-
7
- - Every draft gets a ULID `draft_id` (sortable, unique, NOT derived from content).
8
- - Every draft is signed with HMAC-SHA256 over a canonical byte representation
9
- of (draft_id, draft_kind, target, payload, issued_at, key_version) — the HMAC
10
- scope binds a signature to one concrete instance, so subject-line collisions
11
- or thread reuse cannot replay an approval against a different draft.
12
- - Canonical JSON form is RFC 8785 (sorted keys, no whitespace, UTF-8 NFC).
13
- Written down here AND in docs/inbox_executor_v1.md so the daemon writer
14
- and the executor reader cannot silently disagree on bytes.
15
- - HMAC key lives at ~/.delimit/secrets/inbox-executor-hmac.key (mode 600).
16
- Distinct from wrap-hmac.key — separation of concerns. Auto-generated on
17
- first sign call if missing.
18
- - Schema is versioned via `schema_version` field. v1 is the only version;
19
- future migrations bump it.
20
- - Keys are versioned via `key_version`. Rotation issues a new version side
21
- by side; verifier tries the registered version first.
22
- """
23
-
24
- from __future__ import annotations
25
-
26
- import enum
27
- import hashlib
28
- import hmac
29
- import json
30
- import os
31
- import secrets
32
- import time
33
- import unicodedata
34
- import uuid
35
- from dataclasses import dataclass
36
- from pathlib import Path
37
- from typing import Any, Dict, Optional, Tuple
38
-
39
-
40
- SCHEMA_VERSION = "v1"
41
- HMAC_KEY_PATH = Path.home() / ".delimit" / "secrets" / "inbox-executor-hmac.key"
42
- DEFAULT_TTL_SECONDS = 24 * 60 * 60 # 24h per the LED-1129 deliberation
43
-
44
-
45
- class DraftKind(str, enum.Enum):
46
- """Allowlisted action types the executor will dispatch on (Phase 2+).
47
-
48
- Anything outside this enum terminates at `founder_directive_acked` and
49
- requires a Claude session to execute. Adding a new kind is itself an
50
- authority_class_expansion event under STR-183 — needs founder attestation.
51
- """
52
-
53
- GITHUB_COMMENT = "github_comment"
54
- SOCIAL_POST = "social_post"
55
- LEDGER_DONE = "ledger_done"
56
- NOTIFY_ROUTING_UPDATE = "notify_routing_update"
57
- DEPLOY_PUBLISH_PREVALIDATED_ARTIFACT = "deploy_publish_prevalidated_artifact"
58
-
59
-
60
- class DraftStatus(str, enum.Enum):
61
- """State machine for a single draft.
62
-
63
- Transitions (the only legal ones):
64
- pending → approved (HMAC verified + founder Ship-it match)
65
- pending → expired (TTL elapsed)
66
- pending → cancelled (founder reply detected as negative/cancel)
67
- approved → executing (executor takes the row, before side effect)
68
- executing → completed (executor finished + recorded executed_url)
69
- executing → completed_with_error (executor finished but action failed)
70
- * → terminal_unrecoverable (only set by human reconciliation)
71
-
72
- A row stuck at `executing` after a process restart surfaces for human
73
- reconciliation — we do NOT auto-retry. That's at-most-once semantics.
74
- """
75
-
76
- PENDING = "pending"
77
- APPROVED = "approved"
78
- EXECUTING = "executing"
79
- COMPLETED = "completed"
80
- COMPLETED_WITH_ERROR = "completed_with_error"
81
- EXPIRED = "expired"
82
- CANCELLED = "cancelled"
83
- TERMINAL_UNRECOVERABLE = "terminal_unrecoverable"
84
-
85
-
86
- # ── ULID ──────────────────────────────────────────────────────────────
87
-
88
- # A minimal monotonic ULID generator. We do not pull a third-party dep;
89
- # the gateway has no ulid package and this is the only call site.
90
- # 26 chars: 10 char timestamp (ms since epoch, base32) + 16 chars randomness.
91
- # Sortable lexicographically by time. NOT content-derived (that would defeat
92
- # the purpose — we want monotonic IDs for index locality, content goes in
93
- # the HMAC scope separately).
94
- _ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" # Crockford's base32
95
-
96
-
97
- def new_draft_id() -> str:
98
- """Return a new ULID (26 chars, monotonic by time, urlsafe).
99
-
100
- Not cryptographically derived from content — content binding lives in
101
- the HMAC. The ID is just an instance identifier with index-locality
102
- properties.
103
- """
104
- ts_ms = int(time.time() * 1000)
105
- ts_part = ""
106
- n = ts_ms
107
- for _ in range(10):
108
- ts_part = _ULID_ALPHABET[n & 0x1F] + ts_part
109
- n >>= 5
110
-
111
- rand_bytes = secrets.token_bytes(10)
112
- rand_part = ""
113
- n = int.from_bytes(rand_bytes, "big")
114
- for _ in range(16):
115
- rand_part = _ULID_ALPHABET[n & 0x1F] + rand_part
116
- n >>= 5
117
-
118
- return ts_part + rand_part
119
-
120
-
121
- # ── Canonical JSON (RFC 8785-style) ───────────────────────────────────
122
-
123
-
124
- def canonicalize(value: Any) -> bytes:
125
- """Return the canonical byte representation of `value`.
126
-
127
- Per RFC 8785 / JSON Canonicalization Scheme, with simplifications:
128
- - Object keys sorted lexicographically (string sort, not byte sort).
129
- - No whitespace between tokens.
130
- - Strings normalized to Unicode NFC before serialization (so the same
131
- visual character always hashes the same regardless of input form).
132
- - Numbers serialized via Python's `json.dumps` default; no scientific
133
- notation reformatting (callers should pass ints/floats already in the
134
- form they want signed).
135
- - Booleans, None, lists pass through.
136
-
137
- The function is deterministic: feeding the same Python value at any time
138
- on any machine returns the same bytes. That's the only contract that
139
- matters for HMAC parity between daemon and executor.
140
-
141
- LIMITATION (documented, not fixed in v1): Python's float→str varies in
142
- the lowest bits between platforms. Phase 1 callers MUST stringify floats
143
- upstream if they need cross-host parity. We do not currently sign
144
- payloads with floats, but the spec doc warns about it.
145
- """
146
- return _canon(value).encode("utf-8")
147
-
148
-
149
- def _canon(v: Any) -> str:
150
- if v is None:
151
- return "null"
152
- if v is True:
153
- return "true"
154
- if v is False:
155
- return "false"
156
- if isinstance(v, (int,)) and not isinstance(v, bool):
157
- return str(v)
158
- if isinstance(v, float):
159
- # See LIMITATION above. We use repr to keep round-trip stable on
160
- # the local host, but cross-host parity is not guaranteed for floats.
161
- return repr(v)
162
- if isinstance(v, str):
163
- return json.dumps(unicodedata.normalize("NFC", v), ensure_ascii=False)
164
- if isinstance(v, (list, tuple)):
165
- return "[" + ",".join(_canon(x) for x in v) + "]"
166
- if isinstance(v, dict):
167
- items = sorted(v.items(), key=lambda kv: kv[0])
168
- return "{" + ",".join(
169
- json.dumps(unicodedata.normalize("NFC", k), ensure_ascii=False) + ":" + _canon(val)
170
- for k, val in items
171
- ) + "}"
172
- raise TypeError(f"canonicalize: unsupported type {type(v).__name__}")
173
-
174
-
175
- # ── Content hash ──────────────────────────────────────────────────────
176
-
177
-
178
- def content_hash(
179
- draft_id: str,
180
- draft_kind: str,
181
- target: Dict[str, Any],
182
- payload: Any,
183
- issued_at: int,
184
- key_version: int = 1,
185
- schema_version: str = SCHEMA_VERSION,
186
- ) -> str:
187
- """SHA-256 hex digest binding all signed inputs together.
188
-
189
- Includes draft_id (instance binding), kind (semantic binding), target
190
- (where the action lands), payload (what the action does), issued_at
191
- (anti-replay anchor), and the key + schema versions (rotation safety).
192
-
193
- Returned hex string is what the HMAC key signs.
194
- """
195
- blob = canonicalize({
196
- "draft_id": draft_id,
197
- "draft_kind": draft_kind,
198
- "target": target,
199
- "payload": payload,
200
- "issued_at": issued_at,
201
- "key_version": key_version,
202
- "schema_version": schema_version,
203
- })
204
- return hashlib.sha256(blob).hexdigest()
205
-
206
-
207
- # ── HMAC key management ───────────────────────────────────────────────
208
-
209
-
210
- def _ensure_key(path: Optional[Path] = None) -> bytes:
211
- """Read the HMAC key, generating it on first call if missing.
212
-
213
- Mode 600 is enforced so other system users can't read it. Key is 32
214
- random bytes (256-bit), suitable for HMAC-SHA256.
215
-
216
- Resolves the default at call time (not as a default-arg) so tests can
217
- monkeypatch `ai.inbox_drafts.schema.HMAC_KEY_PATH` and have the change
218
- propagate.
219
- """
220
- if path is None:
221
- # Re-read module attribute each call; tests can monkeypatch.
222
- import ai.inbox_drafts.schema as _self
223
- path = _self.HMAC_KEY_PATH
224
- if path.exists():
225
- return path.read_bytes()
226
- path.parent.mkdir(parents=True, exist_ok=True)
227
- key = secrets.token_bytes(32)
228
- path.write_bytes(key)
229
- try:
230
- os.chmod(path, 0o600)
231
- except OSError:
232
- # Best-effort on filesystems that don't support chmod (Windows).
233
- pass
234
- return key
235
-
236
-
237
- # ── Draft sign / verify ───────────────────────────────────────────────
238
-
239
-
240
- @dataclass
241
- class SignedDraft:
242
- """The signed-draft record stored in the registry.
243
-
244
- All fields are part of the HMAC scope (see content_hash). Mutating any
245
- field after signing invalidates the signature; the verifier rejects.
246
- """
247
-
248
- draft_id: str
249
- draft_kind: str
250
- target: Dict[str, Any]
251
- payload: Any
252
- issued_at: int
253
- key_version: int
254
- schema_version: str
255
- content_hash: str
256
- signature: str
257
-
258
- def to_dict(self) -> Dict[str, Any]:
259
- return {
260
- "draft_id": self.draft_id,
261
- "draft_kind": self.draft_kind,
262
- "target": self.target,
263
- "payload": self.payload,
264
- "issued_at": self.issued_at,
265
- "key_version": self.key_version,
266
- "schema_version": self.schema_version,
267
- "content_hash": self.content_hash,
268
- "signature": self.signature,
269
- }
270
-
271
-
272
- def sign_draft(
273
- draft_kind: str,
274
- target: Dict[str, Any],
275
- payload: Any,
276
- *,
277
- draft_id: Optional[str] = None,
278
- issued_at: Optional[int] = None,
279
- key_version: int = 1,
280
- key_path: Optional[Path] = None,
281
- ) -> SignedDraft:
282
- """Create a SignedDraft for a new action proposal.
283
-
284
- `draft_kind` should be one of DraftKind values; we don't enforce here
285
- so the schema layer stays stringly-typed (the registry/executor enforce
286
- the allowlist with proper errors at their boundaries).
287
- """
288
- if draft_id is None:
289
- draft_id = new_draft_id()
290
- if issued_at is None:
291
- issued_at = int(time.time())
292
-
293
- digest = content_hash(
294
- draft_id=draft_id,
295
- draft_kind=draft_kind,
296
- target=target,
297
- payload=payload,
298
- issued_at=issued_at,
299
- key_version=key_version,
300
- schema_version=SCHEMA_VERSION,
301
- )
302
-
303
- key = _ensure_key(key_path) if key_path else _ensure_key()
304
- sig = hmac.new(key, digest.encode("ascii"), hashlib.sha256).hexdigest()
305
-
306
- return SignedDraft(
307
- draft_id=draft_id,
308
- draft_kind=draft_kind,
309
- target=target,
310
- payload=payload,
311
- issued_at=issued_at,
312
- key_version=key_version,
313
- schema_version=SCHEMA_VERSION,
314
- content_hash=digest,
315
- signature=sig,
316
- )
317
-
318
-
319
- def verify_draft(
320
- record: Dict[str, Any],
321
- *,
322
- now: Optional[int] = None,
323
- ttl_seconds: int = DEFAULT_TTL_SECONDS,
324
- key_path: Optional[Path] = None,
325
- ) -> Tuple[bool, str]:
326
- """Verify a stored draft record. Returns (ok, reason).
327
-
328
- Checks (fail-closed; first failure short-circuits):
329
- - schema_version is recognized
330
- - all required fields present
331
- - issued_at within TTL window (not stale, not in the future)
332
- - content_hash matches recomputed hash from fields
333
- - HMAC signature matches recomputed signature using key for key_version
334
-
335
- Phase 1 only supports key_version=1 (the only key extant). Phase 2
336
- adds the rotation lookup table.
337
- """
338
- required = {"draft_id", "draft_kind", "target", "payload", "issued_at",
339
- "key_version", "schema_version", "content_hash", "signature"}
340
- missing = required - set(record.keys())
341
- if missing:
342
- return False, f"missing fields: {sorted(missing)}"
343
-
344
- if record["schema_version"] != SCHEMA_VERSION:
345
- return False, f"unsupported schema_version: {record['schema_version']}"
346
-
347
- if record["key_version"] != 1:
348
- return False, f"unknown key_version: {record['key_version']}"
349
-
350
- if now is None:
351
- now = int(time.time())
352
- if record["issued_at"] > now + 60: # 60s clock skew tolerance
353
- return False, "issued_at is in the future"
354
- if now - record["issued_at"] > ttl_seconds:
355
- return False, "draft expired (TTL elapsed)"
356
-
357
- expected_hash = content_hash(
358
- draft_id=record["draft_id"],
359
- draft_kind=record["draft_kind"],
360
- target=record["target"],
361
- payload=record["payload"],
362
- issued_at=record["issued_at"],
363
- key_version=record["key_version"],
364
- schema_version=record["schema_version"],
365
- )
366
- if not hmac.compare_digest(expected_hash, record["content_hash"]):
367
- return False, "content_hash mismatch (record was tampered)"
368
-
369
- key = _ensure_key(key_path) if key_path else _ensure_key()
370
- expected_sig = hmac.new(key, expected_hash.encode("ascii"), hashlib.sha256).hexdigest()
371
- if not hmac.compare_digest(expected_sig, record["signature"]):
372
- return False, "signature mismatch (HMAC failed)"
373
-
374
- return True, "ok"