@luckydraw/cumulus 0.31.66 → 1.0.1

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 (63) hide show
  1. package/CHANGELOG.md +12 -556
  2. package/LICENSE +150 -0
  3. package/README.md +27 -8
  4. package/dist/gateway/adapters/webchat.d.ts +15 -0
  5. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  6. package/dist/gateway/adapters/webchat.js +78 -5
  7. package/dist/gateway/adapters/webchat.js.map +1 -1
  8. package/dist/gateway/config.d.ts +17 -2
  9. package/dist/gateway/config.d.ts.map +1 -1
  10. package/dist/gateway/config.js +10 -3
  11. package/dist/gateway/config.js.map +1 -1
  12. package/dist/gateway/daemon.d.ts +3 -1
  13. package/dist/gateway/daemon.d.ts.map +1 -1
  14. package/dist/gateway/daemon.js +128 -39
  15. package/dist/gateway/daemon.js.map +1 -1
  16. package/dist/gateway/namespaces.d.ts +34 -0
  17. package/dist/gateway/namespaces.d.ts.map +1 -1
  18. package/dist/gateway/namespaces.js +58 -0
  19. package/dist/gateway/namespaces.js.map +1 -1
  20. package/dist/gateway/server.d.ts +8 -0
  21. package/dist/gateway/server.d.ts.map +1 -1
  22. package/dist/gateway/server.js +150 -41
  23. package/dist/gateway/server.js.map +1 -1
  24. package/dist/gateway/setup.d.ts +32 -0
  25. package/dist/gateway/setup.d.ts.map +1 -1
  26. package/dist/gateway/setup.js +23 -3
  27. package/dist/gateway/setup.js.map +1 -1
  28. package/dist/gateway/static/blex-render.js +341 -0
  29. package/dist/gateway/static/chat.html +1 -0
  30. package/dist/gateway/static/widget.js +1009 -738
  31. package/dist/lib/gateway.d.ts +30 -8
  32. package/dist/lib/gateway.d.ts.map +1 -1
  33. package/dist/lib/gateway.js +36 -11
  34. package/dist/lib/gateway.js.map +1 -1
  35. package/dist/lib/history.d.ts +22 -0
  36. package/dist/lib/history.d.ts.map +1 -1
  37. package/dist/lib/history.js +59 -21
  38. package/dist/lib/history.js.map +1 -1
  39. package/dist/lib/huggingface-provider.d.ts.map +1 -1
  40. package/dist/lib/huggingface-provider.js +11 -3
  41. package/dist/lib/huggingface-provider.js.map +1 -1
  42. package/dist/lib/license.d.ts +76 -0
  43. package/dist/lib/license.d.ts.map +1 -0
  44. package/dist/lib/license.js +141 -0
  45. package/dist/lib/license.js.map +1 -0
  46. package/docs/agentic-harness-primer.md +283 -0
  47. package/docs/conditional-continuation.md +167 -0
  48. package/docs/web-app-agent-guide.md +559 -0
  49. package/examples/web-app-agent/README.md +334 -0
  50. package/examples/web-app-agent/agent/mcp-shim.js +105 -0
  51. package/examples/web-app-agent/gateway.config.example.json +70 -0
  52. package/examples/web-app-agent/package.json +13 -0
  53. package/examples/web-app-agent/public/agent/blex-mount.js +136 -0
  54. package/examples/web-app-agent/public/agent/bridge-mount.js +91 -0
  55. package/examples/web-app-agent/public/agent/chat-client.js +104 -0
  56. package/examples/web-app-agent/public/agent/commands.js +256 -0
  57. package/examples/web-app-agent/public/agent/device-thread.js +48 -0
  58. package/examples/web-app-agent/public/agent/panel.css +113 -0
  59. package/examples/web-app-agent/public/agent/panel.js +392 -0
  60. package/examples/web-app-agent/public/app.js +250 -0
  61. package/examples/web-app-agent/public/index.html +126 -0
  62. package/examples/web-app-agent/server.js +379 -0
  63. package/package.json +7 -3
