@frockbot/kernel-contracts 0.1.3 → 0.2.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.
@@ -0,0 +1,25 @@
1
+ // Generated by scripts/generate-isolate-context-catalog.ts. Do not edit.
2
+ export const BOT_ISOLATE_CONTEXT_DTS_V1 =
3
+ "/**\n * The common exact `ctx` passed to a Bot-authored Package's tool and hook.\n * The model-facing declarations are generated from these interfaces, and the\n * wrapper's implementation is compile- and test-checked against the same\n * keys.\n */\ninterface BotPackageContextV1 {\n readonly tool?: string;\n readonly event?: BotIsolateHookEventNameV1;\n readonly botId: string;\n readonly sessionId: string;\n readonly runId: string;\n readonly turnId: string;\n readonly generationId: string;\n readonly packageId: string;\n readonly deadlineMs: number;\n readonly bindings: string[];\n readonly capabilities: {\n list(): Promise<IsolateCapabilityListOutcomeV1>;\n };\n readonly model: {\n invoke(request: NormalizedModelRequest): Promise<BotPackageModelOutcomeV1>;\n };\n readonly tools: {\n /** Runs through the trusted registry's active-Composition and deny guards. */\n invoke(request: IsolateToolRequestV1): Promise<IsolateToolOutcomeV1>;\n };\n readonly memory: {\n read(request: IsolateMemoryReadRequestV1): Promise<IsolateMemoryOutcomeV1>;\n write(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>;\n forget(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>;\n };\n readonly workspace: {\n read(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>;\n list(request: IsolateWorkspaceListRequestV1): Promise<IsolateWorkspaceOutcomeV1>;\n stat(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>;\n write(request: IsolateWorkspaceWriteRequestV1): Promise<IsolateWorkspaceOutcomeV1>;\n delete(request: IsolateWorkspaceDeleteRequestV1): Promise<IsolateWorkspaceOutcomeV1>;\n };\n connection(connectionId: string): Promise<IsolateConnectionOutcomeV1>;\n notify(request: IsolateNotificationRequestV1): Promise<IsolateNotificationOutcomeV1>;\n schedule(request: IsolateScheduleRequestV1): Promise<IsolateScheduleOutcomeV1>;\n}\n\ninterface BotPackageExecutionContextV1 extends BotPackageContextV1 {\n readonly tool: string;\n readonly event?: never;\n}\n\ninterface BotPackageHookContextV1 extends BotPackageContextV1 {\n readonly tool?: never;\n readonly event: BotIsolateHookEventNameV1;\n}";
4
+ export const BOT_ISOLATE_CONTEXT_KEYS_V1 = [
5
+ "tool",
6
+ "event",
7
+ "botId",
8
+ "sessionId",
9
+ "runId",
10
+ "turnId",
11
+ "generationId",
12
+ "packageId",
13
+ "deadlineMs",
14
+ "bindings",
15
+ "capabilities",
16
+ "model",
17
+ "tools",
18
+ "memory",
19
+ "workspace",
20
+ "connection",
21
+ "notify",
22
+ "schedule",
23
+ ] as const;
24
+ export const BOT_ISOLATE_CONTEXT_SUMMARY_V1 =
25
+ "Package execute and hook ctx share: readonly tool?: string; readonly event?: BotIsolateHookEventNameV1; readonly botId: string; readonly sessionId: string; readonly runId: string; readonly turnId: string; readonly generationId: string; readonly packageId: string; readonly deadlineMs: number; readonly bindings: string[]; readonly capabilities: { list(): Promise<IsolateCapabilityListOutcomeV1>; }; readonly model: { invoke(request: NormalizedModelRequest): Promise<BotPackageModelOutcomeV1>; }; readonly tools: { /** Runs through the trusted registry's active-Composition and deny guards. */ invoke(request: IsolateToolRequestV1): Promise<IsolateToolOutcomeV1>; }; readonly memory: { read(request: IsolateMemoryReadRequestV1): Promise<IsolateMemoryOutcomeV1>; write(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>; forget(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>; }; readonly workspace: { read(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>; list(request: IsolateWorkspaceListRequestV1): Promise<IsolateWorkspaceOutcomeV1>; stat(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>; write(request: IsolateWorkspaceWriteRequestV1): Promise<IsolateWorkspaceOutcomeV1>; delete(request: IsolateWorkspaceDeleteRequestV1): Promise<IsolateWorkspaceOutcomeV1>; }; connection(connectionId: string): Promise<IsolateConnectionOutcomeV1>; notify(request: IsolateNotificationRequestV1): Promise<IsolateNotificationOutcomeV1>; schedule(request: IsolateScheduleRequestV1): Promise<IsolateScheduleOutcomeV1>;";
@@ -1,13 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
- decodeIsolateAuthorityRequestV1,
4
3
  decodeIsolateCapabilityFailureV1,
