@zackbart/connecta-eve 0.36.0 → 0.36.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,6 +55,40 @@ again for each request, so rotation requires no process restart. A `getToken`
55
55
  callback can return a string or a promise of one; it overrides the environment.
56
56
  No user's identity is forwarded.
57
57
 
58
+ ## Choosing the pool at runtime
59
+
60
+ Pass the same selector to both factories when the pool depends on the chat.
61
+ For example, an app that sets trusted `direct` channel metadata can keep its
62
+ choice fixed for a turn:
63
+
64
+ ```ts
65
+ import type { DynamicContext } from "eve/connections";
66
+
67
+ export const selectPool = (ctx: DynamicContext) =>
68
+ ctx.memo(["connecta-pool", ctx.turn?.id ?? null], () =>
69
+ ctx.channel.metadata.direct === true && ctx.session.auth.current?.principalId === "alice"
70
+ ? "notes_alice"
71
+ : "assistant",
72
+ );
73
+ ```
74
+
75
+ Use `pool: selectPool` in both `connectaConnection` and `connectaInstructions`.
76
+ Eve evaluates the dynamic connection before every model call; the selected
77
+ pool is its `instanceKey`, so a parked call cannot replay against a changed
78
+ pool. A string still creates the same static connection.
79
+
80
+ Both factories call the selector independently. The helper never memoizes its
81
+ result. Memoize selector I/O with `ctx.memo` for your policy's lifetime so the
82
+ connection and instructions agree and repeated evaluations avoid repeated
83
+ lookups. Eve scopes memos per definition, so the two factories each perform
84
+ their first lookup. A turn key fixes each choice for that turn; omit the memo
85
+ to choose again at every evaluation. Ensure both lookups use consistent
86
+ app-owned policy data.
87
+
88
+ Each selected name must match `[a-z0-9_-]+`. A throwing selector or an invalid
89
+ name fails the turn, even when guidance is cached. Handle lookup failures in
90
+ your selector if it should fall back to a least-privileged pool.
91
+
58
92
  ## Eve 0.84.0 dependency override
59
93
 
60
94
  Eve 0.84.0 pins undici 8.10.2. Connecta's development/test override does not ship
@@ -93,14 +127,19 @@ Instructions use `server/discover` (MCP 2026-07-28), falling back to legacy
93
127
  non-OK status or an unsupported-method response. Network errors and timeouts
94
128
  never trigger initialize. Both JSON and SSE replies are accepted. Each request
95
129
  has a five-second timeout. Successful guidance is cached for ten minutes per
96
- resolver; `ttlMs` overrides that duration. Eve evaluates instructions before
97
- every model call; the helper keeps one result per turn in Eve's memo, so a turn
98
- sees the same guidance throughout and at most one refresh. A failed refresh
99
- keeps the last copy, or adds nothing if no fetch has succeeded, and waits 30
100
- seconds before retrying.
130
+ endpoint in each resolver; `ttlMs` overrides that duration. Eve evaluates
131
+ instructions before every model call; the helper keeps one result per endpoint
132
+ and turn in Eve's memo, with at most one refresh for that endpoint while its
133
+ memo is retained. Switching pools selects that endpoint's guidance, never the
134
+ previous pool's. A failed refresh keeps that endpoint's last copy, or adds
135
+ nothing if none has succeeded, and waits 30 seconds before retrying it. Other
136
+ endpoints keep their own TTL and backoff. Eve retains only memo keys used by the
137
+ latest evaluation; returning to a pool can refresh again if its cache expired.
101
138
  When Eve cancels an evaluation, the helper rethrows the cancellation instead of
102
139
  counting it as a failure: nothing is kept for that turn and no wait starts.
103
140
  Tools remain usable. Instructions do not grant access.
141
+ Set `clientName` on `connectaInstructions` to identify its requests in server
142
+ logs; the default is `connecta-eve-instructions`.
104
143
 
105
144
  ## Vercel OIDC token source
106
145
 
