@frockbot/kernel-contracts 0.1.4 → 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,7 +485,7 @@ describe("isolate capability failure v1", () => {
368
485
  });
369
486
 
370
487
  describe("isolate loader identity", () => {
371
- test("is 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",
@@ -377,25 +494,24 @@ describe("isolate loader identity", () => {
377
494
  ).toBe(`bot-package:user-1:${"a".repeat(64)}`);
378
495
  });
379
496
 
380
- test("two Users never share an id", () => {
381
- const hash = "b".repeat(64);
497
+ test("different binding digests produce different ids", () => {
382
498
  expect(
383
499
  isolateLoaderIdV1({
384
500
  userId: "user-1",
385
- artifactSetHash: hash,
501
+ artifactSetHash: "b".repeat(64),
386
502
  }),
387
503
  ).not.toBe(
388
504
  isolateLoaderIdV1({
389
- userId: "user-2",
390
- artifactSetHash: hash,
505
+ userId: "user-1",
506
+ artifactSetHash: "c".repeat(64),
391
507
  }),
392
508
  );
393
509
  });
394
510
 
395
- test("rejects a component that could forge another User's id", () => {
511
+ test("rejects a component that could forge another Bot's id", () => {
396
512
  expect(() =>
397
513
  isolateLoaderIdV1({
398
- userId: "user-1:other",
514
+ userId: "user-1:bot-2",
399
515
  artifactSetHash: "c".repeat(64),
400
516
  }),
401
517
  ).toThrow(/components are invalid/);