@sequenceholdings/agent-spec 0.1.3 → 0.1.5
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 +65 -1
- package/dist/agent-definition.d.ts +14 -0
- package/dist/agent-definition.js +14 -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
|
@@ -25,11 +25,16 @@ import { defineAgent } from '@sequenceholdings/agent-spec'
|
|
|
25
25
|
export const agent = defineAgent({
|
|
26
26
|
id: '680000000000000000000001',
|
|
27
27
|
name: 'Support Agent',
|
|
28
|
-
model: { provider: 'anthropic', name: 'claude-sonnet-
|
|
28
|
+
model: { provider: 'anthropic', name: 'claude-sonnet-5-5' },
|
|
29
29
|
systemPrompt: 'Help the support team investigate and resolve requests.',
|
|
30
30
|
})
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
`RECOMMENDED_AGENT_MODEL` is the model new agents start on; `seq-studio agents
|
|
34
|
+
init` writes it into the scaffold. A definition that omits `model` keeps the
|
|
35
|
+
schema's original default, so upgrading this package never moves an existing
|
|
36
|
+
agent to another model.
|
|
37
|
+
|
|
33
38
|
Definitions must be pure data literals: do not read environment variables, call
|
|
34
39
|
functions, or compute fields in `agent.ts`. Keep deployment targeting separate
|
|
35
40
|
from a definition, then use `seq-studio agents validate`, `plan`, and `apply`.
|
|
@@ -68,6 +73,45 @@ with no such identity, such as a Lattice step without
|
|
|
68
73
|
`run_as.service_account`, gets no function tools and records each function as
|
|
69
74
|
unavailable (`no_user_identity`).
|
|
70
75
|
|
|
76
|
+
## Author client proposals (preview)
|
|
77
|
+
|
|
78
|
+
The upcoming `0.1.4` release adds optional `clientProposals` to text agent
|
|
79
|
+
definitions. Each literal descriptor supplies its exact tool name, description
|
|
80
|
+
and a bounded JSON input schema:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
clientProposals: [{
|
|
84
|
+
name: 'propose_label',
|
|
85
|
+
description: 'Propose a support label for the user to review.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: { label: { type: 'string', enum: ['ready', 'needs-review'] } },
|
|
89
|
+
required: ['label'],
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
},
|
|
92
|
+
}],
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`clientProposalSchema`, `clientProposalsSchema` and the `ClientProposal` type
|
|
96
|
+
are root exports. Definitions accept at most 64 uniquely named proposals,
|
|
97
|
+
32,000 characters per description, 128 KiB per input schema and 512 KiB for all
|
|
98
|
+
descriptors. Schemas are limited to depth 32 and 10,000 nodes. They use a finite
|
|
99
|
+
JSON Schema subset: executable values and unsupported keywords such as
|
|
100
|
+
`pattern`, `format`, `default` and `$ref` are rejected. Schema URL annotations
|
|
101
|
+
are never fetched. Keep descriptors as authored literals so their exact
|
|
102
|
+
instructions and schemas enter the captured execution definition. Omitting
|
|
103
|
+
the field preserves existing definition hashes.
|
|
104
|
+
|
|
105
|
+
The matching Vercel-framework runtime validates arguments, bounded to 256 KiB,
|
|
106
|
+
depth 64, 50,000 nodes and 100,000 validation work units, then returns
|
|
107
|
+
`{ status: 'pending_client_review', applied: false }`. This acknowledgement
|
|
108
|
+
proves neither a backend apply nor client delivery. It does not execute a
|
|
109
|
+
provider call or authorize a mutation.
|
|
110
|
+
The application owns proposal rendering, validation, human review and any
|
|
111
|
+
separate authorized write. Proposal names that collide with another resolved
|
|
112
|
+
tool are rejected. Confirm package publication and matching runtime support
|
|
113
|
+
before relying on this preview contract.
|
|
114
|
+
|
|
71
115
|
## Exports
|
|
72
116
|
|
|
73
117
|
- `@sequenceholdings/agent-spec` provides `defineAgent`,
|
|
@@ -78,6 +122,26 @@ unavailable (`no_user_identity`).
|
|
|
78
122
|
- `@sequenceholdings/agent-spec/yaml` provides `parseYamlAgent` only for
|
|
79
123
|
migrating legacy YAML definitions.
|
|
80
124
|
|
|
125
|
+
## Job request fingerprints (preview)
|
|
126
|
+
|
|
127
|
+
The upcoming `0.1.4` release adds `hashJobCreateRequest` at the package root.
|
|
128
|
+
Confirm registry publication before installing that version. This pure helper
|
|
129
|
+
hashes the job kind, resource ID, expected source identity and exact parsed
|
|
130
|
+
payload using the platform's versioned request format. It sorts object keys
|
|
131
|
+
while preserving array order and string bytes. `normalizeJobRequestJson`
|
|
132
|
+
normalizes dates to ISO strings and omits undefined object fields; non-JSON,
|
|
133
|
+
cyclic, excessively nested or oversized input is refused with
|
|
134
|
+
`JobRequestHashError` (`status` 400 or 413). The serialized input is bounded
|
|
135
|
+
to 16 MiB.
|
|
136
|
+
|
|
137
|
+
The fingerprint excludes transport credentials and caller identity. It is not
|
|
138
|
+
an authorization grant, an admission receipt, proof that a job ran, or an
|
|
139
|
+
application's business-source association. An application comparing an
|
|
140
|
+
authorized admission receipt must still verify the actual caller, current
|
|
141
|
+
protected intent, expected source identity and recorded job separately. Use
|
|
142
|
+
the exact parsed request payload, not a display projection or reconstructed
|
|
143
|
+
subset.
|
|
144
|
+
|
|
81
145
|
## Voice definitions (preview)
|
|
82
146
|
|
|
83
147
|
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.
|
|
@@ -2,6 +2,10 @@ import { z } from 'zod';
|
|
|
2
2
|
export { canonicalJson } from './canonical-json.js';
|
|
3
3
|
export { voiceDeclarationSchema } from './voice-authoring.js';
|
|
4
4
|
export type { VoiceDeclaration, VoiceDeclarationInput } from './voice-authoring.js';
|
|
5
|
+
export declare const RECOMMENDED_AGENT_MODEL: {
|
|
6
|
+
readonly provider: "anthropic";
|
|
7
|
+
readonly name: "claude-sonnet-5-5";
|
|
8
|
+
};
|
|
5
9
|
export type UnsupportedConstraint = {
|
|
6
10
|
path: string;
|
|
7
11
|
type: string;
|
|
@@ -59,6 +63,11 @@ export declare const textAgentDefinitionSchema: z.ZodObject<{
|
|
|
59
63
|
temperature: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
60
64
|
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
61
65
|
tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
66
|
+
clientProposals: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
67
|
+
name: z.ZodString;
|
|
68
|
+
description: z.ZodString;
|
|
69
|
+
inputSchema: z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
70
|
+
}, z.core.$strict>>>;
|
|
62
71
|
mcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
63
72
|
requiredMcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
64
73
|
requiredReferences: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -338,6 +347,11 @@ export declare const agentDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject
|
|
|
338
347
|
temperature: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
339
348
|
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
340
349
|
tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
350
|
+
clientProposals: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
351
|
+
name: z.ZodString;
|
|
352
|
+
description: z.ZodString;
|
|
353
|
+
inputSchema: z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
354
|
+
}, z.core.$strict>>>;
|
|
341
355
|
mcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
342
356
|
requiredMcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
343
357
|
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}$/;
|
|
@@ -11,6 +12,10 @@ const DEFAULT_MODEL = {
|
|
|
11
12
|
provider: 'anthropic',
|
|
12
13
|
name: 'claude-sonnet-4-5-20250929',
|
|
13
14
|
};
|
|
15
|
+
export const RECOMMENDED_AGENT_MODEL = {
|
|
16
|
+
provider: 'anthropic',
|
|
17
|
+
name: 'claude-sonnet-5-5',
|
|
18
|
+
};
|
|
14
19
|
const jsonObjectSchema = z.record(z.string(), z.unknown());
|
|
15
20
|
const unsupportedConstraints = {
|
|
16
21
|
integer: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum'],
|
|
@@ -118,6 +123,7 @@ const textDefinitionBaseSchema = z
|
|
|
118
123
|
temperature: z.number().min(0).max(1).nullish(),
|
|
119
124
|
systemPrompt: z.string().optional(),
|
|
120
125
|
tools: z.array(z.string()).default([]),
|
|
126
|
+
clientProposals: clientProposalsSchema.optional(),
|
|
121
127
|
mcpServers: z.array(z.string()).default([]),
|
|
122
128
|
requiredMcpServers: z.array(z.string()).default([]),
|
|
123
129
|
requiredReferences: z.array(agentReferenceSchema).optional(),
|
|
@@ -146,6 +152,14 @@ export const textAgentDefinitionSchema = textDefinitionBaseSchema.safeExtend({
|
|
|
146
152
|
mode: z.literal('text').optional(),
|
|
147
153
|
voice: z.never().optional(),
|
|
148
154
|
}).superRefine((definition, context) => {
|
|
155
|
+
if (definition.framework === 'claude-agent' && (definition.clientProposals?.length ?? 0) > 0) {
|
|
156
|
+
context.addIssue({ code: 'custom', path: ['clientProposals'], message: 'Client proposals require the Vercel tool runtime' });
|
|
157
|
+
}
|
|
158
|
+
for (const [index, proposal] of (definition.clientProposals ?? []).entries()) {
|
|
159
|
+
if (definition.tools.includes(proposal.name)) {
|
|
160
|
+
context.addIssue({ code: 'custom', path: ['clientProposals', index, 'name'], message: 'Client proposal name collides with a selected built-in tool' });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
149
163
|
for (const [index, serverId] of definition.requiredMcpServers.entries()) {
|
|
150
164
|
if (definition.mcpServers.includes(serverId))
|
|
151
165
|
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
|
}>;
|