@zackbart/connecta 0.10.0 → 0.10.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/AGENTS.md +113 -0
- package/CHANGELOG.md +83 -0
- package/README.md +62 -12
- package/bin/connecta.mjs +272 -0
- package/dist/access-tokens.d.ts +32 -0
- package/dist/access-tokens.d.ts.map +1 -0
- package/dist/access-tokens.js +225 -0
- package/dist/access-tokens.js.map +1 -0
- package/dist/catalog-service.d.ts +39 -1
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +133 -11
- package/dist/catalog-service.js.map +1 -1
- package/dist/catalog.d.ts +17 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +113 -13
- package/dist/catalog.js.map +1 -1
- package/dist/execute.d.ts +45 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +265 -68
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +1 -5
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +1 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +410 -12
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/operator-ui/model.d.ts +2 -0
- package/dist/operator-ui/model.d.ts.map +1 -1
- package/dist/operator-ui/model.js.map +1 -1
- package/dist/routes/access-tokens.d.ts +7 -0
- package/dist/routes/access-tokens.d.ts.map +1 -0
- package/dist/routes/access-tokens.js +84 -0
- package/dist/routes/access-tokens.js.map +1 -0
- package/dist/routes/shared.d.ts +3 -0
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/routes/ui.d.ts.map +1 -1
- package/dist/routes/ui.js +9 -1
- package/dist/routes/ui.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +5 -0
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +8 -0
- package/dist/storage/memory.js.map +1 -1
- package/dist/tool-safety.d.ts +10 -0
- package/dist/tool-safety.d.ts.map +1 -0
- package/dist/tool-safety.js +12 -0
- package/dist/tool-safety.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +4 -4
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +44 -1
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +7 -0
- package/documentation/auth.md +58 -0
- package/documentation/call-admission.md +7 -0
- package/documentation/code-first-exploration.md +292 -0
- package/documentation/code-mode.md +697 -0
- package/documentation/connector-guides.md +7 -0
- package/documentation/connectors.md +63 -0
- package/documentation/mcp-2026-07-28.md +46 -0
- package/documentation/meta-tools.md +167 -0
- package/documentation/operations.md +7 -0
- package/documentation/operator-ui.md +7 -0
- package/documentation/request-admission.md +7 -0
- package/documentation/storage-and-credentials.md +54 -0
- package/ethos.md +132 -0
- package/examples/node/README.md +53 -0
- package/examples/node/src/index.ts +73 -0
- package/examples/worker/README.md +160 -0
- package/examples/worker/src/cloudflare-kv.ts +43 -0
- package/examples/worker/src/d1-activity-row.ts +100 -0
- package/examples/worker/src/d1-activity.ts +144 -0
- package/examples/worker/src/index.ts +136 -0
- package/examples/worker/wrangler.jsonc +26 -0
- package/package.json +11 -1
- package/src/access-tokens.ts +289 -0
- package/src/catalog-service.ts +177 -15
- package/src/catalog.ts +143 -12
- package/src/execute.ts +372 -96
- package/src/index.ts +33 -1
- package/src/invocation.ts +1 -8
- package/src/meta-tools.ts +504 -11
- package/src/operator-ui/browser.css +63 -0
- package/src/operator-ui/browser.ts +288 -2
- package/src/operator-ui/generated.ts +2 -2
- package/src/operator-ui/model.ts +6 -0
- package/src/routes/access-tokens.ts +115 -0
- package/src/routes/shared.ts +3 -0
- package/src/routes/ui.ts +9 -0
- package/src/server.ts +5 -0
- package/src/skills.ts +1 -1
- package/src/storage/file.ts +5 -0
- package/src/storage/memory.ts +8 -0
- package/src/tool-safety.ts +15 -0
- package/src/types.ts +5 -0
- package/src/ui.ts +50 -1
- package/src/version.ts +1 -1
- package/templates/node/.env.example +5 -0
- package/templates/node/AGENTS.md +19 -0
- package/templates/node/README.md +33 -0
- package/templates/node/package.json +23 -0
- package/templates/node/src/index.ts +43 -0
- package/templates/node/tsconfig.json +12 -0
package/src/invocation.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "./errors.js";
|
|
19
19
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
20
20
|
import type { RegistryView } from "./registry.js";
|
|
21
|
+
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
21
22
|
import type { ToolDef } from "./types.js";
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -55,14 +56,6 @@ export function retryBackoffMs(
|
|
|
55
56
|
return retryAfterMs <= MAX_RETRY_BACKOFF_MS ? retryAfterMs : undefined;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
/** The one fail-closed admission predicate for every invocation adapter. */
|
|
59
|
-
function isExplicitlyReadOnly(definition: ToolDef): boolean {
|
|
60
|
-
return (
|
|
61
|
-
definition.annotations?.readOnlyHint === true &&
|
|
62
|
-
definition.annotations?.destructiveHint !== true
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
59
|
function retrySafe(definition: ToolDef): boolean {
|
|
67
60
|
return (
|
|
68
61
|
definition.annotations?.readOnlyHint === true ||
|
package/src/meta-tools.ts
CHANGED
|
@@ -236,23 +236,491 @@ function resolvePath(value: unknown, segments: string[]): unknown {
|
|
|
236
236
|
return resolvePath(next, rest);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
const MAX_PROJECTION_AVAILABLE_FIELDS = 20;
|
|
240
|
+
const MAX_PROJECTION_SCHEMA_DEPTH = 12;
|
|
241
|
+
const MAX_PROJECTION_SCHEMA_NODES = 200;
|
|
242
|
+
const MAX_PROJECTION_SCHEMA_PATHS = 100;
|
|
243
|
+
const MAX_PROJECTION_PATH_CHARS = 256;
|
|
244
|
+
const MAX_PROJECTION_PATH_BYTES = 512;
|
|
245
|
+
const MAX_PROJECTION_TOTAL_PATH_CHARS = 512;
|
|
246
|
+
const MAX_PROJECTION_TOTAL_PATH_BYTES = 768;
|
|
247
|
+
const JSON_SCHEMA_TYPES = new Set([
|
|
248
|
+
"array",
|
|
249
|
+
"boolean",
|
|
250
|
+
"integer",
|
|
251
|
+
"null",
|
|
252
|
+
"number",
|
|
253
|
+
"object",
|
|
254
|
+
"string",
|
|
255
|
+
]);
|
|
256
|
+
const NON_SEMANTIC_REF_SIBLINGS = new Set([
|
|
257
|
+
"$anchor",
|
|
258
|
+
"$comment",
|
|
259
|
+
"$defs",
|
|
260
|
+
"$id",
|
|
261
|
+
"$ref",
|
|
262
|
+
"$schema",
|
|
263
|
+
"default",
|
|
264
|
+
"definitions",
|
|
265
|
+
"deprecated",
|
|
266
|
+
"description",
|
|
267
|
+
"examples",
|
|
268
|
+
"readOnly",
|
|
269
|
+
"title",
|
|
270
|
+
"writeOnly",
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
interface FieldProjection {
|
|
274
|
+
data: Record<string, unknown>;
|
|
275
|
+
unmatchedFields: string[];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface ProjectionFeedback {
|
|
279
|
+
data: Record<string, unknown>;
|
|
280
|
+
$connecta: {
|
|
281
|
+
type: "field_projection";
|
|
282
|
+
unmatchedFields: string[];
|
|
283
|
+
schemaDeclared?: true;
|
|
284
|
+
schemaCoverage?: "complete" | "partial";
|
|
285
|
+
invalidFields?: string[];
|
|
286
|
+
availableFields?: string[];
|
|
287
|
+
availableFieldsTruncated?: true;
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
interface SchemaFieldAnalysis {
|
|
292
|
+
paths: string[];
|
|
293
|
+
complete: boolean;
|
|
294
|
+
truncated: boolean;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function localSchemaRef(root: unknown, ref: string): unknown | undefined {
|
|
298
|
+
if (ref === "#") return root;
|
|
299
|
+
if (
|
|
300
|
+
!ref.startsWith("#/") ||
|
|
301
|
+
ref.length > 2_048 ||
|
|
302
|
+
/~(?![01])/.test(ref)
|
|
303
|
+
) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const segments = ref.slice(2).split("/");
|
|
307
|
+
if (segments.length > MAX_PROJECTION_SCHEMA_DEPTH) return undefined;
|
|
308
|
+
let current = root;
|
|
309
|
+
for (const encoded of segments) {
|
|
310
|
+
if (current === null || typeof current !== "object") return undefined;
|
|
311
|
+
const key = encoded.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
312
|
+
if (!Object.prototype.hasOwnProperty.call(current, key)) return undefined;
|
|
313
|
+
current = (current as Record<string, unknown>)[key];
|
|
314
|
+
}
|
|
315
|
+
return current;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function hasSchemaType(
|
|
319
|
+
schema: Record<string, unknown>,
|
|
320
|
+
wanted: string,
|
|
321
|
+
): boolean {
|
|
322
|
+
return (
|
|
323
|
+
schema.type === wanted ||
|
|
324
|
+
(Array.isArray(schema.type) && schema.type.includes(wanted))
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function selectableFieldName(name: string): boolean {
|
|
329
|
+
return name.length > 0 && !name.includes(".") && !name.endsWith("[]");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Collect selectable output paths without trusting a schema more than JSON
|
|
334
|
+
* Schema permits. Traversal is iterative and budgeted before sorting or
|
|
335
|
+
* rendering, so a cyclic, extremely deep, or extremely broad downstream
|
|
336
|
+
* schema cannot turn projection feedback into unbounded host work.
|
|
337
|
+
*/
|
|
338
|
+
function analyzeSchemaFields(root: unknown): SchemaFieldAnalysis {
|
|
339
|
+
interface PendingSchema {
|
|
340
|
+
schema: unknown;
|
|
341
|
+
prefix: string;
|
|
342
|
+
depth: number;
|
|
343
|
+
ancestors: Set<unknown>;
|
|
344
|
+
}
|
|
345
|
+
const pending: PendingSchema[] = [
|
|
346
|
+
{ schema: root, prefix: "", depth: 0, ancestors: new Set() },
|
|
347
|
+
];
|
|
348
|
+
const paths = new Set<string>();
|
|
349
|
+
let nodes = 0;
|
|
350
|
+
let totalPathChars = 0;
|
|
351
|
+
let totalPathBytes = 0;
|
|
352
|
+
let complete = true;
|
|
353
|
+
let truncated = false;
|
|
354
|
+
|
|
355
|
+
const addPath = (path: string): boolean => {
|
|
356
|
+
if (paths.has(path)) return true;
|
|
357
|
+
// Check UTF-16 length before encoding, so a hostile multi-megabyte key
|
|
358
|
+
// never causes a same-sized temporary allocation merely to reject it.
|
|
359
|
+
if (
|
|
360
|
+
path.length > MAX_PROJECTION_PATH_CHARS ||
|
|
361
|
+
totalPathChars + path.length > MAX_PROJECTION_TOTAL_PATH_CHARS
|
|
362
|
+
) {
|
|
363
|
+
complete = false;
|
|
364
|
+
truncated = true;
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
const pathBytes = enc.encode(path).length;
|
|
368
|
+
if (
|
|
369
|
+
pathBytes > MAX_PROJECTION_PATH_BYTES ||
|
|
370
|
+
totalPathBytes + pathBytes > MAX_PROJECTION_TOTAL_PATH_BYTES ||
|
|
371
|
+
paths.size >= MAX_PROJECTION_SCHEMA_PATHS
|
|
372
|
+
) {
|
|
373
|
+
complete = false;
|
|
374
|
+
truncated = true;
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
paths.add(path);
|
|
378
|
+
totalPathChars += path.length;
|
|
379
|
+
totalPathBytes += pathBytes;
|
|
380
|
+
return true;
|
|
381
|
+
};
|
|
382
|
+
const enqueue = (item: PendingSchema): boolean => {
|
|
383
|
+
if (nodes + pending.length >= MAX_PROJECTION_SCHEMA_NODES) {
|
|
384
|
+
complete = false;
|
|
385
|
+
truncated = true;
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
pending.push(item);
|
|
389
|
+
return true;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
while (pending.length > 0) {
|
|
393
|
+
const item = pending.pop()!;
|
|
394
|
+
if (item.depth > MAX_PROJECTION_SCHEMA_DEPTH) {
|
|
395
|
+
complete = false;
|
|
396
|
+
truncated = true;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (++nodes > MAX_PROJECTION_SCHEMA_NODES) {
|
|
400
|
+
complete = false;
|
|
401
|
+
truncated = true;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
if (item.schema === false) continue;
|
|
405
|
+
if (
|
|
406
|
+
item.schema === true ||
|
|
407
|
+
item.schema === null ||
|
|
408
|
+
typeof item.schema !== "object" ||
|
|
409
|
+
item.ancestors.has(item.schema)
|
|
410
|
+
) {
|
|
411
|
+
complete = false;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const schema = item.schema as Record<string, unknown>;
|
|
416
|
+
const ancestors = new Set(item.ancestors).add(item.schema);
|
|
417
|
+
let recognized = false;
|
|
418
|
+
if (
|
|
419
|
+
schema.type !== undefined &&
|
|
420
|
+
!(
|
|
421
|
+
(typeof schema.type === "string" &&
|
|
422
|
+
JSON_SCHEMA_TYPES.has(schema.type)) ||
|
|
423
|
+
(Array.isArray(schema.type) &&
|
|
424
|
+
schema.type.length > 0 &&
|
|
425
|
+
schema.type.every(
|
|
426
|
+
(type) =>
|
|
427
|
+
typeof type === "string" && JSON_SCHEMA_TYPES.has(type),
|
|
428
|
+
))
|
|
429
|
+
)
|
|
430
|
+
) {
|
|
431
|
+
complete = false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (schema.$ref !== undefined) {
|
|
435
|
+
recognized = true;
|
|
436
|
+
let hasSemanticSiblings = false;
|
|
437
|
+
for (const key in schema) {
|
|
438
|
+
if (
|
|
439
|
+
Object.prototype.hasOwnProperty.call(schema, key) &&
|
|
440
|
+
!NON_SEMANTIC_REF_SIBLINGS.has(key)
|
|
441
|
+
) {
|
|
442
|
+
hasSemanticSiblings = true;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (hasSemanticSiblings) {
|
|
447
|
+
// Modern JSON Schema applies $ref siblings as an intersection. A
|
|
448
|
+
// compact field walker cannot prove that intersection's selectable
|
|
449
|
+
// paths, so do not publish paths from either half as available.
|
|
450
|
+
complete = false;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const target =
|
|
454
|
+
typeof schema.$ref === "string"
|
|
455
|
+
? localSchemaRef(root, schema.$ref)
|
|
456
|
+
: undefined;
|
|
457
|
+
if (target === undefined) complete = false;
|
|
458
|
+
else {
|
|
459
|
+
enqueue({
|
|
460
|
+
schema: target,
|
|
461
|
+
prefix: item.prefix,
|
|
462
|
+
depth: item.depth + 1,
|
|
463
|
+
ancestors,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
469
|
+
const variants = schema[keyword];
|
|
470
|
+
if (!Array.isArray(variants)) continue;
|
|
471
|
+
recognized = true;
|
|
472
|
+
// Combining schemas can close or conditionally expose fields in ways
|
|
473
|
+
// this compact recovery walker intentionally does not prove.
|
|
474
|
+
complete = false;
|
|
475
|
+
for (const variant of variants) {
|
|
476
|
+
if (
|
|
477
|
+
!enqueue({
|
|
478
|
+
schema: variant,
|
|
479
|
+
prefix: item.prefix,
|
|
480
|
+
depth: item.depth + 1,
|
|
481
|
+
ancestors,
|
|
482
|
+
})
|
|
483
|
+
) {
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (truncated) break;
|
|
489
|
+
|
|
490
|
+
const properties = schema.properties;
|
|
491
|
+
const propertyRecord =
|
|
492
|
+
properties !== null &&
|
|
493
|
+
typeof properties === "object" &&
|
|
494
|
+
!Array.isArray(properties)
|
|
495
|
+
? (properties as Record<string, unknown>)
|
|
496
|
+
: undefined;
|
|
497
|
+
if (properties !== undefined && propertyRecord === undefined) {
|
|
498
|
+
complete = false;
|
|
499
|
+
}
|
|
500
|
+
const objectShape =
|
|
501
|
+
hasSchemaType(schema, "object") || propertyRecord !== undefined;
|
|
502
|
+
if (objectShape) {
|
|
503
|
+
recognized = true;
|
|
504
|
+
const patterns = schema.patternProperties;
|
|
505
|
+
let hasPatterns = false;
|
|
506
|
+
if (
|
|
507
|
+
patterns !== undefined &&
|
|
508
|
+
(patterns === null ||
|
|
509
|
+
typeof patterns !== "object" ||
|
|
510
|
+
Array.isArray(patterns))
|
|
511
|
+
) {
|
|
512
|
+
complete = false;
|
|
513
|
+
} else if (patterns !== undefined) {
|
|
514
|
+
for (const key in patterns as Record<string, unknown>) {
|
|
515
|
+
if (Object.prototype.hasOwnProperty.call(patterns, key)) {
|
|
516
|
+
hasPatterns = true;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (schema.additionalProperties !== false || hasPatterns) {
|
|
522
|
+
complete = false;
|
|
523
|
+
}
|
|
524
|
+
if (propertyRecord) {
|
|
525
|
+
for (const key in propertyRecord) {
|
|
526
|
+
if (!Object.prototype.hasOwnProperty.call(propertyRecord, key)) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (++nodes > MAX_PROJECTION_SCHEMA_NODES) {
|
|
530
|
+
complete = false;
|
|
531
|
+
truncated = true;
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
if (key.length > MAX_PROJECTION_PATH_CHARS) {
|
|
535
|
+
complete = false;
|
|
536
|
+
truncated = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
if (!selectableFieldName(key)) {
|
|
540
|
+
complete = false;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const child = propertyRecord[key];
|
|
544
|
+
// A false property schema forbids the property; advertising its name
|
|
545
|
+
// as selectable would turn an impossible value into a valid hint.
|
|
546
|
+
if (child === false) continue;
|
|
547
|
+
const path = item.prefix ? `${item.prefix}.${key}` : key;
|
|
548
|
+
if (!addPath(path)) break;
|
|
549
|
+
if (
|
|
550
|
+
!enqueue({
|
|
551
|
+
schema: child,
|
|
552
|
+
prefix: path,
|
|
553
|
+
depth: item.depth + 1,
|
|
554
|
+
ancestors,
|
|
555
|
+
})
|
|
556
|
+
) {
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (truncated) break;
|
|
563
|
+
|
|
564
|
+
const arrayShape =
|
|
565
|
+
hasSchemaType(schema, "array") ||
|
|
566
|
+
schema.items !== undefined ||
|
|
567
|
+
schema.prefixItems !== undefined;
|
|
568
|
+
if (arrayShape) {
|
|
569
|
+
recognized = true;
|
|
570
|
+
const arrayPath = `${item.prefix}[]`;
|
|
571
|
+
if (addPath(arrayPath)) {
|
|
572
|
+
if (Array.isArray(schema.prefixItems)) {
|
|
573
|
+
complete = false;
|
|
574
|
+
for (const child of schema.prefixItems) {
|
|
575
|
+
if (
|
|
576
|
+
!enqueue({
|
|
577
|
+
schema: child,
|
|
578
|
+
prefix: arrayPath,
|
|
579
|
+
depth: item.depth + 1,
|
|
580
|
+
ancestors,
|
|
581
|
+
})
|
|
582
|
+
) {
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (schema.items === undefined) {
|
|
588
|
+
if (!Array.isArray(schema.prefixItems)) complete = false;
|
|
589
|
+
} else if (schema.items === true || Array.isArray(schema.items)) {
|
|
590
|
+
complete = false;
|
|
591
|
+
const children = Array.isArray(schema.items)
|
|
592
|
+
? schema.items
|
|
593
|
+
: [];
|
|
594
|
+
for (const child of children) {
|
|
595
|
+
if (
|
|
596
|
+
!enqueue({
|
|
597
|
+
schema: child,
|
|
598
|
+
prefix: arrayPath,
|
|
599
|
+
depth: item.depth + 1,
|
|
600
|
+
ancestors,
|
|
601
|
+
})
|
|
602
|
+
) {
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} else if (schema.items !== false) {
|
|
607
|
+
enqueue({
|
|
608
|
+
schema: schema.items,
|
|
609
|
+
prefix: arrayPath,
|
|
610
|
+
depth: item.depth + 1,
|
|
611
|
+
ancestors,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const types = Array.isArray(schema.type)
|
|
618
|
+
? schema.type
|
|
619
|
+
: schema.type === undefined
|
|
620
|
+
? []
|
|
621
|
+
: [schema.type];
|
|
622
|
+
const primitiveOnly =
|
|
623
|
+
types.length > 0 &&
|
|
624
|
+
types.every(
|
|
625
|
+
(type) =>
|
|
626
|
+
type === "string" ||
|
|
627
|
+
type === "number" ||
|
|
628
|
+
type === "integer" ||
|
|
629
|
+
type === "boolean" ||
|
|
630
|
+
type === "null",
|
|
631
|
+
);
|
|
632
|
+
if (!recognized && !primitiveOnly) complete = false;
|
|
633
|
+
if (truncated) break;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
paths: [...paths].sort(),
|
|
638
|
+
complete,
|
|
639
|
+
truncated,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function schemaProjectionFeedback(
|
|
644
|
+
outputSchema: unknown,
|
|
645
|
+
unmatchedFields: string[],
|
|
646
|
+
): Omit<ProjectionFeedback["$connecta"], "type" | "unmatchedFields"> {
|
|
647
|
+
const analysis = analyzeSchemaFields(outputSchema);
|
|
648
|
+
const available = new Set(analysis.paths);
|
|
649
|
+
const invalidFields = analysis.complete
|
|
650
|
+
? unmatchedFields.filter((field) => !available.has(field))
|
|
651
|
+
: [];
|
|
652
|
+
return {
|
|
653
|
+
schemaDeclared: true,
|
|
654
|
+
schemaCoverage: analysis.complete ? "complete" : "partial",
|
|
655
|
+
...(invalidFields.length > 0 ? { invalidFields } : {}),
|
|
656
|
+
...(analysis.paths.length > 0
|
|
657
|
+
? {
|
|
658
|
+
availableFields: analysis.paths.slice(
|
|
659
|
+
0,
|
|
660
|
+
MAX_PROJECTION_AVAILABLE_FIELDS,
|
|
661
|
+
),
|
|
662
|
+
}
|
|
663
|
+
: {}),
|
|
664
|
+
...(analysis.truncated ||
|
|
665
|
+
analysis.paths.length > MAX_PROJECTION_AVAILABLE_FIELDS
|
|
666
|
+
? { availableFieldsTruncated: true as const }
|
|
667
|
+
: {}),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Select the given dot-paths, retaining both matches and exact misses. */
|
|
240
672
|
function applyFields(
|
|
241
673
|
value: unknown,
|
|
242
674
|
fields: string[],
|
|
243
|
-
):
|
|
675
|
+
): FieldProjection {
|
|
244
676
|
const out: Record<string, unknown> = {};
|
|
677
|
+
const unmatchedFields: string[] = [];
|
|
245
678
|
for (const path of fields) {
|
|
246
679
|
const resolved = resolvePath(value, path.split("."));
|
|
247
|
-
if (resolved
|
|
680
|
+
if (resolved === undefined) unmatchedFields.push(path);
|
|
681
|
+
else out[path] = resolved;
|
|
248
682
|
}
|
|
249
|
-
return out;
|
|
683
|
+
return { data: out, unmatchedFields };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Keep the historical flat projection when every path resolves. A miss gets a
|
|
688
|
+
* wrapper with a reserved discriminator so neither `{}` nor downstream fields
|
|
689
|
+
* named `data` / `projection` can be mistaken for projection feedback.
|
|
690
|
+
*/
|
|
691
|
+
function projectionValue(
|
|
692
|
+
value: unknown,
|
|
693
|
+
fields: string[],
|
|
694
|
+
outputSchema?: unknown,
|
|
695
|
+
): Record<string, unknown> | ProjectionFeedback {
|
|
696
|
+
const projected = applyFields(value, fields);
|
|
697
|
+
// `$connecta` is reserved at the top level of a projection. Even a fully
|
|
698
|
+
// matched downstream field with that exact name is escaped below `data`, so
|
|
699
|
+
// no user-controlled value can impersonate Connecta's discriminator.
|
|
700
|
+
const reservedCollision = Object.prototype.hasOwnProperty.call(
|
|
701
|
+
projected.data,
|
|
702
|
+
"$connecta",
|
|
703
|
+
);
|
|
704
|
+
if (projected.unmatchedFields.length === 0 && !reservedCollision) {
|
|
705
|
+
return projected.data;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
data: projected.data,
|
|
709
|
+
$connecta: {
|
|
710
|
+
type: "field_projection",
|
|
711
|
+
unmatchedFields: projected.unmatchedFields,
|
|
712
|
+
...(outputSchema
|
|
713
|
+
? schemaProjectionFeedback(outputSchema, projected.unmatchedFields)
|
|
714
|
+
: {}),
|
|
715
|
+
},
|
|
716
|
+
};
|
|
250
717
|
}
|
|
251
718
|
|
|
252
719
|
/** Apply fields to each JSON-parseable text block; non-JSON blocks pass through. */
|
|
253
720
|
function applyFieldsToContent(
|
|
254
721
|
content: TextContent[],
|
|
255
722
|
fields: string[],
|
|
723
|
+
outputSchema?: unknown,
|
|
256
724
|
): TextContent[] {
|
|
257
725
|
return content.map((b) => {
|
|
258
726
|
if (b.type !== "text") return b;
|
|
@@ -262,7 +730,10 @@ function applyFieldsToContent(
|
|
|
262
730
|
} catch {
|
|
263
731
|
return b;
|
|
264
732
|
}
|
|
265
|
-
return {
|
|
733
|
+
return {
|
|
734
|
+
...b,
|
|
735
|
+
text: JSON.stringify(projectionValue(parsed, fields, outputSchema)),
|
|
736
|
+
};
|
|
266
737
|
});
|
|
267
738
|
}
|
|
268
739
|
|
|
@@ -424,6 +895,7 @@ async function guardContent(
|
|
|
424
895
|
export interface SearchArgs {
|
|
425
896
|
query?: string;
|
|
426
897
|
connector?: string;
|
|
898
|
+
safety?: "readOnly" | "approvalRequired" | "all";
|
|
427
899
|
limit?: number;
|
|
428
900
|
offset?: number;
|
|
429
901
|
fullDescriptions?: boolean;
|
|
@@ -596,7 +1068,13 @@ export function createMetaTools(
|
|
|
596
1068
|
globalCap,
|
|
597
1069
|
);
|
|
598
1070
|
if (call.resultMode === "value") {
|
|
599
|
-
let value = fields
|
|
1071
|
+
let value = fields
|
|
1072
|
+
? projectionValue(
|
|
1073
|
+
result,
|
|
1074
|
+
fields,
|
|
1075
|
+
resolved.definition.outputSchema,
|
|
1076
|
+
)
|
|
1077
|
+
: result;
|
|
600
1078
|
value = await guardValue(value, results, cap);
|
|
601
1079
|
return {
|
|
602
1080
|
toolResult: jsonResult({ ok: true, data: value }),
|
|
@@ -606,10 +1084,22 @@ export function createMetaTools(
|
|
|
606
1084
|
if (resolved.connector.kind === "mcp") {
|
|
607
1085
|
const mcpResult = result as { content?: TextContent[] };
|
|
608
1086
|
let content = mcpResult?.content ?? [];
|
|
609
|
-
if (fields)
|
|
1087
|
+
if (fields) {
|
|
1088
|
+
content = applyFieldsToContent(
|
|
1089
|
+
content,
|
|
1090
|
+
fields,
|
|
1091
|
+
resolved.definition.outputSchema,
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
610
1094
|
return { toolResult: await guardContent(content, results, cap) };
|
|
611
1095
|
}
|
|
612
|
-
const value = fields
|
|
1096
|
+
const value = fields
|
|
1097
|
+
? projectionValue(
|
|
1098
|
+
result,
|
|
1099
|
+
fields,
|
|
1100
|
+
resolved.definition.outputSchema,
|
|
1101
|
+
)
|
|
1102
|
+
: result;
|
|
613
1103
|
return {
|
|
614
1104
|
toolResult: await guardText(
|
|
615
1105
|
serializeResultText(value),
|
|
@@ -1131,10 +1621,10 @@ export function createMetaTools(
|
|
|
1131
1621
|
|
|
1132
1622
|
const LIST_DESC =
|
|
1133
1623
|
"List connectors with status, cached tool count, and recent real-call health. Use probe=false for a fast inventory; use probe=true (default) only to diagnose live health or authorization.";
|
|
1134
|
-
const SEARCH_DESC = `Unknown address: use 2–4 distinctive action/object terms, not the full request; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}) and page only if needed, up to ${MAX_SEARCH_LIMIT}. includeSchemas="compact" adds the input and any declared output shape; matches also carry declared annotations. Call directly when sufficient. Empty query browses all.`;
|
|
1624
|
+
const SEARCH_DESC = `Unknown address: use 2–4 distinctive action/object terms, not the full request; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}) and page only if needed, up to ${MAX_SEARCH_LIMIT}. Partial and no-match searches report term coverage and next-step guidance. safety="readOnly" returns only calls available to call_tool and generated code; "approvalRequired" returns everything else; omitted or "all" preserves the complete catalog. This filters results, not authority. includeSchemas="compact" adds the input and any declared output shape, each bounded; inputSchemaTruncated/outputSchemaTruncated mark shapes that need exact retrieval; matches also carry declared annotations. Call directly when sufficient. Empty query browses all.`;
|
|
1135
1625
|
const DESCRIBE_DESC = `Only when search_tools omitted schemas, a compact shape is ambiguous, or exact JSON constraints are needed. Inspects up to ${MAX_DESCRIBE_ADDRESSES} addresses with schemas and annotations; "compact" is default, while "json" preserves exact constraints.`;
|
|
1136
1626
|
const CALL_DESC =
|
|
1137
|
-
'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths
|
|
1627
|
+
'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths; any misses return data plus `$connecta` field-projection feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
1138
1628
|
const CALL_DESTRUCTIVE_DESC =
|
|
1139
1629
|
"Invoke any tool that is not explicitly annotated readOnlyHint: true, including unannotated, write-capable, or destructive tools. The MCP destructiveHint on this meta-tool lets the host request human approval before execution. Use only after reviewing the downstream tool schema and consequences.";
|
|
1140
1630
|
const GET_RESULT_DESC =
|
|
@@ -1159,7 +1649,7 @@ const SKILLS_DESC =
|
|
|
1159
1649
|
*/
|
|
1160
1650
|
const CODE_FIRST_SEARCH_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
|
|
1161
1651
|
const CODE_FIRST_CALL_DESC =
|
|
1162
|
-
'Use for ONE tool explicitly annotated readOnlyHint: true — the cheapest path for a single cold call. For two or more calls, dependent steps, loops, joins, or data reduction use execute_code, whose connecta.call and connecta.batch reach the same tools. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths
|
|
1652
|
+
'Use for ONE tool explicitly annotated readOnlyHint: true — the cheapest path for a single cold call. For two or more calls, dependent steps, loops, joins, or data reduction use execute_code, whose connecta.call and connecta.batch reach the same tools. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths; any misses return data plus `$connecta` field-projection feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
1163
1653
|
const CODE_FIRST_GET_RESULT_DESC =
|
|
1164
1654
|
"Page a truncated result stashed by call_tool or call_destructive_tool; a program's oversized return is not paged, so reduce it in code instead. Input { id, offset?, maxBytes? } → { text, offset, nextOffset?, totalBytes } sliced by byte offset. maxBytes is a whole number of bytes >= 1 (omit for the deployment default) and offset a whole number of bytes >= 0; an offset inside a multi-byte character is moved back to that character's first byte and the offset served is returned. Unknown/expired id is an error.";
|
|
1165
1655
|
|
|
@@ -1304,6 +1794,9 @@ export function registerMetaTools(
|
|
|
1304
1794
|
inputSchema: z.object({
|
|
1305
1795
|
query: z.string().optional(),
|
|
1306
1796
|
connector: z.string().optional(),
|
|
1797
|
+
safety: z
|
|
1798
|
+
.enum(["readOnly", "approvalRequired", "all"])
|
|
1799
|
+
.optional(),
|
|
1307
1800
|
limit: z.number().int().positive().max(MAX_SEARCH_LIMIT).optional(),
|
|
1308
1801
|
offset: z.number().int().nonnegative().optional(),
|
|
1309
1802
|
fullDescriptions: z.boolean().optional(),
|
|
@@ -285,6 +285,69 @@
|
|
|
285
285
|
.credential-field input { min-width: 0; width: 100%; }
|
|
286
286
|
.danger { text-decoration-style: double; }
|
|
287
287
|
|
|
288
|
+
.token-create {
|
|
289
|
+
border-bottom: 1px solid var(--rule);
|
|
290
|
+
border-top: 1px solid var(--rule);
|
|
291
|
+
padding: .75rem 0;
|
|
292
|
+
}
|
|
293
|
+
.token-create > label {
|
|
294
|
+
display: block;
|
|
295
|
+
margin-bottom: .5rem;
|
|
296
|
+
}
|
|
297
|
+
.token-create input { flex: 1 1 18rem; }
|
|
298
|
+
.token-create button,
|
|
299
|
+
.token-card button,
|
|
300
|
+
.token-reveal button {
|
|
301
|
+
align-items: center;
|
|
302
|
+
display: inline-flex;
|
|
303
|
+
min-height: 2.75rem;
|
|
304
|
+
}
|
|
305
|
+
.token-reveal {
|
|
306
|
+
background: var(--ink);
|
|
307
|
+
color: var(--paper);
|
|
308
|
+
margin-top: 1.5rem;
|
|
309
|
+
padding: 1rem 1.25rem;
|
|
310
|
+
}
|
|
311
|
+
.token-reveal .meta,
|
|
312
|
+
.token-reveal .cap { color: #bbb; }
|
|
313
|
+
.token-reveal-head,
|
|
314
|
+
.token-card-head {
|
|
315
|
+
align-items: baseline;
|
|
316
|
+
display: flex;
|
|
317
|
+
flex-wrap: wrap;
|
|
318
|
+
gap: .25rem var(--gap);
|
|
319
|
+
justify-content: space-between;
|
|
320
|
+
}
|
|
321
|
+
.token-secret {
|
|
322
|
+
border-bottom: 1px solid #555;
|
|
323
|
+
border-top: 1px solid #555;
|
|
324
|
+
margin-top: .75rem;
|
|
325
|
+
}
|
|
326
|
+
.token-secret code {
|
|
327
|
+
color: var(--paper);
|
|
328
|
+
user-select: all;
|
|
329
|
+
}
|
|
330
|
+
.token-ledger {
|
|
331
|
+
border-bottom: 1px solid var(--rule);
|
|
332
|
+
margin-top: 1.5rem;
|
|
333
|
+
}
|
|
334
|
+
.token-card {
|
|
335
|
+
border-top: 1px solid var(--rule);
|
|
336
|
+
padding: .75rem 0 .75rem 1.25rem;
|
|
337
|
+
position: relative;
|
|
338
|
+
}
|
|
339
|
+
.token-card::before {
|
|
340
|
+
background: var(--ink);
|
|
341
|
+
bottom: 0;
|
|
342
|
+
content: "";
|
|
343
|
+
left: .25rem;
|
|
344
|
+
position: absolute;
|
|
345
|
+
top: 0;
|
|
346
|
+
width: 1px;
|
|
347
|
+
}
|
|
348
|
+
.token-card.revoked { color: var(--muted); }
|
|
349
|
+
.token-card.revoked::before { background: var(--rule); }
|
|
350
|
+
|
|
288
351
|
details { margin-top: .75rem; }
|
|
289
352
|
summary { cursor: pointer; list-style: none; width: max-content; }
|
|
290
353
|
summary::-webkit-details-marker { display: none; }
|