@@ -130,6 +169,8 @@ export default connectaInstructions({ pool: "assistant", getToken: getVercelOidc
130
169
 
131
170
  Install `@vercel/oidc` in the Eve app when using this variant. The helper has no
132
171
  dependency on it and never switches credential types automatically.
172
+ Use the default workload audience: Connecta refuses tokens exchanged for a
173
+ custom audience, even when Vercel signed them.
133
174
 
134
175
  ## Contributor validation
135
176
 
@@ -149,3 +190,10 @@ isolate each matrix cell; Eve startup is separate from turn latency. Reports
149
190
  retain actual Eve events and transport inputs. Token usage and model task
150
191
  quality remain unmeasured; these results establish local runtime contracts,
151
192
  not live Vercel workload admission or production model performance.
193
+
194
+ For a disposable workload check, use the
195
+ [contributor fixture recipe](https://github.com/zackbart/connecta/blob/main/packages/connecta-eve/scripts/workload-fixture/README.md) from a source
196
+ checkout. Its preparation command packs and installs the current core/helper,
197
+ checks local signed controls and cancellation, and creates no live resources.
198
+ Live execution requires separate owner authorization for the concrete account,
199
+ project, bounds, cost ceiling and cleanup. Keep its evidence outside the checkout.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type McpClientConnectionDefinition } from "eve/connections";
1
+ import { defineDynamic as defineDynamicConnection, type DynamicContext, type McpClientConnectionDefinition } from "eve/connections";
2
2
  import { type Approval, type ApprovalPolicy } from "eve/tools/approval";
3
3
  /** Where an Eve agent reaches Connecta and how it proves who it is. */
4
4
  export interface ConnectaEndpointOptions {
@@ -15,7 +15,11 @@ export interface ConnectaEndpointOptions {
15
15
  */
16
16
  getToken?: () => string | Promise<string>;
17
17
  }
18
- export interface ConnectaConnectionOptions extends ConnectaEndpointOptions {
18
+ /** App-owned pool policy; use `ctx.memo` when its result should stay fixed. */
19
+ export type ConnectaPoolSelector = (ctx: DynamicContext) => string | Promise<string>;
20
+ export interface ConnectaConnectionOptions extends Omit<ConnectaEndpointOptions, "pool"> {
21
+ /** A fixed pool, or a policy Eve evaluates before every model call. */
22
+ pool?: string | ConnectaPoolSelector;
19
23
  /** What the connection offers, for the model choosing which connection to search. */
20
24
  description: string;
21
25
  /** Eve's approval for every Connecta tool: a policy or configuration. Defaults to `never()`. */
@@ -27,7 +31,11 @@ export interface ConnectaConnectionOptions extends ConnectaEndpointOptions {
27
31
  */
28
32
  gateWrites?: boolean;
29
33
  }
30
- export interface ConnectaInstructionsOptions extends ConnectaEndpointOptions {
34
+ export interface ConnectaInstructionsOptions extends Omit<ConnectaEndpointOptions, "pool"> {
35
+ /** A fixed pool, or the same policy used by the connection. */
36
+ pool?: string | ConnectaPoolSelector;
37
+ /** MCP client name for instruction requests. Default `connecta-eve-instructions`. */
38
+ clientName?: string;
31
39
  /** How long fetched instructions are reused. Default ten minutes. */
32
40
  ttlMs?: number;
33
41
  /** Fetch implementation, for tests. */
@@ -44,12 +52,19 @@ export declare const gateConnectaWrites: ApprovalPolicy;
44
52
  * The Eve connection for a Connecta agent endpoint, with app-owned auth:
45
53
  * one agent token, sent as a bearer and re-read on every request.
46
54
  */
47
- export declare function connectaConnection(options: ConnectaConnectionOptions): McpClientConnectionDefinition;
55
+ export declare function connectaConnection(options: ConnectaConnectionOptions & {
56
+ pool: ConnectaPoolSelector;
57
+ }): ReturnType<typeof defineDynamicConnection>;
58
+ export declare function connectaConnection(options: ConnectaConnectionOptions & {
59
+ pool?: string;
60
+ }): McpClientConnectionDefinition;
61
+ export declare function connectaConnection(options: ConnectaConnectionOptions): McpClientConnectionDefinition | ReturnType<typeof defineDynamicConnection>;
48
62
  /**
49
63
  * Connecta's instructions as Eve system instructions, refreshed once per turn.
50
64
  * Eve does not pass an MCP server's `instructions` to the model, so this
51
65
  * fetches them and caches each success; a failure keeps the last copy, or adds
52
66
  * nothing, and the tools still work. Eve evaluates the resolver before every
53
- * model call; a memo keyed on the turn keeps one result for the whole turn.
67
+ * model call; a memo keyed on the endpoint and turn keeps one result for each.
68
+ * Pool policy runs independently of the connection and owns its memo lifetime.
54
69
  */
55
- export declare function connectaInstructions(options?: ConnectaInstructionsOptions): import("eve/connections").DynamicSentinel<import("eve/instructions").DynamicInstructionsResult, import("eve/connections").DynamicContext>;
70
+ export declare function connectaInstructions(options?: ConnectaInstructionsOptions): import("eve/connections").DynamicSentinel<import("eve/instructions").DynamicInstructionsResult, DynamicContext>;
package/dist/index.js CHANGED
@@ -1,10 +1,15 @@
1
- import { defineMcpClientConnection } from "eve/connections";
1
+ import { defineDynamic as defineDynamicConnection, defineMcpClientConnection, } from "eve/connections";
2
2
  import { defineDynamic, defineInstructions } from "eve/instructions";
3
3
  import { never } from "eve/tools/approval";
4
4
  function environment(name) {
5
5
  const value = globalThis.process?.env?.[name];
6
6
  return value?.trim() || undefined;
7
7
  }
8
+ function validatePool(pool) {
9
+ if (typeof pool !== "string" || !/^[a-z0-9_-]+$/.test(pool)) {
10
+ throw new Error(`Connecta pool "${pool}" must match [a-z0-9_-]+.`);
11
+ }
12
+ }
8
13
  /** The endpoint URL these options name. */
9
14
  export function connectaEndpoint(options = {}) {
10
15
  const url = options.url ?? environment("CONNECTA_URL");
@@ -12,8 +17,7 @@ export function connectaEndpoint(options = {}) {
12
17
  throw new Error("Connecta needs a url, or CONNECTA_URL in the environment.");
13
18
  if (options.pool === undefined)
14
19
  return url;
15
- if (!/^[a-z0-9_-]+$/.test(options.pool))
16
- throw new Error(`Connecta pool "${options.pool}" must match [a-z0-9_-]+.`);
20
+ validatePool(options.pool);
17
21
  return `${url.replace(/\/+$/, "")}/agent/${options.pool}`;
18
22
  }
19
23
  async function tokenFor(options) {
@@ -27,13 +31,21 @@ async function tokenFor(options) {
27
31
  * annotations Connecta derives from its own classification.
28
32
  */
29
33
  export const gateConnectaWrites = ({ toolAnnotations }) => toolAnnotations?.readOnlyHint === true ? "not-applicable" : "user-approval";
30
- /**
31
- * The Eve connection for a Connecta agent endpoint, with app-owned auth:
32
- * one agent token, sent as a bearer and re-read on every request.
33
- */
34
34
  export function connectaConnection(options) {
35
+ const { pool, ...rest } = options;
36
+ if (typeof pool === "function") {
37
+ return defineDynamicConnection(async (ctx) => {
38
+ const selected = await pool(ctx);
39
+ validatePool(selected);
40
+ return connectionDefinition(options, connectaEndpoint({ ...rest, pool: selected }), selected);
41
+ });
42
+ }
43
+ return connectionDefinition(options, connectaEndpoint(pool === undefined ? rest : { ...rest, pool }));
44
+ }
45
+ function connectionDefinition(options, endpoint, instanceKey) {
35
46
  return defineMcpClientConnection({
36
- url: connectaEndpoint(options),
47
+ url: endpoint,
48
+ ...(instanceKey === undefined ? {} : { instanceKey }),
37
49
  description: options.description,
38
50
  auth: {
39
51
  principalType: "app",
@@ -43,9 +55,10 @@ export function connectaConnection(options) {
43
55
  });
44
56
  }
45
57
  /** Connecta's MCP `instructions` for one endpoint, or null when unavailable. */
46
- async function instructionRequest(options, method, signal) {
58
+ async function instructionRequest(options, endpoint, method, signal) {
47
59
  const modern = method === "server/discover";
48
- const response = await (options.fetch ?? fetch)(connectaEndpoint(options), {
60
+ const clientInfo = { name: options.clientName ?? "connecta-eve-instructions", version: "1" };
61
+ const response = await (options.fetch ?? fetch)(endpoint, {
49
62
  method: "POST",
50
63
  headers: {
51
64
  Authorization: `Bearer ${await tokenFor(options)}`,
@@ -62,13 +75,13 @@ async function instructionRequest(options, method, signal) {
62
75
  _meta: {
63
76
  "io.modelcontextprotocol/protocolVersion": "2026-07-28",
64
77
  "io.modelcontextprotocol/clientCapabilities": {},
65
- "io.modelcontextprotocol/clientInfo": { name: "connecta-eve-instructions", version: "1" },
78
+ "io.modelcontextprotocol/clientInfo": clientInfo,
66
79
  },
67
80
  }
68
81
  : {
69
82
  protocolVersion: "2025-06-18",
70
83
  capabilities: {},
71
- clientInfo: { name: "connecta-eve-instructions", version: "1" },
84
+ clientInfo,
72
85
  },
73
86
  }),
74
87
  signal: AbortSignal.any([signal, AbortSignal.timeout(5_000)]),
@@ -88,32 +101,36 @@ async function instructionRequest(options, method, signal) {
88
101
  const instructions = JSON.parse(json).result?.instructions;
89
102
  return typeof instructions === "string" && instructions.trim() ? instructions.trim() : null;
90
103
  }
91
- async function fetchConnectaInstructions(options, signal) {
104
+ async function fetchConnectaInstructions(options, endpoint, signal) {
92
105
  // Only a completed discovery response can justify legacy negotiation.
93
106
  // Network failures and timeouts propagate to the resolver's fail-open path.
94
- const discovered = await instructionRequest(options, "server/discover", signal);
95
- return discovered ?? instructionRequest(options, "initialize", signal);
107
+ const discovered = await instructionRequest(options, endpoint, "server/discover", signal);
108
+ return discovered ?? instructionRequest(options, endpoint, "initialize", signal);
96
109
  }
97
110
  /**
98
111
  * Connecta's instructions as Eve system instructions, refreshed once per turn.
99
112
  * Eve does not pass an MCP server's `instructions` to the model, so this
100
113
  * fetches them and caches each success; a failure keeps the last copy, or adds
101
114
  * nothing, and the tools still work. Eve evaluates the resolver before every
102
- * model call; a memo keyed on the turn keeps one result for the whole turn.
115
+ * model call; a memo keyed on the endpoint and turn keeps one result for each.
116
+ * Pool policy runs independently of the connection and owns its memo lifetime.
103
117
  */
104
118
  export function connectaInstructions(options = {}) {
105
119
  const ttlMs = options.ttlMs ?? 600_000;
106
- let cached;
107
- let retryAfter = 0;
108
- const current = async (signal) => {
120
+ const endpoints = new Map();
121
+ const current = async (endpoint, signal) => {
109
122
  signal.throwIfAborted();
123
+ let state = endpoints.get(endpoint);
124
+ if (!state)
125
+ endpoints.set(endpoint, (state = { retryAfter: 0 }));
126
+ const { cached, retryAfter } = state;
110
127
  if (cached && Date.now() - cached.at <= ttlMs)
111
128
  return cached.content;
112
129
  if (Date.now() < retryAfter)
113
130
  return cached?.content ?? null;
114
131
  let fresh;
115
132
  try {
116
- fresh = await fetchConnectaInstructions(options, signal);
133
+ fresh = await fetchConnectaInstructions(options, endpoint, signal);
117
134
  }
118
135
  catch {
119
136
  // Eve's cancellation is not an endpoint failure: rethrowing keeps it out
@@ -122,12 +139,28 @@ export function connectaInstructions(options = {}) {
122
139
  fresh = null;
123
140
  }
124
141
  if (fresh)
125
- cached = { at: Date.now(), content: fresh };
126
- retryAfter = fresh ? 0 : Date.now() + 30_000;
142
+ state.cached = { at: Date.now(), content: fresh };
143
+ state.retryAfter = fresh ? 0 : Date.now() + 30_000;
127
144
  return fresh ?? cached?.content ?? null;
128
145
  };
129
146
  return defineDynamic(async (ctx) => {
130
- const content = await ctx.memo(["connecta-instructions", ctx.turn?.id ?? null], ({ abortSignal }) => current(abortSignal));
147
+ const { pool, ...rest } = options;
148
+ let endpoint;
149
+ if (typeof pool === "function") {
150
+ const selected = await pool(ctx);
151
+ validatePool(selected);
152
+ endpoint = connectaEndpoint({ ...rest, pool: selected });
153
+ }
154
+ else {
155
+ // Static configuration errors have always omitted optional guidance.
156
+ try {
157
+ endpoint = connectaEndpoint(pool === undefined ? rest : { ...rest, pool });
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ const content = await ctx.memo(["connecta-instructions", endpoint, ctx.turn?.id ?? null], ({ abortSignal }) => current(endpoint, abortSignal));
131
164
  return content ? defineInstructions({ content: `Connecta guidance:\n${content}` }) : null;
132
165
  });
133
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta-eve",
3
- "version": "0.36.0",
3
+ "version": "0.36.2",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Eve connection and instructions helpers for Connecta agent endpoints.",