@zackbart/connecta 0.5.0 → 0.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 (70) hide show
  1. package/CHANGELOG.md +358 -0
  2. package/README.md +53 -12
  3. package/dist/auth/bearer.d.ts +10 -3
  4. package/dist/auth/bearer.d.ts.map +1 -1
  5. package/dist/auth/bearer.js +21 -0
  6. package/dist/auth/bearer.js.map +1 -1
  7. package/dist/auth/clerk.d.ts +26 -1
  8. package/dist/auth/clerk.d.ts.map +1 -1
  9. package/dist/auth/clerk.js +161 -4
  10. package/dist/auth/clerk.js.map +1 -1
  11. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  12. package/dist/connectors/remote-mcp.js +8 -0
  13. package/dist/connectors/remote-mcp.js.map +1 -1
  14. package/dist/credential-health.d.ts +212 -0
  15. package/dist/credential-health.d.ts.map +1 -0
  16. package/dist/credential-health.js +535 -0
  17. package/dist/credential-health.js.map +1 -0
  18. package/dist/execute.d.ts.map +1 -1
  19. package/dist/execute.js +16 -4
  20. package/dist/execute.js.map +1 -1
  21. package/dist/index.d.ts +46 -5
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +96 -13
  24. package/dist/index.js.map +1 -1
  25. package/dist/meta-tools.d.ts +56 -5
  26. package/dist/meta-tools.d.ts.map +1 -1
  27. package/dist/meta-tools.js +249 -92
  28. package/dist/meta-tools.js.map +1 -1
  29. package/dist/registry.d.ts +62 -0
  30. package/dist/registry.d.ts.map +1 -1
  31. package/dist/registry.js +85 -1
  32. package/dist/registry.js.map +1 -1
  33. package/dist/server.d.ts.map +1 -1
  34. package/dist/server.js +292 -37
  35. package/dist/server.js.map +1 -1
  36. package/dist/skills.d.ts +1 -1
  37. package/dist/skills.d.ts.map +1 -1
  38. package/dist/skills.js +1 -1
  39. package/dist/timeout.d.ts +16 -0
  40. package/dist/timeout.d.ts.map +1 -0
  41. package/dist/timeout.js +38 -0
  42. package/dist/timeout.js.map +1 -0
  43. package/dist/toolkits.d.ts +95 -1
  44. package/dist/toolkits.d.ts.map +1 -1
  45. package/dist/toolkits.js +190 -5
  46. package/dist/toolkits.js.map +1 -1
  47. package/dist/types.d.ts +70 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/ui.d.ts +35 -0
  50. package/dist/ui.d.ts.map +1 -1
  51. package/dist/ui.js +87 -3
  52. package/dist/ui.js.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +1 -1
  56. package/src/auth/bearer.ts +35 -1
  57. package/src/auth/clerk.ts +202 -5
  58. package/src/connectors/remote-mcp.ts +9 -0
  59. package/src/credential-health.ts +736 -0
  60. package/src/execute.ts +28 -4
  61. package/src/index.ts +176 -20
  62. package/src/meta-tools.ts +286 -109
  63. package/src/registry.ts +125 -1
  64. package/src/server.ts +349 -34
  65. package/src/skills.ts +1 -1
  66. package/src/timeout.ts +49 -0
  67. package/src/toolkits.ts +241 -6
  68. package/src/types.ts +76 -1
  69. package/src/ui.ts +98 -3
  70. package/src/version.ts +1 -1
