agentex-creator-sdk 1.0.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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/packages/contracts/src/deployer-investigation.d.ts +1015 -0
- package/dist/packages/contracts/src/deployer-investigation.js +101 -0
- package/dist/packages/contracts/src/index.d.ts +1701 -0
- package/dist/packages/contracts/src/index.js +380 -0
- package/dist/packages/contracts/src/indexed-activity.d.ts +684 -0
- package/dist/packages/contracts/src/indexed-activity.js +71 -0
- package/dist/packages/contracts/src/indexed-agents.d.ts +299 -0
- package/dist/packages/contracts/src/indexed-agents.js +131 -0
- package/dist/packages/contracts/src/inspection.d.ts +906 -0
- package/dist/packages/contracts/src/inspection.js +114 -0
- package/dist/packages/contracts/src/kinds.d.ts +5398 -0
- package/dist/packages/contracts/src/kinds.js +156 -0
- package/dist/packages/contracts/src/report-presentation.d.ts +346 -0
- package/dist/packages/contracts/src/report-presentation.js +120 -0
- package/dist/packages/contracts/src/solana-inspection.d.ts +451 -0
- package/dist/packages/contracts/src/solana-inspection.js +94 -0
- package/dist/packages/contracts/src/token-market.d.ts +193 -0
- package/dist/packages/contracts/src/token-market.js +335 -0
- package/dist/packages/contracts/src/wallet-analysis.d.ts +866 -0
- package/dist/packages/contracts/src/wallet-analysis.js +89 -0
- package/dist/packages/contracts/src/watchtower.d.ts +1141 -0
- package/dist/packages/contracts/src/watchtower.js +196 -0
- package/dist/packages/contracts/src/workflow.d.ts +1568 -0
- package/dist/packages/contracts/src/workflow.js +651 -0
- package/dist/packages/inspector/src/decode.d.ts +23 -0
- package/dist/packages/inspector/src/decode.js +150 -0
- package/dist/packages/inspector/src/scope.d.ts +87 -0
- package/dist/packages/inspector/src/scope.js +64 -0
- package/dist/packages/model/src/analysis.d.ts +149 -0
- package/dist/packages/model/src/analysis.js +387 -0
- package/dist/packages/model/src/pricing.d.ts +38 -0
- package/dist/packages/model/src/pricing.js +49 -0
- package/dist/packages/model/src/retry.d.ts +20 -0
- package/dist/packages/model/src/retry.js +31 -0
- package/dist/packages/model/src/schema.d.ts +10 -0
- package/dist/packages/model/src/schema.js +51 -0
- package/dist/packages/model/src/summary.d.ts +91 -0
- package/dist/packages/model/src/summary.js +177 -0
- package/dist/packages/model/src/types.d.ts +81 -0
- package/dist/packages/model/src/types.js +19 -0
- package/dist/packages/monitoring/src/delivery.d.ts +32 -0
- package/dist/packages/monitoring/src/delivery.js +53 -0
- package/dist/packages/providers/src/chain-transport.d.ts +42 -0
- package/dist/packages/providers/src/chain-transport.js +57 -0
- package/dist/packages/providers/src/coverage.d.ts +105 -0
- package/dist/packages/providers/src/coverage.js +260 -0
- package/dist/packages/providers/src/health.d.ts +273 -0
- package/dist/packages/providers/src/health.js +505 -0
- package/dist/packages/providers/src/keyed.d.ts +96 -0
- package/dist/packages/providers/src/keyed.js +240 -0
- package/dist/packages/providers/src/snapshot.d.ts +61 -0
- package/dist/packages/providers/src/snapshot.js +77 -0
- package/dist/packages/publication/src/fixtures.d.ts +45 -0
- package/dist/packages/publication/src/fixtures.js +350 -0
- package/dist/packages/research/src/index.d.ts +188 -0
- package/dist/packages/research/src/index.js +829 -0
- package/dist/packages/runtime/src/checkpoints.d.ts +65 -0
- package/dist/packages/runtime/src/checkpoints.js +214 -0
- package/dist/packages/runtime/src/policy.d.ts +57 -0
- package/dist/packages/runtime/src/policy.js +296 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.js +4 -0
- package/dist/packages/sdk/src/bin/agentex.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex.js +3 -0
- package/dist/packages/sdk/src/buyer-cli.d.ts +10 -0
- package/dist/packages/sdk/src/buyer-cli.js +210 -0
- package/dist/packages/sdk/src/buyer.d.ts +534 -0
- package/dist/packages/sdk/src/buyer.js +441 -0
- package/dist/packages/sdk/src/cli.d.ts +14 -0
- package/dist/packages/sdk/src/cli.js +149 -0
- package/dist/packages/sdk/src/errors.d.ts +31 -0
- package/dist/packages/sdk/src/errors.js +24 -0
- package/dist/packages/sdk/src/index.d.ts +236 -0
- package/dist/packages/sdk/src/index.js +150 -0
- package/dist/packages/sdk/src/local.d.ts +11 -0
- package/dist/packages/sdk/src/local.js +110 -0
- package/dist/packages/sdk/src/rails.d.ts +59 -0
- package/dist/packages/sdk/src/rails.js +96 -0
- package/dist/packages/sdk/src/report.d.ts +121 -0
- package/dist/packages/sdk/src/report.js +114 -0
- package/dist/packages/sdk/src/version.d.ts +2 -0
- package/dist/packages/sdk/src/version.js +2 -0
- package/dist/packages/watchtower/src/index.d.ts +150 -0
- package/dist/packages/watchtower/src/index.js +786 -0
- package/dist/packages/workflow/src/registry.d.ts +61 -0
- package/dist/packages/workflow/src/registry.js +76 -0
- package/examples/README.md +34 -0
- package/examples/cli-usage.sh +30 -0
- package/examples/fixtures/base-weth-input.json +4 -0
- package/examples/focused-researcher.json +127 -0
- package/examples/pay-with-eth-robinhood.mts +37 -0
- package/examples/pay-with-usdc.mts +41 -0
- package/examples/quickstart.mts +73 -0
- package/package.json +50 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { isAddress } from 'viem';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { AtomicAmountSchema, FailureCategorySchema, contentHash } from './index.js';
|
|
4
|
+
import { presentationIssues, ReportCoverageSchema, ReportDatasetSchema, ReportPresentationSchema, ReportSignalSchema, WorkflowPresentationSchema } from './report-presentation.js';
|
|
5
|
+
/**
|
|
6
|
+
* General creator workflows (`agentex.workflow.v1`). Kept outside index.ts so the fixed first-party manifests, their schemas
|
|
7
|
+
* and hashes stay byte-for-byte unchanged. A workflow is typed data: declared tools from the platform registry, typed bindings,
|
|
8
|
+
* deterministic conditions/transforms and report sections. Descriptive text never grants a capability; permissions are derived
|
|
9
|
+
* only from `tools`, `permissions`, `secrets` and `limits` and are enforced by runtime policy outside nodes.
|
|
10
|
+
*/
|
|
11
|
+
export const WORKFLOW_SCHEMA_ID = 'agentex.workflow.v1';
|
|
12
|
+
export const MODEL_PROVIDER_UNCONFIGURED = 'AI analysis is not available on this server, so AI analysis steps stay switched off and no model is called.';
|
|
13
|
+
/** L04: models a creator may select for an AI analysis node. Mirrors the priced server tariff table (packages/model MODEL_TARIFFS); others are refused. */
|
|
14
|
+
export const WORKFLOW_ALLOWED_MODELS = ['claude-opus-5', 'claude-opus-4-8'];
|
|
15
|
+
export const WorkflowChainKeySchema = z.enum(['ethereum', 'base', 'robinhood', 'solana']);
|
|
16
|
+
export const ToolCapabilitySchema = z.enum(['evm.read', 'solana.read', 'http.read']);
|
|
17
|
+
/** Methods a read capability may ever request. Signing, sending and account-management methods are absent by construction. */
|
|
18
|
+
export const CAPABILITY_METHODS = Object.freeze({
|
|
19
|
+
// R02: `debug_traceTransaction` (only with the fixed prestate-diff tracer, enforced by the runtime) for transaction flows through the keyed archive endpoint.
|
|
20
|
+
'evm.read': ['eth_chainId', 'eth_blockNumber', 'eth_getBlockByNumber', 'eth_getBlockByHash', 'eth_getCode', 'eth_getStorageAt', 'eth_call', 'eth_estimateGas', 'eth_getLogs', 'eth_getBalance', 'eth_getTransactionByHash', 'eth_getTransactionReceipt', 'eth_getTransactionCount', 'debug_traceTransaction'],
|
|
21
|
+
// R02: validator health reads, each filtered to one in-scope vote account or identity by the runtime.
|
|
22
|
+
'solana.read': ['getGenesisHash', 'getSlot', 'getAccountInfo', 'getMultipleAccounts', 'getBalance', 'getSignaturesForAddress', 'getTransaction', 'getTokenSupply', 'getTokenLargestAccounts', 'simulateTransaction', 'getVoteAccounts', 'getEpochInfo', 'getBlockProduction', 'getRecentPerformanceSamples'],
|
|
23
|
+
'http.read': ['GET'],
|
|
24
|
+
});
|
|
25
|
+
const CHAIN_CAPABILITIES = { ethereum: ['evm.read', 'http.read'], base: ['evm.read', 'http.read'], robinhood: ['evm.read', 'http.read'], solana: ['solana.read', 'http.read'] };
|
|
26
|
+
const Identifier = z.string().regex(/^[a-z][a-z0-9._-]{0,63}$/, 'Use lowercase letters, digits, dots, underscores or hyphens (at most 64 characters)');
|
|
27
|
+
const PropertyIdentifier = z.string().regex(/^[a-z][a-zA-Z0-9_]{0,63}$/, 'Use a camelCase property name').refine((value) => !['constructor', 'prototype', '__proto__'].includes(value), 'Reserved property name');
|
|
28
|
+
const Semver = z.string().regex(/^\d+\.\d+\.\d+$/);
|
|
29
|
+
const Hash = z.string().regex(/^[a-f0-9]{64}$/);
|
|
30
|
+
const ToolRef = z.string().regex(/^[a-z][a-z0-9._-]{0,63}@\d+\.\d+\.\d+$/);
|
|
31
|
+
const Scalar = z.union([z.string().max(512), z.number().finite(), z.boolean()]);
|
|
32
|
+
const DecimalString = z.string().regex(/^-?(0|[1-9][0-9]{0,77})(\.[0-9]{1,78})?$/, 'Expected an exact decimal number');
|
|
33
|
+
const ValuePath = z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]{0,63}(\.[a-zA-Z0-9_]{1,64}){0,3}$/, 'Expected a dotted property path');
|
|
34
|
+
const Hostname = z.string().regex(/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/, 'Expected a lowercase public DNS hostname');
|
|
35
|
+
/** Input names that would collect credentials. Buyer inputs are public research parameters; secrets are server-side references only. */
|
|
36
|
+
const SECRET_NAME = /(private_?key|secret|password|passphrase|mnemonic|seed_?phrase|api_?key|access_?token|auth_?token|credential)/i;
|
|
37
|
+
const SECRET_CONTENT = /-----BEGIN [A-Z ]*PRIVATE KEY|\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,}|\bsk-[A-Za-z0-9_-]{16,}|\bAKIA[0-9A-Z]{16}\b|\bxox[abprs]-[A-Za-z0-9-]{8,}|\bgh[pousr]_[A-Za-z0-9]{20,}|\b(?:api[_-]?key|secret|password|mnemonic|private[_-]?key)\s*[:=]\s*\S{6,}/i;
|
|
38
|
+
export const WorkflowFieldSchema = z.strictObject({ type: z.enum(['string', 'evm-address', 'solana-address', 'integer', 'boolean']), description: z.string().max(500), enum: z.array(z.string().max(200)).min(1).max(50).optional() });
|
|
39
|
+
export const WorkflowInputSchemaDefinition = z.strictObject({ type: z.literal('object'), properties: z.record(PropertyIdentifier, WorkflowFieldSchema), required: z.array(PropertyIdentifier).max(32), additionalProperties: z.literal(false) }).superRefine((schema, ctx) => {
|
|
40
|
+
if (Object.keys(schema.properties).length > 32)
|
|
41
|
+
ctx.addIssue({ code: 'custom', message: 'At most 32 input properties' });
|
|
42
|
+
for (const key of schema.required)
|
|
43
|
+
if (!Object.hasOwn(schema.properties, key))
|
|
44
|
+
ctx.addIssue({ code: 'custom', path: ['required'], message: `Required input ${key} is not declared` });
|
|
45
|
+
for (const [key, field] of Object.entries(schema.properties)) {
|
|
46
|
+
if (SECRET_NAME.test(key))
|
|
47
|
+
ctx.addIssue({ code: 'custom', path: ['properties', key], message: `Input ${key} looks like a credential. Buyer inputs cannot collect secrets; declare a secret reference instead.` });
|
|
48
|
+
if (field.enum && field.type !== 'string')
|
|
49
|
+
ctx.addIssue({ code: 'custom', path: ['properties', key, 'enum'], message: 'Only string inputs can declare an enum' });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
export const WorkflowBindingSchema = z.discriminatedUnion('source', [
|
|
53
|
+
z.strictObject({ source: z.literal('input'), path: PropertyIdentifier }),
|
|
54
|
+
z.strictObject({ source: z.literal('node'), nodeId: Identifier, path: ValuePath.optional() }),
|
|
55
|
+
z.strictObject({ source: z.literal('literal'), value: Scalar }),
|
|
56
|
+
]);
|
|
57
|
+
export const ConditionOperatorSchema = z.enum(['equals', 'exists', 'gt', 'lt', 'gte', 'lte']);
|
|
58
|
+
export const TransformSchema = z.discriminatedUnion('op', [
|
|
59
|
+
z.strictObject({ op: z.literal('pick'), from: WorkflowBindingSchema }),
|
|
60
|
+
z.strictObject({ op: z.literal('compare'), left: WorkflowBindingSchema, operator: ConditionOperatorSchema.exclude(['exists']), right: WorkflowBindingSchema }),
|
|
61
|
+
z.strictObject({ op: z.literal('threshold'), value: WorkflowBindingSchema, threshold: DecimalString }),
|
|
62
|
+
z.strictObject({ op: z.literal('format-amount'), amount: WorkflowBindingSchema, decimals: WorkflowBindingSchema }),
|
|
63
|
+
]);
|
|
64
|
+
export const FindingValueTypeSchema = z.enum(['string', 'boolean', 'integer', 'decimal', 'address']);
|
|
65
|
+
const NodeBase = { id: Identifier, label: z.string().max(100).optional(), dependsOn: z.array(Identifier).max(16) };
|
|
66
|
+
export const WorkflowNodeSchema = z.discriminatedUnion('type', [
|
|
67
|
+
z.strictObject({ ...NodeBase, type: z.literal('input') }),
|
|
68
|
+
z.strictObject({ ...NodeBase, type: z.literal('tool'), tool: Identifier, arguments: z.record(PropertyIdentifier, WorkflowBindingSchema) }),
|
|
69
|
+
z.strictObject({ ...NodeBase, type: z.literal('condition'), field: WorkflowBindingSchema, operator: ConditionOperatorSchema, value: Scalar.optional(), whenTrue: Identifier.nullable(), whenFalse: Identifier.nullable() }),
|
|
70
|
+
z.strictObject({ ...NodeBase, type: z.literal('transform'), transform: TransformSchema }),
|
|
71
|
+
z.strictObject({ ...NodeBase, type: z.literal('report-section'), section: Identifier, findings: z.array(z.strictObject({ code: Identifier, value: WorkflowBindingSchema })).min(1).max(16) }),
|
|
72
|
+
z.strictObject({ ...NodeBase, type: z.literal('ai-analysis'), instructions: z.string().min(1).max(2000), inputs: z.record(PropertyIdentifier, WorkflowBindingSchema), maximumTokens: z.number().int().min(1).max(8000), model: z.enum(WORKFLOW_ALLOWED_MODELS).optional() }),
|
|
73
|
+
z.strictObject({ ...NodeBase, type: z.literal('output'), sections: z.array(Identifier).min(1).max(12) }),
|
|
74
|
+
/**
|
|
75
|
+
* Deterministic creator transform source (one ES module exporting `default (input) => JSON`). It is data in the package and is
|
|
76
|
+
* executed only by the platform's isolated sandbox runner (no network, read-only filesystem, CPU/memory/time caps). Without a
|
|
77
|
+
* configured runner the node is declared but disabled, exactly like an AI analysis node without a model provider.
|
|
78
|
+
*/
|
|
79
|
+
z.strictObject({ ...NodeBase, type: z.literal('sandboxed-transform'), inputs: z.record(PropertyIdentifier, WorkflowBindingSchema), source: z.string().min(1).max(16000), timeoutMs: z.number().int().min(500).max(10000) }),
|
|
80
|
+
]);
|
|
81
|
+
export const WorkflowToolDeclarationSchema = z.strictObject({ id: Identifier, version: Semver, capability: ToolCapabilitySchema, chains: z.array(WorkflowChainKeySchema).min(1).max(4), maximumCalls: z.number().int().min(1).max(64) });
|
|
82
|
+
/**
|
|
83
|
+
* N03/N04: a reviewed remote connector pinned by the version. `id` is the alias tool nodes name, `schemaHash` the approved connector version's
|
|
84
|
+
* declaration hash, `maximumCalls` the per-run call ceiling and `maximumCostMicrousd` the per-run metered cost ceiling. Absent keeps existing
|
|
85
|
+
* manifests byte-identical. Connector nodes run only on servers with AGENTEX_CONNECTORS_ENABLED=true, through the connector gateway.
|
|
86
|
+
*/
|
|
87
|
+
export const WorkflowConnectorDeclarationSchema = z.strictObject({ id: Identifier, connectorId: z.uuid(), schemaHash: Hash, maximumCalls: z.number().int().min(1).max(16), maximumCostMicrousd: z.number().int().min(0).max(1_000_000) });
|
|
88
|
+
export const WORKFLOW_CONNECTOR_TOOL_VERSION = '1.0.0';
|
|
89
|
+
/** Evidence and step tool reference of a connector node. Aliases never collide with declared tool ids. */
|
|
90
|
+
export const connectorToolRef = (alias) => `${alias}@${WORKFLOW_CONNECTOR_TOOL_VERSION}`;
|
|
91
|
+
export const CONNECTOR_CALL_METHOD = 'POST';
|
|
92
|
+
export const CONNECTORS_DISABLED = 'Remote connectors are switched off on this server, so workflows that declare connectors cannot be saved, versioned or run.';
|
|
93
|
+
export const WorkflowOutputSchemaDefinition = z.strictObject({
|
|
94
|
+
sections: z.array(z.strictObject({ id: Identifier, title: z.string().min(1).max(100), required: z.boolean(),
|
|
95
|
+
findings: z.array(z.strictObject({ code: Identifier, label: z.string().min(1).max(100), valueType: FindingValueTypeSchema })).min(1).max(16) })).min(1).max(12),
|
|
96
|
+
});
|
|
97
|
+
/** Hosted use, source viewing and fork permission are separate rights. Per-run fees pay for platform execution only. */
|
|
98
|
+
export const WorkflowRightsSchema = z.strictObject({
|
|
99
|
+
hostedUse: z.enum(['owner-only', 'listed-buyers']), sourceView: z.enum(['owner-only', 'public']), fork: z.enum(['not-permitted', 'permitted', 'permitted-with-attribution']),
|
|
100
|
+
attributionNotice: z.string().max(300).optional(),
|
|
101
|
+
});
|
|
102
|
+
export const AttributionSchema = z.strictObject({ sourceId: Identifier, sourceVersionHash: Hash, author: z.string().min(1).max(100), license: z.string().min(1).max(100), notice: z.string().max(300) });
|
|
103
|
+
export const WorkflowManifestSchema = z.strictObject({
|
|
104
|
+
schemaVersion: z.literal('1.0'), id: Identifier, version: Semver, name: z.string().min(1).max(100), description: z.string().min(1).max(2000),
|
|
105
|
+
author: z.strictObject({ id: Identifier, name: z.string().min(1).max(100), ownership: z.enum(['first-party', 'independent']) }),
|
|
106
|
+
license: z.string().min(1).max(100), visibility: z.enum(['private', 'source-visible', 'source-private']), rights: WorkflowRightsSchema, attribution: z.array(AttributionSchema).max(8),
|
|
107
|
+
chains: z.array(WorkflowChainKeySchema).min(1).max(4),
|
|
108
|
+
inputSchema: WorkflowInputSchemaDefinition, outputSchemaId: z.literal(WORKFLOW_SCHEMA_ID), outputSchema: WorkflowOutputSchemaDefinition,
|
|
109
|
+
tools: z.array(WorkflowToolDeclarationSchema).min(1).max(16),
|
|
110
|
+
/** N03/N04 reviewed remote connectors pinned by this version (optional; absent keeps older manifests byte-identical). */
|
|
111
|
+
connectors: z.array(WorkflowConnectorDeclarationSchema).max(4).optional(),
|
|
112
|
+
permissions: z.strictObject({ addressScope: z.enum(['input-bound', 'input-derived']), domains: z.array(Hostname).max(8), destinations: z.tuple([z.literal('run-report')]) }),
|
|
113
|
+
modelPolicy: z.discriminatedUnion('mode', [z.strictObject({ mode: z.literal('deterministic'), maximumModelTokens: z.literal(0) }), z.strictObject({ mode: z.literal('bounded-model'), maximumModelTokens: z.number().int().min(1).max(32000),
|
|
114
|
+
/** L04: per-run model cost cap in micro-USD. Required to price a workflow with AI analysis; quotes and the worker enforce the same cap. Absent keeps older manifests byte-identical. */
|
|
115
|
+
maximumCostMicrousd: z.number().int().min(1).max(200000).optional() })]),
|
|
116
|
+
secrets: z.array(z.strictObject({ name: z.string().regex(/^[A-Z][A-Z0-9_]{0,63}$/), reference: z.string().regex(/^secret:\/\/[a-z0-9-]{1,40}\/[a-z0-9._-]{1,80}$/, 'Secrets are references such as secret://binding/name, never values'), purpose: z.string().max(200) })).max(8),
|
|
117
|
+
limits: z.strictObject({ maximumSteps: z.number().int().min(3).max(32), maximumRpcCalls: z.number().int().min(1).max(64), maximumDurationMs: z.number().int().min(1000).max(120000), stepTimeoutMs: z.number().int().min(500).max(60000),
|
|
118
|
+
maximumOutputBytes: z.number().int().min(1024).max(262144), maximumGraphDepth: z.number().int().min(2).max(16), maximumBlockRange: z.number().int().min(0).max(250_000_000), maximumPages: z.number().int().min(0).max(5), maximumExecutionAtomic: AtomicAmountSchema }),
|
|
119
|
+
workflow: z.array(WorkflowNodeSchema).min(3).max(32),
|
|
120
|
+
sampleOutput: z.array(z.strictObject({ section: Identifier, findings: z.record(Identifier, Scalar) })).max(12),
|
|
121
|
+
testCases: z.array(z.strictObject({ id: Identifier, chain: WorkflowChainKeySchema, values: z.record(PropertyIdentifier, Scalar) })).max(20),
|
|
122
|
+
/** R01 report archetype and blocks (optional; absent keeps older manifests byte-identical and renders the generic workflow layout). */
|
|
123
|
+
presentation: WorkflowPresentationSchema.optional(),
|
|
124
|
+
}).superRefine((manifest, ctx) => {
|
|
125
|
+
const issue = (message, path = []) => ctx.addIssue({ code: 'custom', path, message });
|
|
126
|
+
const nodes = new Map(manifest.workflow.map((node) => [node.id, node]));
|
|
127
|
+
const index = new Map(manifest.workflow.map((node, position) => [node.id, position]));
|
|
128
|
+
const tools = new Map(manifest.tools.map((tool) => [tool.id, tool]));
|
|
129
|
+
const sections = new Map(manifest.outputSchema.sections.map((section) => [section.id, section]));
|
|
130
|
+
if (nodes.size !== manifest.workflow.length)
|
|
131
|
+
issue('Duplicate workflow node id', ['workflow']);
|
|
132
|
+
if (tools.size !== manifest.tools.length)
|
|
133
|
+
issue('Duplicate tool id', ['tools']);
|
|
134
|
+
const connectors = new Map((manifest.connectors ?? []).map((connector) => [connector.id, connector]));
|
|
135
|
+
if (connectors.size !== (manifest.connectors ?? []).length)
|
|
136
|
+
issue('Duplicate connector alias', ['connectors']);
|
|
137
|
+
if (new Set((manifest.connectors ?? []).map((connector) => connector.connectorId)).size !== (manifest.connectors ?? []).length)
|
|
138
|
+
issue('A connector is pinned more than once', ['connectors']);
|
|
139
|
+
(manifest.connectors ?? []).forEach((connector, position) => {
|
|
140
|
+
if (tools.has(connector.id))
|
|
141
|
+
issue(`Connector alias ${connector.id} collides with a declared tool id`, ['connectors', position, 'id']);
|
|
142
|
+
if (connector.maximumCalls > manifest.limits.maximumRpcCalls)
|
|
143
|
+
issue(`Connector ${connector.id} exceeds the workflow call budget`, ['connectors', position, 'maximumCalls']);
|
|
144
|
+
});
|
|
145
|
+
if (sections.size !== manifest.outputSchema.sections.length)
|
|
146
|
+
issue('Duplicate output section id', ['outputSchema', 'sections']);
|
|
147
|
+
if (new Set(manifest.chains).size !== manifest.chains.length)
|
|
148
|
+
issue('Duplicate chain', ['chains']);
|
|
149
|
+
if (manifest.visibility !== 'source-visible' && manifest.rights.sourceView === 'public')
|
|
150
|
+
issue('Public source viewing requires source-visible visibility', ['rights', 'sourceView']);
|
|
151
|
+
if (manifest.rights.fork !== 'not-permitted' && manifest.rights.sourceView !== 'public')
|
|
152
|
+
issue('Fork permission requires publicly viewable source', ['rights', 'fork']);
|
|
153
|
+
if (manifest.rights.fork === 'permitted-with-attribution' && !manifest.rights.attributionNotice)
|
|
154
|
+
issue('Attribution-required forks need an attribution notice', ['rights', 'attributionNotice']);
|
|
155
|
+
if (new Set(manifest.attribution.map((item) => item.sourceVersionHash)).size !== manifest.attribution.length)
|
|
156
|
+
issue('Duplicate attribution source', ['attribution']);
|
|
157
|
+
if (manifest.workflow.length > manifest.limits.maximumSteps)
|
|
158
|
+
issue(`The workflow has ${manifest.workflow.length} nodes but maximumSteps is ${manifest.limits.maximumSteps}`, ['limits', 'maximumSteps']);
|
|
159
|
+
if (manifest.workflow.filter((node) => node.type === 'input').length !== 1)
|
|
160
|
+
issue('Workflow must have exactly one input node', ['workflow']);
|
|
161
|
+
if (manifest.workflow.filter((node) => node.type === 'output').length !== 1)
|
|
162
|
+
issue('Workflow must have exactly one output node', ['workflow']);
|
|
163
|
+
for (const section of manifest.outputSchema.sections)
|
|
164
|
+
if (new Set(section.findings.map((finding) => finding.code)).size !== section.findings.length)
|
|
165
|
+
issue(`Duplicate finding code in section ${section.id}`, ['outputSchema', 'sections']);
|
|
166
|
+
const capabilities = new Set(manifest.tools.map((tool) => tool.capability));
|
|
167
|
+
manifest.tools.forEach((tool, position) => {
|
|
168
|
+
for (const chain of manifest.chains) {
|
|
169
|
+
if (!tool.chains.includes(chain))
|
|
170
|
+
issue(`Tool ${tool.id} cannot execute on declared chain ${chain}`, ['tools', position, 'chains']);
|
|
171
|
+
else if (!CHAIN_CAPABILITIES[chain].includes(tool.capability))
|
|
172
|
+
issue(`Capability ${tool.capability} of tool ${tool.id} is impossible on chain ${chain}`, ['tools', position, 'capability']);
|
|
173
|
+
}
|
|
174
|
+
if (tool.maximumCalls > manifest.limits.maximumRpcCalls)
|
|
175
|
+
issue(`Tool ${tool.id} exceeds the workflow call budget`, ['tools', position, 'maximumCalls']);
|
|
176
|
+
});
|
|
177
|
+
if (capabilities.has('http.read') !== manifest.permissions.domains.length > 0)
|
|
178
|
+
issue(capabilities.has('http.read') ? 'An http.read tool needs at least one allowed domain' : 'Domain grants require a declared http.read tool', ['permissions', 'domains']);
|
|
179
|
+
if (manifest.secrets.length && !capabilities.has('http.read'))
|
|
180
|
+
issue('Secret references are only usable by declared http.read tools', ['secrets']);
|
|
181
|
+
if (new Set(manifest.secrets.map((secret) => secret.name)).size !== manifest.secrets.length)
|
|
182
|
+
issue('Duplicate secret reference name', ['secrets']);
|
|
183
|
+
const aiNodes = manifest.workflow.filter((node) => node.type === 'ai-analysis');
|
|
184
|
+
if (aiNodes.length && manifest.modelPolicy.mode !== 'bounded-model')
|
|
185
|
+
issue('AI analysis nodes need a bounded-model policy', ['modelPolicy']);
|
|
186
|
+
if (!aiNodes.length && manifest.modelPolicy.mode !== 'deterministic')
|
|
187
|
+
issue('A bounded-model policy without AI analysis nodes grants an unused model budget', ['modelPolicy']);
|
|
188
|
+
const declaredModelTokens = aiNodes.reduce((total, node) => total + (node.type === 'ai-analysis' ? node.maximumTokens : 0), 0);
|
|
189
|
+
if (declaredModelTokens > manifest.modelPolicy.maximumModelTokens)
|
|
190
|
+
issue(`AI analysis nodes declare ${declaredModelTokens} output tokens but the model policy allows ${manifest.modelPolicy.maximumModelTokens}`, ['modelPolicy', 'maximumModelTokens']);
|
|
191
|
+
manifest.workflow.forEach((node, position) => { if (node.type === 'sandboxed-transform' && node.timeoutMs > manifest.limits.stepTimeoutMs)
|
|
192
|
+
issue(`Sandboxed transform ${node.id} timeout exceeds the step timeout`, ['workflow', position, 'timeoutMs']); });
|
|
193
|
+
const producedSections = new Map();
|
|
194
|
+
const nodeTarget = (id) => nodes.get(id)?.type;
|
|
195
|
+
manifest.workflow.forEach((node, position) => {
|
|
196
|
+
const at = (...rest) => ['workflow', position, ...rest];
|
|
197
|
+
for (const dependency of node.dependsOn) {
|
|
198
|
+
if (!nodes.has(dependency))
|
|
199
|
+
issue(`Node ${node.id} references missing dependency ${dependency}`, at('dependsOn'));
|
|
200
|
+
else if (['report-section', 'output'].includes(nodeTarget(dependency)) && node.type !== 'output')
|
|
201
|
+
issue(`Node ${node.id} cannot depend on ${nodeTarget(dependency)} node ${dependency}`, at('dependsOn'));
|
|
202
|
+
}
|
|
203
|
+
if (new Set(node.dependsOn).size !== node.dependsOn.length)
|
|
204
|
+
issue(`Node ${node.id} repeats a dependency`, at('dependsOn'));
|
|
205
|
+
if (node.type === 'input' && node.dependsOn.length)
|
|
206
|
+
issue('Input node cannot have dependencies', at('dependsOn'));
|
|
207
|
+
if (node.type !== 'input' && !node.dependsOn.length)
|
|
208
|
+
issue(`Node ${node.id} is disconnected from input`, at('dependsOn'));
|
|
209
|
+
if (node.type === 'tool' && !tools.has(node.tool) && !connectors.has(node.tool))
|
|
210
|
+
issue(`Undeclared tool ${node.tool}`, at('tool'));
|
|
211
|
+
// Item 10: fixed http.read URLs must already be inside the granted domains (bound URLs are checked by the runtime policy at call time).
|
|
212
|
+
if (node.type === 'tool' && tools.get(node.tool)?.capability === 'http.read')
|
|
213
|
+
for (const [key, binding] of Object.entries(node.arguments)) {
|
|
214
|
+
if (binding.source !== 'literal' || typeof binding.value !== 'string' || !/url/i.test(key))
|
|
215
|
+
continue;
|
|
216
|
+
let host = null;
|
|
217
|
+
try {
|
|
218
|
+
const url = new URL(binding.value);
|
|
219
|
+
if (url.protocol === 'https:' && !url.username && !url.password && !url.port)
|
|
220
|
+
host = url.hostname.toLowerCase();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
host = null;
|
|
224
|
+
}
|
|
225
|
+
if (host === null)
|
|
226
|
+
issue(`Tool ${node.id} argument ${key} must be a credential-free https URL on the default port`, at('arguments', key));
|
|
227
|
+
else if (!manifest.permissions.domains.includes(host))
|
|
228
|
+
issue(`Tool ${node.id} reads ${host}, which is outside the granted domains`, at('arguments', key));
|
|
229
|
+
}
|
|
230
|
+
if (node.type === 'condition') {
|
|
231
|
+
if (node.whenTrue === null && node.whenFalse === null)
|
|
232
|
+
issue('A condition needs at least one branch target', at());
|
|
233
|
+
for (const target of [node.whenTrue, node.whenFalse])
|
|
234
|
+
if (target !== null && !nodes.get(target)?.dependsOn.includes(node.id))
|
|
235
|
+
issue(`Condition branch ${target} must exist and depend on ${node.id}`, at());
|
|
236
|
+
if (node.whenTrue !== null && node.whenTrue === node.whenFalse)
|
|
237
|
+
issue('Condition branches must differ', at());
|
|
238
|
+
if (node.operator === 'exists' && node.value !== undefined)
|
|
239
|
+
issue('An exists condition takes no comparison value', at('value'));
|
|
240
|
+
if (node.operator === 'equals' && node.value === undefined)
|
|
241
|
+
issue('Equality condition needs a value', at('value'));
|
|
242
|
+
if (['gt', 'lt', 'gte', 'lte'].includes(node.operator) && !isDecimal(node.value))
|
|
243
|
+
issue('Numeric conditions need an exact numeric value', at('value'));
|
|
244
|
+
}
|
|
245
|
+
if (node.type === 'report-section') {
|
|
246
|
+
const section = sections.get(node.section);
|
|
247
|
+
if (!section)
|
|
248
|
+
issue(`Report section ${node.section} is not declared in the output schema`, at('section'));
|
|
249
|
+
else
|
|
250
|
+
for (const finding of node.findings)
|
|
251
|
+
if (!section.findings.some((item) => item.code === finding.code))
|
|
252
|
+
issue(`Finding ${finding.code} is not declared in section ${node.section}`, at('findings'));
|
|
253
|
+
if (producedSections.has(node.section))
|
|
254
|
+
issue(`Section ${node.section} is produced by more than one node`, at('section'));
|
|
255
|
+
producedSections.set(node.section, node.id);
|
|
256
|
+
if (new Set(node.findings.map((finding) => finding.code)).size !== node.findings.length)
|
|
257
|
+
issue(`Duplicate finding in ${node.id}`, at('findings'));
|
|
258
|
+
}
|
|
259
|
+
if (node.type === 'output') {
|
|
260
|
+
for (const id of node.sections)
|
|
261
|
+
if (nodeTarget(id) !== 'report-section' || !node.dependsOn.includes(id))
|
|
262
|
+
issue(`Output section ${id} must be a report-section dependency`, at('sections'));
|
|
263
|
+
if (node.dependsOn.some((id) => !node.sections.includes(id)))
|
|
264
|
+
issue('Output may depend only on its report sections', at('dependsOn'));
|
|
265
|
+
}
|
|
266
|
+
for (const binding of workflowBindings(node)) {
|
|
267
|
+
if (binding.source === 'node') {
|
|
268
|
+
if (!node.dependsOn.includes(binding.nodeId))
|
|
269
|
+
issue(`Binding to ${binding.nodeId} must name an explicit dependency of ${node.id}`, at());
|
|
270
|
+
else if (!['tool', 'condition', 'transform', 'ai-analysis', 'sandboxed-transform'].includes(nodeTarget(binding.nodeId) ?? ''))
|
|
271
|
+
issue(`Node ${binding.nodeId} produces no bindable value`, at());
|
|
272
|
+
}
|
|
273
|
+
if (binding.source === 'input' && !Object.hasOwn(manifest.inputSchema.properties, binding.path))
|
|
274
|
+
issue(`Unknown input property ${binding.path}`, at());
|
|
275
|
+
if (binding.source === 'literal' && typeof binding.value === 'string' && (/^0x[a-fA-F0-9]{64}$/.test(binding.value) || SECRET_CONTENT.test(binding.value)))
|
|
276
|
+
issue('Literal values cannot embed 32-byte secrets or credentials; bind them as inputs or secret references', at());
|
|
277
|
+
}
|
|
278
|
+
if (node.type === 'transform' && node.transform.op === 'threshold' && !isDecimal(node.transform.threshold))
|
|
279
|
+
issue('Threshold must be an exact decimal', at('transform'));
|
|
280
|
+
if (node.type === 'condition' && typeof node.value === 'string' && (/^0x[a-fA-F0-9]{64}$/.test(node.value) || SECRET_CONTENT.test(node.value)))
|
|
281
|
+
issue('Condition values cannot embed 32-byte secrets or credentials; bind them as inputs', at('value'));
|
|
282
|
+
});
|
|
283
|
+
for (const section of manifest.outputSchema.sections)
|
|
284
|
+
if (section.required && !producedSections.has(section.id))
|
|
285
|
+
issue(`Required section ${section.id} has no report-section node`, ['outputSchema', 'sections']);
|
|
286
|
+
const output = manifest.workflow.find((node) => node.type === 'output');
|
|
287
|
+
if (output?.type === 'output')
|
|
288
|
+
for (const section of manifest.outputSchema.sections) {
|
|
289
|
+
const producer = producedSections.get(section.id);
|
|
290
|
+
if (producer && !output.sections.includes(producer))
|
|
291
|
+
issue(`Section ${section.id} is not included in the output`, ['workflow', index.get(output.id), 'sections']);
|
|
292
|
+
}
|
|
293
|
+
const visiting = new Set();
|
|
294
|
+
const depths = new Map();
|
|
295
|
+
let cycle = false;
|
|
296
|
+
const visit = (id) => {
|
|
297
|
+
if (visiting.has(id)) {
|
|
298
|
+
if (!cycle)
|
|
299
|
+
issue('Workflow contains a cycle', ['workflow']);
|
|
300
|
+
cycle = true;
|
|
301
|
+
return manifest.limits.maximumGraphDepth + 1;
|
|
302
|
+
}
|
|
303
|
+
const cached = depths.get(id);
|
|
304
|
+
if (cached !== undefined)
|
|
305
|
+
return cached;
|
|
306
|
+
visiting.add(id);
|
|
307
|
+
const depth = 1 + Math.max(0, ...(nodes.get(id)?.dependsOn ?? []).filter((dependency) => nodes.has(dependency)).map(visit));
|
|
308
|
+
visiting.delete(id);
|
|
309
|
+
depths.set(id, depth);
|
|
310
|
+
return depth;
|
|
311
|
+
};
|
|
312
|
+
for (const id of nodes.keys())
|
|
313
|
+
visit(id);
|
|
314
|
+
if (!cycle && Math.max(0, ...depths.values()) > manifest.limits.maximumGraphDepth)
|
|
315
|
+
issue('Workflow exceeds maximum graph depth', ['limits', 'maximumGraphDepth']);
|
|
316
|
+
const used = new Set();
|
|
317
|
+
const ancestors = (id) => { if (used.has(id))
|
|
318
|
+
return; used.add(id); for (const dependency of nodes.get(id)?.dependsOn ?? [])
|
|
319
|
+
ancestors(dependency); };
|
|
320
|
+
if (output)
|
|
321
|
+
ancestors(output.id);
|
|
322
|
+
for (const node of manifest.workflow)
|
|
323
|
+
if (!used.has(node.id))
|
|
324
|
+
issue(`Node ${node.id} does not contribute to the output`, ['workflow', index.get(node.id)]);
|
|
325
|
+
manifest.sampleOutput.forEach((sample, position) => {
|
|
326
|
+
const section = sections.get(sample.section);
|
|
327
|
+
if (!section) {
|
|
328
|
+
issue(`Sample section ${sample.section} is not declared`, ['sampleOutput', position]);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
for (const [code, value] of Object.entries(sample.findings)) {
|
|
332
|
+
const declared = section.findings.find((finding) => finding.code === code);
|
|
333
|
+
if (!declared)
|
|
334
|
+
issue(`Sample finding ${code} is not declared in ${sample.section}`, ['sampleOutput', position]);
|
|
335
|
+
else if (!findingValueMatches(value, declared.valueType))
|
|
336
|
+
issue(`Sample finding ${code} must be a ${declared.valueType}`, ['sampleOutput', position]);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
manifest.testCases.forEach((testCase, position) => { if (!manifest.chains.includes(testCase.chain))
|
|
340
|
+
issue(`Test case ${testCase.id} uses an undeclared chain`, ['testCases', position]); });
|
|
341
|
+
if (manifest.presentation)
|
|
342
|
+
for (const item of presentationIssues(manifest.presentation, manifest))
|
|
343
|
+
issue(item.message, item.path);
|
|
344
|
+
const serialized = JSON.stringify({ ...manifest, secrets: manifest.secrets.map((secret) => ({ ...secret, reference: '' })) });
|
|
345
|
+
if (SECRET_CONTENT.test(serialized))
|
|
346
|
+
issue('The package appears to embed a credential. Secrets must be references in private bindings, never values.', []);
|
|
347
|
+
});
|
|
348
|
+
export function workflowBindings(node) {
|
|
349
|
+
switch (node.type) {
|
|
350
|
+
case 'tool': return Object.values(node.arguments);
|
|
351
|
+
case 'condition': return [node.field];
|
|
352
|
+
case 'report-section': return node.findings.map((finding) => finding.value);
|
|
353
|
+
case 'ai-analysis':
|
|
354
|
+
case 'sandboxed-transform': return Object.values(node.inputs);
|
|
355
|
+
case 'transform': {
|
|
356
|
+
const t = node.transform;
|
|
357
|
+
return t.op === 'pick' ? [t.from] : t.op === 'compare' ? [t.left, t.right] : t.op === 'threshold' ? [t.value] : [t.amount, t.decimals];
|
|
358
|
+
}
|
|
359
|
+
default: return [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
export const isWorkflowManifest = (manifest) => manifest !== null && typeof manifest === 'object' && manifest.outputSchemaId === WORKFLOW_SCHEMA_ID;
|
|
363
|
+
/** Exact decimal helpers. Never floating point: numbers are only accepted when they are safe integers or already exact finite literals. */
|
|
364
|
+
export function isDecimal(value) {
|
|
365
|
+
return (typeof value === 'number' && Number.isFinite(value) && /^-?\d+(\.\d+)?$/.test(String(value))) || (typeof value === 'string' && /^-?(0|[1-9][0-9]{0,77})(\.[0-9]{1,78})?$/.test(value));
|
|
366
|
+
}
|
|
367
|
+
function scaled(value, scale) {
|
|
368
|
+
const text = String(value);
|
|
369
|
+
const negative = text.startsWith('-');
|
|
370
|
+
const [whole, fraction = ''] = (negative ? text.slice(1) : text).split('.');
|
|
371
|
+
const result = BigInt(whole + fraction.padEnd(scale, '0'));
|
|
372
|
+
return negative ? -result : result;
|
|
373
|
+
}
|
|
374
|
+
/** Compares two exact decimals; returns -1, 0 or 1. */
|
|
375
|
+
export function compareDecimal(left, right) {
|
|
376
|
+
const scale = Math.max(String(left).split('.')[1]?.length ?? 0, String(right).split('.')[1]?.length ?? 0);
|
|
377
|
+
const a = scaled(left, scale);
|
|
378
|
+
const b = scaled(right, scale);
|
|
379
|
+
return a === b ? 0 : a > b ? 1 : -1;
|
|
380
|
+
}
|
|
381
|
+
/** Formats an unsigned atomic integer with `decimals` places exactly, trimming only trailing fractional zeros. */
|
|
382
|
+
export function formatExactAmount(atomic, decimals) {
|
|
383
|
+
if (!/^(0|[1-9][0-9]{0,77})$/.test(String(atomic)) || !Number.isSafeInteger(decimals) || decimals < 0 || decimals > 255)
|
|
384
|
+
throw new RangeError('Expected an unsigned atomic integer and 0-255 decimals');
|
|
385
|
+
const digits = String(atomic).padStart(decimals + 1, '0');
|
|
386
|
+
const whole = digits.slice(0, digits.length - decimals);
|
|
387
|
+
const fraction = decimals ? digits.slice(-decimals).replace(/0+$/, '') : '';
|
|
388
|
+
return fraction ? `${whole}.${fraction}` : whole;
|
|
389
|
+
}
|
|
390
|
+
export function findingValueMatches(value, type) {
|
|
391
|
+
if (type === 'boolean')
|
|
392
|
+
return typeof value === 'boolean';
|
|
393
|
+
if (type === 'string')
|
|
394
|
+
return typeof value === 'string';
|
|
395
|
+
if (type === 'integer')
|
|
396
|
+
return (typeof value === 'number' && Number.isSafeInteger(value)) || (typeof value === 'string' && /^-?(0|[1-9][0-9]{0,77})$/.test(value));
|
|
397
|
+
if (type === 'decimal')
|
|
398
|
+
return isDecimal(value);
|
|
399
|
+
return typeof value === 'string' && (isAddress(value) || /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value));
|
|
400
|
+
}
|
|
401
|
+
/** Run input envelope. `values` are validated against the pinned manifest's input schema by `parseWorkflowRunInput`. */
|
|
402
|
+
export const WorkflowRunInputSchema = z.strictObject({ workflow: z.literal(WORKFLOW_SCHEMA_ID), chain: WorkflowChainKeySchema, values: z.record(PropertyIdentifier, Scalar) });
|
|
403
|
+
export function parseWorkflowRunInput(raw, manifest) {
|
|
404
|
+
const input = WorkflowRunInputSchema.parse(raw);
|
|
405
|
+
const issues = [];
|
|
406
|
+
const add = (path, message) => issues.push({ code: 'custom', path, message, input: undefined });
|
|
407
|
+
if (!manifest.chains.includes(input.chain))
|
|
408
|
+
add(['chain'], `Chain ${input.chain} is outside the version's declared chains`);
|
|
409
|
+
const { properties, required } = manifest.inputSchema;
|
|
410
|
+
for (const key of required)
|
|
411
|
+
if (input.values[key] === undefined || input.values[key] === '')
|
|
412
|
+
add(['values', key], `${key} is required`);
|
|
413
|
+
for (const [key, value] of Object.entries(input.values)) {
|
|
414
|
+
const field = properties[key];
|
|
415
|
+
if (!field) {
|
|
416
|
+
add(['values', key], `Unknown input ${key}`);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const ok = field.type === 'boolean' ? typeof value === 'boolean' : field.type === 'integer' ? typeof value === 'string' && /^(0|[1-9][0-9]{0,77})$/.test(value)
|
|
420
|
+
: field.type === 'evm-address' ? typeof value === 'string' && isAddress(value) : field.type === 'solana-address' ? typeof value === 'string' && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value)
|
|
421
|
+
: typeof value === 'string' && value.length <= 200 && (!field.enum || field.enum.includes(value));
|
|
422
|
+
if (!ok)
|
|
423
|
+
add(['values', key], `${key} must be a valid ${field.type}${field.enum ? ` (${field.enum.join(', ')})` : ''}`);
|
|
424
|
+
if (field.type === 'evm-address' && ['solana'].includes(input.chain))
|
|
425
|
+
add(['values', key], `${key} is an EVM address but the selected chain is Solana`);
|
|
426
|
+
if (field.type === 'solana-address' && input.chain !== 'solana')
|
|
427
|
+
add(['values', key], `${key} is a Solana address but the selected chain is EVM`);
|
|
428
|
+
}
|
|
429
|
+
if (issues.length)
|
|
430
|
+
throw new z.ZodError(issues);
|
|
431
|
+
return input;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* F02 evidence envelope produced by every registry tool. It mirrors `agentex.tool-evidence.v1` in packages/tools/src/evidence.ts,
|
|
435
|
+
* which owns construction (`makeToolEvidence`); keep both definitions identical. Ids derive from the artifact hash, so neither a
|
|
436
|
+
* tool, a node nor a model can invent or reuse one. Contracts keep a copy so reports validate without tool implementations.
|
|
437
|
+
*/
|
|
438
|
+
export const TOOL_EVIDENCE_VERSION = 'agentex.tool-evidence.v1';
|
|
439
|
+
const Exact = z.string().regex(/^(0|[1-9][0-9]{0,77})$/);
|
|
440
|
+
export const ToolSnapshotSchema = z.discriminatedUnion('kind', [
|
|
441
|
+
z.strictObject({ kind: z.literal('evm-block'), chainId: z.number().int().positive(), blockNumber: Exact, blockHash: z.string().regex(/^0x[a-f0-9]{64}$/), timestamp: Exact.nullable() }),
|
|
442
|
+
// New Solana reads use `confirmed` commitment (instant-read rule); `finalized` parses evidence captured before the rule.
|
|
443
|
+
z.strictObject({ kind: z.literal('solana-slot'), genesisHash: z.string().min(32).max(44), slot: Exact, commitment: z.enum(['finalized', 'confirmed']), exactness: z.enum(['observed-context', 'lower-bound']) }),
|
|
444
|
+
z.strictObject({ kind: z.literal('http-retrieval'), retrievedAt: z.iso.datetime() }),
|
|
445
|
+
z.strictObject({ kind: z.literal('offline-input'), inputHash: Hash }),
|
|
446
|
+
]);
|
|
447
|
+
export const ToolEvidenceCompletenessSchema = z.enum(['complete-for-request', 'truncated', 'empty-result', 'unavailable', 'error']);
|
|
448
|
+
const ToolEvidenceFields = {
|
|
449
|
+
schemaVersion: z.literal(TOOL_EVIDENCE_VERSION), id: z.string().regex(/^ev-[a-f0-9]{32}$/, 'Evidence ids are content-derived (ev- and 32 hex characters).'), tool: ToolRef, capability: ToolCapabilitySchema,
|
|
450
|
+
source: z.strictObject({ kind: z.enum(['rpc', 'http', 'offline']), provider: z.string().min(1).max(253).regex(/^[a-zA-Z0-9.:-]+$/), method: z.string().min(1).max(64), documentationUrl: z.url().nullable() }),
|
|
451
|
+
chain: z.string().regex(/^[a-z][a-z0-9-]{1,31}$/),
|
|
452
|
+
scope: z.strictObject({
|
|
453
|
+
subjects: z.array(z.string().min(1).max(128)).max(16),
|
|
454
|
+
window: z.strictObject({ unit: z.enum(['block', 'slot']), from: Exact, to: Exact }).nullable(),
|
|
455
|
+
page: z.strictObject({ index: z.number().int().min(0).max(1000), size: z.number().int().min(1).max(1000), cursor: z.string().max(128).nullable() }).nullable(),
|
|
456
|
+
}),
|
|
457
|
+
request: z.json(), snapshot: ToolSnapshotSchema.nullable(), observedAt: z.iso.datetime(),
|
|
458
|
+
// New runs record `sequencer-confirmed` (EVM latest block), `confirmed` or `confirmed-lower-bound` (Solana). The `finalized` values parse older evidence.
|
|
459
|
+
finality: z.enum(['finalized', 'finalized-lower-bound', 'sequencer-confirmed', 'confirmed', 'confirmed-lower-bound', 'unverified', 'not-applicable']),
|
|
460
|
+
canonicality: z.enum(['verified-at-completion', 'unchecked', 'changed', 'not-applicable']),
|
|
461
|
+
completeness: ToolEvidenceCompletenessSchema, errorCategory: z.string().regex(/^[a-z][a-z-]{1,63}$/).nullable(),
|
|
462
|
+
result: z.json(), contentHash: Hash, artifactHash: Hash,
|
|
463
|
+
privacy: z.enum(['public-chain-data', 'public-web-content', 'private-input']),
|
|
464
|
+
trust: z.enum(['chain-state', 'provider-assertion', 'untrusted-third-party-content', 'offline-decoding']),
|
|
465
|
+
};
|
|
466
|
+
/** Canonicality is excluded: it is the only field a completion recheck may legitimately change. */
|
|
467
|
+
export function toolArtifactHash(item) { const { id: _id, artifactHash: _artifact, canonicality: _canonicality, ...rest } = item; return contentHash(rest); }
|
|
468
|
+
export const ToolEvidenceItemSchema = z.strictObject(ToolEvidenceFields).superRefine((item, ctx) => {
|
|
469
|
+
if (contentHash(item.result) !== item.contentHash)
|
|
470
|
+
ctx.addIssue({ code: 'custom', path: ['contentHash'], message: 'Evidence content hash does not match its captured result.' });
|
|
471
|
+
if (toolArtifactHash(item) !== item.artifactHash)
|
|
472
|
+
ctx.addIssue({ code: 'custom', path: ['artifactHash'], message: 'Evidence artifact hash does not match its envelope.' });
|
|
473
|
+
if (item.id !== `ev-${item.artifactHash.slice(0, 32)}`)
|
|
474
|
+
ctx.addIssue({ code: 'custom', path: ['id'], message: 'Evidence id is not derived from its artifact hash.' });
|
|
475
|
+
if ((item.completeness === 'error' || item.completeness === 'unavailable') !== (item.errorCategory !== null))
|
|
476
|
+
ctx.addIssue({ code: 'custom', path: ['errorCategory'], message: 'Only failed or unavailable evidence carries an error category.' });
|
|
477
|
+
if ((item.finality === 'finalized' || item.finality === 'confirmed') && item.snapshot?.kind !== 'evm-block' && !(item.snapshot?.kind === 'solana-slot' && item.snapshot.exactness === 'observed-context'))
|
|
478
|
+
ctx.addIssue({ code: 'custom', path: ['finality'], message: 'Evidence with a confirmation level needs an exact block or observed slot context.' });
|
|
479
|
+
if (item.finality === 'sequencer-confirmed' && item.snapshot?.kind !== 'evm-block')
|
|
480
|
+
ctx.addIssue({ code: 'custom', path: ['finality'], message: 'Sequencer-confirmed evidence needs the pinned EVM block.' });
|
|
481
|
+
if (item.capability === 'http.read' && (item.privacy !== 'public-web-content' || item.trust !== 'untrusted-third-party-content'))
|
|
482
|
+
ctx.addIssue({ code: 'custom', path: ['trust'], message: 'Web content is always untrusted third-party content.' });
|
|
483
|
+
});
|
|
484
|
+
export const WorkflowErrorCategorySchema = z.enum([...FailureCategorySchema.options, 'permission-denied', 'invalid-arguments', 'invalid-tool-output', 'invented-evidence', 'unregistered-tool', 'model-unconfigured', 'step-timeout', 'condition-unknown', 'model-failed', 'sandbox-unavailable', 'sandbox-failed']);
|
|
485
|
+
const FindingResultSchema = z.discriminatedUnion('status', [
|
|
486
|
+
z.strictObject({ code: Identifier, label: z.string(), status: z.literal('observed'), value: Scalar, evidenceIds: z.array(z.string()).min(1) }),
|
|
487
|
+
z.strictObject({ code: Identifier, label: z.string(), status: z.literal('from-input'), value: Scalar, evidenceIds: z.array(z.string()).length(0) }),
|
|
488
|
+
z.strictObject({ code: Identifier, label: z.string(), status: z.enum(['unknown', 'not-run']), reason: z.string().max(500), evidenceIds: z.array(z.string()) }),
|
|
489
|
+
]);
|
|
490
|
+
export const WorkflowStepSchema = z.strictObject({
|
|
491
|
+
nodeId: Identifier, type: z.enum(['input', 'tool', 'condition', 'transform', 'report-section', 'ai-analysis', 'output', 'sandboxed-transform']), tool: ToolRef.nullable(),
|
|
492
|
+
status: z.enum(['succeeded', 'skipped', 'failed', 'disabled']), reason: z.string().max(500).nullable(), branch: z.enum(['true', 'false', 'unknown']).nullable(),
|
|
493
|
+
rpcCalls: z.number().int().nonnegative(), evidenceIds: z.array(z.string()), elapsedMs: z.number().int().nonnegative(),
|
|
494
|
+
});
|
|
495
|
+
export const WorkflowReportSchema = z.strictObject({
|
|
496
|
+
schemaVersion: z.literal(WORKFLOW_SCHEMA_ID), agentVersion: Semver, manifestHash: Hash, input: WorkflowRunInputSchema, status: z.enum(['complete', 'partial', 'failed']), generatedAt: z.iso.datetime(),
|
|
497
|
+
requiredSections: z.array(Identifier).max(12),
|
|
498
|
+
sections: z.array(z.strictObject({ id: Identifier, title: z.string(), status: z.enum(['complete', 'partial', 'skipped', 'failed']), reason: z.string().max(500).nullable(), findings: z.array(FindingResultSchema).max(16) })).max(12),
|
|
499
|
+
steps: z.array(WorkflowStepSchema).max(32), evidence: z.array(ToolEvidenceItemSchema).max(128),
|
|
500
|
+
errors: z.array(z.strictObject({ category: WorkflowErrorCategorySchema, message: z.string().max(1000), step: z.string().max(100), evidenceId: z.string().optional() })).max(64),
|
|
501
|
+
limitations: z.array(z.string().max(1000)).max(16),
|
|
502
|
+
usage: z.strictObject({ rpcCalls: z.number().int().nonnegative(), maximumRpcCalls: z.number().int().positive(), toolExecutions: z.number().int().nonnegative(), elapsedMs: z.number().int().nonnegative(), modelTokens: z.number().int().nonnegative(), providerCostAtomic: z.null(), providerCostStatus: z.literal('unpriced'),
|
|
503
|
+
/** Metered model cost of executed AI analysis nodes (micro-USD). Absent on reports without model work. */
|
|
504
|
+
modelCostMicrousd: z.number().int().nonnegative().optional() }),
|
|
505
|
+
/** R01: present when the version declares a presentation. Rows and signals cite the evidence of the step that produced them. */
|
|
506
|
+
presentation: ReportPresentationSchema.optional(), datasets: z.array(ReportDatasetSchema).max(8).optional(), signals: z.array(ReportSignalSchema).max(32).optional(), coverage: z.array(ReportCoverageSchema).max(4).optional(),
|
|
507
|
+
}).superRefine((report, ctx) => {
|
|
508
|
+
const issue = (message) => ctx.addIssue({ code: 'custom', message });
|
|
509
|
+
const ids = new Set(report.evidence.map((item) => item.id));
|
|
510
|
+
if (ids.size !== report.evidence.length)
|
|
511
|
+
issue('Duplicate evidence id');
|
|
512
|
+
const owners = new Map();
|
|
513
|
+
for (const step of report.steps)
|
|
514
|
+
for (const id of step.evidenceIds) {
|
|
515
|
+
if (!ids.has(id))
|
|
516
|
+
issue(`Step ${step.nodeId} references unknown evidence ${id}`);
|
|
517
|
+
if (step.type !== 'tool')
|
|
518
|
+
issue(`Only tool executions produce evidence; ${step.nodeId} is a ${step.type} step`);
|
|
519
|
+
if (owners.has(id))
|
|
520
|
+
issue(`Evidence ${id} is claimed by more than one step`);
|
|
521
|
+
owners.set(id, step.nodeId);
|
|
522
|
+
}
|
|
523
|
+
for (const item of report.evidence) {
|
|
524
|
+
if (!owners.has(item.id))
|
|
525
|
+
issue(`Evidence ${item.id} was not produced by a recorded tool execution`);
|
|
526
|
+
if (contentHash(item.result) !== item.contentHash)
|
|
527
|
+
issue('Evidence content hash does not match its captured result');
|
|
528
|
+
if (item.chain !== report.input.chain)
|
|
529
|
+
issue('Evidence chain differs from the accepted input chain');
|
|
530
|
+
}
|
|
531
|
+
for (const section of report.sections)
|
|
532
|
+
for (const finding of section.findings)
|
|
533
|
+
for (const id of finding.evidenceIds)
|
|
534
|
+
if (!ids.has(id))
|
|
535
|
+
issue(`Unknown evidence reference ${id}`);
|
|
536
|
+
for (const error of report.errors)
|
|
537
|
+
if (error.evidenceId && !ids.has(error.evidenceId))
|
|
538
|
+
issue(`Unknown error evidence reference ${error.evidenceId}`);
|
|
539
|
+
for (const item of [...(report.datasets ?? []), ...(report.signals ?? [])])
|
|
540
|
+
for (const id of item.evidenceIds)
|
|
541
|
+
if (!ids.has(id))
|
|
542
|
+
issue(`Unknown presentation evidence reference ${id}`);
|
|
543
|
+
if (report.usage.rpcCalls > report.usage.maximumRpcCalls)
|
|
544
|
+
issue('RPC usage exceeds the immutable budget');
|
|
545
|
+
const modelRan = report.steps.some((step) => step.type === 'ai-analysis' && (step.status === 'succeeded' || step.status === 'failed'));
|
|
546
|
+
if (!modelRan && (report.usage.modelTokens > 0 || (report.usage.modelCostMicrousd ?? 0) > 0))
|
|
547
|
+
issue('Model usage was recorded without an executed AI analysis step');
|
|
548
|
+
if (report.steps.some((step) => step.type === 'ai-analysis' && step.status === 'succeeded') && report.errors.some((error) => error.category === 'model-unconfigured'))
|
|
549
|
+
issue('AI analysis cannot succeed while the model provider is unconfigured');
|
|
550
|
+
if (report.status === 'complete' && report.requiredSections.some((id) => report.sections.find((section) => section.id === id)?.status !== 'complete'))
|
|
551
|
+
issue('A complete report needs every required section complete');
|
|
552
|
+
});
|
|
553
|
+
export const SANDBOX_UNAVAILABLE = 'Custom code steps are not available on this server, so sandboxed transform steps stay switched off. Creator code only ever runs inside the isolated sandbox.';
|
|
554
|
+
export const MODEL_MINIMUM_OUTPUT_TOKENS = 256;
|
|
555
|
+
/** Effective permissions, derived only from typed fields and server capabilities. Listing text, names and descriptions are never consulted. */
|
|
556
|
+
export function workflowPermissions(manifest, capabilities = {}) {
|
|
557
|
+
const toolNodes = manifest.workflow.filter((node) => node.type === 'tool');
|
|
558
|
+
const aiNodes = manifest.workflow.filter((node) => node.type === 'ai-analysis');
|
|
559
|
+
const sandboxNodes = manifest.workflow.filter((node) => node.type === 'sandboxed-transform');
|
|
560
|
+
return {
|
|
561
|
+
capabilities: [...new Set(manifest.tools.map((tool) => tool.capability))].sort(), chains: [...manifest.chains],
|
|
562
|
+
tools: manifest.tools.map((tool) => ({ ref: `${tool.id}@${tool.version}`, capability: tool.capability, maximumCalls: tool.maximumCalls, executions: toolNodes.filter((node) => node.type === 'tool' && node.tool === tool.id).length })),
|
|
563
|
+
addressScope: manifest.permissions.addressScope, addressInputs: Object.entries(manifest.inputSchema.properties).filter(([, field]) => field.type.endsWith('-address')).map(([key]) => key),
|
|
564
|
+
domains: [...manifest.permissions.domains], destinations: [...manifest.permissions.destinations], secrets: manifest.secrets.map((secret) => ({ name: secret.name, reference: secret.reference })),
|
|
565
|
+
budget: { maximumRpcCalls: manifest.limits.maximumRpcCalls, maximumSteps: manifest.limits.maximumSteps, maximumDurationMs: manifest.limits.maximumDurationMs, stepTimeoutMs: manifest.limits.stepTimeoutMs, maximumBlockRange: manifest.limits.maximumBlockRange, maximumPages: manifest.limits.maximumPages, maximumExecutionAtomic: manifest.limits.maximumExecutionAtomic, maximumModelTokens: manifest.modelPolicy.maximumModelTokens },
|
|
566
|
+
aiAnalysis: { declared: aiNodes.length, enabled: aiNodes.length > 0 && capabilities.modelAnalysis === true, tools: [],
|
|
567
|
+
reason: !aiNodes.length ? null : capabilities.modelAnalysis ? 'Bounded model synthesis of captured evidence only: no tools, no network, output tokens capped by the model policy.' : MODEL_PROVIDER_UNCONFIGURED },
|
|
568
|
+
sandboxedTransforms: { declared: sandboxNodes.length, enabled: sandboxNodes.length > 0 && capabilities.sandboxedTransforms === true, network: 'none',
|
|
569
|
+
reason: !sandboxNodes.length ? null : capabilities.sandboxedTransforms ? 'Runs in the isolated sandbox: no network, read-only filesystem, CPU, memory and time limits.' : SANDBOX_UNAVAILABLE },
|
|
570
|
+
connectors: (manifest.connectors ?? []).map((connector) => ({ alias: connector.id, connectorId: connector.connectorId, schemaHash: connector.schemaHash, maximumCalls: connector.maximumCalls, maximumCostMicrousd: connector.maximumCostMicrousd,
|
|
571
|
+
executions: toolNodes.filter((node) => node.type === 'tool' && node.tool === connector.id).length, enabled: capabilities.connectors === true,
|
|
572
|
+
reason: capabilities.connectors ? 'Reviewed remote tool behind the connector gateway: pinned approved version, SSRF-safe egress, untrusted output, metered per call.' : CONNECTORS_DISABLED })),
|
|
573
|
+
signing: false, broadcasting: false,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/** Worst-case RPC attempts: every tool execution at its declared ceiling, bounded by the workflow budget. Provider cost stays unpriced. */
|
|
577
|
+
export function estimateWorkflowRpcCost(manifest) {
|
|
578
|
+
const tools = new Map(manifest.tools.map((tool) => [tool.id, tool]));
|
|
579
|
+
const connectors = new Map((manifest.connectors ?? []).map((connector) => [connector.id, connector]));
|
|
580
|
+
// N03: each connector call is one metered operation against the same run RPC budget.
|
|
581
|
+
const steps = manifest.workflow.flatMap((node) => node.type === 'tool' ? [{ nodeId: node.id, tool: node.tool, maximumCalls: tools.get(node.tool)?.maximumCalls ?? connectors.get(node.tool)?.maximumCalls ?? 0 }] : []);
|
|
582
|
+
const declared = steps.reduce((total, step) => total + step.maximumCalls, 0);
|
|
583
|
+
return { steps, declaredMaximum: declared, maximumRpcCalls: Math.min(declared, manifest.limits.maximumRpcCalls), budgetBinding: declared > manifest.limits.maximumRpcCalls, providerCostAtomic: null, providerCostStatus: 'unpriced' };
|
|
584
|
+
}
|
|
585
|
+
/** Blocking reasons for creating an immutable test version or executing. Drafts may still contain disabled declarations. */
|
|
586
|
+
export function workflowExecutionBlockers(manifest, capabilities = {}) {
|
|
587
|
+
const connectorBlockers = manifest.connectors?.length && !capabilities.connectors ? [`Connectors ${manifest.connectors.map((connector) => connector.id).join(', ')}: ${CONNECTORS_DISABLED}`] : [];
|
|
588
|
+
return [...connectorBlockers, ...manifest.workflow.flatMap((node) => {
|
|
589
|
+
if (node.type === 'ai-analysis')
|
|
590
|
+
return !capabilities.modelAnalysis ? [`Node ${node.id}: ${MODEL_PROVIDER_UNCONFIGURED}`]
|
|
591
|
+
: node.maximumTokens < MODEL_MINIMUM_OUTPUT_TOKENS ? [`Node ${node.id}: AI analysis needs at least ${MODEL_MINIMUM_OUTPUT_TOKENS} maximum output tokens.`] : [];
|
|
592
|
+
if (node.type === 'sandboxed-transform' && !capabilities.sandboxedTransforms)
|
|
593
|
+
return [`Node ${node.id}: ${SANDBOX_UNAVAILABLE}`];
|
|
594
|
+
return [];
|
|
595
|
+
})];
|
|
596
|
+
}
|
|
597
|
+
/** Structured validation for editors: returns every issue with its JSON path, never throws. */
|
|
598
|
+
export function validateWorkflowManifest(raw) {
|
|
599
|
+
const parsed = WorkflowManifestSchema.safeParse(raw);
|
|
600
|
+
return parsed.success ? { ok: true, manifest: parsed.data, issues: [] } : { ok: false, issues: parsed.error.issues.map((item) => ({ path: item.path.map(String).join('.'), message: item.message })) };
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Buyer result contract for workflow runs: a non-failed report bound to its manifest in which every required section is complete,
|
|
604
|
+
* every finding is observed (citing complete captured tool evidence) or taken from input, at least one finding is tool-observed,
|
|
605
|
+
* and no policy/integrity failure or disabled model step occurred. Returns the unmet conditions; an empty list satisfies it.
|
|
606
|
+
*/
|
|
607
|
+
export function workflowResultContractIssues(raw, manifest) {
|
|
608
|
+
const parsed = WorkflowReportSchema.safeParse(raw);
|
|
609
|
+
if (!parsed.success)
|
|
610
|
+
return ['The report does not match agentex.workflow.v1.'];
|
|
611
|
+
const report = parsed.data;
|
|
612
|
+
const issues = [];
|
|
613
|
+
if (report.status === 'failed')
|
|
614
|
+
issues.push('The run failed.');
|
|
615
|
+
if (manifest) {
|
|
616
|
+
if (contentHash(manifest) !== report.manifestHash || manifest.version !== report.agentVersion)
|
|
617
|
+
issues.push('The report is not bound to this manifest.');
|
|
618
|
+
const required = manifest.outputSchema.sections.filter((section) => section.required).map((section) => section.id).sort();
|
|
619
|
+
if (contentHash(required) !== contentHash([...report.requiredSections].sort()))
|
|
620
|
+
issues.push('Required sections differ from the manifest.');
|
|
621
|
+
try {
|
|
622
|
+
parseWorkflowRunInput(report.input, manifest);
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
issues.push('The report input is outside the manifest input schema.');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (!report.requiredSections.length)
|
|
629
|
+
issues.push('The output schema declares no required section.');
|
|
630
|
+
const evidence = new Map(report.evidence.map((item) => [item.id, item]));
|
|
631
|
+
for (const id of report.requiredSections) {
|
|
632
|
+
const section = report.sections.find((item) => item.id === id);
|
|
633
|
+
if (!section || section.status !== 'complete') {
|
|
634
|
+
issues.push(`Required section ${id} is not complete.`);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
for (const finding of section.findings) {
|
|
638
|
+
if (finding.status !== 'observed' && finding.status !== 'from-input')
|
|
639
|
+
issues.push(`Finding ${id}.${finding.code} is ${finding.status}.`);
|
|
640
|
+
else if (finding.status === 'observed' && finding.evidenceIds.some((ref) => !['complete-for-request', 'empty-result'].includes(evidence.get(ref)?.completeness ?? '')))
|
|
641
|
+
issues.push(`Finding ${id}.${finding.code} cites incomplete evidence.`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (!report.sections.some((section) => section.findings.some((finding) => finding.status === 'observed')))
|
|
645
|
+
issues.push('No finding is backed by tool evidence.');
|
|
646
|
+
if (report.evidence.some((item) => item.canonicality === 'changed'))
|
|
647
|
+
issues.push('Captured evidence changed canonicality.');
|
|
648
|
+
if (report.errors.some((error) => ['permission-denied', 'invented-evidence', 'invalid-tool-output', 'model-unconfigured', 'unregistered-tool', 'sandbox-unavailable'].includes(error.category)))
|
|
649
|
+
issues.push('The run recorded a policy, integrity or disabled-capability failure.');
|
|
650
|
+
return issues;
|
|
651
|
+
}
|