nexus-ai-pro 2.4.0 → 2.6.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.
Files changed (134) hide show
  1. package/API_STABILITY.md +121 -0
  2. package/CHANGELOG.md +248 -1
  3. package/README.md +14 -3
  4. package/SECURITY.md +78 -9
  5. package/dist/agent/create-agent.d.ts +37 -2
  6. package/dist/agent/create-agent.js +62 -4
  7. package/dist/agent/index.d.ts +2 -1
  8. package/dist/agent/tool.d.ts +19 -4
  9. package/dist/agent/tool.js +27 -4
  10. package/dist/context-hub/hub.d.ts +53 -0
  11. package/dist/context-hub/hub.js +108 -3
  12. package/dist/context-hub/index.d.ts +2 -1
  13. package/dist/context-hub/index.js +2 -1
  14. package/dist/context-hub/signing.d.ts +95 -0
  15. package/dist/context-hub/signing.js +88 -0
  16. package/dist/doctor/index.d.ts +4 -2
  17. package/dist/doctor/index.js +29 -0
  18. package/dist/evaluate/threads.d.ts +187 -0
  19. package/dist/evaluate/threads.js +187 -0
  20. package/dist/graph/event-stream.js +2 -0
  21. package/dist/graph/graph.js +4 -1
  22. package/dist/mcp/client.d.ts +9 -1
  23. package/dist/mcp/client.js +38 -8
  24. package/dist/mcp/server.js +42 -2
  25. package/dist/operations/adapters.d.ts +2 -1
  26. package/dist/operations/runner.js +2 -0
  27. package/dist/operations/stats.js +11 -4
  28. package/dist/postgres/index.d.ts +2 -1
  29. package/dist/postgres/index.js +4 -0
  30. package/dist/postgres/operations.js +11 -4
  31. package/dist/postgres/store.d.ts +7 -0
  32. package/dist/postgres/store.js +21 -0
  33. package/dist/postgres/tenancy.d.ts +43 -0
  34. package/dist/postgres/tenancy.js +71 -0
  35. package/dist/protocols/a2a.d.ts +166 -1
  36. package/dist/protocols/a2a.js +499 -132
  37. package/dist/protocols/acp-editor.d.ts +43 -0
  38. package/dist/protocols/acp-editor.js +164 -0
  39. package/dist/protocols/acp.d.ts +76 -3
  40. package/dist/protocols/acp.js +180 -7
  41. package/dist/protocols/ag-ui.d.ts +23 -0
  42. package/dist/protocols/ag-ui.js +128 -8
  43. package/dist/server/deployments.d.ts +127 -12
  44. package/dist/server/deployments.js +402 -37
  45. package/dist/server/metrics.d.ts +2 -0
  46. package/dist/server/metrics.js +7 -0
  47. package/dist/server/runs.d.ts +9 -1
  48. package/dist/server/runs.js +46 -7
  49. package/dist/server/server.js +4 -1
  50. package/dist/server/state.d.ts +12 -2
  51. package/dist/server/state.js +15 -0
  52. package/dist/sqlite/operations.js +15 -4
  53. package/dist/sqlite/store.d.ts +7 -0
  54. package/dist/sqlite/store.js +28 -0
  55. package/dist/store/memory.d.ts +8 -0
  56. package/dist/store/memory.js +12 -1
  57. package/dist/store/redis.d.ts +13 -0
  58. package/dist/store/redis.js +38 -0
  59. package/dist/tracing/instrument.js +15 -3
  60. package/dist/tracing/tracer.d.ts +4 -0
  61. package/dist/tracing/tracer.js +6 -0
  62. package/dist/types/graph.d.ts +7 -0
  63. package/dist/types/messages.d.ts +12 -0
  64. package/dist/types/operations.d.ts +7 -0
  65. package/dist/types/server.d.ts +61 -2
  66. package/dist/types/store.d.ts +9 -0
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/dist-cjs/agent/create-agent.d.ts +37 -2
  70. package/dist-cjs/agent/create-agent.js +62 -4
  71. package/dist-cjs/agent/index.d.ts +2 -1
  72. package/dist-cjs/agent/tool.d.ts +19 -4
  73. package/dist-cjs/agent/tool.js +27 -4
  74. package/dist-cjs/context-hub/hub.d.ts +53 -0
  75. package/dist-cjs/context-hub/hub.js +110 -4
  76. package/dist-cjs/context-hub/index.d.ts +2 -1
  77. package/dist-cjs/context-hub/index.js +7 -1
  78. package/dist-cjs/context-hub/signing.d.ts +95 -0
  79. package/dist-cjs/context-hub/signing.js +96 -0
  80. package/dist-cjs/doctor/index.d.ts +4 -2
  81. package/dist-cjs/doctor/index.js +29 -0
  82. package/dist-cjs/evaluate/threads.d.ts +187 -0
  83. package/dist-cjs/evaluate/threads.js +198 -0
  84. package/dist-cjs/graph/event-stream.js +2 -0
  85. package/dist-cjs/graph/graph.js +4 -1
  86. package/dist-cjs/mcp/client.d.ts +9 -1
  87. package/dist-cjs/mcp/client.js +37 -7
  88. package/dist-cjs/mcp/server.js +42 -2
  89. package/dist-cjs/operations/adapters.d.ts +2 -1
  90. package/dist-cjs/operations/runner.js +2 -0
  91. package/dist-cjs/operations/stats.js +11 -4
  92. package/dist-cjs/postgres/index.d.ts +2 -1
  93. package/dist-cjs/postgres/index.js +8 -1
  94. package/dist-cjs/postgres/operations.js +11 -4
  95. package/dist-cjs/postgres/store.d.ts +7 -0
  96. package/dist-cjs/postgres/store.js +21 -0
  97. package/dist-cjs/postgres/tenancy.d.ts +43 -0
  98. package/dist-cjs/postgres/tenancy.js +110 -0
  99. package/dist-cjs/protocols/a2a.d.ts +166 -1
  100. package/dist-cjs/protocols/a2a.js +501 -132
  101. package/dist-cjs/protocols/acp-editor.d.ts +43 -0
  102. package/dist-cjs/protocols/acp-editor.js +167 -0
  103. package/dist-cjs/protocols/acp.d.ts +76 -3
  104. package/dist-cjs/protocols/acp.js +183 -8
  105. package/dist-cjs/protocols/ag-ui.d.ts +23 -0
  106. package/dist-cjs/protocols/ag-ui.js +127 -7
  107. package/dist-cjs/server/deployments.d.ts +127 -12
  108. package/dist-cjs/server/deployments.js +402 -37
  109. package/dist-cjs/server/metrics.d.ts +2 -0
  110. package/dist-cjs/server/metrics.js +8 -0
  111. package/dist-cjs/server/runs.d.ts +9 -1
  112. package/dist-cjs/server/runs.js +45 -6
  113. package/dist-cjs/server/server.js +4 -1
  114. package/dist-cjs/server/state.d.ts +12 -2
  115. package/dist-cjs/server/state.js +15 -0
  116. package/dist-cjs/sqlite/operations.js +15 -4
  117. package/dist-cjs/sqlite/store.d.ts +7 -0
  118. package/dist-cjs/sqlite/store.js +28 -0
  119. package/dist-cjs/store/memory.d.ts +8 -0
  120. package/dist-cjs/store/memory.js +12 -1
  121. package/dist-cjs/store/redis.d.ts +13 -0
  122. package/dist-cjs/store/redis.js +38 -0
  123. package/dist-cjs/tracing/instrument.js +15 -3
  124. package/dist-cjs/tracing/tracer.d.ts +4 -0
  125. package/dist-cjs/tracing/tracer.js +6 -0
  126. package/dist-cjs/types/graph.d.ts +7 -0
  127. package/dist-cjs/types/messages.d.ts +12 -0
  128. package/dist-cjs/types/operations.d.ts +7 -0
  129. package/dist-cjs/types/server.d.ts +61 -2
  130. package/dist-cjs/types/store.d.ts +9 -0
  131. package/dist-cjs/version.d.ts +1 -1
  132. package/dist-cjs/version.js +1 -1
  133. package/llms.txt +1 -1
  134. package/package.json +43 -2
