@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.
- package/package.json +1 -1
- package/src/authoring.ts +64 -0
- package/src/catalog-change.test.ts +56 -0
- package/src/iframe-ui.test.ts +99 -0
- package/src/iframe-ui.ts +400 -0
- package/src/index.ts +3 -0
- package/src/isolate-context-catalog.generated.ts +25 -0
- package/src/isolate.test.ts +154 -43
- package/src/isolate.ts +689 -101
- package/src/loop-events.test.ts +80 -0
- package/src/loop-events.ts +711 -0
- package/src/send-to-user.ts +7 -56
- package/src/session.test.ts +16 -0
- package/src/tool-execution.ts +16 -0
- package/src/types.ts +229 -0
package/src/send-to-user.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
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
|
}
|
package/src/session.test.ts
CHANGED
|
@@ -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 = [
|
package/src/tool-execution.ts
CHANGED
|
@@ -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,
|