@sequenceholdings/agent-spec 0.1.3 → 0.1.4
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/README.md +59 -0
- package/dist/agent-definition.d.ts +10 -0
- package/dist/agent-definition.js +10 -0
- package/dist/client-proposals.d.ts +20 -0
- package/dist/client-proposals.js +172 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/job-request-hash.d.ts +27 -0
- package/dist/job-request-hash.js +54 -0
- package/dist/voice-authority-contracts.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,45 @@ with no such identity, such as a Lattice step without
|
|
|
68
68
|
`run_as.service_account`, gets no function tools and records each function as
|
|
69
69
|
unavailable (`no_user_identity`).
|
|
70
70
|
|
|
71
|
+
## Author client proposals (preview)
|
|
72
|
+
|
|
73
|
+
The upcoming `0.1.4` release adds optional `clientProposals` to text agent
|
|
74
|
+
definitions. Each literal descriptor supplies its exact tool name, description
|
|
75
|
+
and a bounded JSON input schema:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
clientProposals: [{
|
|
79
|
+
name: 'propose_label',
|
|
80
|
+
description: 'Propose a support label for the user to review.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: { label: { type: 'string', enum: ['ready', 'needs-review'] } },
|
|
84
|
+
required: ['label'],
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
},
|
|
87
|
+
}],
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`clientProposalSchema`, `clientProposalsSchema` and the `ClientProposal` type
|
|
91
|
+
are root exports. Definitions accept at most 64 uniquely named proposals,
|
|
92
|
+
32,000 characters per description, 128 KiB per input schema and 512 KiB for all
|
|
93
|
+
descriptors. Schemas are limited to depth 32 and 10,000 nodes. They use a finite
|
|
94
|
+
JSON Schema subset: executable values and unsupported keywords such as
|
|
95
|
+
`pattern`, `format`, `default` and `$ref` are rejected. Schema URL annotations
|
|
96
|
+
are never fetched. Keep descriptors as authored literals so their exact
|
|
97
|
+
instructions and schemas enter the captured execution definition. Omitting
|
|
98
|
+
the field preserves existing definition hashes.
|
|
99
|
+
|
|
100
|
+
The matching Vercel-framework runtime validates arguments, bounded to 256 KiB,
|
|
101
|
+
depth 64, 50,000 nodes and 100,000 validation work units, then returns
|
|
102
|
+
`{ status: 'pending_client_review', applied: false }`. This acknowledgement
|
|
103
|
+
proves neither a backend apply nor client delivery. It does not execute a
|
|
104
|
+
provider call or authorize a mutation.
|
|
105
|
+
The application owns proposal rendering, validation, human review and any
|
|
106
|
+
separate authorized write. Proposal names that collide with another resolved
|
|
107
|
+
tool are rejected. Confirm package publication and matching runtime support
|
|
108
|
+
before relying on this preview contract.
|
|
109
|
+
|
|
71
110
|
## Exports
|
|
72
111
|
|
|
73
112
|
- `@sequenceholdings/agent-spec` provides `defineAgent`,
|
|
@@ -78,6 +117,26 @@ unavailable (`no_user_identity`).
|
|
|
78
117
|
- `@sequenceholdings/agent-spec/yaml` provides `parseYamlAgent` only for
|
|
79
118
|
migrating legacy YAML definitions.
|
|
80
119
|
|
|
120
|
+
## Job request fingerprints (preview)
|
|
121
|
+
|
|
122
|
+
The upcoming `0.1.4` release adds `hashJobCreateRequest` at the package root.
|
|
123
|
+
Confirm registry publication before installing that version. This pure helper
|
|
124
|
+
hashes the job kind, resource ID, expected source identity and exact parsed
|
|
125
|
+
payload using the platform's versioned request format. It sorts object keys
|
|
126
|
+
while preserving array order and string bytes. `normalizeJobRequestJson`
|
|
127
|
+
normalizes dates to ISO strings and omits undefined object fields; non-JSON,
|
|
128
|
+
cyclic, excessively nested or oversized input is refused with
|
|
129
|
+
`JobRequestHashError` (`status` 400 or 413). The serialized input is bounded
|
|
130
|
+
to 16 MiB.
|
|
131
|
+
|
|
132
|
+
The fingerprint excludes transport credentials and caller identity. It is not
|
|
133
|
+
an authorization grant, an admission receipt, proof that a job ran, or an
|
|
134
|
+
application's business-source association. An application comparing an
|
|
135
|
+
authorized admission receipt must still verify the actual caller, current
|
|
136
|
+
protected intent, expected source identity and recorded job separately. Use
|
|
137
|
+
the exact parsed request payload, not a display projection or reconstructed
|
|
138
|
+
subset.
|
|
139
|
+
|
|
81
140
|
## Voice definitions (preview)
|
|
82
141
|
|
|
83
142
|
Choose either `text` or `voice` when creating an Agent. Definitions without `mode` retain their existing text defaults, source format, and bundle hashes. `validateAgentModeUpdate` rejects changing an existing Agent's mode in either direction. Voice uses the same identity, prompt, model, native Skill references, permissions, source compiler, and bundle hashing as text; its supported authoring fields are deliberately smaller.
|
|
@@ -59,6 +59,11 @@ export declare const textAgentDefinitionSchema: z.ZodObject<{
|
|
|
59
59
|
temperature: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
60
60
|
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
61
61
|
tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
62
|
+
clientProposals: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
63
|
+
name: z.ZodString;
|
|
64
|
+
description: z.ZodString;
|
|
65
|
+
inputSchema: z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
66
|
+
}, z.core.$strict>>>;
|
|
62
67
|
mcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
63
68
|
requiredMcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
64
69
|
requiredReferences: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -338,6 +343,11 @@ export declare const agentDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject
|
|
|
338
343
|
temperature: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
339
344
|
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
340
345
|
tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
346
|
+
clientProposals: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
347
|
+
name: z.ZodString;
|
|
348
|
+
description: z.ZodString;
|
|
349
|
+
inputSchema: z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
350
|
+
}, z.core.$strict>>>;
|
|
341
351
|
mcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
342
352
|
requiredMcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
343
353
|
requiredReferences: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
package/dist/agent-definition.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { inputSchemaSchema } from './input-schema.js';
|
|
4
4
|
import { canonicalJson, sortJson } from './canonical-json.js';
|
|
5
5
|
import { voiceDeclarationSchema } from './voice-authoring.js';
|
|
6
|
+
import { clientProposalsSchema } from './client-proposals.js';
|
|
6
7
|
export { canonicalJson } from './canonical-json.js';
|
|
7
8
|
export { voiceDeclarationSchema } from './voice-authoring.js';
|
|
8
9
|
const OBJECT_ID_PATTERN = /^[0-9a-f]{24}$/;
|
|
@@ -118,6 +119,7 @@ const textDefinitionBaseSchema = z
|
|
|
118
119
|
temperature: z.number().min(0).max(1).nullish(),
|
|
119
120
|
systemPrompt: z.string().optional(),
|
|
120
121
|
tools: z.array(z.string()).default([]),
|
|
122
|
+
clientProposals: clientProposalsSchema.optional(),
|
|
121
123
|
mcpServers: z.array(z.string()).default([]),
|
|
122
124
|
requiredMcpServers: z.array(z.string()).default([]),
|
|
123
125
|
requiredReferences: z.array(agentReferenceSchema).optional(),
|
|
@@ -146,6 +148,14 @@ export const textAgentDefinitionSchema = textDefinitionBaseSchema.safeExtend({
|
|
|
146
148
|
mode: z.literal('text').optional(),
|
|
147
149
|
voice: z.never().optional(),
|
|
148
150
|
}).superRefine((definition, context) => {
|
|
151
|
+
if (definition.framework === 'claude-agent' && (definition.clientProposals?.length ?? 0) > 0) {
|
|
152
|
+
context.addIssue({ code: 'custom', path: ['clientProposals'], message: 'Client proposals require the Vercel tool runtime' });
|
|
153
|
+
}
|
|
154
|
+
for (const [index, proposal] of (definition.clientProposals ?? []).entries()) {
|
|
155
|
+
if (definition.tools.includes(proposal.name)) {
|
|
156
|
+
context.addIssue({ code: 'custom', path: ['clientProposals', index, 'name'], message: 'Client proposal name collides with a selected built-in tool' });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
149
159
|
for (const [index, serverId] of definition.requiredMcpServers.entries()) {
|
|
150
160
|
if (definition.mcpServers.includes(serverId))
|
|
151
161
|
continue;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const CLIENT_PROPOSAL_LIMITS: {
|
|
3
|
+
readonly count: 64;
|
|
4
|
+
readonly descriptionLength: 32000;
|
|
5
|
+
readonly schemaBytes: number;
|
|
6
|
+
readonly totalBytes: number;
|
|
7
|
+
readonly schemaDepth: 32;
|
|
8
|
+
readonly schemaNodes: 10000;
|
|
9
|
+
};
|
|
10
|
+
export declare const clientProposalSchema: z.ZodObject<{
|
|
11
|
+
name: z.ZodString;
|
|
12
|
+
description: z.ZodString;
|
|
13
|
+
inputSchema: z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
export type ClientProposal = z.infer<typeof clientProposalSchema>;
|
|
16
|
+
export declare const clientProposalsSchema: z.ZodArray<z.ZodObject<{
|
|
17
|
+
name: z.ZodString;
|
|
18
|
+
description: z.ZodString;
|
|
19
|
+
inputSchema: z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
20
|
+
}, z.core.$strict>>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const CLIENT_PROPOSAL_LIMITS = {
|
|
3
|
+
count: 64, descriptionLength: 32_000, schemaBytes: 128 * 1024,
|
|
4
|
+
totalBytes: 512 * 1024, schemaDepth: 32, schemaNodes: 10_000,
|
|
5
|
+
};
|
|
6
|
+
const TYPES = new Set(['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']);
|
|
7
|
+
const RESERVED_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
|
|
8
|
+
const ANNOTATIONS = new Set(['$schema', 'title', 'description']);
|
|
9
|
+
const NUMBER_KEYS = new Set(['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum']);
|
|
10
|
+
const SIZE_KEYS = new Set(['minLength', 'maxLength', 'minItems', 'maxItems']);
|
|
11
|
+
const CHILD_KEYS = new Set(['items', 'additionalProperties', 'propertyNames']);
|
|
12
|
+
const UNION_KEYS = new Set(['anyOf', 'oneOf', 'allOf']);
|
|
13
|
+
const KEYWORDS = new Set([...ANNOTATIONS, ...NUMBER_KEYS, ...SIZE_KEYS, ...CHILD_KEYS, ...UNION_KEYS,
|
|
14
|
+
'type', 'properties', 'required', 'enum', 'const']);
|
|
15
|
+
function isObject(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function requireBoundedJson(value) {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
const pending = [{ value, depth: 0 }];
|
|
21
|
+
let nodes = 0;
|
|
22
|
+
while (pending.length > 0) {
|
|
23
|
+
const item = pending.pop();
|
|
24
|
+
if (!item)
|
|
25
|
+
break;
|
|
26
|
+
if (++nodes > CLIENT_PROPOSAL_LIMITS.schemaNodes || item.depth > CLIENT_PROPOSAL_LIMITS.schemaDepth) {
|
|
27
|
+
throw new Error('Client proposal schema exceeds its depth or node limit');
|
|
28
|
+
}
|
|
29
|
+
const current = item.value;
|
|
30
|
+
if (typeof current === 'string' && Buffer.byteLength(current) > CLIENT_PROPOSAL_LIMITS.schemaBytes) {
|
|
31
|
+
throw new Error('Client proposal schema exceeds its byte limit');
|
|
32
|
+
}
|
|
33
|
+
if (current === null || typeof current === 'string' || typeof current === 'boolean')
|
|
34
|
+
continue;
|
|
35
|
+
if (typeof current === 'number' && Number.isFinite(current))
|
|
36
|
+
continue;
|
|
37
|
+
if (typeof current !== 'object' || current === null)
|
|
38
|
+
throw new Error('Client proposal schema must be literal JSON');
|
|
39
|
+
requireLiteralContainer({ current, seen, pending, depth: item.depth });
|
|
40
|
+
}
|
|
41
|
+
if (Buffer.byteLength(JSON.stringify(value)) > CLIENT_PROPOSAL_LIMITS.schemaBytes) {
|
|
42
|
+
throw new Error('Client proposal schema exceeds its byte limit');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function requireLiteralContainer({ current, seen, pending, depth }) {
|
|
46
|
+
const prototype = Object.getPrototypeOf(current);
|
|
47
|
+
if ((!Array.isArray(current) && prototype !== Object.prototype && prototype !== null) || seen.has(current)) {
|
|
48
|
+
throw new Error('Client proposal schema must be acyclic literal JSON');
|
|
49
|
+
}
|
|
50
|
+
seen.add(current);
|
|
51
|
+
const keys = Reflect.ownKeys(current);
|
|
52
|
+
if (Array.isArray(current))
|
|
53
|
+
requireLiteralArray({ value: current, keys });
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
if (Array.isArray(current) && key === 'length')
|
|
56
|
+
continue;
|
|
57
|
+
if (typeof key !== 'string' || RESERVED_NAMES.has(key))
|
|
58
|
+
throw new Error('Client proposal schema contains an unsupported key');
|
|
59
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
60
|
+
if (!descriptor || !('value' in descriptor) || !descriptor.enumerable)
|
|
61
|
+
throw new Error('Client proposal schema must contain JSON values');
|
|
62
|
+
pending.push({ value: descriptor.value, depth: depth + 1 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function requireLiteralArray({ value, keys }) {
|
|
66
|
+
if (Object.getPrototypeOf(value) !== Array.prototype || keys.length !== value.length + 1) {
|
|
67
|
+
throw new Error('Client proposal schema arrays must be dense literal JSON');
|
|
68
|
+
}
|
|
69
|
+
for (let index = 0; index < value.length; index++) {
|
|
70
|
+
if (!Object.hasOwn(value, String(index)))
|
|
71
|
+
throw new Error('Client proposal schema arrays must be dense literal JSON');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function requireType(value) {
|
|
75
|
+
const types = Array.isArray(value) ? value : [value];
|
|
76
|
+
if (types.length === 0 || types.length > TYPES.size || types.some(type => typeof type !== 'string' || !TYPES.has(type))
|
|
77
|
+
|| new Set(types).size !== types.length)
|
|
78
|
+
throw new Error('Client proposal schema has an invalid type');
|
|
79
|
+
}
|
|
80
|
+
function requireProperties(value) {
|
|
81
|
+
if (!isObject(value) || Object.keys(value).length > 512)
|
|
82
|
+
throw new Error('Client proposal properties must be a bounded object');
|
|
83
|
+
for (const child of Object.values(value))
|
|
84
|
+
requireSchema(child);
|
|
85
|
+
}
|
|
86
|
+
function requireRequired({ value, properties }) {
|
|
87
|
+
if (!Array.isArray(value) || value.some(name => typeof name !== 'string') || new Set(value).size !== value.length) {
|
|
88
|
+
throw new Error('Client proposal required must contain unique property names');
|
|
89
|
+
}
|
|
90
|
+
if (value.some(name => !isObject(properties) || !Object.hasOwn(properties, name))) {
|
|
91
|
+
throw new Error('Client proposal required must name declared properties');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function requireBound({ key, value }) {
|
|
95
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
96
|
+
throw new Error(`Client proposal ${key} must be a finite number`);
|
|
97
|
+
if (SIZE_KEYS.has(key) && (!Number.isSafeInteger(value) || value < 0)) {
|
|
98
|
+
throw new Error(`Client proposal ${key} must be a nonnegative integer`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function requireEntry({ key, entry, schema }) {
|
|
102
|
+
if (!KEYWORDS.has(key))
|
|
103
|
+
throw new Error(`Unsupported client proposal schema keyword: ${key}`);
|
|
104
|
+
if (ANNOTATIONS.has(key)) {
|
|
105
|
+
if (typeof entry !== 'string')
|
|
106
|
+
throw new Error(`Client proposal ${key} must be a string`);
|
|
107
|
+
}
|
|
108
|
+
else if (NUMBER_KEYS.has(key) || SIZE_KEYS.has(key))
|
|
109
|
+
requireBound({ key, value: entry });
|
|
110
|
+
else if (key === 'type')
|
|
111
|
+
requireType(entry);
|
|
112
|
+
else if (CHILD_KEYS.has(key))
|
|
113
|
+
requireSchema(entry);
|
|
114
|
+
else if (UNION_KEYS.has(key)) {
|
|
115
|
+
if (!Array.isArray(entry) || entry.length === 0 || entry.length > 32)
|
|
116
|
+
throw new Error(`Client proposal ${key} requires 1–32 schemas`);
|
|
117
|
+
for (const child of entry)
|
|
118
|
+
requireSchema(child);
|
|
119
|
+
}
|
|
120
|
+
else if (key === 'properties')
|
|
121
|
+
requireProperties(entry);
|
|
122
|
+
else if (key === 'required')
|
|
123
|
+
requireRequired({ value: entry, properties: schema.properties });
|
|
124
|
+
else if (key === 'enum') {
|
|
125
|
+
if (!Array.isArray(entry) || entry.length === 0 || entry.length > 512)
|
|
126
|
+
throw new Error('Client proposal enum must be a bounded nonempty array');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function requireSchema(value) {
|
|
130
|
+
if (typeof value === 'boolean')
|
|
131
|
+
return;
|
|
132
|
+
if (!isObject(value))
|
|
133
|
+
throw new Error('Client proposal schema nodes must be objects or booleans');
|
|
134
|
+
for (const [key, entry] of Object.entries(value))
|
|
135
|
+
requireEntry({ key, entry, schema: value });
|
|
136
|
+
}
|
|
137
|
+
const literalJsonSchema = z.unknown().superRefine((value, context) => {
|
|
138
|
+
try {
|
|
139
|
+
requireBoundedJson(value);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
context.addIssue({ code: 'custom', message: error instanceof Error ? error.message : 'Invalid client proposal JSON' });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const proposalInputSchema = literalJsonSchema.pipe(z.record(z.string(), z.unknown())).superRefine((value, context) => {
|
|
146
|
+
try {
|
|
147
|
+
if (value.type !== 'object')
|
|
148
|
+
throw new Error('Client proposal root schema type must be object');
|
|
149
|
+
requireSchema(value);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
context.addIssue({ code: 'custom', message: error instanceof Error ? error.message : 'Invalid client proposal schema' });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
export const clientProposalSchema = z.strictObject({
|
|
156
|
+
name: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}$/).refine(name => !RESERVED_NAMES.has(name), 'Reserved client proposal name'),
|
|
157
|
+
description: z.string().min(1).max(CLIENT_PROPOSAL_LIMITS.descriptionLength),
|
|
158
|
+
inputSchema: proposalInputSchema,
|
|
159
|
+
});
|
|
160
|
+
export const clientProposalsSchema = z.array(clientProposalSchema).max(CLIENT_PROPOSAL_LIMITS.count).superRefine((value, context) => {
|
|
161
|
+
if (new Set(value.map(proposal => proposal.name)).size !== value.length) {
|
|
162
|
+
context.addIssue({ code: 'custom', message: 'Client proposal names must be unique' });
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
if (Buffer.byteLength(JSON.stringify(value)) > CLIENT_PROPOSAL_LIMITS.totalBytes) {
|
|
166
|
+
context.addIssue({ code: 'custom', message: 'Client proposal descriptors exceed their total byte limit' });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
context.addIssue({ code: 'custom', message: 'Client proposal descriptors must be literal JSON' });
|
|
171
|
+
}
|
|
172
|
+
});
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const JobCreateKindSchema: z.ZodEnum<{
|
|
3
|
+
agent: "agent";
|
|
4
|
+
workflow: "workflow";
|
|
5
|
+
}>;
|
|
6
|
+
export type JobCreateKind = z.infer<typeof JobCreateKindSchema>;
|
|
7
|
+
export type JobRequestJsonValue = null | boolean | number | string | JobRequestJsonValue[] | {
|
|
8
|
+
[key: string]: JobRequestJsonValue;
|
|
9
|
+
};
|
|
10
|
+
export declare const JobRequestExecutionIdentitySchema: z.ZodObject<{
|
|
11
|
+
id: z.ZodString;
|
|
12
|
+
version: z.ZodString;
|
|
13
|
+
definitionSha256: z.ZodString;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
export type JobRequestExecutionIdentity = z.infer<typeof JobRequestExecutionIdentitySchema>;
|
|
16
|
+
export interface JobCreateRequestFingerprintInput {
|
|
17
|
+
kind: JobCreateKind;
|
|
18
|
+
resourceId: string;
|
|
19
|
+
expectedIdentity: JobRequestExecutionIdentity;
|
|
20
|
+
payload: unknown;
|
|
21
|
+
}
|
|
22
|
+
export declare class JobRequestHashError extends Error {
|
|
23
|
+
readonly status: 400 | 413;
|
|
24
|
+
constructor(status: 400 | 413, message: string);
|
|
25
|
+
}
|
|
26
|
+
export declare function normalizeJobRequestJson(value: unknown): JobRequestJsonValue;
|
|
27
|
+
export declare function hashJobCreateRequest({ kind, resourceId, expectedIdentity, payload }: JobCreateRequestFingerprintInput): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { canonicalJson } from './canonical-json.js';
|
|
4
|
+
const MAX_JSON_BYTES = 16 * 1024 * 1024;
|
|
5
|
+
export const JobCreateKindSchema = z.enum(['agent', 'workflow']);
|
|
6
|
+
export const JobRequestExecutionIdentitySchema = z.strictObject({
|
|
7
|
+
id: z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/),
|
|
8
|
+
version: z.string().min(1).max(128),
|
|
9
|
+
definitionSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
10
|
+
});
|
|
11
|
+
export class JobRequestHashError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
constructor(status, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.name = 'JobRequestHashError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function assertJsonValue({ value, ancestors, depth }) {
|
|
20
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean'
|
|
21
|
+
|| typeof value === 'number' && Number.isFinite(value))
|
|
22
|
+
return;
|
|
23
|
+
if (typeof value !== 'object' || !value || depth > 64 || ancestors.has(value)) {
|
|
24
|
+
throw new JobRequestHashError(400, 'Job admission requires bounded JSON values.');
|
|
25
|
+
}
|
|
26
|
+
if (value instanceof Date) {
|
|
27
|
+
if (Number.isNaN(value.getTime()))
|
|
28
|
+
throw new JobRequestHashError(400, 'Job admission contains an invalid date.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
32
|
+
throw new JobRequestHashError(400, 'Job admission requires plain JSON objects.');
|
|
33
|
+
}
|
|
34
|
+
ancestors.add(value);
|
|
35
|
+
const values = Array.isArray(value) ? value : Object.values(value).filter((entry) => entry !== undefined);
|
|
36
|
+
for (const entry of values)
|
|
37
|
+
assertJsonValue({ value: entry, ancestors, depth: depth + 1 });
|
|
38
|
+
ancestors.delete(value);
|
|
39
|
+
}
|
|
40
|
+
export function normalizeJobRequestJson(value) {
|
|
41
|
+
assertJsonValue({ value, ancestors: new Set(), depth: 0 });
|
|
42
|
+
const encoded = JSON.stringify(value);
|
|
43
|
+
if (Buffer.byteLength(encoded) > MAX_JSON_BYTES)
|
|
44
|
+
throw new JobRequestHashError(413, 'Job admission exceeds the 16 MiB JSON budget.');
|
|
45
|
+
return JSON.parse(encoded);
|
|
46
|
+
}
|
|
47
|
+
export function hashJobCreateRequest({ kind, resourceId, expectedIdentity, payload }) {
|
|
48
|
+
const parsed = z.strictObject({ kind: JobCreateKindSchema, resourceId: z.string().min(1).max(128),
|
|
49
|
+
expectedIdentity: JobRequestExecutionIdentitySchema }).safeParse({ kind, resourceId, expectedIdentity });
|
|
50
|
+
if (!parsed.success)
|
|
51
|
+
throw new JobRequestHashError(400, 'Job request fingerprint requires a valid expected execution identity.');
|
|
52
|
+
const envelope = normalizeJobRequestJson({ format: 'agents-job-create-request-v1', ...parsed.data, payload });
|
|
53
|
+
return createHash('sha256').update(canonicalJson(envelope)).digest('hex');
|
|
54
|
+
}
|
|
@@ -188,8 +188,8 @@ export declare const voiceVerificationAttemptSchema: z.ZodObject<{
|
|
|
188
188
|
attemptedAt: z.ZodISODateTime;
|
|
189
189
|
attemptNumber: z.ZodNumber;
|
|
190
190
|
status: z.ZodEnum<{
|
|
191
|
-
verified: "verified";
|
|
192
191
|
pending: "pending";
|
|
192
|
+
verified: "verified";
|
|
193
193
|
rejected: "rejected";
|
|
194
194
|
uncertain: "uncertain";
|
|
195
195
|
}>;
|