@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.
@@ -12,18 +12,16 @@
12
12
  // type admits, when a payload ends a Turn, or how a client draws one.
13
13
  //
14
14
  // Only the `widget` shape is host-source (§4.2 of `docs/research/
15
- // grokbot-computer.md`). The other four members are named in the same section
15
+ // grokbot-computer.md`). The other members are named in the same section
16
16
  // but their field lists are not recorded, so they are declared here in the
17
17
  // narrowest shape that carries the observed meaning, and widened when a
18
18
  // primary source says more.
19
19
  //
20
- // `approval` has no GrokBot payload behind it at all: row 53
21
- // records only the harness sentence "when your own action needs approval". It
22
- // is declared here because the constitution's *Self-modification* rule "a
23
- // request for more becomes a durable pending decision for the User, never a
24
- // grant" — needs one shape to carry that request, and a card the Bot sends is
25
- // the only path a Turn has to a person. Like `widget` it ends the Turn: the
26
- // Bot has nothing to do until a human answers.
20
+ // `approval` has no GrokBot payload behind it at all: row 53 records only the
21
+ // harness sentence "when your own action needs approval". It carries a human
22
+ // confirmation required by a deny-only guard; it never widens Bot authority.
23
+ // Like `widget` it ends the Turn: the Bot has nothing to do until a human
24
+ // answers.
27
25
 
28
26
  /** The widget shape, verbatim from §4.2: `options` holds 1–6 entries. */