@@ -0,0 +1,535 @@
1
+ // Proactive liveness checks for the credentials connecta itself stores —
2
+ // downstream-OAuth tokens and operator-managed vault credentials (issue #24).
3
+ //
4
+ // The problem this solves: a connector's auth state used to flip only when
5
+ // something *observed* a failure, so an expired or revoked token surfaced
6
+ // mid-task as a failed agent call. A liveness check asks the connector whether
7
+ // the credential it holds still works, records the verdict, and lets the cached
8
+ // status surfaces (`list_connectors({ probe: false })`, `/ui`) report
9
+ // `auth_required` BEFORE a real call discovers it.
10
+ //
11
+ // Runtime-agnostic on purpose: nothing here schedules itself. The core exposes a
12
+ // due-gated sweep (piggybacked on inbound traffic by the server) and an awaited
13
+ // entry point (`Connecta.checkCredentials()`) an operator wires to whatever
14
+ // scheduler their runtime has — a Worker cron trigger, a Node `setInterval`.
15
+ // There is no background daemon and no long-lived timer, so Workers and Node run
16
+ // the same code.
17
+ import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withTimeout } from "./timeout.js";
18
+ export const DEFAULT_CREDENTIAL_CHECK_INTERVAL_SECONDS = 900;
19
+ export const DEFAULT_CREDENTIAL_CHECK_CONCURRENCY = 4;
20
+ /**
21
+ * How long a read of one connector's record is served from memory before going
22
+ * back to storage. Short: it exists so a burst of `list_connectors` calls costs
23
+ * one storage read rather than one per call, not to cache a verdict.
24
+ */
25
+ const MIRROR_TTL_MS = 5_000;
26
+ /**
27
+ * Minimum gap between storage WRITES of an unchanged verdict. A liveness
28
+ * observation arrives from more than one place (the sweep, and every
29
+ * `list_connectors({ probe: true })`), and re-persisting "still ok" on each one
30
+ * would spend a KV write per probe for no new information. A verdict whose state
31
+ * or message CHANGED is always written immediately.
32
+ */
33
+ const MIN_WRITE_GAP_MS = 60_000;
34
+ function msg(err) {
35
+ return err instanceof Error ? err.message : String(err);
36
+ }
37
+ function storageKey(connectorId) {
38
+ return `credhealth:${connectorId}`;
39
+ }
40
+ /**
41
+ * Generation counter key. Connector ids are `[a-z0-9_-]+`, so the extra colon
42
+ * puts this outside the space `storageKey` can produce — no id can collide with
43
+ * another id's counter.
44
+ */
45
+ function generationKey(connectorId) {
46
+ return `credhealth:gen:${connectorId}`;
47
+ }
48
+ function validRecord(raw) {
49
+ if (!raw)
50
+ return null;
51
+ try {
52
+ const value = JSON.parse(raw);
53
+ if ((value.state !== "ok" &&
54
+ value.state !== "auth_required" &&
55
+ value.state !== "error") ||
56
+ typeof value.checkedAt !== "string" ||
57
+ Number.isNaN(Date.parse(value.checkedAt))) {
58
+ return null;
59
+ }
60
+ const stamped = Date.parse(value.checkedAt);
61
+ const now = Date.now();
62
+ return {
63
+ state: value.state,
64
+ // A verdict from the future is a clock-skewed isolate, and left alone it
65
+ // would be permanently fresh (never re-checked) AND permanently newer than
66
+ // any real-call success (never retired) — a wrong answer that cannot age
67
+ // out. Clamping to now costs at most one early re-check.
68
+ checkedAt: stamped > now ? new Date(now).toISOString() : value.checkedAt,
69
+ ...(typeof value.message === "string" ? { message: value.message } : {}),
70
+ ...(typeof value.authorizationUrl === "string"
71
+ ? { authorizationUrl: value.authorizationUrl }
72
+ : {}),
73
+ };
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ /**
80
+ * The stored verdicts, in the deployment's own KVStorage under
81
+ * `credhealth:<connectorId>`.
82
+ *
83
+ * Persisted rather than held in memory because the two runtimes disagree about
84
+ * what "in memory" means: a Cloudflare cron trigger runs in a different isolate
85
+ * from the fetch handlers, so a verdict only reaches `list_connectors` if it
86
+ * goes through storage. The in-memory mirror is a read cache over that, not the
87
+ * source of truth.
88
+ *
89
+ * Never throws: a check that cannot be persisted (or read back) must degrade to
90
+ * "no verdict" rather than break the status surface that reads it.
91
+ */
92
+ class CredentialHealthStore {
93
+ storage;
94
+ logger;
95
+ mirror = new Map();
96
+ constructor(storage, logger) {
97
+ this.storage = storage;
98
+ this.logger = logger;
99
+ }
100
+ async get(connectorId) {
101
+ const cached = this.mirror.get(connectorId);
102
+ if (cached && Date.now() - cached.readAt < MIRROR_TTL_MS) {
103
+ return cached.record ?? undefined;
104
+ }
105
+ let record = null;
106
+ try {
107
+ record = validRecord(await this.storage.get(storageKey(connectorId)));
108
+ }
109
+ catch (err) {
110
+ this.logger.warn(`[connecta] connector "${connectorId}" credential-health read failed: ${msg(err)}`);
111
+ return cached?.record ?? undefined;
112
+ }
113
+ this.mirror.set(connectorId, { record, readAt: Date.now() });
114
+ return record ?? undefined;
115
+ }
116
+ /**
117
+ * Monotonic per-connector counter, advanced by {@link clear}. Read straight
118
+ * from storage, never from the mirror: its whole job is to notice a change
119
+ * another isolate made, which a read cache would hide.
120
+ *
121
+ * A read failure answers 0. Paired with the fence in `put`, that fails
122
+ * *closed* — a mismatched generation drops the verdict — because losing one
123
+ * verdict costs a re-check, while resurrecting one costs an operator a
124
+ * connector that reports dead after they just fixed it.
125
+ */
126
+ async generation(connectorId) {
127
+ try {
128
+ const raw = await this.storage.get(generationKey(connectorId));
129
+ const value = raw ? Number(raw) : 0;
130
+ return Number.isFinite(value) ? value : 0;
131
+ }
132
+ catch {
133
+ return 0;
134
+ }
135
+ }
136
+ /**
137
+ * Write a verdict. `expectedGeneration` fences the write against a `clear`
138
+ * that landed while the check was in flight: pass the generation captured
139
+ * before the check started, and the write is dropped if it has since advanced.
140
+ * Omit it for a verdict observed synchronously (a live probe, an operator's
141
+ * Test), where there is no window to race.
142
+ *
143
+ * Returns whether the verdict was actually stored.
144
+ */
145
+ async put(connectorId, record, expectedGeneration) {
146
+ if (expectedGeneration !== undefined &&
147
+ (await this.generation(connectorId)) !== expectedGeneration) {
148
+ // The credential this verdict judged was replaced or removed mid-check.
149
+ // Drop the mirror too: this isolate's idea of the verdict is as stale as
150
+ // the write it just declined to make.
151
+ this.mirror.delete(connectorId);
152
+ return false;
153
+ }
154
+ const current = await this.get(connectorId);
155
+ const unchanged = current !== undefined &&
156
+ current.state === record.state &&
157
+ current.message === record.message &&
158
+ current.authorizationUrl === record.authorizationUrl;
159
+ if (unchanged &&
160
+ Date.parse(record.checkedAt) - Date.parse(current.checkedAt) <
161
+ MIN_WRITE_GAP_MS) {
162
+ return true;
163
+ }
164
+ this.mirror.set(connectorId, { record, readAt: Date.now() });
165
+ try {
166
+ await this.storage.set(storageKey(connectorId), JSON.stringify(record));
167
+ }
168
+ catch (err) {
169
+ this.logger.warn(`[connecta] connector "${connectorId}" credential-health persistence failed: ${msg(err)}`);
170
+ }
171
+ return true;
172
+ }
173
+ /**
174
+ * Forget a connector's verdict, and advance its generation so a check already
175
+ * in flight — in this isolate or any other — cannot write the verdict it
176
+ * formed about the credential that was just replaced.
177
+ *
178
+ * Bump BEFORE the delete, the same ordering the OAuth force path uses: a
179
+ * racing writer must see the advance rather than land between the two writes.
180
+ */
181
+ async clear(connectorId) {
182
+ this.mirror.delete(connectorId);
183
+ try {
184
+ const next = (await this.generation(connectorId)) + 1;
185
+ await this.storage.set(generationKey(connectorId), String(next));
186
+ await this.storage.delete(storageKey(connectorId));
187
+ }
188
+ catch (err) {
189
+ this.logger.warn(`[connecta] connector "${connectorId}" credential-health reset failed: ${msg(err)}`);
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * Whether a connector holds a credential connecta stores AND exposes a way to
195
+ * ask whether it still works.
196
+ *
197
+ * Deliberately narrow. `listTools`/`callTool` are NOT liveness probes here: a
198
+ * tool call may mutate downstream state, and the catalog path is already covered
199
+ * by the existing probe. So a connector is checkable only through the two hooks
200
+ * that exist to answer exactly this question — `testCredential(s)` (what /ui's
201
+ * Test button runs) and `status()` — and only when it has a credential of ours
202
+ * to be asked about: an operator-managed `credential`, or a stored downstream
203
+ * grant it reports via `hasStoredCredential`. A static-token connector stores
204
+ * nothing here and is never probed on a timer.
205
+ */
206
+ export function isCheckableConnector(connector) {
207
+ const hasCredentialStore = Boolean(connector.credential || connector.hasStoredCredential);
208
+ const canAsk = Boolean((connector.credential &&
209
+ (connector.testCredentials || connector.testCredential)) ||
210
+ connector.status);
211
+ return hasCredentialStore && canAsk;
212
+ }
213
+ /**
214
+ * The credential test that fits the STORED value's shape, or undefined.
215
+ *
216
+ * `isCheckableConnector` answers the static question ("could this connector be
217
+ * asked at all"); this answers it against what is actually in the vault. The gap
218
+ * that matters is a connector with named fields but only the single-value
219
+ * `testCredential` hook: handing it `values.value` — the reserved single-value
220
+ * field, absent here — would test the empty string and record a confident
221
+ * `auth_required` about a credential nothing examined. The credential API
222
+ * refuses that same shape with a 409 rather than testing it; here the connector
223
+ * is skipped rather than given an invented verdict.
224
+ */
225
+ function testHookFor(connector, values) {
226
+ if (!values)
227
+ return undefined;
228
+ if (connector.testCredentials) {
229
+ return (ctx) => connector.testCredentials(values, ctx);
230
+ }
231
+ if (connector.testCredential && typeof values.value === "string") {
232
+ return (ctx) => connector.testCredential(values.value, ctx);
233
+ }
234
+ return undefined;
235
+ }
236
+ /** Run `fn` over `items` with at most `limit` in flight, preserving order. */
237
+ async function mapWithConcurrency(items, limit, fn) {
238
+ const out = new Array(items.length);
239
+ let next = 0;
240
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
241
+ while (next < items.length) {
242
+ const index = next++;
243
+ out[index] = await fn(items[index]);
244
+ }
245
+ });
246
+ await Promise.all(workers);
247
+ return out;
248
+ }
249
+ /**
250
+ * Runs and caches credential liveness checks. One instance per `Registry`.
251
+ *
252
+ * Cost is bounded four ways, because a status surface an agent polls must never
253
+ * become a way to hammer a downstream auth endpoint:
254
+ *
255
+ * 1. **Eligibility** — only connectors holding a credential of ours are probed
256
+ * at all (`isCheckableConnector`), and only when something is actually stored.
257
+ * 2. **Freshness (cross-isolate)** — a persisted verdict younger than
258
+ * `intervalSeconds` short-circuits the check, so every isolate and every
259
+ * trigger share one budget.
260
+ * 3. **Sweep gate (per isolate)** — `sweepIfDue` runs at most one traffic-
261
+ * triggered sweep per interval per isolate, and never two at once, so a burst
262
+ * of requests costs one sweep, not one per request.
263
+ * 4. **Deadline + fan-out bound** — each check is bounded by `timeoutMs` and at
264
+ * most `concurrency` run together (the same shape as the `probeTimeoutMs`
265
+ * bound on the discovery fan-out, issue #19).
266
+ */
267
+ export class CredentialHealthChecker {
268
+ deps;
269
+ store;
270
+ intervalMs;
271
+ concurrency;
272
+ timeoutMs;
273
+ onRequest;
274
+ /** Per-connector checks in flight in THIS isolate. */
275
+ inFlight = new Map();
276
+ /** Earliest a traffic-triggered sweep may run again in this isolate. */
277
+ nextSweepAt = 0;
278
+ sweeping;
279
+ constructor(deps, config = {}) {
280
+ this.deps = deps;
281
+ this.store = new CredentialHealthStore(deps.storage, deps.logger);
282
+ // Out-of-range tuning falls back to the default rather than being coerced:
283
+ // a zero or negative interval would turn the rate limit off, which is the
284
+ // one thing this class is for.
285
+ const seconds = config.intervalSeconds;
286
+ this.intervalMs =
287
+ seconds !== undefined && Number.isFinite(seconds) && seconds > 0
288
+ ? seconds * 1000
289
+ : DEFAULT_CREDENTIAL_CHECK_INTERVAL_SECONDS * 1000;
290
+ this.concurrency =
291
+ config.concurrency !== undefined &&
292
+ Number.isInteger(config.concurrency) &&
293
+ config.concurrency > 0
294
+ ? config.concurrency
295
+ : DEFAULT_CREDENTIAL_CHECK_CONCURRENCY;
296
+ this.timeoutMs =
297
+ normalizeTimeoutMs(config.timeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
298
+ this.onRequest = config.onRequest ?? true;
299
+ }
300
+ /**
301
+ * The stored verdict, if any. No downstream I/O — and no storage read at all
302
+ * for a connector that stores no credential of ours, since only a checkable
303
+ * connector can ever have had a verdict written. That keeps
304
+ * `list_connectors({ probe: false })` exactly as cheap as it was for the
305
+ * deployments this feature does not apply to.
306
+ */
307
+ healthFor(connectorId) {
308
+ const connector = this.deps.getConnector(connectorId);
309
+ if (!connector || !isCheckableConnector(connector)) {
310
+ return Promise.resolve(undefined);
311
+ }
312
+ return this.store.get(connectorId);
313
+ }
314
+ /**
315
+ * Record a liveness verdict observed elsewhere — today, the live status a
316
+ * `list_connectors({ probe: true })` just performed. Filtered by the same
317
+ * eligibility rule as a check, so this stays a record of *credential* health
318
+ * rather than a general status cache, and so it also counts against the
319
+ * freshness budget: an operator who just probed live does not get swept again
320
+ * moments later.
321
+ */
322
+ async record(connectorId, record) {
323
+ const connector = this.deps.getConnector(connectorId);
324
+ if (!connector || !isCheckableConnector(connector))
325
+ return;
326
+ await this.store.put(connectorId, record);
327
+ }
328
+ /** Forget a connector's verdict — its credential just changed under us. */
329
+ clear(connectorId) {
330
+ return this.store.clear(connectorId);
331
+ }
332
+ /** Whether any connector in this deployment could be checked at all. */
333
+ hasCheckableConnectors() {
334
+ return this.deps.listConnectors().some(isCheckableConnector);
335
+ }
336
+ /**
337
+ * Check every (or the named) connector's stored credential and return one
338
+ * outcome per connector considered. Never rejects: a connector that throws,
339
+ * hangs past `timeoutMs`, or cannot be persisted becomes an `error` verdict.
340
+ */
341
+ async check(baseUrl, opts = {}) {
342
+ // An id naming no connector is reported, not dropped: a typo in a scheduled
343
+ // check would otherwise return an empty list that looks exactly like a
344
+ // deployment with nothing to check.
345
+ const targets = opts.ids
346
+ ? opts.ids.map((id) => this.deps.getConnector(id) ?? id)
347
+ : this.deps.listConnectors();
348
+ return mapWithConcurrency(targets, this.concurrency, (target) => typeof target === "string"
349
+ ? Promise.resolve({ connectorId: target, skipped: "not_found" })
350
+ : this.checkOne(target, baseUrl, opts));
351
+ }
352
+ /**
353
+ * The traffic-triggered sweep: a promise to hand to `ctx.waitUntil`, or
354
+ * `undefined` when nothing is due (the common case, and free — no I/O). The
355
+ * gate is armed BEFORE the sweep starts, so a burst of concurrent requests
356
+ * produces one sweep.
357
+ */
358
+ sweepIfDue(baseUrl) {
359
+ if (!this.onRequest || this.sweeping)
360
+ return undefined;
361
+ const now = Date.now();
362
+ if (now < this.nextSweepAt)
363
+ return undefined;
364
+ if (!this.hasCheckableConnectors())
365
+ return undefined;
366
+ this.nextSweepAt = now + this.intervalMs;
367
+ const sweep = this.check(baseUrl).finally(() => {
368
+ this.sweeping = undefined;
369
+ });
370
+ this.sweeping = sweep;
371
+ return sweep;
372
+ }
373
+ async checkOne(connector, baseUrl, opts) {
374
+ const connectorId = connector.id;
375
+ if (!isCheckableConnector(connector)) {
376
+ return { connectorId, skipped: "not_checkable" };
377
+ }
378
+ if (this.inFlight.has(connectorId)) {
379
+ // Report rather than join: the caller wants to know a check happened, not
380
+ // to be blocked behind one someone else already pays for.
381
+ return {
382
+ connectorId,
383
+ skipped: "in_flight",
384
+ ...(await this.recordOrNothing(connectorId)),
385
+ };
386
+ }
387
+ if (!opts.force) {
388
+ const current = await this.store.get(connectorId);
389
+ if (current &&
390
+ Date.now() - Date.parse(current.checkedAt) < this.intervalMs) {
391
+ return { connectorId, skipped: "fresh", record: current };
392
+ }
393
+ }
394
+ const run = this.runCheck(connector, baseUrl, opts.requestScope);
395
+ this.inFlight.set(connectorId, run);
396
+ try {
397
+ return await run;
398
+ }
399
+ finally {
400
+ this.inFlight.delete(connectorId);
401
+ }
402
+ }
403
+ async recordOrNothing(connectorId) {
404
+ const record = await this.store.get(connectorId);
405
+ return record ? { record } : {};
406
+ }
407
+ async runCheck(connector, baseUrl, requestScope) {
408
+ const connectorId = connector.id;
409
+ const started = Date.now();
410
+ // Captured BEFORE anything downstream happens: everything after this point
411
+ // is a window in which the operator may replace the very credential being
412
+ // judged, and `settle` fences the write against exactly that.
413
+ const generation = await this.store.generation(connectorId);
414
+ const ctx = this.deps.contextFor(connectorId, baseUrl, requestScope);
415
+ let values = null;
416
+ if (connector.credential && this.deps.credentialVault) {
417
+ try {
418
+ values = await this.deps.credentialVault.getAll(connectorId);
419
+ }
420
+ catch (err) {
421
+ // A stored credential that cannot be decrypted (rotated key, corrupt
422
+ // envelope) is exactly the kind of dead credential this feature exists
423
+ // to surface early, so it is a verdict rather than a skip.
424
+ return this.settle(connectorId, started, generation, {
425
+ state: "auth_required",
426
+ checkedAt: new Date().toISOString(),
427
+ message: msg(err),
428
+ });
429
+ }
430
+ }
431
+ const stored = connector.hasStoredCredential
432
+ ? await connector
433
+ .hasStoredCredential(ctx)
434
+ .catch(() => values !== null)
435
+ : values !== null;
436
+ if (!stored)
437
+ return { connectorId, skipped: "no_credential" };
438
+ if (!this.canAsk(connector, values)) {
439
+ return { connectorId, skipped: "not_checkable" };
440
+ }
441
+ try {
442
+ const verdict = await withTimeout(this.probe(connector, ctx, values), this.timeoutMs, `credential check of "${connectorId}"`);
443
+ return await this.settle(connectorId, started, generation, {
444
+ ...verdict,
445
+ checkedAt: new Date().toISOString(),
446
+ });
447
+ }
448
+ catch (err) {
449
+ return await this.settle(connectorId, started, generation, {
450
+ state: "error",
451
+ checkedAt: new Date().toISOString(),
452
+ message: msg(err),
453
+ });
454
+ }
455
+ }
456
+ /**
457
+ * `isCheckableConnector` re-asked against what is actually stored: a hook that
458
+ * fits the value's shape (see {@link testHookFor}), or a `status()` to fall
459
+ * back on. Neither ⇒ there is no honest question to put to this connector.
460
+ */
461
+ canAsk(connector, values) {
462
+ return Boolean(testHookFor(connector, values) || connector.status);
463
+ }
464
+ /**
465
+ * Ask the connector whether the credential it holds still works — with no
466
+ * downstream mutation and no tool call. A credential test is preferred for a
467
+ * vault credential because it validates the stored value itself; `status()` is
468
+ * the downstream-OAuth answer (it refreshes the grant, which is the liveness
469
+ * question for a token).
470
+ */
471
+ async probe(connector, ctx, values) {
472
+ const test = testHookFor(connector, values);
473
+ if (test) {
474
+ const result = await test(ctx);
475
+ if (result.ok) {
476
+ return { state: "ok", ...(result.message ? { message: result.message } : {}) };
477
+ }
478
+ // A rejected stored credential needs an operator, not a retry — the same
479
+ // actionable state a revoked OAuth grant reports. There is no consent URL
480
+ // for a vault credential; /ui's credential form is where it is replaced.
481
+ return {
482
+ state: "auth_required",
483
+ message: result.message ??
484
+ "Stored credential was rejected by the connector — replace it in /ui.",
485
+ };
486
+ }
487
+ const status = await connector.status(ctx);
488
+ return {
489
+ state: status.state,
490
+ ...(status.message ? { message: status.message } : {}),
491
+ ...(status.authorizationUrl
492
+ ? { authorizationUrl: status.authorizationUrl }
493
+ : {}),
494
+ };
495
+ }
496
+ async settle(connectorId, started, generation, record) {
497
+ const stored = await this.store.put(connectorId, record, generation);
498
+ return {
499
+ connectorId,
500
+ record,
501
+ ...(stored ? {} : { discarded: true }),
502
+ latencyMs: Date.now() - started,
503
+ };
504
+ }
505
+ }
506
+ /**
507
+ * Whether a liveness verdict may DECIDE a connector's cached status.
508
+ *
509
+ * Only `auth_required` ever does, and only while nothing better has happened
510
+ * since. Two separate judgements:
511
+ *
512
+ * 1. **`error` is not credential evidence.** A check that timed out, threw, or
513
+ * got a 502 from the provider's status endpoint failed to *complete* — it
514
+ * learned nothing about the credential. Letting it set the status would flip
515
+ * a connector whose calls are fine to `error` for a whole interval on a DNS
516
+ * blip. Error verdicts stay visible in `credentialCheck` (an operator wants
517
+ * to know checks are failing) but the status keeps coming from observed real
518
+ * calls, which is evidence.
519
+ * 2. **A successful real call retires the verdict.** Traffic beats a background
520
+ * probe, so a `lastSuccessAt` at or after `checkedAt` means the credential
521
+ * demonstrably works whatever the check concluded. The next check re-decides.
522
+ *
523
+ * `auth_required` deliberately outranks an observed real-call *failure*: both
524
+ * say something is wrong, and only one of them carries the URL that fixes it.
525
+ * The failure stays visible as `lastError`.
526
+ */
527
+ export function credentialVerdictApplies(record, lastSuccessAt) {
528
+ if (!record || record.state !== "auth_required")
529
+ return false;
530
+ if (!lastSuccessAt)
531
+ return true;
532
+ const success = Date.parse(lastSuccessAt);
533
+ return Number.isNaN(success) || success < Date.parse(record.checkedAt);
534
+ }
535
+ //# sourceMappingURL=credential-health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-health.js","sourceRoot":"","sources":["../src/credential-health.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../src/execute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EACV,SAAS,EACT,QAAQ,EACR,gBAAgB,EAChB,MAAM,EAEP,MAAM,YAAY,CAAC;AAKpB,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,eAAO,MAAM,4BAA4B,QAAS,CAAC;AA+CnD,gEAAgE;AAChE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKvD;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,OAAO,GACd,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,sBAAsB,EACjC,MAAM,GAAE;IACN,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACvB,GACL,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA2U7B;AAmBD,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,sBAAsB,IAEnB,UAAU;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,KAAG,OAAO,CAAC,UAAU,CAAC,CA4C/D;AAeD,uFAAuF;AACvF,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,YAAY,EACtB,GAAG,EAAE;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACnC,GACA,IAAI,CA2BN"}
1
+ {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../src/execute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EACV,SAAS,EACT,QAAQ,EACR,gBAAgB,EAChB,MAAM,EAEP,MAAM,YAAY,CAAC;AAKpB,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,eAAO,MAAM,4BAA4B,QAAS,CAAC;AA+CnD,gEAAgE;AAChE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKvD;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,OAAO,GACd,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,sBAAsB,EACjC,MAAM,GAAE;IACN,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACvB,GACL,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA6V7B;AAoBD,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,sBAAsB,IAEnB,UAAU;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,KAAG,OAAO,CAAC,UAAU,CAAC,CA4C/D;AAeD,uFAAuF;AACvF,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,YAAY,EACtB,GAAG,EAAE;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACnC,GACA,IAAI,CA2BN"}
package/dist/execute.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
3
3
  import { recordToolActivity } from "./activity.js";
4
- import { errorResult, jsonResult } from "./meta-tools.js";
4
+ import { errorResult, jsonResult, serializeResultText, } from "./meta-tools.js";
5
5
  import { classifyCallError, ConnectorCallError } from "./errors.js";
6
6
  import { unwrapMcpResult } from "./mcp-result.js";
7
7
  /** ~6k tokens. Sandbox code should filter data down before returning. */
@@ -100,6 +100,7 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
100
100
  "__log",
101
101
  ]);
102
102
  const connectors = registry.listConnectors();
103
+ const catalogStarted = Date.now();
103
104
  const loaded = await Promise.allSettled(connectors.map((connector) => registry.getTools(connector.id, baseUrl, requestScope)));
104
105
  const catalogs = new Map();
105
106
  const callAddress = async (address, args) => {
@@ -188,7 +189,17 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
188
189
  }
189
190
  const loadedTools = loaded[i];
190
191
  if (loadedTools.status === "rejected") {
191
- logger.warn(`[connecta] execute_code: connector "${connector.id}" skipped: ${msg(loadedTools.reason)}`);
192
+ // Same health accounting as the call_tool catalog catch: a connector whose
193
+ // catalog cannot be fetched is unusable, and dropping its namespace with
194
+ // only a warn would leave the cheap `list_connectors({ probe: false })`
195
+ // signal clean for a code-mode deployment whose downstream grant was
196
+ // revoked. Recorded through `registry` — this run's view — so a
197
+ // toolkit-scoped execute_code lands in that toolkit's log as well.
198
+ registry.recordFailure(connector.id, Date.now() - catalogStarted, loadedTools.reason);
199
+ // classifyCallError so a typed auth_required thrown while listing tools
200
+ // keeps its code where an operator can see it; health stores the message.
201
+ const details = classifyCallError(loadedTools.reason, "catalog_lookup_failed");
202
+ logger.warn(`[connecta] execute_code: connector "${connector.id}" skipped (${details.code}): ${msg(loadedTools.reason)}`);
192
203
  continue;
193
204
  }
194
205
  const tools = loadedTools.value;
@@ -351,8 +362,9 @@ function truncate(text, max) {
351
362
  return `${text.slice(0, max)}\n--- TRUNCATED (${text.length} chars total) — filter/map/slice data inside your code and return only what you need ---`;
352
363
  }
353
364
  function guardResultValue(value) {
354
- const serialized = JSON.stringify(value, null, 2);
355
- const text = serialized === undefined ? String(value) : serialized;
365
+ // Same serialization the call_tool guards measure, so a program returning
366
+ // nothing is rendered one way across every result path (issue #42).
367
+ const text = serializeResultText(value);
356
368
  if (text.length <= MAX_RESULT_CHARS)
357
369
  return value;
358
370
  return {