@zackbart/connecta-eve 0.35.0 → 0.36.1

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
@@ -1,18 +1,18 @@
1
1
  # @zackbart/connecta-eve
2
2
 
3
3
  Connect an Eve agent to a Connecta agent pool and load its endpoint instructions
4
- at the start of each turn. The helper runs in the Eve app; the Connecta server
4
+ once per turn. The helper runs in the Eve app; the Connecta server
5
5
  runs separately. No `@zackbart/connecta` dependency is installed by this package.
6
6
 
7
- Requires **Node 24+** and **Eve >=0.83.0 <0.84.0**. Development and consumer
8
- checks use real Eve 0.83.0. The range covers its patch line; newer minor releases
7
+ Requires **Node 24+** and **Eve >=0.84.0 <0.85.0**. Development and consumer
8
+ checks use real Eve 0.84.0. The range covers its patch line; newer minor releases
9
9
  need a compatibility check. Helper releases share Connecta's version and tag;
10
10
  choose the helper version matching your server release.
11
11
 
12
12
  ## Install and add two files
13
13
 
14
14
  ```sh
15
- npm install @zackbart/connecta-eve eve@0.83.0
15
+ npm install @zackbart/connecta-eve eve@0.84.0
16
16
  ```
17
17
 
18
18
  The server must declare your agent, admit it to the named pool and issue its
@@ -55,9 +55,43 @@ 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
- ## Eve 0.83.0 dependency override
58
+ ## Choosing the pool at runtime
59
59
 
60
- Eve 0.83.0 pins undici 8.10.2. Connecta's development/test override does not ship
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
+
92
+ ## Eve 0.84.0 dependency override
93
+
94
+ Eve 0.84.0 pins undici 8.10.2. Connecta's development/test override does not ship
61
95
  with this helper, so the Eve app must apply its own override to 8.12.3 before
62
96
  installing. For an npm app, add this to its `package.json`, then reinstall and
63
97
  commit the lockfile:
@@ -66,7 +100,7 @@ commit the lockfile:
66
100
  { "overrides": { "eve": { "undici": "8.12.3" } } }
67
101
  ```
68
102
 
69
- For a pnpm app, add `overrides: { "eve@0.83.0>undici": "8.12.3" }` to its
103
+ For a pnpm app, add `overrides: { "eve@0.84.0>undici": "8.12.3" }` to its
70
104
  `pnpm-workspace.yaml`. Check the resolved dependency graph and audit the app.
71
105
  Remove this override after upgrading to an Eve release that pins a fixed version
72
106
  and validating compatibility. The server's optional undici 6.29.1 peer is a
@@ -79,7 +113,8 @@ read-only Connecta pool when the agent must never write. For an interactive
79
113
  agent, add `gateWrites: true` to the connection to request user approval before
80
114
  any tool whose `readOnlyHint` is not `true`, including writable `execute_code`.
81
115
  `gateConnectaWrites` exports that policy for composition. An explicit Eve
82
- `approval` policy takes precedence over `gateWrites`.
116
+ `approval`, either a policy or an approval configuration, takes precedence over
117
+ `gateWrites`.
83
118
 
84
119
  The gate uses the host's cached annotations. If a read becomes a write before
85
120
  dispatch, Connecta applies its current classification; a writable pool can
@@ -92,9 +127,19 @@ Instructions use `server/discover` (MCP 2026-07-28), falling back to legacy
92
127
  non-OK status or an unsupported-method response. Network errors and timeouts
93
128
  never trigger initialize. Both JSON and SSE replies are accepted. Each request
94
129
  has a five-second timeout. Successful guidance is cached for ten minutes per
95
- resolver; `ttlMs` overrides that duration. A failed refresh keeps the last copy,
96
- or adds nothing if no fetch has succeeded, and waits 30 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.
138
+ When Eve cancels an evaluation, the helper rethrows the cancellation instead of
139
+ counting it as a failure: nothing is kept for that turn and no wait starts.
97
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`.
98
143
 
99
144
  ## Vercel OIDC token source
100
145
 