@@ -23,16 +23,17 @@ import * as path from 'path';
23
23
  import { loadThreadConfig, saveThreadConfig, setConfigThreadsDir } from '../lib/config.js';
24
24
  import { loadEmbeddings, generateMissingEmbeddings } from '../lib/embeddings.js';
25
25
  import { sendMessage, getOrCreateThread, clearThreadCache, getActiveSubprocessCount, setGatewayThreadsDir, } from '../lib/gateway.js';
26
+ import { verifyLicense, majorOf, LICENSE_CONTACT } from '../lib/license.js';
26
27
  import { getPromptsDirForThreadPath, isValidCaptureId, readPromptCapture, } from '../lib/prompt-capture.js';
27
28
  import { catchUpSegmentation, loadSegmentBoundaries, saveSegmentBoundaries, } from '../lib/segments.js';
28
29
  import { listTemplates, scaffoldFromTemplate } from '../lib/templates.js';
29
- import { checkForUpdate, performUpdate, readChangelog } from '../lib/version-check.js';
30
+ import { checkForUpdate, getCurrentVersion, performUpdate, readChangelog, } from '../lib/version-check.js';
30
31
  import { serveStaticFile, getMediaDir } from './adapters/webchat.js';
31
32
  import { authenticate, getPresentedKey } from './auth.js';
32
33
  import { BridgeGateway } from './bridge/gateway.js';
33
34
  import { applyConfigPatch, maskedConfigView } from './config.js';
34
35
  import { handleEmailHook, handleFormHook, handleGenericWebhook, } from './hooks.js';
35
- import { allNamespaceKeys, extraMcpServersForThread, namespaceForKey, namespaceForThread, resolveExecutorProxy, threadAccessibleToScope, threadVisibleToScope, } from './namespaces.js';
36
+ import { allNamespaceKeys, demoMintDenial, extraMcpServersForThread, licenseBannerLines, namespaceForKey, namespaceForThread, resolveExecutorProxy, threadAccessibleToScope, threadVisibleToScope, } from './namespaces.js';
36
37
  import { configureVapid, sendNotification, subscribe as pushSubscribe, unsubscribe as pushUnsubscribe, } from './push.js';
37
38
  import { validateTranscriptFlush, ingestTranscriptFlush } from './transcript-ingest.js';
38
39
  const threadQueues = new Map();
@@ -379,6 +380,39 @@ function denyOutOfNamespace(res, threadName, scope) {
379
380
  jsonResponse(res, 403, { error: 'Thread not in your namespace' });
380
381
  return true;
381
382
  }
