blun-king-cli 9.1.310 → 9.1.311

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.
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
4
+ const AUTHORITIES = new Set(['runtime_user_prompt_hook', 'runtime_tool_policy']);
5
+ const RECEIPT_MAX_AGE_MS = 5 * 60 * 1000;
6
+
7
+ function fail(code = 'COGNITIVE_ATTENTION_AUTH_INVALID') {
8
+ const error = new Error(code);
9
+ error.code = code;
10
+ throw error;
11
+ }
12
+
13
+ function exactKeys(value, keys) {
14
+ return value && typeof value === 'object' && !Array.isArray(value)
15
+ && Object.keys(value).every((key) => keys.has(key));
16
+ }
17
+
18
+ function safeId(value) {
19
+ const text = String(value ?? '').trim();
20
+ return SAFE_ID_RE.test(text) ? text : '';
21
+ }
22
+
23
+ function authorizeAttentionCandidate({ store, tenantId, currentAgent, now, input } = {}) {
24
+ if (!store || typeof store.authorizeAttention !== 'function' || typeof now !== 'function'
25
+ || !exactKeys(input, new Set(['candidate', 'channel', 'authorization']))) fail();
26
+ const tenant = safeId(tenantId);
27
+ const agent = safeId(currentAgent);
28
+ const channel = safeId(input.channel);
29
+ const candidate = input.candidate;
30
+ const authorization = input.authorization;
31
+ const candidateKeys = new Set([
32
+ 'candidate_id', 'subject_kind', 'subject_id', 'reason', 'responsible_agent',
33
+ 'evidence_id', 'evidence_scope', 'evidence_at', 'confidence', 'allowed_channel',
34
+ 'detected_at', 'cooldown_ms', 'requires_runtime_authorization',
35
+ ]);
36
+ const authorizationKeys = new Set(['authority', 'decision', 'receiptId', 'scope', 'issuedAt']);
37
+ if (!tenant || !agent || !channel || !exactKeys(candidate, candidateKeys)
38
+ || !exactKeys(authorization, authorizationKeys)) fail();
39
+
40
+ const candidateId = safeId(candidate.candidate_id);
41
+ const responsibleAgent = safeId(candidate.responsible_agent);
42
+ const allowedChannel = safeId(candidate.allowed_channel);
43
+ const authority = String(authorization.authority ?? '');
44
+ const receiptId = safeId(authorization.receiptId);
45
+ const scope = String(authorization.scope ?? '');
46
+ const nowMs = Date.parse(String(now()));
47
+ const issuedAtMs = Date.parse(String(authorization.issuedAt ?? ''));
48
+ if (!candidateId || !responsibleAgent || responsibleAgent !== agent
49
+ || !allowedChannel || channel !== allowedChannel
50
+ || candidate.requires_runtime_authorization !== true
51
+ || !AUTHORITIES.has(authority) || authorization.decision !== 'passed'
52
+ || !receiptId || scope !== `attention:${candidateId}`
53
+ || !Number.isFinite(nowMs) || !Number.isFinite(issuedAtMs)
54
+ || issuedAtMs > nowMs || nowMs - issuedAtMs > RECEIPT_MAX_AGE_MS) fail();
55
+
56
+ const admitted = store.authorizeAttention({
57
+ tenantId: tenant,
58
+ candidate,
59
+ claimedBy: agent,
60
+ claimedAt: new Date(nowMs).toISOString(),
61
+ receiptId,
62
+ authority,
63
+ authorizedAt: new Date(issuedAtMs).toISOString(),
64
+ });
65
+ if (!admitted.claimed) {
66
+ return {
67
+ accepted: false,
68
+ reason: 'cooldown',
69
+ candidate_id: candidateId,
70
+ claimed_by: admitted.claimed_by,
71
+ next_allowed_at: admitted.next_allowed_at,
72
+ };
73
+ }
74
+ return {
75
+ accepted: true,
76
+ idempotent: admitted.idempotent === true,
77
+ candidate_id: candidateId,
78
+ subject_kind: candidate.subject_kind,
79
+ subject_id: candidate.subject_id,
80
+ reason: candidate.reason,
81
+ evidence_id: candidate.evidence_id,
82
+ evidence_scope: candidate.evidence_scope,
83
+ confidence: candidate.confidence,
84
+ allowed_channel: allowedChannel,
85
+ authorization_receipt_id: receiptId,
86
+ dispatch_intent: 'attention_notice',
87
+ requires_delivery_policy: true,
88
+ };
89
+ }
90
+
91
+ module.exports = { RECEIPT_MAX_AGE_MS, authorizeAttentionCandidate };
@@ -155,6 +155,17 @@ function initialize(db) {
155
155
  next_allowed_at TEXT NOT NULL,
156
156
  PRIMARY KEY (tenant_id, candidate_id)
157
157
  );