5
4
  decodeIsolateCapabilityListV1,
6
5
  decodeIsolateHealthV1,
6
+ decodeIsolateHookInvocationV1,
7
+ decodeIsolateHookResultV1,
7
8
  decodeIsolateIdentityV1,
8
9
  decodeIsolateModelEventV1,
9
10
  decodeIsolateModelInvocationV1,
10
- decodeIsolatePendingDecisionV1,
11
+ decodeIsolateScheduleRequestV1,
11
12
  decodeIsolateToolDescriptorV1,
12
13
  decodeIsolateToolInvocationV1,
13
14
  decodeIsolateToolResultV1,
@@ -15,6 +16,7 @@ import {
15
16
  isolateLoaderIdV1,
16
17
  isolateToolSchemaV1,
17
18
  ISOLATE_MAX_DEADLINE_MS,
19
+ type IsolateHookInvocationV1,
18
20
  } from "./isolate.js";
19
21
 
20
22
  function descriptor(overrides: Record<string, unknown> = {}) {
@@ -161,7 +163,7 @@ describe("isolate health v1", () => {
161
163
 
162
164
  test("rejects an unsupported contract version", () => {
163
165
  expect(() =>
164
- decodeIsolateHealthV1({ ...health, contractVersion: 3 }),
166
+ decodeIsolateHealthV1({ ...health, contractVersion: 4 }),
165
167
  ).toThrow(/contractVersion is unsupported/);
166
168
  expect(() =>
167
169
  decodeIsolateHealthV1({ ...health, contractVersion: 0 }),
@@ -196,6 +198,26 @@ describe("isolate health v1", () => {
196
198
  ).toThrow(/invalid fields/);
197
199
  });
198
200
 
201
+ test("requires and decodes declared hooks on contract v3", () => {
202
+ expect(
203
+ decodeIsolateHealthV1({
204
+ ...health,
205
+ contractVersion: 3,
206
+ hooks: ["agent/tool-exposure", "tools/post-execute"],
207
+ }).hooks,
208
+ ).toEqual(["agent/tool-exposure", "tools/post-execute"]);
209
+ expect(() =>
210
+ decodeIsolateHealthV1({ ...health, contractVersion: 3 }),
211
+ ).toThrow(/invalid fields/);
212
+ expect(() =>
213
+ decodeIsolateHealthV1({
214
+ ...health,
215
+ contractVersion: 3,
216
+ hooks: ["agent/request"],
217
+ }),
218
+ ).toThrow(/hooks\[0\] is invalid/);
219
+ });
220
+
199
221
  test("rejects an unknown turn type in a v2 descriptor", () => {
200
222
  expect(() =>
201
223
  decodeIsolateHealthV1({
@@ -220,6 +242,61 @@ describe("isolate health v1", () => {
220
242
  });
221
243
  });
222
244
 
245
+ describe("isolate hook v1", () => {
246
+ const hookInvocation: IsolateHookInvocationV1<"agent/tool-exposure"> = {
247
+ schemaVersion: 1,
248
+ event: "agent/tool-exposure",
249
+ payload: {
250
+ step: {
251
+ botId: "bot-1",
252
+ agentId: "bot-1",
253
+ sessionId: "session-1",
254
+ status: "running",
255
+ compositionGenerationId: "gen-1",
256
+ turn: 1,
257
+ step: 1,
258
+ turnType: "chat",
259
+ },
260
+ tools: [],
261
+ },
262
+ botId: "bot-1",
263
+ sessionId: "session-1",
264
+ runId: "run-1",
265
+ turnId: "turn-1",
266
+ generationId: "gen-1",
267
+ deadlineMs: 1_000,
268
+ };
269
+
270
+ test("decodes an exact hook invocation and result envelope", () => {
271
+ expect(decodeIsolateHookInvocationV1(hookInvocation)).toEqual(
272
+ hookInvocation,
273
+ );
274
+ expect(
275
+ decodeIsolateHookResultV1({
276
+ schemaVersion: 1,
277
+ status: "replaced",
278
+ replacement: [],
279
+ }),
280
+ ).toEqual({ schemaVersion: 1, status: "replaced", replacement: [] });
281
+ });
282
+
283
+ test("refuses undeclared events and inexact result envelopes", () => {
284
+ expect(() =>
285
+ decodeIsolateHookInvocationV1({
286
+ ...hookInvocation,
287
+ event: "agent/request",
288
+ }),
289
+ ).toThrow(/event is invalid/);
290
+ expect(() =>
291
+ decodeIsolateHookResultV1({
292
+ schemaVersion: 1,
293
+ status: "unchanged",
294
+ replacement: [],
295
+ }),
296
+ ).toThrow(/invalid fields/);
297
+ });
298
+ });
299
+
223
300
  describe("isolate identity and capabilities", () => {
224
301
  test("decodes the identity binding", () => {
225
302
  expect(
@@ -233,42 +310,79 @@ describe("isolate identity and capabilities", () => {
233
310
 
234
311
  test("decodes a capability list", () => {
235
312
  expect(
236
- decodeIsolateCapabilityListV1([
237
- { capabilityId: "models:chat", kind: "model" },
238
- ]),
239
- ).toHaveLength(1);
313
+ decodeIsolateCapabilityListV1({
314
+ status: "available",
315
+ connections: [
316
+ {
317
+ connectionId: "connection-1",
318
+ packageId: "provider",
319
+ connectionTypeId: "account",
320
+ displayName: "Account",
321
+ generation: "generation-1",
322
+ safeMetadata: { region: "au" },
323
+ },
324
+ ],
325
+ model: {
326
+ connectionId: "connection-1",
327
+ packageId: "provider",
328
+ provider: "provider",
329
+ providerModelId: "model-1",
330
+ connectionGeneration: "generation-1",
331
+ },
332
+ tools: true,
333
+ memory: true,
334
+ workspace: false,
335
+ notify: true,
336
+ schedule: true,
337
+ }),
338
+ ).toMatchObject({
339
+ status: "available",
340
+ connections: [{ connectionId: "connection-1" }],
341
+ tools: true,
342
+ memory: true,
343
+ workspace: false,
344
+ notify: true,
345
+ schedule: true,
346
+ });
240
347
  });
241
348
 
242
- test("rejects an unknown capability kind", () => {
349
+ test("rejects authority fields outside the Bot authority contract", () => {
243
350
  expect(() =>
244
- decodeIsolateCapabilityListV1([
245
- { capabilityId: "models:chat", kind: "network" },
246
- ]),
247
- ).toThrow(/kind is invalid/);
351
+ decodeIsolateCapabilityListV1({
352
+ status: "available",
353
+ connections: [],
354
+ tools: true,
355
+ memory: true,
356
+ workspace: true,
357
+ notify: true,
358
+ schedule: true,
359
+ packageId: "package-local-authority",
360
+ }),
361
+ ).toThrow(/invalid fields/);
248
362
  });
363
+ });
249
364
 
250
- test("decodes an authority request and its pending answer", () => {
365
+ describe("isolate schedule request v1", () => {
366
+ test("decodes the durable Routine input and call id", () => {
251
367
  expect(
252
- decodeIsolateAuthorityRequestV1({
253
- capabilityId: "models:chat",
254
- reason: "translate",
368
+ decodeIsolateScheduleRequestV1({
369
+ callId: "schedule-1",
370
+ input: { action: "create", schedule: "@daily" },
255
371
  }),
256
- ).toEqual({ capabilityId: "models:chat", reason: "translate" });
257
- expect(
258
- decodeIsolatePendingDecisionV1({
259
- status: "pending-user-decision",
260
- decisionId: "decision-1",
261
- }).decisionId,
262
- ).toBe("decision-1");
372
+ ).toEqual({
373
+ callId: "schedule-1",
374
+ input: { action: "create", schedule: "@daily" },
375
+ });
263
376
  });
264
377
 
265
- test("refuses to decode a grant as a decision", () => {
378
+ test("rejects an undeclared field", () => {
266
379
  expect(() =>
267
- decodeIsolatePendingDecisionV1({
268
- status: "granted",
269
- decisionId: "decision-1",
380
+ decodeIsolateScheduleRequestV1({
381
+ callId: "schedule-1",
382
+ input: {},
383
+ packageId: "package-local-authority",
270
384
  }),
271
- ).toThrow(/pending-user-decision/);
385
+ ).toThrow(/invalid fields/);
272
386
  });
273
387
  });
274
388
 
@@ -314,13 +428,16 @@ describe("isolate model invocation v1", () => {
314
428
  if (outcome.status === "streaming") expect(outcome.events).toBe(events);
315
429
  });
316
430
 
317
- test("decodes a pending decision outcome", () => {
431
+ test("decodes an unavailable outcome when no model is configured", () => {
318
432
  expect(
319
433
  decodeIsolateModelInvocationV1({
320
- status: "pending-user-decision",
321
- decisionId: "decision-9",
434
+ status: "unavailable",
435
+ reason: "this Bot has no configured model",
322
436
  }),
323
- ).toEqual({ status: "pending-user-decision", decisionId: "decision-9" });
437
+ ).toEqual({
438
+ status: "unavailable",
439
+ reason: "this Bot has no configured model",
440
+ });
324
441
  });
325
442
 
326
443
  test("rejects a streaming outcome without a stream", () => {
@@ -368,29 +485,25 @@ describe("isolate capability failure v1", () => {
368
485
  });
369
486
 
370
487
  describe("isolate loader identity", () => {
371
- test("is the Bot, the User, and the content address — nothing else", () => {
488
+ test("is the User and binding-addressed module set — nothing else", () => {
372
489
  expect(
373
490
  isolateLoaderIdV1({
374
491
  userId: "user-1",
375
- botId: "bot-1",
376
492
  artifactSetHash: "a".repeat(64),
377
493
  }),
378
- ).toBe(`bot-package:user-1:bot-1:${"a".repeat(64)}`);
494
+ ).toBe(`bot-package:user-1:${"a".repeat(64)}`);
379
495
  });
380
496
 
381
- test("two Bots of one User never share an id", () => {
382
- const hash = "b".repeat(64);
497
+ test("different binding digests produce different ids", () => {
383
498
  expect(
384
499
  isolateLoaderIdV1({
385
500
  userId: "user-1",
386
- botId: "bot-1",
387
- artifactSetHash: hash,
501
+ artifactSetHash: "b".repeat(64),
388
502
  }),
389
503
  ).not.toBe(
390
504
  isolateLoaderIdV1({
391
505
  userId: "user-1",
392
- botId: "bot-2",
393
- artifactSetHash: hash,
506
+ artifactSetHash: "c".repeat(64),
394
507
  }),
395
508
  );
396
509
  });
@@ -399,7 +512,6 @@ describe("isolate loader identity", () => {
399
512
  expect(() =>
400
513
  isolateLoaderIdV1({
401
514
  userId: "user-1:bot-2",
402
- botId: "bot-1",
403
515
  artifactSetHash: "c".repeat(64),
404
516
  }),
405
517
  ).toThrow(/components are invalid/);
@@ -409,7 +521,6 @@ describe("isolate loader identity", () => {
409
521
  expect(() =>
410
522
  isolateLoaderIdV1({
411
523
  userId: "user-1",
412
- botId: "bot-1",
413
524
  artifactSetHash: "not-a-hash",
414
525
  }),
415
526
  ).toThrow(/components are invalid/);