29
27
  export interface SendToUserWidgetV1 {
@@ -40,22 +38,6 @@ export type SendToUserPayloadV1 =
40
38
  | { type: "widget"; widget: SendToUserWidgetV1 }
41
39
  | { type: "secret-request"; prompt: string; secretName: string }
42
40
  | { type: "agent-card"; agentId: string; title: string; body?: string }
43
- /**
44
- * A Connection the Bot has recorded a pending authorization decision for.
45
- *
46
- * There is deliberately **no URL** on this payload, and there never will be.
47
- * A Bot may create a durable *request* for authorization; only an
48
- * authenticated User action mints a redirect, and a single-use ten-minute
49
- * link sitting in a client-readable transcript would outlive the decision it
50
- * belonged to. The client draws a card from the Connection's own projection
51
- * and the User presses it; the host authors the link at that moment.
52
- */
53
- | {
54
- type: "connect-card";
55
- connectionId: string;
56
- title: string;
57
- body?: string;
58
- }
59
41
  | {
60
42
  type: "approval";
61
43
  /** The Bot's own id for the decision, and the key it is recorded under. */
@@ -76,15 +58,7 @@ export const SEND_TO_USER_APPROVAL_RISKS_V1: readonly SendToUserApprovalRiskV1[]
76
58
  ["low", "medium", "high"];
77
59
 
78
60
  export const SEND_TO_USER_PAYLOAD_TYPES_V1: readonly SendToUserPayloadV1["type"][] =
79
- [
80
- "text",
81
- "attachment",
82
- "widget",
83
- "secret-request",
84
- "agent-card",
85
- "connect-card",
86
- "approval",
87
- ];
61
+ ["text", "attachment", "widget", "secret-request", "agent-card", "approval"];
88
62
 
89
63
  /**
90
64
  * Bounds, so a payload cannot be the way a Turn writes an unbounded record
@@ -103,7 +77,6 @@ export const SEND_TO_USER_LIMITS_V1 = {
103
77
  maxOptions: 6,
104
78
  secretName: 128,
105
79
  agentId: 128,
106
- connectionId: 128,
107
80
  title: 200,
108
81
  body: 8_000,
109
82
  approvalId: 128,
@@ -356,28 +329,6 @@ export function decodeSendToUserPayloadV1(
356
329
  ...(body === undefined ? {} : { body }),
357
330
  };
358
331
  }
359
- case "connect-card": {
360
- exactPayloadKeys(
361
- payload,
362
- ["type", "connectionId", "title", "body"],
363
- label,
364
- );
365
- const body = optionalBoundedString(
366
- payload.body,
367
- limits.body,
368
- `${label}.body`,
369
- );
370
- return {
371
- type: "connect-card",
372
- connectionId: boundedString(
373
- payload.connectionId,
374
- limits.connectionId,
375
- `${label}.connectionId`,
376
- ),
377
- title: boundedString(payload.title, limits.title, `${label}.title`),
378
- ...(body === undefined ? {} : { body }),
379
- };
380
- }
381
332
  default:
382
333
  throw new Error(`${label}.type is invalid`);
383
334
  }
@@ -39,6 +39,22 @@ afterEach(async () => {
39
39
  await Promise.all(roots.splice(0).map((root) => root.fiber.dispose()));
40
40
  });
41
41
 
42
+ test("a Bot-isolate hook failure is an exact durable session event", () => {
43
+ const event = {
44
+ type: "package/hook-failed",
45
+ packageId: "bot-authored",
46
+ event: "agent/tool-exposure",
47
+ generationId: "gen-1",
48
+ message: "hook exploded",
49
+ seq: 0,
50
+ timestamp,
51
+ } as const;
52
+ expect(decodeSessionEvent(event)).toEqual(event);
53
+ expect(() => decodeSessionEvent({ ...event, retry: true })).toThrow(
54
+ /invalid fields/,
55
+ );
56
+ });
57
+
42
58
  describe("SessionStore", () => {
43
59
  test("accepts resumable tool crash states only while their step is open", () => {
44
60
  const assistant = [
@@ -187,12 +187,28 @@ export interface ToolRegistrationOptions {
187
187
 
188
188
  /** Contributing Packages register tool definitions through this surface. */
189
189
  export interface ToolRegistration {
190
+ /** Every registered name, before turn/role admission trims the catalog. */
191
+ registeredNames?(): string[];
190
192
  register(
191
193
  definition: ToolDefinition,
192
194
  options?: ToolRegistrationOptions,
193
195
  ): () => void;
196
+ /**
197
+ * First-party, deny-only policy evaluated after `tools/pre-execute` and
198
+ * before `tools/execute`. The isolate contract never exposes this method.
199
+ */
200
+ guard(guard: ToolGuard): () => void;
194
201
  }
195
202
 
203
+ export interface ToolGuardDenial {
204
+ reason: string;
205
+ }
206
+
207
+ export type ToolGuard = (
208
+ call: ToolCall,
209
+ context: ToolExecutionContext,
210
+ ) => ToolGuardDenial | undefined | Promise<ToolGuardDenial | undefined>;
211
+
196
212
  declare module "cordis" {
197
213
  interface Context {
198
214
  tools: ToolExecution & ToolRegistration;
package/src/types.ts CHANGED
@@ -328,6 +328,27 @@ export interface SessionEventMap {
328
328
  /** Durable references to binaries the tool produced. Never their bytes. */
329
329
  attachments?: ToolAttachmentV1[];
330
330
  };
331
+ /** A tool one loaded Package invoked through the Bot's shared registry. */
332
+ "package/tool-call": {
333
+ turn: number;
334
+ step: number;
335
+ effectId: string;
336
+ packageId: string;
337
+ callId: string;
338
+ name: string;
339
+ input: unknown;
340
+ };
341
+ /** The exact result returned to that Package. */
342
+ "package/tool-result": {
343
+ turn: number;
344
+ step: number;
345
+ effectId: string;
346
+ packageId: string;
347
+ callId: string;
348
+ name: string;
349
+ content: string;
350
+ isError: boolean;
351
+ };
331
352
  /**
332
353
  * The Bot recorded the intent to author a Package, before the bundler ran.
333
354
  * Constitution, Durable effects: intent is recorded before the effect.
@@ -349,6 +370,48 @@ export interface SessionEventMap {
349
370
  contentHash: string;
350
371
  generationId: string;
351
372
  };
373
+ /** A Bot-isolate loop hook failed open for one invocation. */
374
+ "package/hook-failed": {
375
+ packageId: string;
376
+ event: string;
377
+ generationId: string;
378
+ message: string;
379
+ };
380
+ /** Durable session intent before a Catalog installation effect. */
381
+ "package/catalog-change-intent": {
382
+ turn: number;
383
+ step: number;
384
+ effectId: string;
385
+ action: "install" | "update" | "remove";
386
+ catalogId?: string;
387
+ packageId?: string;
388
+ contentHash?: string;
389
+ };
390
+ /** The pending Composition generation produced by that Catalog change. */
391
+ "package/catalog-changed": {
392
+ turn: number;
393
+ step: number;
394
+ effectId: string;
395
+ action: "install" | "update" | "remove";
396
+ packageId: string;
397
+ contentHash?: string;
398
+ generationId: string;
399
+ };
400
+ /** Durable intent before a Bot-origin Composition revert is proposed. */
401
+ "package/undo-intent": {
402
+ turn: number;
403
+ step: number;
404
+ effectId: string;
405
+ requestedGenerationId?: string;
406
+ };
407
+ /** The new pending generation recorded by a Bot-origin revert. */
408
+ "package/undo-recorded": {
409
+ turn: number;
410
+ step: number;
411
+ effectId: string;
412
+ generationId: string;
413
+ targetGenerationId: string;
414
+ };
352
415
  /**
353
416
  * The Skills this Turn loaded as instructions, and the candidates it
354
417
  * refused. Constitution, Memory: "the session event log records exactly what
@@ -1179,6 +1242,54 @@ export function decodeSessionEvent(input: unknown): SessionEvent {
1179
1242
  );
1180
1243
  }
1181
1244
  break;
1245
+ case "package/tool-call":
1246
+ requireEventKeys(
1247
+ event,
1248
+ keys(
1249
+ "turn",
1250
+ "step",
1251
+ "effectId",
1252
+ "packageId",
1253
+ "callId",
1254
+ "name",
1255
+ "input",
1256
+ ),
1257
+ "session event",
1258
+ );
1259
+ turn();
1260
+ step();
1261
+ eventString(event.effectId, "session event.effectId");
1262
+ eventString(event.packageId, "session event.packageId");
1263
+ eventString(event.callId, "session event.callId");
1264
+ eventString(event.name, "session event.name");
1265
+ requireJsonValue(event.input, "session event.input");
1266
+ break;
1267
+ case "package/tool-result":
1268
+ requireEventKeys(
1269
+ event,
1270
+ keys(
1271
+ "turn",
1272
+ "step",
1273
+ "effectId",
1274
+ "packageId",
1275
+ "callId",
1276
+ "name",
1277
+ "content",
1278
+ "isError",
1279
+ ),
1280
+ "session event",
1281
+ );
1282
+ turn();
1283
+ step();
1284
+ eventString(event.effectId, "session event.effectId");
1285
+ eventString(event.packageId, "session event.packageId");
1286
+ eventString(event.callId, "session event.callId");
1287
+ eventString(event.name, "session event.name");
1288
+ eventString(event.content, "session event.content", true);
1289
+ if (typeof event.isError !== "boolean") {
1290
+ throw new Error("session event.isError must be a boolean");
1291
+ }
1292
+ break;
1182
1293
  case "package/author-intent":
1183
1294
  requireEventKeys(
1184
1295
  event,
@@ -1213,6 +1324,124 @@ export function decodeSessionEvent(input: unknown): SessionEvent {
1213
1324
  eventString(event.contentHash, "session event.contentHash");
1214
1325
  eventString(event.generationId, "session event.generationId");
1215
1326
  break;
1327
+ case "package/hook-failed":
1328
+ requireEventKeys(
1329
+ event,
1330
+ keys("packageId", "event", "generationId", "message"),
1331
+ "session event",
1332
+ );
1333
+ eventString(event.packageId, "session event.packageId");
1334
+ eventString(event.event, "session event.event");
1335
+ eventString(event.generationId, "session event.generationId");
1336
+ eventString(event.message, "session event.message");
1337
+ break;
1338
+ case "package/catalog-change-intent":
1339
+ requireEventKeys(
1340
+ event,
1341
+ keys(
1342
+ "turn",
1343
+ "step",
1344
+ "effectId",
1345
+ "action",
1346
+ ...(Object.hasOwn(event, "catalogId") ? ["catalogId"] : []),
1347
+ ...(Object.hasOwn(event, "packageId") ? ["packageId"] : []),
1348
+ ...(Object.hasOwn(event, "contentHash") ? ["contentHash"] : []),
1349
+ ),
1350
+ "session event",
1351
+ );
1352
+ turn();
1353
+ step();
1354
+ eventString(event.effectId, "session event.effectId");
1355
+ if (
1356
+ event.action !== "install" &&
1357
+ event.action !== "update" &&
1358
+ event.action !== "remove"
1359
+ ) {
1360
+ throw new Error("session event.action is invalid");
1361
+ }
1362
+ if (event.catalogId !== undefined)
1363
+ eventString(event.catalogId, "session event.catalogId");
1364
+ if (event.packageId !== undefined)
1365
+ eventString(event.packageId, "session event.packageId");
1366
+ if (event.contentHash !== undefined)
1367
+ eventString(event.contentHash, "session event.contentHash");
1368
+ if (
1369
+ (event.action === "remove" &&
1370
+ (event.packageId === undefined ||
1371
+ event.catalogId !== undefined ||
1372
+ event.contentHash !== undefined)) ||
1373
+ (event.action !== "remove" &&
1374
+ (event.catalogId === undefined ||
1375
+ event.contentHash === undefined ||
1376
+ event.packageId !== undefined))
1377
+ ) {
1378
+ throw new Error("session Catalog change intent identity is invalid");
1379
+ }
1380
+ break;
1381
+ case "package/catalog-changed":
1382
+ requireEventKeys(
1383
+ event,
1384
+ keys(
1385
+ "turn",
1386
+ "step",
1387
+ "effectId",
1388
+ "action",
1389
+ "packageId",
1390
+ "generationId",
1391
+ ...(Object.hasOwn(event, "contentHash") ? ["contentHash"] : []),
1392
+ ),
1393
+ "session event",
1394
+ );
1395
+ turn();
1396
+ step();
1397
+ eventString(event.effectId, "session event.effectId");
1398
+ eventString(event.packageId, "session event.packageId");
1399
+ eventString(event.generationId, "session event.generationId");
1400
+ if (event.contentHash !== undefined)
1401
+ eventString(event.contentHash, "session event.contentHash");
1402
+ if (
1403
+ event.action !== "install" &&
1404
+ event.action !== "update" &&
1405
+ event.action !== "remove"
1406
+ ) {
1407
+ throw new Error("session event.action is invalid");
1408
+ }
1409
+ break;
1410
+ case "package/undo-intent":
1411
+ requireEventKeys(
1412
+ event,
1413
+ keys(
1414
+ "turn",
1415
+ "step",
1416
+ "effectId",
1417
+ ...(Object.hasOwn(event, "requestedGenerationId")
1418
+ ? ["requestedGenerationId"]
1419
+ : []),
1420
+ ),
1421
+ "session event",
1422
+ );
1423
+ turn();
1424
+ step();
1425
+ eventString(event.effectId, "session event.effectId");
1426
+ if (event.requestedGenerationId !== undefined) {
1427
+ eventString(
1428
+ event.requestedGenerationId,
1429
+ "session event.requestedGenerationId",
1430
+ );
1431
+ }
1432
+ break;
1433
+ case "package/undo-recorded":
1434
+ requireEventKeys(
1435
+ event,
1436
+ keys("turn", "step", "effectId", "generationId", "targetGenerationId"),
1437
+ "session event",
1438
+ );
1439
+ turn();
1440
+ step();
1441
+ eventString(event.effectId, "session event.effectId");
1442
+ eventString(event.generationId, "session event.generationId");
1443
+ eventString(event.targetGenerationId, "session event.targetGenerationId");
1444
+ break;
1216
1445
  case "skill/injected": {
1217
1446
  requireEventKeys(
1218
1447
  event,