@frockbot/plugin-authoring 0.0.0 → 0.1.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/frockbot.json +18 -0
- package/package.json +31 -6
- package/src/agent.test.ts +162 -0
- package/src/agent.ts +190 -0
- package/src/index.ts +5 -0
- package/src/manifest.ts +3 -0
- package/src/quota.test.ts +213 -0
- package/src/quota.ts +308 -0
- package/src/records.test.ts +90 -0
- package/src/records.ts +172 -0
- package/src/shared.test.ts +138 -0
- package/src/shared.ts +303 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
package/src/shared.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// The Package Authoring DTOs.
|
|
2
|
+
//
|
|
3
|
+
// Modelled on the DeepSeek Harness `cordis_define` / `cordis_run` split
|
|
4
|
+
// (`docs/research/deepseek-harness-extension.md` §2): authoring *defines* a
|
|
5
|
+
// Package — it mints its identity, records its source, and produces an
|
|
6
|
+
// immutable artifact and a pending Composition generation. Activation is a
|
|
7
|
+
// separate event, at the next admitted Turn. A model never overwrites a
|
|
8
|
+
// version; re-authoring the same `packageId` appends the next one.
|
|
9
|
+
import { PACKAGE_BUNDLE_MAX_SOURCE_BYTES } from "@frockbot/kernel-contracts";
|
|
10
|
+
|
|
11
|
+
/** The `package_author` tool input. */
|
|
12
|
+
export interface AuthorPackageInputV1 {
|
|
13
|
+
/** Stable Plugin identity; re-authoring appends a version. */
|
|
14
|
+
packageId: string;
|
|
15
|
+
displayName: string;
|
|
16
|
+
tool: { name: string; description: string; inputSchema: unknown };
|
|
17
|
+
/** TypeScript text; exactly one `package.ts`. */
|
|
18
|
+
source: string;
|
|
19
|
+
/**
|
|
20
|
+
* D6 addendum. The authored Package declares a model Contribution: an
|
|
21
|
+
* adapter that forwards to `CAPABILITIES.invokeModel`. It is a translation
|
|
22
|
+
* layer over a kernel-declared binding, never a network client, and it is
|
|
23
|
+
* callable only where an enabled model Assignment matches.
|
|
24
|
+
*/
|
|
25
|
+
model?: { providerId: string; modelId: string };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type AuthorPackageOutcomeV1 =
|
|
29
|
+
| {
|
|
30
|
+
status: "authored";
|
|
31
|
+
packageId: string;
|
|
32
|
+
version: string;
|
|
33
|
+
contentHash: string;
|
|
34
|
+
generationId: string;
|
|
35
|
+
/** True when a prior version of this Package was superseded. */
|
|
36
|
+
supersededVersion?: string;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
status: "refused";
|
|
40
|
+
reason: string;
|
|
41
|
+
/** The durable failure record the User can inspect. */
|
|
42
|
+
failureId: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const AUTHORED_PACKAGE_ID = /^[a-z][a-z0-9-]{2,63}$/;
|
|
46
|
+
export const AUTHORED_TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
47
|
+
/**
|
|
48
|
+
* The shape of an authored id, not its authority: a Bot may not shadow a
|
|
49
|
+
* first-party or User Package, and that rule is enforced against the Bot's
|
|
50
|
+
* current Composition by the authoring host, which knows each member's
|
|
51
|
+
* provenance.
|
|
52
|
+
*/
|
|
53
|
+
export const AUTHORED_PACKAGE_ID_MAX_LENGTH = 64;
|
|
54
|
+
|
|
55
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
57
|
+
throw new Error(`${label} must be an object`);
|
|
58
|
+
}
|
|
59
|
+
return value as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function exactKeys(
|
|
63
|
+
value: Record<string, unknown>,
|
|
64
|
+
required: readonly string[],
|
|
65
|
+
optional: readonly string[],
|
|
66
|
+
label: string,
|
|
67
|
+
): void {
|
|
68
|
+
const allowed = new Set<string>([...required, ...optional]);
|
|
69
|
+
if (
|
|
70
|
+
!required.every((key) => Object.hasOwn(value, key)) ||
|
|
71
|
+
!Object.keys(value).every((key) => allowed.has(key))
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(`${label} has invalid fields`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function boundedString(value: unknown, label: string, maximum: number): string {
|
|
78
|
+
if (
|
|
79
|
+
typeof value !== "string" ||
|
|
80
|
+
value.length === 0 ||
|
|
81
|
+
value.length > maximum
|
|
82
|
+
) {
|
|
83
|
+
throw new Error(`${label} must be a bounded non-empty string`);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function requireJsonValue(value: unknown, label: string, depth = 0): void {
|
|
89
|
+
if (depth > 16) throw new Error(`${label} is too deeply nested`);
|
|
90
|
+
if (
|
|
91
|
+
value === null ||
|
|
92
|
+
typeof value === "string" ||
|
|
93
|
+
typeof value === "boolean" ||
|
|
94
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
95
|
+
) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
if (value.length > 256) throw new Error(`${label} has too many entries`);
|
|
100
|
+
for (const entry of value) requireJsonValue(entry, label, depth + 1);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const source = record(value, label);
|
|
104
|
+
const entries = Object.entries(source);
|
|
105
|
+
if (entries.length > 256) throw new Error(`${label} has too many fields`);
|
|
106
|
+
for (const [, entry] of entries) requireJsonValue(entry, label, depth + 1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The exact v1 decoder for the `package_author` tool input. */
|
|
110
|
+
export function decodeAuthorPackageInputV1(
|
|
111
|
+
input: unknown,
|
|
112
|
+
label = "package_author input",
|
|
113
|
+
): AuthorPackageInputV1 {
|
|
114
|
+
const value = record(input, label);
|
|
115
|
+
exactKeys(
|
|
116
|
+
value,
|
|
117
|
+
["packageId", "displayName", "tool", "source"],
|
|
118
|
+
["model"],
|
|
119
|
+
label,
|
|
120
|
+
);
|
|
121
|
+
const packageId = boundedString(
|
|
122
|
+
value.packageId,
|
|
123
|
+
`${label}.packageId`,
|
|
124
|
+
AUTHORED_PACKAGE_ID_MAX_LENGTH,
|
|
125
|
+
);
|
|
126
|
+
if (!AUTHORED_PACKAGE_ID.test(packageId)) {
|
|
127
|
+
throw new Error(`${label}.packageId is invalid`);
|
|
128
|
+
}
|
|
129
|
+
const displayName = boundedString(
|
|
130
|
+
value.displayName,
|
|
131
|
+
`${label}.displayName`,
|
|
132
|
+
128,
|
|
133
|
+
);
|
|
134
|
+
const tool = record(value.tool, `${label}.tool`);
|
|
135
|
+
exactKeys(tool, ["name", "description", "inputSchema"], [], `${label}.tool`);
|
|
136
|
+
const name = boundedString(tool.name, `${label}.tool.name`, 64);
|
|
137
|
+
if (!AUTHORED_TOOL_NAME.test(name)) {
|
|
138
|
+
throw new Error(`${label}.tool.name is invalid`);
|
|
139
|
+
}
|
|
140
|
+
const description = boundedString(
|
|
141
|
+
tool.description,
|
|
142
|
+
`${label}.tool.description`,
|
|
143
|
+
1_024,
|
|
144
|
+
);
|
|
145
|
+
const inputSchema = record(tool.inputSchema, `${label}.tool.inputSchema`);
|
|
146
|
+
requireJsonValue(inputSchema, `${label}.tool.inputSchema`);
|
|
147
|
+
const source = boundedString(
|
|
148
|
+
value.source,
|
|
149
|
+
`${label}.source`,
|
|
150
|
+
PACKAGE_BUNDLE_MAX_SOURCE_BYTES,
|
|
151
|
+
);
|
|
152
|
+
if (
|
|
153
|
+
new TextEncoder().encode(source).byteLength >
|
|
154
|
+
PACKAGE_BUNDLE_MAX_SOURCE_BYTES
|
|
155
|
+
) {
|
|
156
|
+
throw new Error(`${label}.source exceeds the per-Package source quota`);
|
|
157
|
+
}
|
|
158
|
+
let model: AuthorPackageInputV1["model"];
|
|
159
|
+
if (value.model !== undefined) {
|
|
160
|
+
const declared = record(value.model, `${label}.model`);
|
|
161
|
+
exactKeys(declared, ["providerId", "modelId"], [], `${label}.model`);
|
|
162
|
+
model = {
|
|
163
|
+
providerId: boundedString(
|
|
164
|
+
declared.providerId,
|
|
165
|
+
`${label}.model.providerId`,
|
|
166
|
+
128,
|
|
167
|
+
),
|
|
168
|
+
modelId: boundedString(declared.modelId, `${label}.model.modelId`, 128),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
packageId,
|
|
173
|
+
displayName,
|
|
174
|
+
tool: { name, description, inputSchema },
|
|
175
|
+
source,
|
|
176
|
+
...(model ? { model } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The JSON Schema the model sees for `package_author`. */
|
|
181
|
+
export const AUTHOR_PACKAGE_INPUT_SCHEMA_V1 = {
|
|
182
|
+
type: "object",
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
required: ["packageId", "displayName", "tool", "source"],
|
|
185
|
+
properties: {
|
|
186
|
+
packageId: {
|
|
187
|
+
type: "string",
|
|
188
|
+
description:
|
|
189
|
+
"Stable lowercase Package identity. Re-authoring it appends a version.",
|
|
190
|
+
},
|
|
191
|
+
displayName: { type: "string" },
|
|
192
|
+
tool: {
|
|
193
|
+
type: "object",
|
|
194
|
+
additionalProperties: false,
|
|
195
|
+
required: ["name", "description", "inputSchema"],
|
|
196
|
+
properties: {
|
|
197
|
+
name: { type: "string" },
|
|
198
|
+
description: { type: "string" },
|
|
199
|
+
inputSchema: { type: "object" },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
source: {
|
|
203
|
+
type: "string",
|
|
204
|
+
description:
|
|
205
|
+
"TypeScript for one package.ts that exports `tools` and `execute(tool, input, ctx)`. No imports: the isolate has no network and no npm. `ctx.invokeModel(request)` is the only model path.",
|
|
206
|
+
},
|
|
207
|
+
model: {
|
|
208
|
+
type: "object",
|
|
209
|
+
additionalProperties: false,
|
|
210
|
+
required: ["providerId", "modelId"],
|
|
211
|
+
description:
|
|
212
|
+
"Declare a model Contribution that forwards to the kernel model binding.",
|
|
213
|
+
properties: {
|
|
214
|
+
providerId: { type: "string" },
|
|
215
|
+
modelId: { type: "string" },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
} as const;
|
|
220
|
+
|
|
221
|
+
export async function sha256HexV1(value: string): Promise<string> {
|
|
222
|
+
const digest = await crypto.subtle.digest(
|
|
223
|
+
"SHA-256",
|
|
224
|
+
new TextEncoder().encode(value),
|
|
225
|
+
);
|
|
226
|
+
return [...new Uint8Array(digest)]
|
|
227
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
228
|
+
.join("");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The idempotency key for one authoring effect. Deterministic in the admitted
|
|
233
|
+
* run and the exact source, so a resumed Turn that re-executes the same tool
|
|
234
|
+
* call lands on the same effect instead of bundling a second time.
|
|
235
|
+
*/
|
|
236
|
+
export async function authoringEffectIdV1(input: {
|
|
237
|
+
runId: string;
|
|
238
|
+
packageId: string;
|
|
239
|
+
sourceHash: string;
|
|
240
|
+
}): Promise<string> {
|
|
241
|
+
const digest = await sha256HexV1(
|
|
242
|
+
JSON.stringify([input.runId, input.packageId, input.sourceHash]),
|
|
243
|
+
);
|
|
244
|
+
return `author-${digest.slice(0, 32)}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** `0.0.1`, `0.0.2`, … — a version is appended, never overwritten. */
|
|
248
|
+
export function authoredVersionV1(ordinal: number): string {
|
|
249
|
+
if (!Number.isSafeInteger(ordinal) || ordinal < 1) {
|
|
250
|
+
throw new Error("authored version ordinal must be a positive integer");
|
|
251
|
+
}
|
|
252
|
+
return `0.0.${ordinal}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The specifier a Bot-authored Package is recorded under. */
|
|
256
|
+
export function authoredSpecifierV1(packageId: string): string {
|
|
257
|
+
return `bot-authored:${packageId}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The manifest an authored Package is content-addressed by. It is synthesized
|
|
262
|
+
* rather than authored so a Bot cannot declare a Contribution host the kernel
|
|
263
|
+
* did not offer it: exactly one Bot isolate runtime Contribution, plus the
|
|
264
|
+
* declared model binding when the Package asked for one.
|
|
265
|
+
*/
|
|
266
|
+
export function authoredManifestV1(input: {
|
|
267
|
+
packageId: string;
|
|
268
|
+
displayName: string;
|
|
269
|
+
version: string;
|
|
270
|
+
tool: AuthorPackageInputV1["tool"];
|
|
271
|
+
model?: AuthorPackageInputV1["model"];
|
|
272
|
+
}): Record<string, unknown> {
|
|
273
|
+
return {
|
|
274
|
+
schemaVersion: 3,
|
|
275
|
+
id: input.packageId,
|
|
276
|
+
displayName: input.displayName,
|
|
277
|
+
version: input.version,
|
|
278
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
279
|
+
dependencies: {},
|
|
280
|
+
contributions: {
|
|
281
|
+
runtime: { entry: "./package.js", host: "bot-isolate" },
|
|
282
|
+
...(input.model
|
|
283
|
+
? {
|
|
284
|
+
model: {
|
|
285
|
+
entry: "./package.js",
|
|
286
|
+
host: "bot-isolate",
|
|
287
|
+
binding: "capabilities.invokeModel",
|
|
288
|
+
providerId: input.model.providerId,
|
|
289
|
+
modelId: input.model.modelId,
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
: {}),
|
|
293
|
+
},
|
|
294
|
+
tools: [
|
|
295
|
+
{
|
|
296
|
+
name: input.tool.name,
|
|
297
|
+
description: input.tool.description,
|
|
298
|
+
inputSchema: input.tool.inputSchema,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
permissions: [],
|
|
302
|
+
};
|
|
303
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"lib": ["ES2023", "DOM"],
|
|
12
|
+
"types": ["bun"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts"]
|
|
15
|
+
}
|
package/README.md
DELETED