@zanii/blackbox 0.2.0 → 0.4.0

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 (94) hide show
  1. package/README.md +26 -1
  2. package/dist/a2a/index.d.ts +77 -0
  3. package/dist/a2a/index.js +305 -0
  4. package/dist/agents/index.d.ts +8 -0
  5. package/dist/analysis/accuracy.d.ts +24 -0
  6. package/dist/analysis/accuracy.js +45 -0
  7. package/dist/analysis/credential.d.ts +101 -0
  8. package/dist/analysis/credential.js +142 -0
  9. package/dist/analysis/faults.js +115 -0
  10. package/dist/analysis/grounding.d.ts +122 -0
  11. package/dist/analysis/grounding.js +445 -0
  12. package/dist/analysis/hallucination.d.ts +32 -0
  13. package/dist/analysis/hallucination.js +357 -0
  14. package/dist/analysis/index.d.ts +23 -0
  15. package/dist/analysis/index.js +93 -0
  16. package/dist/analysis/memory.d.ts +8 -0
  17. package/dist/analysis/memory.js +35 -8
  18. package/dist/analysis/reference.d.ts +49 -0
  19. package/dist/analysis/reference.js +164 -0
  20. package/dist/analysis/taxonomy.d.ts +18 -0
  21. package/dist/analysis/taxonomy.js +66 -0
  22. package/dist/approvals/index.d.ts +23 -0
  23. package/dist/approvals/index.js +48 -0
  24. package/dist/archive/index.d.ts +39 -0
  25. package/dist/archive/index.js +96 -0
  26. package/dist/archive/parquet.d.ts +2 -0
  27. package/dist/archive/parquet.js +185 -0
  28. package/dist/badge/index.d.ts +16 -0
  29. package/dist/badge/index.js +48 -0
  30. package/dist/bom/index.d.ts +14 -0
  31. package/dist/bom/index.js +152 -0
  32. package/dist/cli.js +114 -10
  33. package/dist/compliance/art12.d.ts +35 -0
  34. package/dist/compliance/art12.js +190 -0
  35. package/dist/compliance/index.d.ts +36 -2
  36. package/dist/compliance/index.js +78 -11
  37. package/dist/compliance/zanii.d.ts +29 -0
  38. package/dist/compliance/zanii.js +84 -0
  39. package/dist/constitution/index.d.ts +57 -0
  40. package/dist/constitution/index.js +131 -0
  41. package/dist/cv/index.d.ts +39 -0
  42. package/dist/cv/index.js +108 -0
  43. package/dist/disclosure/index.d.ts +31 -0
  44. package/dist/disclosure/index.js +113 -0
  45. package/dist/encryption/index.d.ts +9 -0
  46. package/dist/encryption/index.js +31 -0
  47. package/dist/evidence/index.d.ts +60 -0
  48. package/dist/evidence/index.js +151 -0
  49. package/dist/federation/index.d.ts +35 -0
  50. package/dist/federation/index.js +102 -0
  51. package/dist/finance/index.d.ts +126 -0
  52. package/dist/finance/index.js +320 -0
  53. package/dist/fleet/index.js +9 -0
  54. package/dist/gov/index.d.ts +108 -0
  55. package/dist/gov/index.js +225 -0
  56. package/dist/health/index.d.ts +120 -0
  57. package/dist/health/index.js +233 -0
  58. package/dist/index.d.ts +34 -5
  59. package/dist/index.js +34 -5
  60. package/dist/memory/index.d.ts +36 -0
  61. package/dist/memory/index.js +85 -0
  62. package/dist/occurrence/index.d.ts +11 -0
  63. package/dist/occurrence/index.js +18 -0
  64. package/dist/ocsf/index.d.ts +1 -1
  65. package/dist/ocsf/index.js +36 -3
  66. package/dist/otlp/index.d.ts +8 -1
  67. package/dist/otlp/index.js +258 -1
  68. package/dist/packs/index.js +44 -4
  69. package/dist/policy/delta.js +7 -1
  70. package/dist/policy/index.d.ts +40 -6
  71. package/dist/policy/index.js +186 -8
  72. package/dist/policy/zanii.d.ts +31 -0
  73. package/dist/policy/zanii.js +87 -0
  74. package/dist/pq/index.d.ts +23 -0
  75. package/dist/pq/index.js +104 -0
  76. package/dist/search/index.d.ts +23 -0
  77. package/dist/search/index.js +69 -0
  78. package/dist/session/index.d.ts +107 -1
  79. package/dist/session/index.js +189 -11
  80. package/dist/sla/index.d.ts +61 -0
  81. package/dist/sla/index.js +197 -0
  82. package/dist/succession/index.d.ts +50 -0
  83. package/dist/succession/index.js +123 -0
  84. package/dist/timestamp/index.d.ts +24 -0
  85. package/dist/timestamp/index.js +274 -0
  86. package/dist/tokens/index.d.ts +6 -0
  87. package/dist/tokens/index.js +46 -0
  88. package/dist/transparency/index.d.ts +188 -0
  89. package/dist/transparency/index.js +712 -0
  90. package/dist/version.d.ts +1 -1
  91. package/dist/version.js +1 -1
  92. package/dist/walls/index.d.ts +31 -0
  93. package/dist/walls/index.js +119 -0
  94. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { type Attestation } from "../attest/index.ts";