@@ -124,12 +169,14 @@ export default connectaInstructions({ pool: "assistant", getToken: getVercelOidc
124
169
 
125
170
  Install `@vercel/oidc` in the Eve app when using this variant. The helper has no
126
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.
127
174
 
128
175
  ## Contributor validation
129
176
 
130
177
  In a source checkout with dependencies installed and both packages built, run
131
178
  `pnpm --filter @zackbart/connecta-eve run probe:catalog --output /absolute/path/outside/checkout`
132
- on Node 24. This boots real Eve 0.83.0 with a packed, installed helper against
179
+ on Node 24. This boots real Eve 0.84.0 with a packed, installed helper against
133
180
  seeded local catalogs of 40, 100, 250, 500, and 1,000 downstream tools on the
134
181
  meta, native, and native-plus-code surfaces. Native listings include two
135
182
  Connecta tools beyond the downstream count. A 1,001-tool native catalog must
@@ -143,3 +190,10 @@ isolate each matrix cell; Eve startup is separate from turn latency. Reports
143
190
  retain actual Eve events and transport inputs. Token usage and model task
144
191
  quality remain unmeasured; these results establish local runtime contracts,
145
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,5 +1,5 @@
1
- import { type McpClientConnectionDefinition } from "eve/connections";
2
- import { type ApprovalPolicy } from "eve/tools/approval";
1
+ import { defineDynamic as defineDynamicConnection, type DynamicContext, type McpClientConnectionDefinition } from "eve/connections";
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 {
5
5
  /**
@@ -15,11 +15,15 @@ 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
- /** Eve's approval policy for every Connecta tool. Defaults to `never()`. */
22
- approval?: ApprovalPolicy;
25
+ /** Eve's approval for every Connecta tool: a policy or configuration. Defaults to `never()`. */
26
+ approval?: Approval;
23
27
  /**
24
28
  * Ask a person before every tool not annotated read-only: each write tool
25
29
  * on a native endpoint, and `execute_code` on a writable one. Ignored when
@@ -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,11 +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
- * Connecta's instructions as Eve system instructions, refreshed at turn start.
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
- * nothing, and the tools still work.
66
+ * nothing, and the tools still work. Eve evaluates the resolver before every
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.
53
69
  */
54
- export declare function connectaInstructions(options?: ConnectaInstructionsOptions): import("eve/connections").DynamicSentinel<import("eve/instructions").DynamicInstructionsResult>;
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,25 +31,34 @@ 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
- credentialOwner: "app",
51
+ principalType: "app",
40
52
  getToken: async () => ({ token: await tokenFor(options) }),
41
53
  },
42
54
  approval: options.approval ?? (options.gateWrites ? gateConnectaWrites : never()),
43
55
  });
44
56
  }
45
57
  /** Connecta's MCP `instructions` for one endpoint, or null when unavailable. */
46
- async function instructionRequest(options, method) {
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,16 +75,16 @@ async function instructionRequest(options, method) {
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
- signal: AbortSignal.timeout(5_000),
87
+ signal: AbortSignal.any([signal, AbortSignal.timeout(5_000)]),
75
88
  });
76
89
  if (!response.ok)
77
90
  return null;
@@ -88,39 +101,66 @@ async function instructionRequest(options, method) {
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) {
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");
95
- return discovered ?? instructionRequest(options, "initialize");
107
+ const discovered = await instructionRequest(options, endpoint, "server/discover", signal);
108
+ return discovered ?? instructionRequest(options, endpoint, "initialize", signal);
96
109
  }
97
110
  /**
98
- * Connecta's instructions as Eve system instructions, refreshed at turn start.
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
- * nothing, and the tools still work.
114
+ * nothing, and the tools still work. Eve evaluates the resolver before every
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.
102
117
  */
103
118
  export function connectaInstructions(options = {}) {
104
119
  const ttlMs = options.ttlMs ?? 600_000;
105
- let cached;
106
- let retryAfter = 0;
107
- const current = async () => {
120
+ const endpoints = new Map();
121
+ const current = async (endpoint, signal) => {
122
+ signal.throwIfAborted();
123
+ let state = endpoints.get(endpoint);
124
+ if (!state)
125
+ endpoints.set(endpoint, (state = { retryAfter: 0 }));
126
+ const { cached, retryAfter } = state;
108
127
  if (cached && Date.now() - cached.at <= ttlMs)
109
128
  return cached.content;
110
129
  if (Date.now() < retryAfter)
111
130
  return cached?.content ?? null;
112
- const fresh = await fetchConnectaInstructions(options).catch(() => null);
131
+ let fresh;
132
+ try {
133
+ fresh = await fetchConnectaInstructions(options, endpoint, signal);
134
+ }
135
+ catch {
136
+ // Eve's cancellation is not an endpoint failure: rethrowing keeps it out
137
+ // of the memo and the backoff, which every session of this resolver shares.
138
+ signal.throwIfAborted();
139
+ fresh = null;
140
+ }
113
141
  if (fresh)
114
- cached = { at: Date.now(), content: fresh };
115
- retryAfter = fresh ? 0 : Date.now() + 30_000;
142
+ state.cached = { at: Date.now(), content: fresh };
143
+ state.retryAfter = fresh ? 0 : Date.now() + 30_000;
116
144
  return fresh ?? cached?.content ?? null;
117
145
  };
118
- return defineDynamic({
119
- events: {
120
- "turn.started": async () => {
121
- const content = await current();
122
- return content ? defineInstructions({ content: `Connecta guidance:\n${content}` }) : null;
123
- },
124
- },
146
+ return defineDynamic(async (ctx) => {
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));
164
+ return content ? defineInstructions({ content: `Connecta guidance:\n${content}` }) : null;
125
165
  });
126
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta-eve",
3
- "version": "0.35.0",
3
+ "version": "0.36.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Eve connection and instructions helpers for Connecta agent endpoints.",
@@ -28,11 +28,11 @@
28
28
  }
29
29
  },
30
30
  "peerDependencies": {
31
- "eve": ">=0.83.0 <0.84.0"
31
+ "eve": ">=0.84.0 <0.85.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^24.0.0",
35
- "eve": "0.83.0",
35
+ "eve": "0.84.0",
36
36
  "typescript": "^5.9.3",
37
37
  "vitest": "^4.1.6"
38
38
  },