158
+ CREATE TABLE IF NOT EXISTS cognitive_attention_authorizations (
159
+ tenant_id TEXT NOT NULL,
160
+ receipt_id TEXT NOT NULL,
161
+ candidate_id TEXT NOT NULL,
162
+ authority TEXT NOT NULL,
163
+ authorized_at TEXT NOT NULL,
164
+ accepted INTEGER NOT NULL,
165
+ claimed_by TEXT NOT NULL,
166
+ next_allowed_at TEXT NOT NULL,
167
+ PRIMARY KEY (tenant_id, receipt_id)
168
+ );
158
169
  `);
159
170
  }
160
171
 
@@ -289,11 +300,84 @@ function openCognitiveStateStore({ home } = {}) {
289
300
  }
290
301
  }
291
302
 
303
+ function authorizeAttention(input) {
304
+ const allowedInput = new Set([
305
+ 'tenantId', 'candidate', 'claimedBy', 'claimedAt', 'receiptId', 'authority', 'authorizedAt',
306
+ ]);
307
+ const allowedCandidate = new Set([
308
+ 'candidate_id', 'subject_kind', 'subject_id', 'reason', 'responsible_agent',
309
+ 'evidence_id', 'evidence_scope', 'evidence_at', 'confidence', 'allowed_channel',
310
+ 'detected_at', 'cooldown_ms', 'requires_runtime_authorization',
311
+ ]);
312
+ if (!exactKeys(input, allowedInput) || !exactKeys(input.candidate, allowedCandidate)
313
+ || hasForbiddenKey({ candidate: input.candidate })) fail('COGNITIVE_FORBIDDEN_FIELD');
314
+ const tenant = safeId(input.tenantId);
315
+ const candidateId = safeId(input.candidate.candidate_id);
316
+ const claimedBy = safeId(input.claimedBy);
317
+ const receiptId = safeId(input.receiptId);
318
+ const authority = safeId(input.authority);
319
+ const claimedAtMs = Date.parse(String(input.claimedAt ?? ''));
320
+ const authorizedAtMs = Date.parse(String(input.authorizedAt ?? ''));
321
+ const cooldownMs = Number(input.candidate.cooldown_ms);
322
+ if (!tenant || !candidateId || !claimedBy || !receiptId || !authority
323
+ || !Number.isFinite(claimedAtMs) || !Number.isFinite(authorizedAtMs)
324
+ || !Number.isSafeInteger(cooldownMs) || cooldownMs < 1000
325
+ || input.candidate.requires_runtime_authorization !== true) fail('COGNITIVE_INVALID_EVENT');
326
+ const claimedAt = new Date(claimedAtMs).toISOString();
327
+ const authorizedAt = new Date(authorizedAtMs).toISOString();
328
+ const nextAllowedAt = new Date(claimedAtMs + cooldownMs).toISOString();
329
+ db.exec('BEGIN IMMEDIATE');
330
+ try {
331
+ const receipt = db.prepare(`SELECT candidate_id, accepted, claimed_by, next_allowed_at
332
+ FROM cognitive_attention_authorizations WHERE tenant_id = ? AND receipt_id = ?`).get(tenant, receiptId);
333
+ if (receipt) {
334
+ if (receipt.candidate_id !== candidateId) fail('COGNITIVE_ATTENTION_RECEIPT_REUSE');
335
+ db.exec('COMMIT');
336
+ return {
337
+ claimed: receipt.accepted === 1,
338
+ claimed_by: receipt.claimed_by,
339
+ next_allowed_at: receipt.next_allowed_at,
340
+ idempotent: true,
341
+ };
342
+ }
343
+ const existing = db.prepare(`SELECT claimed_by, next_allowed_at FROM cognitive_attention_claims
344
+ WHERE tenant_id = ? AND candidate_id = ?`).get(tenant, candidateId);
345
+ const accepted = !(existing && Date.parse(existing.next_allowed_at) > claimedAtMs);
346
+ const effectiveClaimedBy = accepted ? claimedBy : existing.claimed_by;
347
+ const effectiveNextAllowedAt = accepted ? nextAllowedAt : existing.next_allowed_at;
348
+ if (accepted) {
349
+ db.prepare(`INSERT INTO cognitive_attention_claims
350
+ (tenant_id, candidate_id, claimed_by, claimed_at, next_allowed_at) VALUES (?, ?, ?, ?, ?)
351
+ ON CONFLICT(tenant_id, candidate_id) DO UPDATE SET
352
+ claimed_by = excluded.claimed_by,
353
+ claimed_at = excluded.claimed_at,
354
+ next_allowed_at = excluded.next_allowed_at`)
355
+ .run(tenant, candidateId, claimedBy, claimedAt, nextAllowedAt);
356
+ }
357
+ db.prepare(`INSERT INTO cognitive_attention_authorizations
358
+ (tenant_id, receipt_id, candidate_id, authority, authorized_at, accepted, claimed_by, next_allowed_at)
359
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
360
+ .run(tenant, receiptId, candidateId, authority, authorizedAt, accepted ? 1 : 0,
361
+ effectiveClaimedBy, effectiveNextAllowedAt);
362
+ db.exec('COMMIT');
363
+ return {
364
+ claimed: accepted,
365
+ claimed_by: effectiveClaimedBy,
366
+ next_allowed_at: effectiveNextAllowedAt,
367
+ idempotent: false,
368
+ };
369
+ } catch (error) {
370
+ try { db.exec('ROLLBACK'); } catch {}
371
+ throw error;
372
+ }
373
+ }
374
+
292
375
  return {
293
376
  commit,
294
377
  read,
295
378
  verify,
296
379
  claimAttention,
380
+ authorizeAttention,
297
381
  close: () => {
298
382
  try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch {}
299
383
  db.close();
@@ -4,6 +4,7 @@ const crypto = require('node:crypto');
4
4
  const { openCognitiveStateStore } = require('./cognitive-state-store.cjs');
5
5
  const { buildCognitiveContextProjection } = require('./cognitive-context-projection.cjs');
6
6
  const { buildCognitiveFocusProjection } = require('./cognitive-focus-projection.cjs');
7
+ const { authorizeAttentionCandidate } = require('./cognitive-attention-runtime.cjs');
7
8
 
8
9
  const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
9
10
  const RIGHTS_AUTHORITIES = new Set(['runtime_user_prompt_hook', 'runtime_tool_policy']);
@@ -210,12 +211,23 @@ function createCognitiveTurnLifecycle({ home, tenantId, agentId, runtimeId, now
210
211
  return [focus, continuity].filter(Boolean).join('\n\n') || null;
211
212
  }
212
213
 
214
+ function authorizeAttention(input) {
215
+ return authorizeAttentionCandidate({
216
+ store,
217
+ tenantId: tenant,
218
+ currentAgent: agent,
219
+ now,
220
+ input,
221
+ });
222
+ }
223
+
213
224
  return {
214
225
  startTurn,
215
226
  recordRightsCheck,
216
227
  recordToolPolicy,
217
228
  recordToolResult,
218
229
  recordFocusSnapshot,
230
+ authorizeAttention,
219
231
  projectForTurn,
220
232
  endTurn,
221
233
  read: () => store.read({ tenantId: tenant, agentId: agent }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.310",
3
+ "version": "9.1.311",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {