@melra/protocol 0.3.0-alpha.10
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/LICENSE +202 -0
- package/README.md +42 -0
- package/dist/index.d.ts +8138 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +757 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +109 -0
- package/dist/index.test.js.map +1 -0
- package/dist/workflow.test.d.ts +2 -0
- package/dist/workflow.test.d.ts.map +1 -0
- package/dist/workflow.test.js +125 -0
- package/dist/workflow.test.js.map +1 -0
- package/package.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
// Copyright 2026 XAGI Labs Private Limited
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const PROTOCOL_VERSION = "2025-11-25";
|
|
5
|
+
export const PRODUCT_VERSION = "0.3.0-alpha.10";
|
|
6
|
+
const boundedPath = z.string().min(1).max(4096);
|
|
7
|
+
const boundedText = z.string().max(200_000);
|
|
8
|
+
export const FileOperationSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
kind: z.literal("file"),
|
|
11
|
+
action: z.enum([
|
|
12
|
+
"list",
|
|
13
|
+
"read",
|
|
14
|
+
"stat",
|
|
15
|
+
"hash",
|
|
16
|
+
"write",
|
|
17
|
+
"move",
|
|
18
|
+
"delete",
|
|
19
|
+
"mkdir",
|
|
20
|
+
]),
|
|
21
|
+
path: boundedPath,
|
|
22
|
+
destination: boundedPath.optional(),
|
|
23
|
+
content: boundedText.optional(),
|
|
24
|
+
encoding: z.enum(["utf8", "base64"]).default("utf8"),
|
|
25
|
+
recursive: z.boolean().default(false),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
export const TerminalOperationSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
kind: z.literal("terminal"),
|
|
31
|
+
action: z.enum(["run", "start", "status", "output", "stop", "send"]),
|
|
32
|
+
command: z.string().min(1).max(512).optional(),
|
|
33
|
+
args: z.array(z.string().max(4096)).max(100).default([]),
|
|
34
|
+
jobId: z.string().uuid().optional(),
|
|
35
|
+
cwd: boundedPath.optional(),
|
|
36
|
+
env: z.record(z.string(), z.string().max(8192)).optional(),
|
|
37
|
+
/**
|
|
38
|
+
* Hold `start`'s stdin open so `send` can answer a prompt. Off by default
|
|
39
|
+
* because it changes what the program sees: with stdin ignored a read gets
|
|
40
|
+
* EOF at once, while an open pipe makes the same program wait for input
|
|
41
|
+
* that may never come.
|
|
42
|
+
*/
|
|
43
|
+
interactive: z.boolean().default(false),
|
|
44
|
+
/** What `send` writes to the job's stdin. */
|
|
45
|
+
input: z.string().max(8_192).optional(),
|
|
46
|
+
/**
|
|
47
|
+
* Append a newline to `input`. A prompt waits for the line to end, so the
|
|
48
|
+
* common case is the default; turn it off to send a bare keystroke.
|
|
49
|
+
*/
|
|
50
|
+
appendNewline: z.boolean().default(true),
|
|
51
|
+
timeoutMs: z.number().int().min(100).max(120_000).default(30_000),
|
|
52
|
+
maxOutputChars: z.number().int().min(1_000).max(1_000_000).default(100_000),
|
|
53
|
+
})
|
|
54
|
+
.strict();
|
|
55
|
+
export const BrowserTargetSchema = z
|
|
56
|
+
.object({
|
|
57
|
+
selector: z.string().min(1).max(2_000).optional(),
|
|
58
|
+
role: z.string().min(1).max(100).optional(),
|
|
59
|
+
name: z.string().max(500).optional(),
|
|
60
|
+
text: z.string().max(2_000).optional(),
|
|
61
|
+
})
|
|
62
|
+
.strict();
|
|
63
|
+
export const BrowserOperationSchema = z
|
|
64
|
+
.object({
|
|
65
|
+
kind: z.literal("browser"),
|
|
66
|
+
action: z.enum([
|
|
67
|
+
"navigate",
|
|
68
|
+
"back",
|
|
69
|
+
"forward",
|
|
70
|
+
"reload",
|
|
71
|
+
"inspect",
|
|
72
|
+
"wait",
|
|
73
|
+
"click",
|
|
74
|
+
"type",
|
|
75
|
+
"fill_form",
|
|
76
|
+
"select",
|
|
77
|
+
"press",
|
|
78
|
+
"scroll",
|
|
79
|
+
"screenshot",
|
|
80
|
+
"upload",
|
|
81
|
+
"download",
|
|
82
|
+
"tabs",
|
|
83
|
+
"tab_new",
|
|
84
|
+
"tab_switch",
|
|
85
|
+
"close",
|
|
86
|
+
]),
|
|
87
|
+
url: z.string().url().max(8_192).optional(),
|
|
88
|
+
target: BrowserTargetSchema.optional(),
|
|
89
|
+
value: z.string().max(100_000).optional(),
|
|
90
|
+
values: z.array(z.string().max(2_000)).max(50).optional(),
|
|
91
|
+
// One task, one approval: a login form is two fields and a submit, not
|
|
92
|
+
// three separate governed mutations.
|
|
93
|
+
fields: z
|
|
94
|
+
.array(z
|
|
95
|
+
.object({
|
|
96
|
+
target: BrowserTargetSchema,
|
|
97
|
+
value: z.string().max(100_000),
|
|
98
|
+
})
|
|
99
|
+
.strict())
|
|
100
|
+
.min(1)
|
|
101
|
+
.max(50)
|
|
102
|
+
.optional(),
|
|
103
|
+
filePaths: z.array(boundedPath).min(1).max(20).optional(),
|
|
104
|
+
key: z.string().max(100).optional(),
|
|
105
|
+
direction: z.enum(["up", "down", "top", "bottom", "into_view"]).optional(),
|
|
106
|
+
// `wait` conditions. A URL is matched by substring rather than by equality
|
|
107
|
+
// because the interesting part of a post-login redirect is the path, not
|
|
108
|
+
// the session token stapled to it.
|
|
109
|
+
state: z.enum(["visible", "hidden", "attached", "detached"]).optional(),
|
|
110
|
+
urlContains: z.string().min(1).max(2_000).optional(),
|
|
111
|
+
tabIndex: z.number().int().min(0).max(100).optional(),
|
|
112
|
+
// Per-keystroke delay. Some widgets debounce, and some deliberately ignore
|
|
113
|
+
// input that arrives faster than a human could produce it.
|
|
114
|
+
delayMs: z.number().int().min(0).max(500).default(0),
|
|
115
|
+
clearFirst: z.boolean().default(true),
|
|
116
|
+
pixels: z.number().int().min(1).max(100_000).default(600),
|
|
117
|
+
fullPage: z.boolean().default(false),
|
|
118
|
+
timeoutMs: z.number().int().min(100).max(120_000).default(30_000),
|
|
119
|
+
settleQuietMs: z.number().int().min(25).max(2_000).default(180),
|
|
120
|
+
settleTimeoutMs: z.number().int().min(25).max(10_000).default(1_500),
|
|
121
|
+
maxChars: z.number().int().min(100).max(200_000).default(20_000),
|
|
122
|
+
})
|
|
123
|
+
.strict();
|
|
124
|
+
export const MemoryScopeSchema = z.enum([
|
|
125
|
+
"session",
|
|
126
|
+
"task",
|
|
127
|
+
"project",
|
|
128
|
+
"workspace",
|
|
129
|
+
"user",
|
|
130
|
+
"procedural",
|
|
131
|
+
]);
|
|
132
|
+
export const MemoryOperationSchema = z
|
|
133
|
+
.object({
|
|
134
|
+
kind: z.literal("memory"),
|
|
135
|
+
action: z.enum(["put", "search", "list", "delete", "clear"]),
|
|
136
|
+
id: z.string().uuid().optional(),
|
|
137
|
+
scope: MemoryScopeSchema.default("workspace"),
|
|
138
|
+
key: z.string().min(1).max(512).optional(),
|
|
139
|
+
value: boundedText.optional(),
|
|
140
|
+
query: z.string().max(10_000).optional(),
|
|
141
|
+
source: z.string().max(4_096).optional(),
|
|
142
|
+
confidence: z.number().min(0).max(1).default(1),
|
|
143
|
+
tags: z.array(z.string().min(1).max(128)).max(50).default([]),
|
|
144
|
+
speaker: z.string().min(1).max(256).optional(),
|
|
145
|
+
episodeId: z.string().min(1).max(256).optional(),
|
|
146
|
+
sequence: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
147
|
+
expiresAt: z.string().datetime().optional(),
|
|
148
|
+
supersedesId: z.string().uuid().optional(),
|
|
149
|
+
includeSuperseded: z.boolean().default(false),
|
|
150
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
151
|
+
})
|
|
152
|
+
.strict();
|
|
153
|
+
export const ComputerOperationSchema = z
|
|
154
|
+
.object({
|
|
155
|
+
kind: z.literal("computer"),
|
|
156
|
+
action: z.enum([
|
|
157
|
+
"capabilities",
|
|
158
|
+
"inspect",
|
|
159
|
+
"screenshot",
|
|
160
|
+
"click",
|
|
161
|
+
"move",
|
|
162
|
+
"drag",
|
|
163
|
+
"type",
|
|
164
|
+
"key",
|
|
165
|
+
"scroll",
|
|
166
|
+
]),
|
|
167
|
+
coordinateSpace: z.enum(["normalized", "pixel"]).default("normalized"),
|
|
168
|
+
x: z.number().min(0).max(100_000).optional(),
|
|
169
|
+
y: z.number().min(0).max(100_000).optional(),
|
|
170
|
+
// `drag` only: where the press is released. Kept as separate fields rather
|
|
171
|
+
// than a nested point so `x`/`y` keep meaning "where the action starts" for
|
|
172
|
+
// every action that has a position.
|
|
173
|
+
toX: z.number().min(0).max(100_000).optional(),
|
|
174
|
+
toY: z.number().min(0).max(100_000).optional(),
|
|
175
|
+
text: z.string().max(100_000).optional(),
|
|
176
|
+
key: z
|
|
177
|
+
.enum([
|
|
178
|
+
"ENTER",
|
|
179
|
+
"TAB",
|
|
180
|
+
"SPACE",
|
|
181
|
+
"BACKSPACE",
|
|
182
|
+
"ESCAPE",
|
|
183
|
+
"LEFT",
|
|
184
|
+
"RIGHT",
|
|
185
|
+
"DOWN",
|
|
186
|
+
"UP",
|
|
187
|
+
"HOME",
|
|
188
|
+
"END",
|
|
189
|
+
"PAGEUP",
|
|
190
|
+
"PAGEDOWN",
|
|
191
|
+
"DELETE",
|
|
192
|
+
])
|
|
193
|
+
.optional(),
|
|
194
|
+
deltaY: z.number().int().min(-2_000).max(2_000).optional(),
|
|
195
|
+
/**
|
|
196
|
+
* Where to act, named rather than measured. `inspect` reports the elements
|
|
197
|
+
* the platform can see with the roles and names that address them, and a
|
|
198
|
+
* caller naming one here has the runtime resolve it to a point instead of
|
|
199
|
+
* reading pixels off a screenshot. Coordinates still work unchanged;
|
|
200
|
+
* supplying both is rejected rather than silently resolved one way, because
|
|
201
|
+
* the two can disagree and the caller is the only one who knows which it
|
|
202
|
+
* meant.
|
|
203
|
+
*/
|
|
204
|
+
target: z
|
|
205
|
+
.object({
|
|
206
|
+
role: z.string().min(1).max(100).optional(),
|
|
207
|
+
name: z.string().min(1).max(500).optional(),
|
|
208
|
+
})
|
|
209
|
+
.strict()
|
|
210
|
+
.optional(),
|
|
211
|
+
// Capped at 120s like every other operation kind rather than 30s. A
|
|
212
|
+
// computer action spawns a whole interpreter — `powershell.exe` plus the
|
|
213
|
+
// .NET assemblies it loads, `osascript`, `xdotool` — and the first such
|
|
214
|
+
// spawn after boot exceeded the old 30s ceiling on a cold Windows machine,
|
|
215
|
+
// which made the maximum itself unreachable there. The default stays 10s
|
|
216
|
+
// because a warm call is fast; a caller who knows it is cold can now ask
|
|
217
|
+
// for more instead of being denied by the schema.
|
|
218
|
+
timeoutMs: z.number().int().min(100).max(120_000).default(10_000),
|
|
219
|
+
})
|
|
220
|
+
.strict();
|
|
221
|
+
export const SystemOperationSchema = z
|
|
222
|
+
.object({
|
|
223
|
+
kind: z.literal("system"),
|
|
224
|
+
action: z.literal("info"),
|
|
225
|
+
})
|
|
226
|
+
.strict();
|
|
227
|
+
export const OperationSchema = z.discriminatedUnion("kind", [
|
|
228
|
+
FileOperationSchema,
|
|
229
|
+
TerminalOperationSchema,
|
|
230
|
+
BrowserOperationSchema,
|
|
231
|
+
MemoryOperationSchema,
|
|
232
|
+
ComputerOperationSchema,
|
|
233
|
+
SystemOperationSchema,
|
|
234
|
+
]);
|
|
235
|
+
export const EvidencePredicateSchema = z.discriminatedUnion("type", [
|
|
236
|
+
z
|
|
237
|
+
.object({
|
|
238
|
+
type: z.literal("result_equals"),
|
|
239
|
+
path: z.string().min(1).max(512),
|
|
240
|
+
value: z.union([z.string(), z.number(), z.boolean(), z.null()]),
|
|
241
|
+
})
|
|
242
|
+
.strict(),
|
|
243
|
+
z
|
|
244
|
+
.object({
|
|
245
|
+
type: z.literal("result_contains"),
|
|
246
|
+
path: z.string().min(1).max(512),
|
|
247
|
+
value: z.string().max(10_000),
|
|
248
|
+
})
|
|
249
|
+
.strict(),
|
|
250
|
+
z
|
|
251
|
+
.object({
|
|
252
|
+
type: z.literal("file_exists"),
|
|
253
|
+
path: boundedPath,
|
|
254
|
+
})
|
|
255
|
+
.strict(),
|
|
256
|
+
z
|
|
257
|
+
.object({
|
|
258
|
+
type: z.literal("file_absent"),
|
|
259
|
+
path: boundedPath,
|
|
260
|
+
})
|
|
261
|
+
.strict(),
|
|
262
|
+
z
|
|
263
|
+
.object({
|
|
264
|
+
type: z.literal("file_hash"),
|
|
265
|
+
path: boundedPath,
|
|
266
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
267
|
+
})
|
|
268
|
+
.strict(),
|
|
269
|
+
z
|
|
270
|
+
.object({
|
|
271
|
+
type: z.literal("exit_code"),
|
|
272
|
+
value: z.number().int(),
|
|
273
|
+
})
|
|
274
|
+
.strict(),
|
|
275
|
+
z
|
|
276
|
+
.object({
|
|
277
|
+
type: z.literal("url_matches"),
|
|
278
|
+
pattern: z.string().min(1).max(2_000),
|
|
279
|
+
})
|
|
280
|
+
.strict(),
|
|
281
|
+
z
|
|
282
|
+
.object({
|
|
283
|
+
type: z.literal("page_contains"),
|
|
284
|
+
text: z.string().min(1).max(10_000),
|
|
285
|
+
})
|
|
286
|
+
.strict(),
|
|
287
|
+
]);
|
|
288
|
+
export const PrincipalKindSchema = z.enum([
|
|
289
|
+
"organization",
|
|
290
|
+
"human",
|
|
291
|
+
"harness",
|
|
292
|
+
"agent",
|
|
293
|
+
"session",
|
|
294
|
+
"subagent",
|
|
295
|
+
"service",
|
|
296
|
+
]);
|
|
297
|
+
export const PrincipalSchema = z
|
|
298
|
+
.object({
|
|
299
|
+
kind: PrincipalKindSchema,
|
|
300
|
+
id: z.string().min(1).max(200),
|
|
301
|
+
})
|
|
302
|
+
.strict();
|
|
303
|
+
/**
|
|
304
|
+
* Who is asking, and on whose behalf.
|
|
305
|
+
*
|
|
306
|
+
* `principal` is the immediate caller — the agent or session that dispatched
|
|
307
|
+
* this effect. `onBehalfOf` is the delegation chain behind it, outermost first
|
|
308
|
+
* (organization → human → harness → parent agent), so a receipt answers "who
|
|
309
|
+
* authorised this" rather than only "what ran". MELRA does not authenticate any
|
|
310
|
+
* of it; a link is a claim the layer above makes, and it is worth exactly as
|
|
311
|
+
* much as that layer's own boundary.
|
|
312
|
+
*/
|
|
313
|
+
export const IdentitySchema = z
|
|
314
|
+
.object({
|
|
315
|
+
principal: PrincipalSchema,
|
|
316
|
+
onBehalfOf: z.array(PrincipalSchema).max(7).default([]),
|
|
317
|
+
})
|
|
318
|
+
.strict();
|
|
319
|
+
/** The one principal assumed when a caller declares none. */
|
|
320
|
+
export const LOCAL_IDENTITY = {
|
|
321
|
+
principal: { kind: "agent", id: "local" },
|
|
322
|
+
onBehalfOf: [],
|
|
323
|
+
};
|
|
324
|
+
export function principalRef(principal) {
|
|
325
|
+
return `${principal.kind}:${principal.id}`;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Records a caller the transport authenticated above whatever the request
|
|
329
|
+
* declared.
|
|
330
|
+
*
|
|
331
|
+
* The authenticated link sits at the outermost end, because anything the
|
|
332
|
+
* client names — a session, a subagent — is inside it and never above it. A
|
|
333
|
+
* request that declared nothing is simply that client acting for itself. This
|
|
334
|
+
* is the one link in a chain that is not a claim; the rest still are, so a
|
|
335
|
+
* capability grant matching the immediate principal is a narrowing control
|
|
336
|
+
* rather than an authentication boundary.
|
|
337
|
+
*/
|
|
338
|
+
export function attributeTo(principal, identity) {
|
|
339
|
+
if (identity === undefined)
|
|
340
|
+
return { principal, onBehalfOf: [] };
|
|
341
|
+
const onBehalfOf = [principal, ...identity.onBehalfOf];
|
|
342
|
+
if (onBehalfOf.length > 7)
|
|
343
|
+
throw new Error("identity_chain_too_deep");
|
|
344
|
+
return { principal: identity.principal, onBehalfOf };
|
|
345
|
+
}
|
|
346
|
+
/** The delegation chain as one line, outermost first. */
|
|
347
|
+
export function delegationChain(identity) {
|
|
348
|
+
return [...identity.onBehalfOf, identity.principal]
|
|
349
|
+
.map(principalRef)
|
|
350
|
+
.join("/");
|
|
351
|
+
}
|
|
352
|
+
export const TaskBudgetSchema = z
|
|
353
|
+
.object({
|
|
354
|
+
maxSteps: z.number().int().min(1).max(100).default(10),
|
|
355
|
+
maxDurationMs: z.number().int().min(100).max(900_000).default(120_000),
|
|
356
|
+
maxRetries: z.number().int().min(0).max(10).default(2),
|
|
357
|
+
})
|
|
358
|
+
.strict();
|
|
359
|
+
export const TaskRequestSchema = z
|
|
360
|
+
.object({
|
|
361
|
+
goal: z
|
|
362
|
+
.string()
|
|
363
|
+
.min(1)
|
|
364
|
+
.max(10_000)
|
|
365
|
+
.describe("Why this task is being run, in one human-readable sentence."),
|
|
366
|
+
operation: OperationSchema,
|
|
367
|
+
// These three carry the rules callers get wrong, and a description is the
|
|
368
|
+
// only channel that reaches a model reading the tool's JSON Schema.
|
|
369
|
+
constraints: z
|
|
370
|
+
.array(z.string().max(2_000))
|
|
371
|
+
.max(50)
|
|
372
|
+
.default([])
|
|
373
|
+
.describe("Leave empty. Freeform prose cannot be enforced, so a non-empty array is denied with freeform_constraints_not_enforceable. Express real limits as forbiddenEffects, budget, or requiredEvidence."),
|
|
374
|
+
forbiddenEffects: z
|
|
375
|
+
.array(z.enum(["read", "mutate", "destructive"]))
|
|
376
|
+
.max(3)
|
|
377
|
+
.default([])
|
|
378
|
+
.describe("Effects this task must never take. Denied before the adapter runs, and honoured even in unhinged mode."),
|
|
379
|
+
budget: TaskBudgetSchema.default({
|
|
380
|
+
maxSteps: 10,
|
|
381
|
+
maxDurationMs: 120_000,
|
|
382
|
+
maxRetries: 2,
|
|
383
|
+
}),
|
|
384
|
+
requiredEvidence: z
|
|
385
|
+
.array(EvidencePredicateSchema)
|
|
386
|
+
.max(20)
|
|
387
|
+
.default([])
|
|
388
|
+
.describe("Predicates that must hold after execution for the task to be verified_success. Required for any non-read operation: a mutation with none is denied with mutation_requires_evidence. An adapter that succeeded while a predicate failed is partial, never success."),
|
|
389
|
+
identity: IdentitySchema.optional().describe("Who is asking, and on whose behalf. Recorded on the task and every receipt, and matched against policy.capabilities when the operator has issued grants. Omitted means the implicit local principal."),
|
|
390
|
+
})
|
|
391
|
+
.strict();
|
|
392
|
+
export const EffectSchema = z.enum(["read", "mutate", "destructive"]);
|
|
393
|
+
export const RiskSchema = z.enum(["low", "medium", "high", "critical"]);
|
|
394
|
+
export const PolicyOutcomeSchema = z.enum(["allow", "deny", "confirm"]);
|
|
395
|
+
/**
|
|
396
|
+
* What an operation reaches for, as opposed to what it is named. `npm ls` and
|
|
397
|
+
* `npm install` are the same allowlisted command; only the second one installs
|
|
398
|
+
* a package and talks to a registry. Traits are what `policy.deniedTraits`
|
|
399
|
+
* matches on and what a caller sees before it echoes an approval phrase.
|
|
400
|
+
*/
|
|
401
|
+
export const CapabilityTraitSchema = z.enum(["package-install", "network"]);
|
|
402
|
+
export const PolicyDecisionSchema = z
|
|
403
|
+
.object({
|
|
404
|
+
outcome: PolicyOutcomeSchema,
|
|
405
|
+
effect: EffectSchema,
|
|
406
|
+
risk: RiskSchema,
|
|
407
|
+
reason: z.string(),
|
|
408
|
+
policyVersion: z.string(),
|
|
409
|
+
traits: z
|
|
410
|
+
.array(CapabilityTraitSchema)
|
|
411
|
+
.max(8)
|
|
412
|
+
.default([])
|
|
413
|
+
.describe("What the operation reaches for beyond the local workspace. Denied by policy.deniedTraits; shown so an approver knows a command installs packages or contacts another host before approving it."),
|
|
414
|
+
})
|
|
415
|
+
.strict();
|
|
416
|
+
export const ApprovalChallengeSchema = z
|
|
417
|
+
.object({
|
|
418
|
+
approvalId: z.string().uuid(),
|
|
419
|
+
taskId: z.string().uuid(),
|
|
420
|
+
actionDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
421
|
+
phrase: z.string(),
|
|
422
|
+
expiresAt: z.string().datetime(),
|
|
423
|
+
})
|
|
424
|
+
.strict();
|
|
425
|
+
export const ApprovalResponseSchema = z
|
|
426
|
+
.object({
|
|
427
|
+
approvalId: z.string().uuid(),
|
|
428
|
+
phrase: z.string().min(1).max(256),
|
|
429
|
+
})
|
|
430
|
+
.strict();
|
|
431
|
+
/**
|
|
432
|
+
* Bounded authority to take one kind of effect, issued to a principal by the
|
|
433
|
+
* operator rather than derived from a credential.
|
|
434
|
+
*
|
|
435
|
+
* `capability` and `target` are matched against what `classifyOperation`
|
|
436
|
+
* reports, with `*` standing for any run of characters, so `file.*` over
|
|
437
|
+
* `/repo/build/*` grants file work inside one directory and nothing else. An
|
|
438
|
+
* empty grant list means no capability narrowing is configured — the rest of
|
|
439
|
+
* policy still applies. A non-empty list is a closed world: an effect with no
|
|
440
|
+
* matching grant is denied before any allowlist is consulted.
|
|
441
|
+
*/
|
|
442
|
+
export const CapabilityGrantSchema = z
|
|
443
|
+
.object({
|
|
444
|
+
id: z.string().min(1).max(200),
|
|
445
|
+
capability: z.string().min(1).max(200),
|
|
446
|
+
effects: z.array(EffectSchema).min(1).max(3),
|
|
447
|
+
target: z.string().min(1).max(4096).default("*"),
|
|
448
|
+
/** `kind:id` of the holder, matched against the immediate principal. */
|
|
449
|
+
principal: z.string().min(1).max(200).default("*"),
|
|
450
|
+
validUntil: z.string().datetime().optional(),
|
|
451
|
+
/**
|
|
452
|
+
* Policy version this grant was issued against. A grant that names a
|
|
453
|
+
* version the running policy no longer has is refused rather than silently
|
|
454
|
+
* reinterpreted under different rules.
|
|
455
|
+
*/
|
|
456
|
+
policyVersion: z.string().max(64).optional(),
|
|
457
|
+
})
|
|
458
|
+
.strict();
|
|
459
|
+
export const WorkflowNodeIdSchema = z
|
|
460
|
+
.string()
|
|
461
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/);
|
|
462
|
+
const NodeBaseSchema = z.object({
|
|
463
|
+
id: WorkflowNodeIdSchema,
|
|
464
|
+
dependsOn: z.array(WorkflowNodeIdSchema).max(100).default([]),
|
|
465
|
+
});
|
|
466
|
+
export const OperationNodeSchema = NodeBaseSchema.extend({
|
|
467
|
+
type: z.literal("operation"),
|
|
468
|
+
request: TaskRequestSchema,
|
|
469
|
+
}).strict();
|
|
470
|
+
export const ApprovalNodeSchema = NodeBaseSchema.extend({
|
|
471
|
+
type: z.literal("approval"),
|
|
472
|
+
forNodeId: WorkflowNodeIdSchema,
|
|
473
|
+
}).strict();
|
|
474
|
+
export const ConditionNodeSchema = NodeBaseSchema.extend({
|
|
475
|
+
type: z.literal("condition"),
|
|
476
|
+
sourceNodeId: WorkflowNodeIdSchema,
|
|
477
|
+
predicate: EvidencePredicateSchema,
|
|
478
|
+
whenTrue: z.array(TaskRequestSchema).max(50).default([]),
|
|
479
|
+
whenFalse: z.array(TaskRequestSchema).max(50).default([]),
|
|
480
|
+
}).strict();
|
|
481
|
+
export const ParallelNodeSchema = NodeBaseSchema.extend({
|
|
482
|
+
type: z.literal("parallel"),
|
|
483
|
+
branches: z
|
|
484
|
+
.array(z.array(TaskRequestSchema).min(1).max(50))
|
|
485
|
+
.min(2)
|
|
486
|
+
.max(20),
|
|
487
|
+
}).strict();
|
|
488
|
+
export const BoundedLoopNodeSchema = NodeBaseSchema.extend({
|
|
489
|
+
type: z.literal("bounded_loop"),
|
|
490
|
+
body: z.array(TaskRequestSchema).min(1).max(50),
|
|
491
|
+
maxIterations: z.number().int().min(1).max(100),
|
|
492
|
+
until: EvidencePredicateSchema.optional(),
|
|
493
|
+
}).strict();
|
|
494
|
+
export const CheckpointNodeSchema = NodeBaseSchema.extend({
|
|
495
|
+
type: z.literal("checkpoint"),
|
|
496
|
+
}).strict();
|
|
497
|
+
export const CompensationNodeSchema = NodeBaseSchema.extend({
|
|
498
|
+
type: z.literal("compensation"),
|
|
499
|
+
forNodeId: WorkflowNodeIdSchema,
|
|
500
|
+
request: TaskRequestSchema,
|
|
501
|
+
}).strict();
|
|
502
|
+
// A workflow that needs a person to answer something stops here. The answer is
|
|
503
|
+
// supplied to `advance`, recorded on the node, and readable by later nodes —
|
|
504
|
+
// it is data the graph waited for, not an approval to run a pending mutation.
|
|
505
|
+
export const HumanInputNodeSchema = NodeBaseSchema.extend({
|
|
506
|
+
type: z.literal("human_input"),
|
|
507
|
+
prompt: z.string().min(1).max(2_000),
|
|
508
|
+
// Free text is unconstrained by design; `choices` narrows it to a fixed set
|
|
509
|
+
// so a workflow can branch on an answer it can actually enumerate.
|
|
510
|
+
choices: z.array(z.string().min(1).max(200)).max(20).default([]),
|
|
511
|
+
maxLength: z.number().int().min(1).max(10_000).default(1_000),
|
|
512
|
+
}).strict();
|
|
513
|
+
// Delegation hands a named sub-goal to an outside worker (another agent, a
|
|
514
|
+
// person, a queue). MELRA does not execute it — it records the handoff and
|
|
515
|
+
// waits for a result, so the run stays governed and receipted either way.
|
|
516
|
+
export const DelegationNodeSchema = NodeBaseSchema.extend({
|
|
517
|
+
type: z.literal("delegation"),
|
|
518
|
+
assignee: z.string().min(1).max(200),
|
|
519
|
+
goal: z.string().min(1).max(2_000),
|
|
520
|
+
requiredEvidence: z.array(EvidencePredicateSchema).max(20).default([]),
|
|
521
|
+
}).strict();
|
|
522
|
+
export const WorkflowNodeSchema = z.discriminatedUnion("type", [
|
|
523
|
+
OperationNodeSchema,
|
|
524
|
+
ApprovalNodeSchema,
|
|
525
|
+
ConditionNodeSchema,
|
|
526
|
+
ParallelNodeSchema,
|
|
527
|
+
BoundedLoopNodeSchema,
|
|
528
|
+
CheckpointNodeSchema,
|
|
529
|
+
CompensationNodeSchema,
|
|
530
|
+
HumanInputNodeSchema,
|
|
531
|
+
DelegationNodeSchema,
|
|
532
|
+
]);
|
|
533
|
+
// What an operator supplies to `advance` to satisfy a waiting human_input or
|
|
534
|
+
// delegation node.
|
|
535
|
+
export const WorkflowInputSchema = z
|
|
536
|
+
.object({
|
|
537
|
+
nodeId: WorkflowNodeIdSchema,
|
|
538
|
+
value: z.string().min(1).max(10_000),
|
|
539
|
+
})
|
|
540
|
+
.strict();
|
|
541
|
+
export const WorkflowDefinitionSchema = z
|
|
542
|
+
.object({
|
|
543
|
+
schemaVersion: z.literal("1.0.0"),
|
|
544
|
+
id: z.string().uuid(),
|
|
545
|
+
version: z.number().int().positive(),
|
|
546
|
+
name: z.string().min(1).max(200),
|
|
547
|
+
description: z.string().max(2_000).optional(),
|
|
548
|
+
nodes: z.array(WorkflowNodeSchema).min(1).max(500),
|
|
549
|
+
budget: TaskBudgetSchema.optional(),
|
|
550
|
+
})
|
|
551
|
+
.strict();
|
|
552
|
+
export const WorkflowStatusSchema = z.enum([
|
|
553
|
+
"draft",
|
|
554
|
+
"planned",
|
|
555
|
+
"awaiting_approval",
|
|
556
|
+
// Blocked on a person supplying input. Distinct from `paused`/`suspended`,
|
|
557
|
+
// which are operator halts: this one clears by answering, not by resuming.
|
|
558
|
+
"awaiting_input",
|
|
559
|
+
"running",
|
|
560
|
+
"paused",
|
|
561
|
+
"suspended",
|
|
562
|
+
"partially_complete",
|
|
563
|
+
"verified_complete",
|
|
564
|
+
"recovery_required",
|
|
565
|
+
"failed",
|
|
566
|
+
"cancelled",
|
|
567
|
+
]);
|
|
568
|
+
export const WorkflowNodeStatusSchema = z.enum([
|
|
569
|
+
"pending",
|
|
570
|
+
"ready",
|
|
571
|
+
"awaiting_approval",
|
|
572
|
+
"awaiting_input",
|
|
573
|
+
"running",
|
|
574
|
+
"verifying",
|
|
575
|
+
"verified_complete",
|
|
576
|
+
"skipped",
|
|
577
|
+
"recovery_required",
|
|
578
|
+
"failed",
|
|
579
|
+
"cancelled",
|
|
580
|
+
"compensated",
|
|
581
|
+
]);
|
|
582
|
+
export const EncryptedPayloadSchema = z
|
|
583
|
+
.object({
|
|
584
|
+
version: z.literal(1),
|
|
585
|
+
algorithm: z.literal("aes-256-gcm"),
|
|
586
|
+
iv: z.string().regex(/^[A-Za-z0-9_-]+$/),
|
|
587
|
+
ciphertext: z.string().regex(/^[A-Za-z0-9_-]+$/),
|
|
588
|
+
tag: z.string().regex(/^[A-Za-z0-9_-]+$/),
|
|
589
|
+
})
|
|
590
|
+
.strict();
|
|
591
|
+
export const WorkflowEventSchema = z
|
|
592
|
+
.object({
|
|
593
|
+
schemaVersion: z.literal("1.0.0"),
|
|
594
|
+
id: z.string().uuid(),
|
|
595
|
+
aggregateId: z.string().uuid(),
|
|
596
|
+
sequence: z.number().int().positive(),
|
|
597
|
+
traceId: z.string().uuid(),
|
|
598
|
+
type: z.string().regex(/^[a-z]+(?:\.[a-z_]+)+$/),
|
|
599
|
+
data: z.record(z.string(), z.unknown()),
|
|
600
|
+
occurredAt: z.string().datetime(),
|
|
601
|
+
})
|
|
602
|
+
.strict();
|
|
603
|
+
export const WorkflowNodeStateSchema = z
|
|
604
|
+
.object({
|
|
605
|
+
status: WorkflowNodeStatusSchema,
|
|
606
|
+
taskIds: z.array(z.string().uuid()).max(5_000).default([]),
|
|
607
|
+
approval: ApprovalChallengeSchema.optional(),
|
|
608
|
+
iterations: z.number().int().min(0).max(100).optional(),
|
|
609
|
+
// What a human_input or delegation node is waiting on, and what it got.
|
|
610
|
+
prompt: z.string().max(2_000).optional(),
|
|
611
|
+
input: z.string().max(10_000).optional(),
|
|
612
|
+
error: z.string().max(10_000).optional(),
|
|
613
|
+
})
|
|
614
|
+
.strict();
|
|
615
|
+
export const WorkflowRunSchema = z
|
|
616
|
+
.object({
|
|
617
|
+
schemaVersion: z.literal("1.0.0"),
|
|
618
|
+
id: z.string().uuid(),
|
|
619
|
+
definitionId: z.string().uuid(),
|
|
620
|
+
definitionVersion: z.number().int().positive(),
|
|
621
|
+
status: WorkflowStatusSchema,
|
|
622
|
+
stateVersion: z.number().int().positive(),
|
|
623
|
+
nodes: z.record(WorkflowNodeIdSchema, WorkflowNodeStateSchema),
|
|
624
|
+
traceId: z.string().uuid(),
|
|
625
|
+
error: z.string().max(10_000).optional(),
|
|
626
|
+
createdAt: z.string().datetime(),
|
|
627
|
+
updatedAt: z.string().datetime(),
|
|
628
|
+
})
|
|
629
|
+
.strict();
|
|
630
|
+
export const WorkflowSnapshotSchema = z
|
|
631
|
+
.object({
|
|
632
|
+
schemaVersion: z.literal("1.0.0"),
|
|
633
|
+
workflowId: z.string().uuid(),
|
|
634
|
+
sequence: z.number().int().positive(),
|
|
635
|
+
run: WorkflowRunSchema,
|
|
636
|
+
createdAt: z.string().datetime(),
|
|
637
|
+
})
|
|
638
|
+
.strict()
|
|
639
|
+
.refine((snapshot) => snapshot.sequence === snapshot.run.stateVersion, {
|
|
640
|
+
message: "sequence must match run.stateVersion",
|
|
641
|
+
path: ["sequence"],
|
|
642
|
+
});
|
|
643
|
+
export const WorkflowAdvanceResultSchema = z
|
|
644
|
+
.object({
|
|
645
|
+
run: WorkflowRunSchema,
|
|
646
|
+
tasks: z.array(z.record(z.string(), z.unknown())),
|
|
647
|
+
events: z.array(WorkflowEventSchema),
|
|
648
|
+
})
|
|
649
|
+
.strict();
|
|
650
|
+
export const WorkflowPlanInputSchema = z
|
|
651
|
+
.object({
|
|
652
|
+
definition: WorkflowDefinitionSchema,
|
|
653
|
+
})
|
|
654
|
+
.strict();
|
|
655
|
+
export const WorkflowAdvanceInputSchema = z
|
|
656
|
+
.object({
|
|
657
|
+
workflowId: z.string().uuid(),
|
|
658
|
+
approvals: z.array(ApprovalResponseSchema).max(50).default([]),
|
|
659
|
+
// Answers for nodes parked on a person. Supplied on the same call that
|
|
660
|
+
// resumes the wave, so one round trip both answers and advances.
|
|
661
|
+
inputs: z.array(WorkflowInputSchema).max(50).default([]),
|
|
662
|
+
})
|
|
663
|
+
.strict();
|
|
664
|
+
export const WorkflowIdInputSchema = z
|
|
665
|
+
.object({
|
|
666
|
+
workflowId: z.string().uuid(),
|
|
667
|
+
})
|
|
668
|
+
.strict();
|
|
669
|
+
// Operator halts and their reversal. One tool with an action beats three tools
|
|
670
|
+
// that differ only in a verb.
|
|
671
|
+
export const WorkflowControlInputSchema = z
|
|
672
|
+
.object({
|
|
673
|
+
workflowId: z.string().uuid(),
|
|
674
|
+
action: z.enum(["pause", "resume", "suspend"]),
|
|
675
|
+
})
|
|
676
|
+
.strict();
|
|
677
|
+
export const TaskStatusSchema = z.enum([
|
|
678
|
+
"planned",
|
|
679
|
+
"awaiting_approval",
|
|
680
|
+
"running",
|
|
681
|
+
"verifying",
|
|
682
|
+
"verified_success",
|
|
683
|
+
"partial",
|
|
684
|
+
"failed",
|
|
685
|
+
"cancelled",
|
|
686
|
+
"waiting_user",
|
|
687
|
+
"policy_blocked",
|
|
688
|
+
"budget_exhausted",
|
|
689
|
+
"recovery_required",
|
|
690
|
+
]);
|
|
691
|
+
/**
|
|
692
|
+
* Assemble the contract for a planned task.
|
|
693
|
+
*
|
|
694
|
+
* `classification` comes from `classifyOperation`, which lives in the policy
|
|
695
|
+
* package; passing it in keeps this file free of dependencies and keeps one
|
|
696
|
+
* classifier authoritative instead of a second copy that could disagree.
|
|
697
|
+
*/
|
|
698
|
+
export function effectContract(task, classification) {
|
|
699
|
+
return {
|
|
700
|
+
contractVersion: PROTOCOL_VERSION,
|
|
701
|
+
taskId: task.id,
|
|
702
|
+
identity: task.request.identity ?? LOCAL_IDENTITY,
|
|
703
|
+
capability: classification.capability,
|
|
704
|
+
operation: task.request.operation,
|
|
705
|
+
effect: task.policyDecision.effect,
|
|
706
|
+
risk: task.policyDecision.risk,
|
|
707
|
+
target: classification.target,
|
|
708
|
+
traits: task.policyDecision.traits,
|
|
709
|
+
forbiddenEffects: task.request.forbiddenEffects,
|
|
710
|
+
postconditions: task.request.requiredEvidence,
|
|
711
|
+
budget: task.request.budget,
|
|
712
|
+
...(task.idempotencyKey === undefined
|
|
713
|
+
? {}
|
|
714
|
+
: { idempotencyKey: task.idempotencyKey }),
|
|
715
|
+
policy: task.policyDecision,
|
|
716
|
+
...(task.approval === undefined ? {} : { authorization: task.approval }),
|
|
717
|
+
metadata: { goal: task.request.goal },
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
export const MelraCapabilitiesInputSchema = z.object({}).strict();
|
|
721
|
+
export const MelraPlanInputSchema = TaskRequestSchema;
|
|
722
|
+
export const MelraExecuteInputSchema = z
|
|
723
|
+
.object({
|
|
724
|
+
taskId: z.string().uuid(),
|
|
725
|
+
approval: ApprovalResponseSchema.optional(),
|
|
726
|
+
})
|
|
727
|
+
.strict();
|
|
728
|
+
export const MelraTaskStatusInputSchema = z
|
|
729
|
+
.object({ taskId: z.string().uuid() })
|
|
730
|
+
.strict();
|
|
731
|
+
export const MelraTaskCancelInputSchema = z
|
|
732
|
+
.object({ taskId: z.string().uuid() })
|
|
733
|
+
.strict();
|
|
734
|
+
export const MelraReceiptBaseSchema = z
|
|
735
|
+
.object({
|
|
736
|
+
taskId: z.string().uuid().optional(),
|
|
737
|
+
receiptId: z.string().uuid().optional(),
|
|
738
|
+
})
|
|
739
|
+
.strict();
|
|
740
|
+
export const MelraReceiptInputSchema = MelraReceiptBaseSchema
|
|
741
|
+
.refine((value) => value.taskId !== undefined || value.receiptId !== undefined, {
|
|
742
|
+
message: "taskId or receiptId is required",
|
|
743
|
+
});
|
|
744
|
+
export const TOOL_NAMES = [
|
|
745
|
+
"melra_capabilities",
|
|
746
|
+
"melra_plan",
|
|
747
|
+
"melra_execute",
|
|
748
|
+
"melra_task_status",
|
|
749
|
+
"melra_task_cancel",
|
|
750
|
+
"melra_receipt",
|
|
751
|
+
"melra_workflow_plan",
|
|
752
|
+
"melra_workflow_advance",
|
|
753
|
+
"melra_workflow_status",
|
|
754
|
+
"melra_workflow_cancel",
|
|
755
|
+
"melra_workflow_control",
|
|
756
|
+
];
|
|
757
|
+
//# sourceMappingURL=index.js.map
|