@zackbart/connecta 0.10.1 → 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 +51 -0
- package/README.md +53 -9
- package/bin/connecta.mjs +272 -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/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/skills.d.ts +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +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/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/catalog-service.ts +177 -15
- package/src/catalog.ts +143 -12
- package/src/execute.ts +372 -96
- package/src/invocation.ts +1 -8
- package/src/meta-tools.ts +504 -11
- package/src/skills.ts +1 -1
- package/src/tool-safety.ts +15 -0
- 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/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(),
|
package/src/skills.ts
CHANGED
|
@@ -60,7 +60,7 @@ Use exact addresses returned by discovery; never invent one. Search with 2–4 d
|
|
|
60
60
|
|
|
61
61
|
One async arrow function. The only capabilities are one global per connector (\`<connectorId>.<toolName>(args)\`), the four \`connecta\` functions, and \`console.log\`.
|
|
62
62
|
|
|
63
|
-
- What exists: \`connecta.search({})\` browses every catalog and \`
|
|
63
|
+
- What exists: \`connecta.search({})\` browses every catalog; add \`safety: "readOnly"\` for only calls the program can execute, and \`connector: "<id>"\` to browse one. This filters discovery results, not authority, and each match carries its \`address\` and annotations.
|
|
64
64
|
- Exact schemas for known addresses: \`connecta.describe({ addresses: [...] })\`; \`format: "json"\` only for exact constraints.
|
|
65
65
|
- Two to ten independent calls: \`connecta.batch([...])\`. Each outcome is \`{ address, ok: true, data }\` or \`{ address, ok: false, error, errorDetails: { code, retryable } }\`, which is also how a program tells a policy refusal from a transient failure.
|
|
66
66
|
- Search inside the run rather than searching first, and return only the reduction the answer needs — never raw payloads.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolDef } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one fail-closed classification shared by discovery and invocation.
|
|
5
|
+
*
|
|
6
|
+
* A tool is read-only only when it says so without also saying it is
|
|
7
|
+
* destructive. Missing, false, and contradictory annotations all require the
|
|
8
|
+
* approval-visible call path.
|
|
9
|
+
*/
|
|
10
|
+
export function isExplicitlyReadOnly(definition: ToolDef): boolean {
|
|
11
|
+
return (
|
|
12
|
+
definition.annotations?.readOnlyHint === true &&
|
|
13
|
+
definition.annotations?.destructiveHint !== true
|
|
14
|
+
);
|
|
15
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Working on this Connecta deployment
|
|
2
|
+
|
|
3
|
+
This repository is deployment configuration, not a copy of Connecta itself.
|
|
4
|
+
|
|
5
|
+
- Edit `src/index.ts` for connectors, authentication, storage, and public URL.
|
|
6
|
+
- Keep `executor: quickJsExecutor()` for the prescribed seven-tool code-first
|
|
7
|
+
surface.
|
|
8
|
+
- Keep credentials in environment variables or an external secret store.
|
|
9
|
+
Never commit `.env`, `.connecta-state.json`, tokens, or credential values.
|
|
10
|
+
- Add application logic only inside deliberate `api()` connector handlers.
|
|
11
|
+
Do not copy or modify Connecta package internals here.
|
|
12
|
+
- Prefer `api()` when the agent must see an exact reviewed capability surface;
|
|
13
|
+
`remoteMcp()` follows the downstream server's evolving tool catalog.
|
|
14
|
+
- Run `npm run typecheck` after configuration changes. With the server running,
|
|
15
|
+
run `CONNECTA_TOKEN=... npm run doctor` before calling setup complete.
|
|
16
|
+
|
|
17
|
+
Do not add alternate entrypoints, policy layers, generated connector catalogs,
|
|
18
|
+
or runtime connector registration. Keep the deployment small enough to review
|
|
19
|
+
as configuration.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Connecta deployment
|
|
2
|
+
|
|
3
|
+
This is the prescribed small Node deployment. Install and run it:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install
|
|
7
|
+
CONNECTA_TOKEN=dev-token npm start
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Then point an MCP client at `http://localhost:8787/mcp` with
|
|
11
|
+
`Authorization: Bearer dev-token`.
|
|
12
|
+
|
|
13
|
+
## Deployment contract
|
|
14
|
+
|
|
15
|
+
- Edit `src/index.ts` for connectors, auth, storage, and the public URL.
|
|
16
|
+
- Keep `executor: quickJsExecutor()` for the seven-tool code-first surface.
|
|
17
|
+
- Keep secrets in environment variables or an external secret store.
|
|
18
|
+
- Add application code only inside deliberate `api()` connector handlers.
|
|
19
|
+
- Do not copy Connecta package internals into this deployment.
|
|
20
|
+
- `AGENTS.md` is the canonical convention file; `CLAUDE.md` points to it.
|
|
21
|
+
|
|
22
|
+
Verify a change with:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm run typecheck
|
|
26
|
+
# In another terminal, while npm start is running:
|
|
27
|
+
CONNECTA_TOKEN=dev-token npm run doctor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Doctor checks health, the executor, and the exact prescribed seven-tool
|
|
31
|
+
model-facing surface by running a harmless sandbox program. It reads the bearer
|
|
32
|
+
from `CONNECTA_TOKEN`; it never accepts the secret as a command-line argument.
|
|
33
|
+
Remote URLs must use HTTPS.
|