383
+ /** Thread names that currently exist on disk (one `.jsonl` per thread). */
384
+ export function listExistingThreadNames() {
385
+ if (!fs.existsSync(THREADS_DIR))
386
+ return [];
387
+ return fs
388
+ .readdirSync(THREADS_DIR)
389
+ .filter(f => f.endsWith('.jsonl'))
390
+ .map(f => f.replace(/\.jsonl$/, ''));
391
+ }
392
+ /** The 402 body for a demo-cap refusal. Exported so tests assert one string. */
393
+ export function demoCapMessage(d) {
394
+ return (`Demo mode: namespace "${d.namespace}" has reached its limit of ${d.cap} visitor threads ` +
395
+ `(${d.count} in use). Existing threads keep working. ` +
396
+ `Commercial and production use requires a licence — contact ${LICENSE_CONTACT}.`);
397
+ }
398
+ /**
399
+ * 402s and returns true when the request would MINT a new thread in an
400
+ * unlicensed namespace that is already at the demo cap (task 127). Non-namespaced
401
+ * threads, already-existing threads and licensed namespaces all pass through.
402
+ */
403
+ function denyOverDemoCap(res, threadName, scope) {
404
+ const denial = demoMintDenial(threadName, scope.namespaces, listExistingThreadNames(), scope.license);
405
+ if (!denial)
406
+ return false;
407
+ jsonResponse(res, 402, {
408
+ error: demoCapMessage(denial),
409
+ demoMode: true,
410
+ namespace: denial.namespace,
411
+ cap: denial.cap,
412
+ contact: LICENSE_CONTACT,
413
+ });
414
+ return true;
415
+ }
382
416
  async function handleAgentInject(req, res, pipelineOpts, scope) {
383
417
  let body;
384
418
  try {
@@ -420,6 +454,23 @@ async function handleAgentInject(req, res, pipelineOpts, scope) {
420
454
  jsonResponse(res, 403, { error: `Thread not in your namespace: ${denied.join(', ')}` });
421
455
  return;
422
456
  }
457
+ // Task 127: injecting into a thread that doesn't exist yet mints it. Checked
458
+ // once against a single inventory read so a multi-target inject can't slip
459
+ // extra threads past the cap.
460
+ const existing = listExistingThreadNames();
461
+ for (const t of targetNames) {
462
+ const capDenial = demoMintDenial(t, scope.namespaces, existing, scope.license);
463
+ if (capDenial) {
464
+ jsonResponse(res, 402, {
465
+ error: demoCapMessage(capDenial),
466
+ demoMode: true,
467
+ namespace: capDenial.namespace,
468
+ cap: capDenial.cap,
469
+ contact: LICENSE_CONTACT,
470
+ });
471
+ return;
472
+ }
473
+ }
423
474
  const results = [];
424
475
  for (const targetName of targetNames) {
425
476
  const type = messageType || 'direct';
@@ -1084,9 +1135,12 @@ async function handleIngestTranscript(req, res, scope) {
1084
1135
  jsonResponse(res, 400, { error: parsed.error });
1085
1136
  return;
1086
1137
  }
1087
- // P7: the target thread arrives in the body, so gate it here.
1138
+ // P7: the target thread arrives in the body, so gate it here. Task 127's demo
1139
+ // cap rides the same spot for the same reason.
1088
1140
  if (denyOutOfNamespace(res, parsed.flush.targetThread, scope))
1089
1141
  return;
1142
+ if (denyOverDemoCap(res, parsed.flush.targetThread, scope))
1143
+ return;
1090
1144
  try {
1091
1145
  const result = await ingestTranscriptFlush(parsed.flush);
1092
1146
  jsonResponse(res, 200, result);
@@ -1364,6 +1418,8 @@ async function handleMigrate(req, res, opts, scope) {
1364
1418
  // P7: the thread arrives in the body — gate before creating/cloning anything.
1365
1419
  if (denyOutOfNamespace(res, threadName, scope))
1366
1420
  return;
1421
+ if (denyOverDemoCap(res, threadName, scope))
1422
+ return;
1367
1423
  const result = {
1368
1424
  threadName,
1369
1425
  synced: 0,
@@ -1674,6 +1730,34 @@ export async function startGatewayServer(options) {
1674
1730
  // them into the valid-key set. The namespace→key mapping stays in
1675
1731
  // `namespaces` for scope resolution.
1676
1732
  const namespaces = options.namespaces ?? [];
1733
+ // Licence verification (task 127). Offline, once at startup — SIGHUP config
1734
+ // reload is a full process restart, so this re-runs there too. Pasting a key
1735
+ // therefore requires a reload, which is the right cadence for a rare act.
1736
+ const licenseStatus = verifyLicense(options.licenseKey, majorOf(getCurrentVersion()));
1737
+ // Task 129: the licence key is editable in Settings but NEVER live-applied —
1738
+ // enforcement stays `licenseStatus` above, verified once at startup (127's
1739
+ // reload cadence). This tracks the on-disk value only, so GET /api/config can
1740
+ // render masked state and a truthful pendingReload flag after a PUT.
1741
+ let storedLicenseKey = options.licenseKey;
1742
+ const licenseView = () => ({
1743
+ licensed: licenseStatus.licensed,
1744
+ ...(licenseStatus.licensed
1745
+ ? { licensee: licenseStatus.licensee }
1746
+ : { reason: licenseStatus.reason }),
1747
+ pendingReload: (storedLicenseKey ?? '') !== (options.licenseKey ?? ''),
1748
+ });
1749
+ const emitLicenseBanner = () => {
1750
+ for (const line of licenseBannerLines(licenseStatus, namespaces, listExistingThreadNames())) {
1751
+ console.log(line);
1752
+ }
1753
+ };
1754
+ emitLicenseBanner();
1755
+ // Periodic repeat while unlicensed, so a long-running demo deployment keeps
1756
+ // saying so. `unref` keeps it from holding the process open.
1757
+ const licenseBannerTimer = licenseStatus.licensed
1758
+ ? undefined
1759
+ : setInterval(emitLicenseBanner, 60 * 60 * 1000);
1760
+ licenseBannerTimer?.unref?.();
1677
1761
  const nsKeys = allNamespaceKeys(namespaces);
1678
1762
  let currentApiKeys = Array.from(new Set([...options.apiKeys, ...nsKeys]));
1679
1763
  let scheduler;
@@ -1783,6 +1867,13 @@ export async function startGatewayServer(options) {
1783
1867
  jsonResponse(res, 403, { error: 'Thread not in your namespace' });
1784
1868
  return;
1785
1869
  }
1870
+ // Demo capacity gate (task 127) — rides the same choke point, one notch
1871
+ // later: access says WHO may touch a thread, this says whether a NEW one may
1872
+ // be minted. Ordered after the 403 so an out-of-namespace caller never learns
1873
+ // a namespace's occupancy from the status code.
1874
+ const scope = { ns: scopeNs, namespaces, license: licenseStatus };
1875
+ if (pathThread && denyOverDemoCap(res, pathThread, scope))
1876
+ return;
1786
1877
  try {
1787
1878
  // POST /bridge/call — route a tool call to the connected app tab (task
1788
1879
  // 097). Body: { thread, command, params?, confirm?, summary? }. Commands
@@ -1812,6 +1903,8 @@ export async function startGatewayServer(options) {
1812
1903
  jsonResponse(res, 403, { error: 'Thread not in your namespace' });
1813
1904
  return;
1814
1905
  }
1906
+ if (denyOverDemoCap(res, thread, scope))
1907
+ return;
1815
1908
  const callParams = body.params ?? {};
1816
1909
  const manifestEntry = bridgeGateway.manifest(thread)?.find(c => c.name === command);
1817
1910
  const needsConfirm = Boolean(body.confirm) || manifestEntry?.risk === 'export';
@@ -1929,7 +2022,7 @@ export async function startGatewayServer(options) {
1929
2022
  }
1930
2023
  // POST /api/ingest-transcript — store-only transcript ingestion (task 096)
1931
2024
  if (routePath === '/api/ingest-transcript' && req.method === 'POST') {
1932
- await handleIngestTranscript(req, res, { ns: scopeNs, namespaces });
2025
+ await handleIngestTranscript(req, res, scope);
1933
2026
  return;
1934
2027
  }
1935
2028
  // GET /api/thread/:name/history
@@ -2022,15 +2115,12 @@ export async function startGatewayServer(options) {
2022
2115
  // PUT /api/config reassigns pipelineOpts.* in place, so this is the single
2023
2116
  // authoritative in-memory source (task 104).
2024
2117
  if (routePath === '/api/models' && req.method === 'GET') {
2025
- // Build models list with global default marked
2026
- let models = (pipelineOpts.models || []).map(m => ({ ...m }));
2027
- if (models.length === 0) {
2028
- // Fallback: just Claude
2029
- models = [
2030
- { id: 'claude', label: 'Claude (CLI)', provider: 'claude-cli', default: true },
2031
- ];
2032
- }
2033
- else {
2118
+ // Serve the configured catalog truthfully task 129 deleted the
2119
+ // hardcoded fallbacks (a lone Claude entry / the Opus-Sonnet pair).
2120
+ // An unconfigured gateway gets empty arrays; setup seeds a catalog,
2121
+ // and the Settings gear (topbar, task 128) is the recovery path.
2122
+ const models = (pipelineOpts.models || []).map(m => ({ ...m }));
2123
+ if (models.length > 0) {
2034
2124
  // Clear all defaults, then set the global default (or first if none specified)
2035
2125
  const globalDefault = pipelineOpts.model || models[0].id;
2036
2126
  for (const m of models) {
@@ -2038,31 +2128,39 @@ export async function startGatewayServer(options) {
2038
2128
  }
2039
2129
  }
2040
2130
  // Claude CLI model variants (Opus/Sonnet/...) for the sub-model picker.
2041
- // Config-driven; fall back to the historical Opus/Sonnet pair when unset.
2042
- let claudeModels = (pipelineOpts.claudeModels || []).map(m => ({ ...m }));
2043
- if (claudeModels.length === 0) {
2044
- claudeModels = [
2045
- { id: 'claude-opus-5[1m]', label: 'Opus 5', default: true },
2046
- { id: 'claude-sonnet-5[1m]', label: 'Sonnet 5' },
2047
- ];
2048
- }
2049
- else if (!claudeModels.some(m => m.default)) {
2131
+ // First-is-default is a normalization rule for a configured list, not
2132
+ // model knowledge it stays.
2133
+ const claudeModels = (pipelineOpts.claudeModels || []).map(m => ({ ...m }));
2134
+ if (claudeModels.length > 0 && !claudeModels.some(m => m.default)) {
2050
2135
  claudeModels[0].default = true;
2051
2136
  }
2052
2137
  jsonResponse(res, 200, { models, claudeModels });
2053
2138
  return;
2054
2139
  }
2140
+ // GET/PUT /api/config — gateway-level settings are ADMIN-ONLY (task 128).
2141
+ // Task 104 opened these to "any valid key", which predated scoped app keys
2142
+ // becoming a public surface (097 P4 ships one to every app visitor's
2143
+ // browser); a scoped key must not read or rewrite the gateway's model
2144
+ // catalog, default model, or provider credentials.
2145
+ if (routePath === '/api/config' && scopeNs) {
2146
+ jsonResponse(res, 403, { error: 'Gateway settings require an admin key' });
2147
+ return;
2148
+ }
2055
2149
  // GET /api/config — live-mutable config subset (task 104), credentials
2056
- // MASKED. Any valid gateway key (Karl's poll). Lets the in-app editor
2057
- // render current state without ever exposing a full provider key.
2150
+ // MASKED. Lets the in-app editor render current state without ever
2151
+ // exposing a full provider key.
2058
2152
  if (routePath === '/api/config' && req.method === 'GET') {
2059
- jsonResponse(res, 200, maskedConfigView({
2060
- models: pipelineOpts.models,
2061
- claudeModels: pipelineOpts.claudeModels,
2062
- model: pipelineOpts.model,
2063
- hfApiKey: pipelineOpts.hfApiKey,
2064
- openaiApiKey: pipelineOpts.openaiApiKey,
2065
- }));
2153
+ jsonResponse(res, 200, {
2154
+ ...maskedConfigView({
2155
+ models: pipelineOpts.models,
2156
+ claudeModels: pipelineOpts.claudeModels,
2157
+ model: pipelineOpts.model,
2158
+ hfApiKey: pipelineOpts.hfApiKey,
2159
+ openaiApiKey: pipelineOpts.openaiApiKey,
2160
+ licenseKey: storedLicenseKey,
2161
+ }),
2162
+ license: licenseView(),
2163
+ });
2066
2164
  return;
2067
2165
  }
2068
2166
  // PUT /api/config — live-apply the model catalog / default / provider
@@ -2070,7 +2168,7 @@ export async function startGatewayServer(options) {
2070
2168
  // in-memory options in place so /api/models and the per-turn spawn reflect
2071
2169
  // it with NO restart; onConfigApplied propagates to the other spawn-path
2072
2170
  // holders (webchat). PUT (not PATCH) so it rides the existing CORS
2073
- // allow-methods. Any valid gateway key.
2171
+ // allow-methods. Admin (non-namespaced) keys only — gated above.
2074
2172
  if (routePath === '/api/config' && req.method === 'PUT') {
2075
2173
  let patch;
2076
2174
  try {
@@ -2094,15 +2192,24 @@ export async function startGatewayServer(options) {
2094
2192
  pipelineOpts.hfApiKey = applied.hfApiKey;
2095
2193
  if (patch.openaiApiKey !== undefined)
2096
2194
  pipelineOpts.openaiApiKey = applied.openaiApiKey;
2195
+ // Task 129: licence key is persist-ONLY — track the stored value for
2196
+ // display, but never rewire enforcement (licenseStatus stays as
2197
+ // verified at startup; a reload is what applies it — task 127).
2198
+ if (patch.licenseKey !== undefined)
2199
+ storedLicenseKey = applied.licenseKey;
2097
2200
  // Propagate to the daemon's OTHER holders (webchat pipelineOpts, etc.).
2098
2201
  options.onConfigApplied?.(applied);
2099
- jsonResponse(res, 200, maskedConfigView({
2100
- models: pipelineOpts.models,
2101
- claudeModels: pipelineOpts.claudeModels,
2102
- model: pipelineOpts.model,
2103
- hfApiKey: pipelineOpts.hfApiKey,
2104
- openaiApiKey: pipelineOpts.openaiApiKey,
2105
- }));
2202
+ jsonResponse(res, 200, {
2203
+ ...maskedConfigView({
2204
+ models: pipelineOpts.models,
2205
+ claudeModels: pipelineOpts.claudeModels,
2206
+ model: pipelineOpts.model,
2207
+ hfApiKey: pipelineOpts.hfApiKey,
2208
+ openaiApiKey: pipelineOpts.openaiApiKey,
2209
+ licenseKey: storedLicenseKey,
2210
+ }),
2211
+ license: licenseView(),
2212
+ });
2106
2213
  }
2107
2214
  catch (err) {
2108
2215
  jsonResponse(res, 500, { error: String(err) });
@@ -2125,7 +2232,7 @@ export async function startGatewayServer(options) {
2125
2232
  }
2126
2233
  // POST /api/agents/inject — agent-to-agent messaging
2127
2234
  if (routePath === '/api/agents/inject' && req.method === 'POST') {
2128
- await handleAgentInject(req, res, pipelineOpts, { ns: scopeNs, namespaces });
2235
+ await handleAgentInject(req, res, pipelineOpts, scope);
2129
2236
  return;
2130
2237
  }
2131
2238
  // GET /api/federation/status — connected spokes and their threads (hub only)
@@ -2140,7 +2247,7 @@ export async function startGatewayServer(options) {
2140
2247
  }
2141
2248
  // POST /api/migrate — sync thread + clone repo + set projectDir
2142
2249
  if (routePath === '/api/migrate' && req.method === 'POST') {
2143
- await handleMigrate(req, res, pipelineOpts, { ns: scopeNs, namespaces });
2250
+ await handleMigrate(req, res, pipelineOpts, scope);
2144
2251
  return;
2145
2252
  }
2146
2253
  // POST /api/push/subscribe — register push subscription
@@ -2241,6 +2348,8 @@ export async function startGatewayServer(options) {
2241
2348
  // Abort any in-flight message processing
2242
2349
  threadQueues.clear();
2243
2350
  threadBusy.clear();
2351
+ if (licenseBannerTimer)
2352
+ clearInterval(licenseBannerTimer);
2244
2353
  return new Promise(resolveClose => {
2245
2354
  server.close(() => {
2246
2355
  clearThreadCache();