@@ -8,6 +8,7 @@ const agentChannels = () => ({
8
8
  iterations: counter(),
9
9
  answer: lastValue(''),
10
10
  stopReason: lastValue(undefined),
11
+ clientTools: lastValue(undefined),
11
12
  });
12
13
  export function agentInput(goal) {
13
14
  return { messages: [{ role: 'user', content: goal }] };
@@ -20,16 +21,33 @@ export function createAgent(options) {
20
21
  const executor = new ToolExecutor(options.tools ?? []);
21
22
  const middleware = options.middleware ?? [];
22
23
  const approvals = options.interruptOn ?? {};
24
+ const toolsFor = async (context) => {
25
+ if (!options.toolSource)
26
+ return executor;
27
+ const supplied = await options.toolSource({
28
+ threadId: context.threadId,
29
+ ...(context.tenantId !== undefined ? { tenantId: context.tenantId } : {}),
30
+ ...(context.principal ? { principal: context.principal } : {}),
31
+ signal: context.signal,
32
+ });
33
+ return supplied.length ? new ToolExecutor([...supplied, ...executor.list()]) : executor;
34
+ };
23
35
  const graph = createGraph({ channels: agentChannels() })
24
36
  .addNode('model', async (context) => {
25
37
  const state = context.state;
38
+ const runTools = await toolsFor(context);
39
+ const clientTools = clientToolsOf(state, runTools);
40
+ if (clientTools.length > 0 && waitingOnClient(state.messages, clientTools)) {
41
+ return new Command({ update: { stopReason: 'client_tools' }, goto: END });
42
+ }
43
+ const offered = [...runTools.list(), ...clientTools];
26
44
  let request = {
27
45
  model: options.model ?? 'auto',
28
46
  messages: [
29
47
  ...(options.systemPrompt ? [{ role: 'system', content: options.systemPrompt }] : []),
30
48
  ...state.messages,
31
49
  ],
32
- ...(options.tools?.length ? { tools: executor.list() } : {}),
50
+ ...(offered.length ? { tools: offered } : {}),
33
51
  ...(options.temperature === undefined ? {} : { temperature: options.temperature }),
34
52
  ...(options.maxTokens === undefined ? {} : { maxTokens: options.maxTokens }),
35
53
  ...(options.responseFormat ? { responseFormat: options.responseFormat } : {}),
@@ -88,9 +106,28 @@ export function createAgent(options) {
88
106
  goto: END,
89
107
  });
90
108
  }
109
+ const forClient = new Set(clientTools.map((item) => item.name));
110
+ const ours = calls.filter((call) => !forClient.has(call.function.name));
111
+ for (const call of calls) {
112
+ if (!forClient.has(call.function.name))
113
+ continue;
114
+ const parsed = parseArguments(call.function.arguments);
115
+ context.tool({
116
+ phase: 'start',
117
+ id: call.id,
118
+ name: call.function.name,
119
+ args: parsed.ok ? parsed.args : call.function.arguments,
120
+ });
121
+ }
122
+ if (ours.length === 0) {
123
+ return new Command({
124
+ update: { messages: [assistant], iterations: 1, stopReason: 'client_tools' },
125
+ goto: END,
126
+ });
127
+ }
91
128
  return new Command({
92
129
  update: { messages: [assistant], iterations: 1 },
93
- goto: calls.map((call) => new Send('tools', call)),
130
+ goto: ours.map((call) => new Send('tools', call)),
94
131
  });
95
132
  }, { ends: ['tools', END] })
96
133
  .addNode('tools', async (context) => {
@@ -100,7 +137,8 @@ export function createAgent(options) {
100
137
  return { messages: [toolMessage(call, { ok: false, error: parsed.error })] };
101
138
  }
102
139
  let args = parsed.args;
103
- const definition = executor.get(call.function.name);
140
+ const runTools = await toolsFor(context);
141
+ const definition = runTools.get(call.function.name);
104
142
  const capabilities = capabilitiesOf(definition, args);
105
143
  const description = {
106
144
  id: call.id,
@@ -159,13 +197,14 @@ export function createAgent(options) {
159
197
  if (recheck.decision === 'deny')
160
198
  return { ok: false, error: `Permission denied: ${recheck.reason}` };
161
199
  }
162
- return executor.execute(description.name, current.args, {
200
+ return runTools.execute(description.name, current.args, {
163
201
  toolCallId: call.id,
164
202
  threadId: context.threadId,
165
203
  ...(context.tenantId !== undefined ? { tenantId: context.tenantId } : {}),
166
204
  ...(context.principal ? { principal: context.principal } : {}),
167
205
  signal: context.signal,
168
206
  ...(context.store ? { store: context.store } : {}),
207
+ progress: (progress) => context.tool({ phase: 'progress', id: call.id, name: description.name, progress }),
169
208
  });
170
209
  };
171
210
  context.tool({ phase: 'start', id: call.id, name: description.name, args });
@@ -250,6 +289,25 @@ function parseArguments(raw) {
250
289
  return { ok: false, error: `Tool arguments are not valid JSON (${reason}); the tool was not run` };
251
290
  }
252
291
  }
292
+ function clientToolsOf(state, runTools) {
293
+ return (state.clientTools ?? []).filter((item) => typeof item?.name === 'string' && item.name !== '' && !runTools.has(item.name));
294
+ }
295
+ function waitingOnClient(messages, clientTools) {
296
+ const names = new Set(clientTools.map((item) => item.name));
297
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
298
+ const message = messages[index];
299
+ if (message.role === 'user')
300
+ return false;
301
+ if (message.role !== 'assistant' || !message.toolCalls?.length)
302
+ continue;
303
+ const answered = new Set(messages
304
+ .slice(index + 1)
305
+ .filter((item) => item.role === 'tool')
306
+ .map((item) => item.toolCallId));
307
+ return message.toolCalls.some((call) => names.has(call.function.name) && !answered.has(call.id));
308
+ }
309
+ return false;
310
+ }
253
311
  function hookContext(context, state, store) {
254
312
  return {
255
313
  state,
@@ -1,6 +1,7 @@
1
1
  export { AgentLoop, type AgentModelClient as AgentLoopModelClient } from './loop.js';
2
2
  export { isToolOutput, tool, ToolExecutor, toolMessageContent, toolOutput } from './tool.js';
3
+ export type { ToolBody } from './tool.js';
3
4
  export { limitToolCalls, redactMessages, type RedactOptions, summarizeHistory, type SummarizeOptions, } from './middleware/basic.js';
4
- export { agentAsTool, agentInput, type AgentApproval, type AgentApprovalPolicy, type AgentChannels, type AgentGraph, type AgentMiddleware, type AgentMiddlewareContext, type AgentModelCallContext, type AgentModelClient, type AgentState, type AgentStopReason, type AgentToolCall, type AgentToolResult, createAgent, type CreateAgentOptions, } from './create-agent.js';
5
+ export { agentAsTool, agentInput, type AgentApproval, type AgentApprovalPolicy, type AgentChannels, type AgentGraph, type AgentMiddleware, type AgentMiddlewareContext, type AgentModelCallContext, type AgentModelClient, type AgentState, type AgentStopReason, type AgentToolCall, type AgentToolResult, type ClientToolDefinition, createAgent, type CreateAgentOptions, type ToolSourceContext, } from './create-agent.js';
5
6
  export type { AgentConfig, AgentResult, AgentStep, ToolExecutionResult } from '../types/agent.js';
6
7
  export type { ToolContext } from '../types/messages.js';
@@ -1,15 +1,30 @@
1
1
  import type { ContentPart, Message, ToolContext, ToolDefinition, ToolOutput } from '../types/messages.js';
2
2
  import type { ToolExecutionResult } from '../types/agent.js';
3
- /** Defines a tool the model can call, typing its arguments. */
3
+ /** How a tool built with `tool()` runs: once, with `execute`, or reporting progress, with `stream`. */
4
+ export type ToolBody<TArgs> = {
5
+ /** Runs the tool. In an agent, `context` says who the run is for and carries its signal and store. */
6
+ execute: (args: TArgs, context: ToolContext) => Promise<unknown> | unknown;
7
+ stream?: never;
8
+ } | {
9
+ /**
10
+ * Runs the tool, yielding progress while it works and returning the result. See
11
+ * `ToolDefinition.stream`.
12
+ */
13
+ stream: (args: TArgs, context: ToolContext) => AsyncGenerator<unknown, unknown, undefined>;
14
+ execute?: never;
15
+ };
16
+ /**
17
+ * Defines a tool the model can call, typing its arguments. A tool given `stream` gets an `execute`
18
+ * too, which runs the stream and reports each yield to `context.progress`, so it works wherever a
19
+ * tool is executed.
20
+ */
4
21
  export declare function tool<TArgs extends Record<string, unknown> = Record<string, unknown>>(definition: {
5
22
  name: string;
6
23
  description: string;
7
24
  parameters: Record<string, unknown>;
8
- /** Runs the tool. In an agent, `context` says who the run is for and carries its signal and store. */
9
- execute: (args: TArgs, context: ToolContext) => Promise<unknown> | unknown;
10
25
  /** What the tool does to the world, for a permission policy; see `ToolDefinition.capabilities`. */
11
26
  capabilities?: readonly string[] | ((args: TArgs) => readonly string[]);
12
- }): ToolDefinition;
27
+ } & ToolBody<TArgs>): ToolDefinition;
13
28
  /**
14
29
  * Builds a tool's result as content: text, images, and asset references, in order. A string becomes
15
30
  * a text part. Returned from `execute`, it is sent as the tool message itself, so an image a tool
@@ -1,10 +1,19 @@
1
1
  export function tool(definition) {
2
2
  const capabilities = definition.capabilities;
3
+ const stream = definition.stream;
4
+ const execute = definition.execute;
3
5
  return {
4
6
  name: definition.name,
5
7
  description: definition.description,
6
8
  parameters: definition.parameters,
7
- execute: async (args, context) => definition.execute(args, context ?? {}),
9
+ ...(stream
10
+ ? {
11
+ stream: (args, context) => stream(args, context ?? {}),
12
+ execute: async (args, context) => drain(stream(args, context ?? {}), context?.progress),
13
+ }
14
+ : {
15
+ execute: async (args, context) => execute(args, context ?? {}),
16
+ }),
8
17
  ...(capabilities === undefined
9
18
  ? {}
10
19
  : {
@@ -31,6 +40,18 @@ export function toolMessageContent(result) {
31
40
  return result.result.content;
32
41
  return JSON.stringify(result.ok ? (result.result ?? null) : { error: result.error });
33
42
  }
43
+ async function drain(generator, progress) {
44
+ for (;;) {
45
+ const step = await generator.next();
46
+ if (step.done)
47
+ return step.value;
48
+ try {
49
+ progress?.(step.value);
50
+ }
51
+ catch {
52
+ }
53
+ }
54
+ }
34
55
  export class ToolExecutor {
35
56
  tools = new Map();
36
57
  constructor(tools = []) {
@@ -56,11 +77,13 @@ export class ToolExecutor {
56
77
  if (!toolDefinition) {
57
78
  return { ok: false, error: `Tool "${name}" is not registered` };
58
79
  }
59
- if (!toolDefinition.execute) {
60
- return { ok: false, error: `Tool "${name}" does not have an execute function` };
80
+ if (!toolDefinition.execute && !toolDefinition.stream) {
81
+ return { ok: false, error: `Tool "${name}" does not have an execute or stream function` };
61
82
  }
62
83
  try {
63
- const result = await toolDefinition.execute(args, context);
84
+ const result = toolDefinition.stream
85
+ ? await drain(toolDefinition.stream(args, context), context?.progress)
86
+ : await toolDefinition.execute(args, context);
64
87
  return { ok: true, result };
65
88
  }
66
89
  catch (err) {
@@ -2,6 +2,7 @@ import type { GateResult } from '../prompts/errors.js';
2
2
  import type { PromptRegistry } from '../prompts/registry.js';
3
3
  import type { PromptHistoryEntry, PromptLabel, PromptStore, PromptVersion, RenderedPrompt, RenderOptions } from '../types/prompts.js';
4
4
  import { type ContextDiff } from './diff.js';
5
+ import { type ContextKeyring, type ContextSignature, type ContextSigner } from './signing.js';
5
6
  /** A prompt a bundle uses, pinned to one content version of the prompt registry. */
6
7
  export interface ContextPromptPin {
7
8
  /** The prompt's name in the registry. */
@@ -61,6 +62,8 @@ export interface ContextBundle extends ContextBundleDefinition {
61
62
  author?: string;
62
63
  /** The version that was newest before it. */
63
64
  parent?: string;
65
+ /** Signatures over its content and its pinned prompts' content. Not part of the version. */
66
+ signatures?: ContextSignature[];
64
67
  }
65
68
  /** Which bundle version is being used, as experiments and traces record it. */
66
69
  export interface ContextReference {
@@ -125,9 +128,33 @@ export interface ContextHubOptions {
125
128
  gates?: Record<string, readonly ContextPromotionGate[]>;
126
129
  /** Called after every recorded change. */
127
130
  onChange?: (entry: ContextHistoryEntry) => void | Promise<void>;
131
+ /**
132
+ * Signs every new version, over its content and its pinned prompts' content, so another hub can
133
+ * check where it came from. Signing a bundle that pins prompts needs `prompts`.
134
+ */
135
+ signer?: ContextSigner;
136
+ /**
137
+ * The keys whose signatures this hub trusts, for `verify()`, and for `requireSignature`. A signature
138
+ * by a key the keyring does not hold is kept, but never trusted.
139
+ */
140
+ keyring?: ContextKeyring;
141
+ /**
142
+ * Refuses any bundle without a valid signature by a key in `keyring`: on import, and whenever a
143
+ * version is served through `resolve()` or `renderPrompt()`.
144
+ */
145
+ requireSignature?: boolean;
128
146
  /** Replaces the system clock, for tests. */
129
147
  now?: () => Date;
130
148
  }
149
+ /** What `ContextHub.verify()` found. */
150
+ export interface ContextVerification {
151
+ /** Whether a key in the keyring signed exactly this content. */
152
+ trusted: boolean;
153
+ /** The digest the content has now. */
154
+ digest: string;
155
+ /** The signature that verified, when one did. */
156
+ signature?: ContextSignature;
157
+ }
131
158
  /** Base class for context hub errors, each with a stable `code`. */
132
159
  export declare class ContextHubError extends Error {
133
160
  /** Stable code, such as `CONTEXT_NOT_FOUND`. */
@@ -160,6 +187,18 @@ export declare class ContextPromotionError extends ContextHubError {
160
187
  /** Every gate's verdict. */
161
188
  results: GateResult[]);
162
189
  }
190
+ /** Raised when a bundle has no signature the hub trusts, or its signatures do not match its content. */
191
+ export declare class ContextSignatureError extends ContextHubError {
192
+ /** The bundle. */
193
+ readonly bundle: string;
194
+ /** The version. */
195
+ readonly version: string;
196
+ constructor(
197
+ /** The bundle. */
198
+ bundle: string,
199
+ /** The version. */
200
+ version: string, reason: string);
201
+ }
163
202
  /** Raised when a bundle definition, a pin, or an import is invalid. */
164
203
  export declare class ContextDefinitionError extends ContextHubError {
165
204
  constructor(message: string);
@@ -182,6 +221,8 @@ export declare class ContextHub {
182
221
  /** Where versions, labels, and history live. */
183
222
  readonly store: PromptStore;
184
223
  private readonly now;
224
+ /** Versions already verified as trusted, which never change. */
225
+ private readonly trusted;
185
226
  constructor(options?: ContextHubOptions);
186
227
  /**
187
228
  * Commits a bundle and returns its version. Unchanged content returns the existing version and
@@ -193,8 +234,15 @@ export declare class ContextHub {
193
234
  author?: string;
194
235
  label?: string | readonly string[];
195
236
  }): Promise<ContextBundle>;
237
+ private commitSigned;
196
238
  /** A version by content version, label, or `latest`, the default. Throws `ContextNotFoundError` when there is none. */
197
239
  get(name: string, ref?: string): Promise<ContextBundle>;
240
+ /**
241
+ * Whether a key in the hub's keyring signed exactly this bundle's content, and its pinned prompts'
242
+ * content as the prompt registry holds them. A version's answer never changes, so it is checked
243
+ * once.
244
+ */
245
+ verify(bundle: ContextBundle): Promise<ContextVerification>;
198
246
  /** A version with the reference experiments and traces record for it. */
199
247
  resolve(name: string, ref?: string): Promise<{
200
248
  bundle: ContextBundle;
@@ -262,6 +310,11 @@ export declare class ContextHub {
262
310
  label?: string | readonly string[];
263
311
  by?: string;
264
312
  }): Promise<ContextBundle>;
313
+ /** Refuses a bundle without a trusted signature, when the hub requires one. */
314
+ private assertTrusted;
315
+ private check;
316
+ /** What a signature over this bundle covers, with its pinned prompts read from the registry. */
317
+ private signedContent;
265
318
  private read;
266
319
  private pointer;
267
320
  private record;
@@ -1,6 +1,7 @@
1
1
  import { MemoryPromptStore } from '../prompts/memory.js';
2
2
  import { canonicalJson } from '../prompts/version.js';
3
3
  import { diffContexts } from './diff.js';
4
+ import { contextDigest, signDigest, verifiedBy, } from './signing.js';
4
5
  export class ContextHubError extends Error {
5
6
  code;
6
7
  constructor(message, code) {
@@ -36,6 +37,16 @@ export class ContextPromotionError extends ContextHubError {
36
37
  this.name = 'ContextPromotionError';
37
38
  }
38
39
  }
40
+ export class ContextSignatureError extends ContextHubError {
41
+ bundle;
42
+ version;
43
+ constructor(bundle, version, reason) {
44
+ super(`Context bundle "${bundle}" ${version}: ${reason}`, 'CONTEXT_SIGNATURE');
45
+ this.bundle = bundle;
46
+ this.version = version;
47
+ this.name = 'ContextSignatureError';
48
+ }
49
+ }
39
50
  export class ContextDefinitionError extends ContextHubError {
40
51
  constructor(message) {
41
52
  super(message, 'CONTEXT_DEFINITION_ERROR');
@@ -63,12 +74,19 @@ export class ContextHub {
63
74
  options;
64
75
  store;
65
76
  now;
77
+ trusted = new Map();
66
78
  constructor(options = {}) {
67
79
  this.options = options;
80
+ if (options.requireSignature && !options.keyring) {
81
+ throw new TypeError('requireSignature needs a keyring of the keys the hub trusts');
82
+ }
68
83
  this.store = options.store ?? new MemoryPromptStore();
69
84
  this.now = options.now ?? (() => new Date());
70
85
  }
71
86
  async commit(definition, options = {}) {
87
+ return this.commitSigned(definition, options, []);
88
+ }
89
+ async commitSigned(definition, options, inherited) {
72
90
  validate(definition);
73
91
  if (this.options.prompts) {
74
92
  for (const [role, pin] of Object.entries(definition.prompts ?? {})) {
@@ -92,6 +110,13 @@ export class ContextHub {
92
110
  ...(options.author === undefined ? {} : { author: options.author }),
93
111
  ...(newest ? { parent: newest.version } : {}),
94
112
  };
113
+ const signatures = [...inherited];
114
+ if (this.options.signer) {
115
+ const digest = await contextDigest(await this.signedContent(definition));
116
+ signatures.push(await signDigest(this.options.signer, digest, this.now()));
117
+ }
118
+ if (signatures.length > 0)
119
+ stored.signatures = signatures;
95
120
  await this.store.saveVersion(toStored(stored));
96
121
  await this.record({ name: definition.name, action: 'commit', version: id, by: options.author });
97
122
  }
@@ -118,8 +143,19 @@ export class ContextHub {
118
143
  throw new ContextNotFoundError(name, ref);
119
144
  return found;
120
145
  }
146
+ async verify(bundle) {
147
+ const key = `${bundle.name}@${bundle.version}`;
148
+ let pending = this.trusted.get(key);
149
+ if (!pending) {
150
+ pending = this.check(bundle, await this.signedContent(bundle));
151
+ this.trusted.set(key, pending);
152
+ pending.catch(() => this.trusted.delete(key));
153
+ }
154
+ return pending;
155
+ }
121
156
  async resolve(name, ref = 'latest') {
122
157
  const bundle = await this.get(name, ref);
158
+ await this.assertTrusted(bundle);
123
159
  return {
124
160
  bundle,
125
161
  reference: {
@@ -133,6 +169,7 @@ export class ContextHub {
133
169
  const registry = this.options.prompts;
134
170
  if (!registry)
135
171
  throw new ContextDefinitionError('Rendering a pinned prompt needs the hub to have a prompt registry');
172
+ await this.assertTrusted(bundle);
136
173
  const pin = bundle.prompts?.[role];
137
174
  if (!pin)
138
175
  throw new ContextNotFoundError(bundle.name, `prompt role "${role}"`);
@@ -259,15 +296,52 @@ export class ContextHub {
259
296
  throw new ContextDefinitionError(`Prompt ${prompt.name} was exported as ${version} but its content is ${committed.version}`);
260
297
  }
261
298
  }
262
- const { version, createdAt: _createdAt, parent: _parent, message, author, ...definition } = exported.bundle;
299
+ const { version, createdAt: _createdAt, parent: _parent, message, author, signatures = [], ...definition } = exported.bundle;
263
300
  if ((await contextVersion(definition)) !== version) {
264
301
  throw new ContextDefinitionError(`Bundle ${definition.name} was exported as ${version} but its content has changed`);
265
302
  }
266
- return this.commit(definition, {
303
+ const digest = await contextDigest(signedContentOf(definition, exportedPrompts(exported)));
304
+ const claims = signatures.filter((signature) => signature?.digest === digest);
305
+ if (this.options.requireSignature && this.options.keyring) {
306
+ const signed = await verifiedBy(this.options.keyring, digest, claims);
307
+ if (!signed) {
308
+ throw new ContextSignatureError(definition.name, version, signatures.length > 0
309
+ ? 'no signature verifies against its content with a trusted key'
310
+ : 'it is not signed, and this hub requires a signature');
311
+ }
312
+ }
313
+ return this.commitSigned(definition, {
267
314
  message: message ?? 'imported',
268
315
  ...((options.by ?? author) === undefined ? {} : { author: options.by ?? author }),
269
316
  ...(options.label === undefined ? {} : { label: options.label }),
270
- });
317
+ }, claims);
318
+ }
319
+ async assertTrusted(bundle) {
320
+ if (!this.options.requireSignature)
321
+ return;
322
+ const verification = await this.verify(bundle);
323
+ if (!verification.trusted) {
324
+ throw new ContextSignatureError(bundle.name, bundle.version, bundle.signatures?.length
325
+ ? 'no signature verifies against its content with a trusted key'
326
+ : 'it is not signed, and this hub requires a signature');
327
+ }
328
+ }
329
+ async check(bundle, content) {
330
+ const digest = await contextDigest(content);
331
+ const signature = this.options.keyring
332
+ ? await verifiedBy(this.options.keyring, digest, bundle.signatures)
333
+ : undefined;
334
+ return { trusted: signature !== undefined, digest, ...(signature ? { signature } : {}) };
335
+ }
336
+ async signedContent(definition) {
337
+ const pins = Object.entries(definition.prompts ?? {});
338
+ if (pins.length > 0 && !this.options.prompts) {
339
+ throw new ContextDefinitionError(`Bundle "${definition.name}" pins prompts; signing or verifying it needs the hub to have a prompt registry`);
340
+ }
341
+ const prompts = [];
342
+ for (const [, pin] of pins)
343
+ prompts.push(await this.options.prompts.get(pin.name, pin.version));
344
+ return signedContentOf(definition, prompts);
271
345
  }
272
346
  async read(name, version) {
273
347
  const stored = await this.store.getVersion(name, version);
@@ -304,6 +378,37 @@ function validate(definition) {
304
378
  }
305
379
  }
306
380
  }
381
+ function signedContentOf(definition, prompts) {
382
+ const byPin = new Map(prompts.map((prompt) => [`${prompt.name}@${prompt.version}`, prompt]));
383
+ const pinned = {};
384
+ for (const [role, pin] of Object.entries(definition.prompts ?? {})) {
385
+ const prompt = byPin.get(`${pin.name}@${pin.version}`);
386
+ if (!prompt)
387
+ throw new ContextDefinitionError(`Bundle "${definition.name}" pins ${role} to ${pin.name}@${pin.version}, which is not at hand`);
388
+ pinned[role] = {
389
+ name: prompt.name,
390
+ messages: prompt.messages,
391
+ partials: prompt.partials,
392
+ config: prompt.config,
393
+ defaults: prompt.defaults,
394
+ };
395
+ }
396
+ return {
397
+ bundle: {
398
+ name: definition.name,
399
+ description: definition.description,
400
+ prompts: definition.prompts,
401
+ instructions: definition.instructions,
402
+ tools: definition.tools,
403
+ skills: definition.skills,
404
+ config: definition.config,
405
+ },
406
+ prompts: pinned,
407
+ };
408
+ }
409
+ function exportedPrompts(exported) {
410
+ return exported.prompts;
411
+ }
307
412
  function toStored(bundle) {
308
413
  return { ...bundle, messages: [], variables: [] };
309
414
  }
@@ -4,8 +4,9 @@ import type { Dataset, DatasetExample, Evaluator, Experiment } from '../types/ev
4
4
  import type { ToolDefinition } from '../types/messages.js';
5
5
  import type { ContextBundle, ContextPromotionGate } from './hub.js';
6
6
  export { type ContextChange, type ContextDiff, diffContexts, formatContextDiff } from './diff.js';
7
- export { type ContextBundle, type ContextBundleDefinition, type ContextBundleExport, ContextConflictError, ContextDefinitionError, type ContextHistoryEntry, ContextHub, ContextHubError, type ContextHubOptions, type ContextLabel, ContextNotFoundError, type ContextPromotionContext, ContextPromotionError, type ContextPromotionGate, type ContextPromotionResult, type ContextPromptPin, type ContextReference, type ContextSkill, type ContextTool, contextVersion, } from './hub.js';
7
+ export { type ContextBundle, type ContextBundleDefinition, type ContextBundleExport, ContextConflictError, ContextDefinitionError, type ContextHistoryEntry, ContextHub, ContextHubError, type ContextHubOptions, type ContextLabel, ContextNotFoundError, type ContextPromotionContext, ContextPromotionError, type ContextPromotionGate, ContextSignatureError, type ContextVerification, type ContextPromotionResult, type ContextPromptPin, type ContextReference, type ContextSkill, type ContextTool, contextVersion, } from './hub.js';
8
8
  export type { ExperimentGateOptions } from '../prompts/gates.js';
9
+ export { type ContextCryptoKey, type ContextJsonWebKey, type ContextKeyring, type ContextSignature, type ContextSignedContent, type ContextSigner, type ContextSigningKey, contextDigest, ed25519Keyring, ed25519Signer, generateContextKey, } from './signing.js';
9
10
  /**
10
11
  * A bundle's instructions joined into one system text, in the order the bundle lists them, or only
11
12
  * the ones named in `include`, in that order.
@@ -1,7 +1,8 @@
1
1
  import { fingerprintOf } from '../evaluate/cache.js';
2
2
  import { experimentVerdict } from '../prompts/gates.js';
3
3
  export { diffContexts, formatContextDiff } from './diff.js';
4
- export { ContextConflictError, ContextDefinitionError, ContextHub, ContextHubError, ContextNotFoundError, ContextPromotionError, contextVersion, } from './hub.js';
4
+ export { ContextConflictError, ContextDefinitionError, ContextHub, ContextHubError, ContextNotFoundError, ContextPromotionError, ContextSignatureError, contextVersion, } from './hub.js';
5
+ export { contextDigest, ed25519Keyring, ed25519Signer, generateContextKey, } from './signing.js';
5
6
  export function contextInstructions(bundle, options = {}) {
6
7
  const names = options.include ?? Object.keys(bundle.instructions ?? {});
7
8
  return names
@@ -0,0 +1,95 @@
1
+ import type { PromptDefinition } from '../types/prompts.js';
2
+ /** A JSON Web Key, as Web Crypto exports one. */
3
+ export interface ContextJsonWebKey {
4
+ /** The key type: `OKP` for Ed25519. */
5
+ kty?: string;
6
+ /** The curve: `Ed25519`. */
7
+ crv?: string;
8
+ /** The public key, in base64url. */
9
+ x?: string;
10
+ /** The private key, in base64url, on a private key only. */
11
+ d?: string;
12
+ /** Anything else the exporter wrote. */
13
+ [field: string]: unknown;
14
+ }
15
+ /** A Web Crypto key, as `crypto.subtle` returns one. Any runtime's `CryptoKey` is one. */
16
+ export interface ContextCryptoKey {
17
+ /** Its algorithm. */
18
+ readonly algorithm: unknown;
19
+ /** What it may do. */
20
+ readonly usages: readonly string[];
21
+ }
22
+ /** One signature on a bundle version. Stored with it, and carried by its export. */
23
+ export interface ContextSignature {
24
+ /** The id of the key that signed, which a keyring looks up. */
25
+ keyId: string;
26
+ /** Always `Ed25519`. */
27
+ algorithm: 'Ed25519';
28
+ /**
29
+ * What was signed: `sha256:` and the full SHA-256, in hex, of the bundle's content and the content
30
+ * of every prompt it pins. A bundle's version is only the first 12 hex digits, enough to name a
31
+ * version but not to stand behind a signature.
32
+ */
33
+ digest: string;
34
+ /** The signature over the digest, in base64url. */
35
+ signature: string;
36
+ /** ISO-8601 time it was signed. */
37
+ signedAt: string;
38
+ }
39
+ /** Signs bundle digests with a private key, under a key id. */
40
+ export interface ContextSigner {
41
+ /** The key's id, recorded on each signature so a keyring finds the public key. */
42
+ keyId: string;
43
+ /** Signs bytes. */
44
+ sign(data: Uint8Array): Promise<Uint8Array> | Uint8Array;
45
+ }
46
+ /** Verifies signatures by key id. A signature by a key the keyring does not hold never verifies. */
47
+ export interface ContextKeyring {
48
+ /** Whether `signature` is `keyId`'s signature over `data`. */
49
+ verify(keyId: string, data: Uint8Array, signature: Uint8Array): Promise<boolean> | boolean;
50
+ }
51
+ /** A new signing key, with both halves as JSON Web Keys to keep in a secret store. */
52
+ export interface ContextSigningKey {
53
+ /** The key's id. */
54
+ keyId: string;
55
+ /** Signs with the new key. */
56
+ signer: ContextSigner;
57
+ /** The public half, for every keyring that should trust it. */
58
+ publicKey: ContextJsonWebKey;
59
+ /** The private half. Keep it where the signer runs, and nowhere else. */
60
+ privateKey: ContextJsonWebKey;
61
+ }
62
+ /**
63
+ * Creates an Ed25519 signing key through Web Crypto, so on any runtime. The key id defaults to the
64
+ * first 16 hex digits of the public key's SHA-256, which is stable for the key and names nothing
65
+ * else.
66
+ */
67
+ export declare function generateContextKey(keyId?: string): Promise<ContextSigningKey>;
68
+ /** A signer from an Ed25519 private key, as a JSON Web Key or a Web Crypto key. */
69
+ export declare function ed25519Signer(keyId: string, privateKey: ContextJsonWebKey | ContextCryptoKey): Promise<ContextSigner>;
70
+ /** A keyring of trusted Ed25519 public keys, by key id. */
71
+ export declare function ed25519Keyring(keys: Record<string, ContextJsonWebKey | ContextCryptoKey>): Promise<ContextKeyring>;
72
+ /** What a bundle signature covers: the bundle's versioned content, and its pinned prompts' content. */
73
+ export interface ContextSignedContent {
74
+ /** The bundle's name and the content its version covers. */
75
+ bundle: {
76
+ name: string;
77
+ description?: string;
78
+ prompts?: Record<string, {
79
+ name: string;
80
+ version: string;
81
+ }>;
82
+ instructions?: Record<string, string>;
83
+ tools?: unknown;
84
+ skills?: unknown;
85
+ config?: Record<string, unknown>;
86
+ };
87
+ /** Each pinned prompt's content, by role: what its own version covers. */
88
+ prompts: Record<string, Pick<PromptDefinition, 'name' | 'messages' | 'partials' | 'config' | 'defaults'>>;
89
+ }
90
+ /** The digest a signature covers, as `sha256:` and 64 hex digits. */
91
+ export declare function contextDigest(content: ContextSignedContent): Promise<string>;
92
+ /** Signs a digest. */
93
+ export declare function signDigest(signer: ContextSigner, digest: string, at: Date): Promise<ContextSignature>;
94
+ /** The first signature that is a trusted key's signature over exactly this digest, if any. */
95
+ export declare function verifiedBy(keyring: ContextKeyring, digest: string, signatures?: readonly ContextSignature[]): Promise<ContextSignature | undefined>;