2
+ import { buildPayment, type screenCounterparty } from "../finance/index.ts";
2
3
  export interface SessionOptions {
3
4
  /** The gateway, e.g. http://127.0.0.1:8787 */
4
5
  url: string;
@@ -33,6 +34,8 @@ export interface SessionOptions {
33
34
  tenant?: string;
34
35
  /** spec/authority.md: `supervised` from the start (risky actions wait for a second person). */
35
36
  authority?: "agent" | "supervised";
37
+ /** spec/api.md: where it runs (`production`, `staging`, …); policy rules can match it. */
38
+ environment?: string;
36
39
  /** spec/preflight.md: the equipment the run needs. A no-go returns a disabled session (and
37
40
  * `stats.lastError` says why); optional items that are down show in `state().degraded`. */
38
41
  preflight?: {
@@ -125,6 +128,8 @@ export interface LlmCallIds {
125
128
  }
126
129
  /** N4 (idea R6): the same parts always give the same event id (sha256, 128 bits). */
127
130
  export declare function stableEventId(...parts: readonly string[]): string;
131
+ /** The image type of a screenshot, by its first bytes: PNG, JPEG or WebP. */
132
+ export declare function imageType(image: Uint8Array): "image/png" | "image/jpeg" | "image/webp" | null;
128
133
  /** Opens (or joins) a session. Never throws: on failure it returns a disabled session and logs why. */
129
134
  export declare function session(options: SessionOptions): Promise<BlackboxSession>;
130
135
  export declare class BlackboxSession {
@@ -142,6 +147,10 @@ export declare class BlackboxSession {
142
147
  private timers;
143
148
  private closed;
144
149
  private landings;
150
+ /** spec/approvals.md §2: approved tools with arguments, until their call is reported. */
151
+ private approved;
152
+ /** spec/agents.md §4: the memory chain's tip. */
153
+ private memoryLast;
145
154
  /** A session that records nothing. `reason`, when opening failed, goes to `stats.lastError` (audit K1). */
146
155
  static disabled(options: SessionOptions, reason?: string): BlackboxSession;
147
156
  private readonly options;
@@ -152,8 +161,74 @@ export declare class BlackboxSession {
152
161
  keepToken?: boolean);
153
162
  event(type: string, name?: string, data?: Record<string, unknown>, options?: {
154
163
  eventId?: string;
164
+ /** spec/sdk.md §1.1: an image the event's body is (see `screen`). */
165
+ attachment?: {
166
+ contentType: string;
167
+ bytes: Uint8Array;
168
+ };
155
169
  }): void;
156
170
  step(name: string, data?: Record<string, unknown>): void;
171
+ /**
172
+ * spec/gov.md §2: a decision about a person, under the rulebook `manifestHash`, the person named
173
+ * only by their subject tag. Returns the factors' nonce (keep it to disclose them in a dispute).
174
+ * Throws on a decision that can't be lawful evidence: no rulebook, a bad kind, no tag.
175
+ */
176
+ decision(kind: string, o: {
177
+ manifestHash: string;
178
+ outcome: Record<string, unknown>;
179
+ subjectTag: string;
180
+ factors?: Record<string, unknown>;
181
+ appealBy?: string;
182
+ }): {
183
+ nonce: string;
184
+ };
185
+ /** spec/health.md §2: who accessed a patient's record (an episode tag, never the seed), and why. */
186
+ healthAccess(o: {
187
+ episodeTag: string;
188
+ actor: string;
189
+ purpose: string;
190
+ consentRef?: string;
191
+ details?: Record<string, unknown>;
192
+ }): {
193
+ nonce: string;
194
+ };
195
+ /** spec/health.md §2: a model's recommendation under a protocol. `hash` is what the clinician signs. */
196
+ recommendation(o: {
197
+ episodeTag: string;
198
+ modelId: string;
199
+ manifestHash: string;
200
+ outcome: Record<string, unknown>;
201
+ runtimeHash?: string;
202
+ deviation?: Record<string, unknown>;
203
+ }): {
204
+ nonce: string;
205
+ hash: string;
206
+ };
207
+ /** spec/health.md §2: a clinician's signed confirmation (clinicianConfirmation). Throws on a bad signature. */
208
+ clinicianConfirmation(episodeTag: string, payload: Record<string, unknown>): void;
209
+ /** spec/health.md §2: an emergency access, signed by the clinician (breakGlass). Loud on purpose. */
210
+ breakGlass(episodeTag: string, payload: Record<string, unknown>): void;
211
+ private signedHealth;
212
+ /** spec/finance.md §2: a payment, exact (minor units), with its settlement reference. Throws on a bad amount. */
213
+ payment(input: Parameters<typeof buildPayment>[0]): void;
214
+ /** spec/finance.md §3: a counterparty screening (screenCounterparty), before paying them. */
215
+ screening(result: ReturnType<typeof screenCounterparty>): void;
216
+ /** spec/finance.md §4: a tax filing the agent prepared. It never files; a Tax Agent does. */
217
+ ftaPrepared(o: {
218
+ filingType: string;
219
+ period: string;
220
+ filingRef?: string;
221
+ manifestHash?: string;
222
+ by?: string;
223
+ }): void;
224
+ /** spec/finance.md §4: the prepared filing handed to a licensed Tax Agent. */
225
+ ftaHandoff(o: {
226
+ filingType: string;
227
+ period: string;
228
+ toTaxAgent: string;
229
+ filingRef?: string;
230
+ by?: string;
231
+ }): void;
157
232
  toolCall(name: string, args?: unknown): void;
158
233
  toolResult(name: string, result?: {
159
234
  ok?: boolean;
@@ -193,6 +268,15 @@ export declare class BlackboxSession {
193
268
  summary?: string;
194
269
  file_path?: string;
195
270
  }): void;
271
+ /** spec/findings.md §11 (H3): pin the documents the agent read, by hash; the content never leaves.
272
+ * Each doc is `{id, uri?, version?}` with its `content` (hashed here) or its `sha256`. */
273
+ retrieved(docs: ReadonlyArray<{
274
+ id: string;
275
+ uri?: string;
276
+ version?: string;
277
+ content?: string | Uint8Array;
278
+ sha256?: string;
279
+ }>): void;
196
280
  /** Stage 2 R3: the result of one of the customer's own checks (tests, a schema, a policy, a
197
281
  * partial goal), mid-run or at the end. A pass then a fail is VERIFY_REGRESSION; a success whose last
198
282
  * check failed is FALSE_SUCCESS (spec/findings.md §6). */
@@ -215,8 +299,30 @@ export declare class BlackboxSession {
215
299
  nearMiss(description: string): void;
216
300
  /** spec/attestation.md: re-runs a read-only command and records whether the output matches. */
217
301
  attest(argv: string[], claimed: string, cwd?: string, claimedExit?: number): Attestation | undefined;
302
+ /**
303
+ * spec/sdk.md §1.1: a computer-use screenshot (PNG, JPEG or WebP, at most 2 MB), recorded as the
304
+ * body of a `screen` event, so its hash is in the chain. `action` says what the agent did or is
305
+ * about to do (≤ 500 characters). Mask what mustn't be kept before calling: the image isn't
306
+ * redacted. One that's too big or not an image is dropped, and the event says why.
307
+ */
308
+ screen(image: Uint8Array, options?: {
309
+ action?: string;
310
+ width?: number;
311
+ height?: number;
312
+ }): void;
218
313
  /** spec/agents.md §1: the agent's memory store wrote, revoked or returned memories. */
219
- memoryWrite(memoryId: string, summary?: string): void;
314
+ /** spec/agents.md §4: with `content`, the write also carries Zanii's memory entry (a salted
315
+ * commitment, chained after `prev`, else this session's last entry). Keep the salt it returns. */
316
+ memoryWrite(memoryId: string, summary?: string, opts?: {
317
+ content: string;
318
+ kind?: string;
319
+ prev?: Record<string, unknown>;
320
+ agent?: string;
321
+ tags?: string[];
322
+ }): {
323
+ entry: Record<string, unknown>;
324
+ salt: string;
325
+ } | undefined;
220
326
  memoryRevoke(memoryId: string, reason?: string): void;
221
327
  memoryRead(memoryIds: string[], query?: string): void;
222
328
  /** Ships what's pending now. Resolves true when everything recorded is acknowledged. With
@@ -6,7 +6,19 @@ import { request as httpRequest } from "node:http";
6
6
  import { request as httpsRequest } from "node:https";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
+ import { argsHash } from "../approvals/index.js";
9
10
  import { attest, isReadOnly } from "../attest/index.js";
11
+ import { buildPayment, ftaPayload, screeningPayload, } from "../finance/index.js";
12
+ import { decisionPayload } from "../gov/index.js";
13
+ import { accessPayload, checkEpisodeTag, recommendationPayload, verifyHealthSignature, } from "../health/index.js";
14
+ import { memoryEntry } from "../memory/index.js";
15
+ /** spec/preflight.md §1 (L3): with an `egress` item, the SDK checks it here, on the agent's host. */
16
+ async function withEgress(p) {
17
+ if (![...(p.require ?? []), ...(p.optional ?? [])].includes("egress"))
18
+ return p;
19
+ const { url, open } = await checkEgress({ timeoutMs: 3000 });
20
+ return { ...p, egress: { url, open } };
21
+ }
10
22
  /** spec/data.md §6: a direct HTTPS request to `url` (default https://example.com). Never throws. */
11
23
  export async function checkEgress(options = {}) {
12
24
  const url = options.url ?? "https://example.com";
@@ -30,6 +42,20 @@ const MAX_DATA = 64 * 1024;
30
42
  const PREVIEW = 4096;
31
43
  const BATCH = 500;
32
44
  const READ_CHUNK = 4 * 1024 * 1024;
45
+ /** spec/sdk.md §1.1: a screenshot's bytes at most; base64 in its spool line, it stays under READ_CHUNK. */
46
+ const MAX_ATTACHMENT = 2 * 1024 * 1024;
47
+ /** The image type of a screenshot, by its first bytes: PNG, JPEG or WebP. */
48
+ export function imageType(image) {
49
+ const b = Buffer.from(image.buffer, image.byteOffset, image.byteLength);
50
+ if (b.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
51
+ return "image/png";
52
+ if (b.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff])))
53
+ return "image/jpeg";
54
+ if (b.subarray(0, 4).toString("latin1") === "RIFF" &&
55
+ b.subarray(8, 12).toString("latin1") === "WEBP")
56
+ return "image/webp";
57
+ return null;
58
+ }
33
59
  const LOCK_WAIT_MS = 2_000;
34
60
  const LOCK_STALE_MS = 10_000;
35
61
  /** Opens (or joins) a session. Never throws: on failure it returns a disabled session and logs why. */
@@ -57,7 +83,8 @@ export async function session(options) {
57
83
  ...(o.replay ? { replay: o.replay } : {}),
58
84
  ...(o.tenant ? { tenant: o.tenant } : {}),
59
85
  ...(o.authority ? { authority: o.authority } : {}),
60
- ...(o.preflight ? { preflight: o.preflight } : {}),
86
+ ...(o.environment ? { environment: o.environment } : {}),
87
+ ...(o.preflight ? { preflight: await withEgress(o.preflight) } : {}),
61
88
  ...(o.drill ? { drill: o.drill } : {}),
62
89
  sdk: true, // this SDK will report the session's model calls (L2.3.2)
63
90
  });
@@ -89,6 +116,10 @@ export class BlackboxSession {
89
116
  timers = [];
90
117
  closed = false;
91
118
  landings = 0;
119
+ /** spec/approvals.md §2: approved tools with arguments, until their call is reported. */
120
+ approved = new Map();
121
+ /** spec/agents.md §4: the memory chain's tip. */
122
+ memoryLast = null;
92
123
  /** A session that records nothing. `reason`, when opening failed, goes to `stats.lastError` (audit K1). */
93
124
  static disabled(options, reason) {
94
125
  const s = new BlackboxSession(options, "", "", false);
@@ -170,6 +201,14 @@ export class BlackboxSession {
170
201
  ...(eventId === undefined ? {} : { event_id: eventId }),
171
202
  ts: new Date().toISOString(),
172
203
  ...(kept === undefined ? {} : { data: kept }),
204
+ ...(options.attachment
205
+ ? {
206
+ attachment: {
207
+ content_type: options.attachment.contentType,
208
+ data: Buffer.from(options.attachment.bytes).toString("base64"),
209
+ },
210
+ }
211
+ : {}),
173
212
  });
174
213
  appendFileSync(this.spool, `${line}\n`);
175
214
  this.nextSeq++;
@@ -186,8 +225,82 @@ export class BlackboxSession {
186
225
  step(name, data) {
187
226
  this.event("step", name, data);
188
227
  }
228
+ /**
229
+ * spec/gov.md §2: a decision about a person, under the rulebook `manifestHash`, the person named
230
+ * only by their subject tag. Returns the factors' nonce (keep it to disclose them in a dispute).
231
+ * Throws on a decision that can't be lawful evidence: no rulebook, a bad kind, no tag.
232
+ */
233
+ decision(kind, o) {
234
+ if (!/^sha256:[0-9a-f]{64}$/.test(o.subjectTag))
235
+ throw new Error("subjectTag must be a subject tag (subjectTag(did, authority))");
236
+ const { payload, nonce } = decisionPayload({
237
+ kind,
238
+ manifestHash: o.manifestHash,
239
+ outcome: o.outcome,
240
+ ts: new Date().toISOString(),
241
+ ...(o.factors !== undefined ? { factors: o.factors } : {}),
242
+ ...(o.appealBy !== undefined ? { appealBy: o.appealBy } : {}),
243
+ });
244
+ this.event("decision", kind, { decision: payload, subject_tag: o.subjectTag });
245
+ return { nonce };
246
+ }
247
+ /** spec/health.md §2: who accessed a patient's record (an episode tag, never the seed), and why. */
248
+ healthAccess(o) {
249
+ const tag = checkEpisodeTag(o.episodeTag);
250
+ const { payload, nonce } = accessPayload({ ...o, ts: new Date().toISOString() });
251
+ this.event("health.access", "access", { payload, episode_tag: tag });
252
+ return { nonce };
253
+ }
254
+ /** spec/health.md §2: a model's recommendation under a protocol. `hash` is what the clinician signs. */
255
+ recommendation(o) {
256
+ const tag = checkEpisodeTag(o.episodeTag);
257
+ const { payload, nonce, hash } = recommendationPayload({ ...o, ts: new Date().toISOString() });
258
+ this.event("health.recommendation", "recommendation", { payload, episode_tag: tag });
259
+ return { nonce, hash };
260
+ }
261
+ /** spec/health.md §2: a clinician's signed confirmation (clinicianConfirmation). Throws on a bad signature. */
262
+ clinicianConfirmation(episodeTag, payload) {
263
+ this.signedHealth(episodeTag, payload, "confirmation");
264
+ }
265
+ /** spec/health.md §2: an emergency access, signed by the clinician (breakGlass). Loud on purpose. */
266
+ breakGlass(episodeTag, payload) {
267
+ this.signedHealth(episodeTag, payload, "break_glass");
268
+ }
269
+ signedHealth(episodeTag, payload, kind) {
270
+ const tag = checkEpisodeTag(episodeTag);
271
+ if (payload.kind !== kind || !verifyHealthSignature(payload, tag))
272
+ throw new Error(`not a ${kind} signed by the clinician it names, for this episode`);
273
+ this.event(`health.${kind}`, kind, { payload, episode_tag: tag });
274
+ }
275
+ /** spec/finance.md §2: a payment, exact (minor units), with its settlement reference. Throws on a bad amount. */
276
+ payment(input) {
277
+ this.event("payment", input.rail, buildPayment(input));
278
+ }
279
+ /** spec/finance.md §3: a counterparty screening (screenCounterparty), before paying them. */
280
+ screening(result) {
281
+ this.event("kya.screening", result.did, screeningPayload(result, new Date().toISOString(), this.id));
282
+ }
283
+ /** spec/finance.md §4: a tax filing the agent prepared. It never files; a Tax Agent does. */
284
+ ftaPrepared(o) {
285
+ this.event("fta.prepared", o.filingType, ftaPayload({ ...o, action: "prepared", by: o.by ?? this.id }));
286
+ }
287
+ /** spec/finance.md §4: the prepared filing handed to a licensed Tax Agent. */
288
+ ftaHandoff(o) {
289
+ this.event("fta.handoff", o.filingType, ftaPayload({ ...o, action: "handoff", by: o.by ?? this.id }));
290
+ }
189
291
  toolCall(name, args) {
190
- this.event("tool.call", name, args === undefined ? undefined : { args });
292
+ // spec/approvals.md §2: the first call after an approval names it, with what it ran
293
+ const approvalId = this.approved.get(name);
294
+ if (approvalId !== undefined)
295
+ this.approved.delete(name);
296
+ this.event("tool.call", name, args === undefined && approvalId === undefined
297
+ ? undefined
298
+ : {
299
+ ...(args !== undefined ? { args } : {}),
300
+ ...(approvalId !== undefined
301
+ ? { approval_id: approvalId, args_sha256: argsHash(args ?? null) }
302
+ : {}),
303
+ });
191
304
  }
192
305
  toolResult(name, result) {
193
306
  this.event("tool.result", name, result);
@@ -255,6 +368,30 @@ export class BlackboxSession {
255
368
  contextChange(change) {
256
369
  this.event("context.change", change.kind, { ...change });
257
370
  }
371
+ /** spec/findings.md §11 (H3): pin the documents the agent read, by hash; the content never leaves.
372
+ * Each doc is `{id, uri?, version?}` with its `content` (hashed here) or its `sha256`. */
373
+ retrieved(docs) {
374
+ const out = [];
375
+ for (const d of docs.slice(0, 100)) {
376
+ if (typeof d?.id !== "string" || d.id.length < 1)
377
+ continue;
378
+ const bytes = typeof d.content === "string" ? Buffer.from(d.content) : d.content;
379
+ const sha256 = bytes
380
+ ? `sha256:${createHash("sha256").update(bytes).digest("hex")}`
381
+ : d.sha256;
382
+ if (typeof sha256 !== "string" || !/^sha256:[0-9a-f]{64}$/.test(sha256))
383
+ continue;
384
+ out.push({
385
+ id: d.id.slice(0, 256),
386
+ ...(typeof d.uri === "string" ? { uri: d.uri.slice(0, 2048) } : {}),
387
+ ...(typeof d.version === "string" ? { version: d.version.slice(0, 128) } : {}),
388
+ sha256,
389
+ ...(bytes ? { bytes: bytes.length } : {}),
390
+ });
391
+ }
392
+ if (out.length)
393
+ this.event("retrieved", undefined, { docs: out });
394
+ }
258
395
  /** Stage 2 R3: the result of one of the customer's own checks (tests, a schema, a policy, a
259
396
  * partial goal), mid-run or at the end. A pass then a fail is VERIFY_REGRESSION; a success whose last
260
397
  * check failed is FALSE_SUCCESS (spec/findings.md §6). */
@@ -291,9 +428,47 @@ export class BlackboxSession {
291
428
  this.event("attestation", argv[0], { ...result });
292
429
  return result;
293
430
  }
431
+ /**
432
+ * spec/sdk.md §1.1: a computer-use screenshot (PNG, JPEG or WebP, at most 2 MB), recorded as the
433
+ * body of a `screen` event, so its hash is in the chain. `action` says what the agent did or is
434
+ * about to do (≤ 500 characters). Mask what mustn't be kept before calling: the image isn't
435
+ * redacted. One that's too big or not an image is dropped, and the event says why.
436
+ */
437
+ screen(image, options = {}) {
438
+ const data = {
439
+ ...(options.action ? { action: options.action.slice(0, 500) } : {}),
440
+ ...(Number.isSafeInteger(options.width) ? { width: options.width } : {}),
441
+ ...(Number.isSafeInteger(options.height) ? { height: options.height } : {}),
442
+ };
443
+ const type = imageType(image);
444
+ if (!type || image.length > MAX_ATTACHMENT) {
445
+ this.event("screen", undefined, {
446
+ ...data,
447
+ attachment_dropped: type
448
+ ? `${image.length} bytes, over 2 MB`
449
+ : "not a PNG, JPEG or WebP image",
450
+ });
451
+ return;
452
+ }
453
+ this.event("screen", undefined, data, { attachment: { contentType: type, bytes: image } });
454
+ }
294
455
  /** spec/agents.md §1: the agent's memory store wrote, revoked or returned memories. */
295
- memoryWrite(memoryId, summary) {
296
- this.event("memory.write", undefined, { memory_id: memoryId, ...(summary ? { summary } : {}) });
456
+ /** spec/agents.md §4: with `content`, the write also carries Zanii's memory entry (a salted
457
+ * commitment, chained after `prev`, else this session's last entry). Keep the salt it returns. */
458
+ memoryWrite(memoryId, summary, opts) {
459
+ const data = { memory_id: memoryId, ...(summary ? { summary } : {}) };
460
+ let made;
461
+ if (opts) {
462
+ made = memoryEntry({
463
+ ...opts,
464
+ ts: new Date().toISOString(),
465
+ prev: opts.prev ?? this.memoryLast,
466
+ });
467
+ this.memoryLast = made.payload;
468
+ data.entry = made.payload;
469
+ }
470
+ this.event("memory.write", undefined, data);
471
+ return made && { entry: made.payload, salt: made.salt };
297
472
  }
298
473
  memoryRevoke(memoryId, reason) {
299
474
  this.event("memory.revoke", undefined, { memory_id: memoryId, ...(reason ? { reason } : {}) });
@@ -350,17 +525,20 @@ export class BlackboxSession {
350
525
  * and waits (polling) for the answer. Never throws. Only `"approved"` means go ahead; anything
351
526
  * else (`rejected`, `timeout`, or `error` when the gateway can't be asked) means don't. */
352
527
  async requestApproval(tool, options = {}) {
353
- return this.ask("approvals", { tool, ...(options.args ? { args: options.args } : {}) }, options);
528
+ const { status, id } = await this.ask("approvals", { tool, ...(options.args ? { args: options.args } : {}) }, options);
529
+ if (status === "approved" && options.args && id)
530
+ this.approved.set(tool, id);
531
+ return status;
354
532
  }
355
533
  /** N2 (idea C2): asks a person to let this session use a gateway tool its policy denies (the
356
534
  * denial's `requires.tool`, e.g. `mcp__files__delete`). A yes is a standing grant for the
357
535
  * session: retry the call. Answers like `requestApproval`. Never throws. */
358
536
  async requestPermission(tool, options = {}) {
359
- return this.ask("permissions", { tool }, options);
537
+ return (await this.ask("permissions", { tool }, options)).status;
360
538
  }
361
539
  async ask(route, body, options) {
362
540
  if (!this.enabled)
363
- return "error";
541
+ return { status: "error" };
364
542
  try {
365
543
  const r = await post(this.options.url, `/v1/sessions/${this.id}/${route}`, this.token, {
366
544
  ...body,
@@ -369,21 +547,21 @@ export class BlackboxSession {
369
547
  const id = r.json.approval_id;
370
548
  if (r.status !== 201 || typeof id !== "string") {
371
549
  this.fail("rejected", `asking for an approval: the gateway answered ${r.status}`);
372
- return "error";
550
+ return { status: "error" };
373
551
  }
374
552
  const deadline = Date.now() + (options.timeoutMs ?? 310_000);
375
553
  while (Date.now() < deadline) {
376
554
  const s = await send("GET", this.options.url, `/v1/sessions/${this.id}/approvals/${id}`, this.token);
377
555
  const status = s.json.status;
378
556
  if (status === "approved" || status === "rejected" || status === "timeout")
379
- return status;
557
+ return { status, id };
380
558
  await sleep(options.pollMs ?? 1_000);
381
559
  }
382
- return "timeout";
560
+ return { status: "timeout", id };
383
561
  }
384
562
  catch (error) {
385
563
  this.fail("network", `asking for an approval: ${message(error)}`);
386
- return "error";
564
+ return { status: "error" };
387
565
  }
388
566
  }
389
567
  /** Audit S8: `await using s = await session(…)` closes the session when the scope ends. */
@@ -0,0 +1,61 @@
1
+ type Obj = Record<string, unknown>;
2
+ type Commitments = {
3
+ min_actions?: number;
4
+ max_actions?: number;
5
+ max_gap_ms?: number;
6
+ };
7
+ /** Zanii's SLA terms. `commitments` holds at least one of min_actions, max_actions, max_gap_ms. */
8
+ export declare function buildSlaBody(o: {
9
+ provider: string;
10
+ client: string;
11
+ scope: string;
12
+ window: {
13
+ start: string;
14
+ end: string;
15
+ };
16
+ commitments: Commitments;
17
+ price?: {
18
+ amount: string;
19
+ currency: string;
20
+ };
21
+ createdAt: string;
22
+ ref?: string;
23
+ }): Obj;
24
+ /** One party signs the terms. Collect the provider's and the client's. */
25
+ export declare const signSla: (body: Obj, did: string, privateKey: Uint8Array) => {
26
+ did: string;
27
+ sig: string;
28
+ };
29
+ export declare const assembleSla: (body: Obj, signatures: Array<{
30
+ did: string;
31
+ sig: string;
32
+ }>) => {
33
+ body: Obj;
34
+ signatures: {
35
+ did: string;
36
+ sig: string;
37
+ }[];
38
+ };
39
+ export declare const slaHash: (sla: Obj) => string;
40
+ /** Zanii's check: sane terms, and BOTH provider and client signed exactly these. */
41
+ export declare function verifySla(sla: unknown): {
42
+ ok: boolean;
43
+ reasons: string[];
44
+ };
45
+ /**
46
+ * Zanii's verdict over the provider's consecutive receipt chain for the window. Breaches:
47
+ * INVALID_RECEIPT, INCOMPLETE_RECORD (a seam: something recorded was left out), TOO_FEW_ACTIONS,
48
+ * TOO_MANY_ACTIONS, MAX_GAP_EXCEEDED. `asOf` (default the window's end) caps the trailing gap.
49
+ */
50
+ export declare function assessCompliance(sla: Obj, receipts: readonly unknown[], asOf?: string): {
51
+ ok: boolean;
52
+ breaches: {
53
+ kind: string;
54
+ detail: string;
55
+ }[];
56
+ metrics: {
57
+ actions: number;
58
+ max_gap_ms: number | null;
59
+ };
60
+ };
61
+ export {};