@zackbart/connecta 0.25.0 → 0.26.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 (93) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +271 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/artifacts/connector.d.ts +55 -0
  9. package/dist/artifacts/connector.js +724 -0
  10. package/dist/artifacts/document.js +76 -0
  11. package/dist/artifacts/guide.js +140 -0
  12. package/dist/artifacts/html-scan.js +329 -0
  13. package/dist/artifacts/json.js +68 -0
  14. package/dist/artifacts/kv-store.d.ts +20 -0
  15. package/dist/artifacts/kv-store.js +140 -0
  16. package/dist/artifacts/markdown.js +449 -0
  17. package/dist/artifacts/module.d.ts +37 -0
  18. package/dist/artifacts/module.js +60 -0
  19. package/dist/artifacts/operations.d.ts +195 -0
  20. package/dist/artifacts/operations.js +944 -0
  21. package/dist/artifacts/refresh.d.ts +53 -0
  22. package/dist/artifacts/refresh.js +235 -0
  23. package/dist/artifacts/routes.js +194 -0
  24. package/dist/artifacts/types.d.ts +214 -0
  25. package/dist/artifacts/types.js +22 -0
  26. package/dist/artifacts/validate.d.ts +62 -0
  27. package/dist/artifacts/validate.js +652 -0
  28. package/dist/artifacts.d.ts +6 -0
  29. package/dist/artifacts.js +9 -0
  30. package/dist/catalog-service.js +39 -29
  31. package/dist/catalog.js +42 -8
  32. package/dist/connector-access.d.ts +18 -4
  33. package/dist/connector-access.js +65 -20
  34. package/dist/connector-caller.d.ts +8 -0
  35. package/dist/connector-caller.js +19 -0
  36. package/dist/connectors/remote-mcp.js +16 -2
  37. package/dist/errors.d.ts +16 -1
  38. package/dist/errors.js +28 -0
  39. package/dist/execute.js +427 -70
  40. package/dist/executor-admission.d.ts +11 -1
  41. package/dist/executor-admission.js +79 -14
  42. package/dist/executors/quickjs.js +4 -2
  43. package/dist/index.d.ts +62 -6
  44. package/dist/index.js +230 -6
  45. package/dist/invocation.js +56 -10
  46. package/dist/meta-tools.js +7 -2
  47. package/dist/module-contracts.d.ts +21 -1
  48. package/dist/operator-ui/generated.js +2 -2
  49. package/dist/operator-ui/model.d.ts +62 -5
  50. package/dist/operator-ui/view.js +94 -5
  51. package/dist/program-source.js +116 -0
  52. package/dist/registry.d.ts +42 -6
  53. package/dist/registry.js +148 -47
  54. package/dist/resumable.js +1291 -0
  55. package/dist/routes/activity.js +6 -69
  56. package/dist/routes/actor-labels.js +114 -0
  57. package/dist/routes/credentials.js +27 -5
  58. package/dist/routes/mcp.js +116 -59
  59. package/dist/routes/oauth-management.js +22 -9
  60. package/dist/routes/operator.js +1 -0
  61. package/dist/routes/shared.d.ts +23 -1
  62. package/dist/routes/shared.js +20 -2
  63. package/dist/routes/ui-data.js +2 -1
  64. package/dist/routes/ui.js +13 -4
  65. package/dist/run-journal.js +347 -0
  66. package/dist/runtime/shared-read.js +106 -0
  67. package/dist/server.js +26 -0
  68. package/dist/skills.js +27 -8
  69. package/dist/timeout.js +26 -0
  70. package/dist/tool-safety.d.ts +35 -0
  71. package/dist/tool-safety.js +31 -0
  72. package/dist/types.d.ts +18 -0
  73. package/dist/ui.d.ts +12 -5
  74. package/dist/ui.js +32 -6
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/documentation/architecture.md +196 -19
  78. package/documentation/auth.md +83 -2
  79. package/documentation/code-mode.md +221 -48
  80. package/documentation/meta-tools.md +44 -25
  81. package/ethos.md +12 -10
  82. package/examples/worker/AGENTS.md +6 -0
  83. package/examples/worker/README.md +174 -10
  84. package/examples/worker/src/d1-activity-row.ts +14 -2
  85. package/examples/worker/src/d1-activity.ts +4 -2
  86. package/examples/worker/src/index.ts +36 -1
  87. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  88. package/examples/worker/wrangler.jsonc +5 -1
  89. package/package.json +6 -2
  90. package/templates/node/AGENTS.md +9 -2
  91. package/templates/node/README.md +31 -3
  92. package/templates/node/package.json +1 -1
  93. package/templates/node/src/index.ts +27 -0
@@ -64,17 +64,33 @@ function isCallerCancellation(error, signal) {
64
64
  return (signal?.aborted === true ||
65
65
  (isCallAdmissionError(error) && error.admissionKind === "cancelled"));
66
66
  }
67
+ /**
68
+ * A downstream MCP tool answered with `isError`. Classified exactly as the
69
+ * plain `Error` it used to be — same message, same heuristics — and marked so
70
+ * a write's outcome can tell "the service said no" from "nobody answered".
71
+ */
72
+ class DownstreamToolError extends Error {
73
+ }
67
74
  function assertRawMcpSuccess(kind, result) {
68
75
  if (kind !== "mcp" || result == null || typeof result !== "object")
69
76
  return;
70
77
  const mcpResult = result;
71
78
  if (!mcpResult.isError)
72
79
  return;
73
- throw new Error(boundedEchoText(mcpResult.content
80
+ throw new DownstreamToolError(boundedEchoText(mcpResult.content
74
81
  ?.filter((block) => block.type === "text")
75
82
  .map((block) => block.text ?? "")
76
83
  .join("") || "Downstream tool call failed"));
77
84
  }
85
+ /**
86
+ * Whether a failed attempt is the downstream tool's own answer: an MCP
87
+ * `isError`. A connector's typed error is not, by itself, one — a response
88
+ * too large to read or a refused redirect comes after the request was acted
89
+ * on — so its code decides instead (`classifyWriteOutcome`).
90
+ */
91
+ function answeredFailure(error) {
92
+ return error instanceof DownstreamToolError;
93
+ }
78
94
  export class InvocationFailure extends Error {
79
95
  details;
80
96
  code;
@@ -134,6 +150,10 @@ export class InvocationService {
134
150
  let connectorMs = 0;
135
151
  let resultProcessingMs = 0;
136
152
  let attempts = 0;
153
+ let dispatchedToConnector = false;
154
+ let answered = false;
155
+ // How a write gate's refusal is recorded; see WriteGateDecision.
156
+ let gateActivity;
137
157
  let resolved;
138
158
  let activityTarget;
139
159
  // The address as written, used for activity when resolution never reached
@@ -227,6 +247,25 @@ export class InvocationService {
227
247
  const diagnostics = timing();
228
248
  const target = resolved ?? activityTarget;
229
249
  const details = enrich(error, target);
250
+ const outcome = () => ({
251
+ ok: false,
252
+ durationMs: Date.now() - started,
253
+ attempts,
254
+ timing: diagnostics,
255
+ ...defined({ resolved }),
256
+ dispatched: dispatchedToConnector,
257
+ ...(dispatchedToConnector ? { answered } : {}),
258
+ error: details,
259
+ });
260
+ // A pause is not a failure: it leaves one payload-free `paused` event
261
+ // naming the call that waits, and nothing in the operator's log. A call
262
+ // refused only because its run had already stopped leaves neither.
263
+ if (gateActivity === "none")
264
+ return outcome();
265
+ if (gateActivity === "paused") {
266
+ record("paused");
267
+ return outcome();
268
+ }
230
269
  // Activity rows stay payload-free by construction; the operator's log is
231
270
  // where the downstream reason goes, bounded and without arguments.
232
271
  if (target && details.code !== "destructive_tool_requires_approval") {
@@ -249,14 +288,7 @@ export class InvocationService {
249
288
  : details.code === "cancelled"
250
289
  ? "cancelled"
251
290
  : "error", { errorCode: details.code });
252
- return {
253
- ok: false,
254
- durationMs: Date.now() - started,
255
- attempts,
256
- timing: diagnostics,
257
- ...defined({ resolved }),
258
- error: details,
259
- };
291
+ return outcome();
260
292
  };
261
293
  if (context.requestSignal?.aborted) {
262
294
  // Preserve the cancelled admission-attempt count without starting discovery.
@@ -290,7 +322,11 @@ export class InvocationService {
290
322
  const target = resolution.resolved;
291
323
  resolved = target;
292
324
  activityTarget = target;
293
- if (!isExplicitlyReadOnly(target.definition) && !context.allowDestructive) {
325
+ const consequential = !isExplicitlyReadOnly(target.definition) && !context.allowDestructive;
326
+ const gate = consequential && (context.gates?.(target) ?? true)
327
+ ? context.writeGate
328
+ : undefined;
329
+ if (consequential && !gate) {
294
330
  const canonicalAddress = `${target.connector.id}.${target.toolName}`;
295
331
  return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`);
296
332
  }
@@ -308,6 +344,13 @@ export class InvocationService {
308
344
  if (invalid)
309
345
  return classifyCallError(invalid);
310
346
  }
347
+ if (gate) {
348
+ const decision = yield* gate(target, args ?? {});
349
+ if (decision.kind === "refuse") {
350
+ gateActivity = decision.activity;
351
+ return decision.error;
352
+ }
353
+ }
311
354
  try {
312
355
  context.beforeDispatch?.();
313
356
  }
@@ -329,6 +372,7 @@ export class InvocationService {
329
372
  // Cancellation can arrive during admission or context construction.
330
373
  if (callSignal?.aborted)
331
374
  throw callSignal.reason;
375
+ dispatchedToConnector = true;
332
376
  return target.connector.callTool(target.toolName, args ?? {}, connectorContext);
333
377
  };
334
378
  // The permit belongs to this scope, so success, failure, and the
@@ -351,6 +395,7 @@ export class InvocationService {
351
395
  if (callSignal?.aborted)
352
396
  return yield* Effect.fail(callSignal.reason);
353
397
  const attemptError = Cause.squash(attempt.cause);
398
+ answered = answeredFailure(attemptError);
354
399
  return isCallerCancellation(attemptError, context.requestSignal)
355
400
  ? callerCancelledDetails()
356
401
  : classifyCallError(attemptError);
@@ -415,6 +460,7 @@ export class InvocationService {
415
460
  durationMs: Date.now() - started,
416
461
  attempts,
417
462
  timing: diagnostics,
463
+ dispatched: dispatchedToConnector,
418
464
  };
419
465
  }
420
466
  catch {
@@ -400,6 +400,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
400
400
  concurrency: discoveryConcurrency,
401
401
  defer: opts.defer,
402
402
  requestSignal: opts.requestSignal,
403
+ approval: opts.approval,
403
404
  // searchRoute keeps its top-level default. In-program callers use a
404
405
  // separate CatalogService configured for connecta.search.
405
406
  });
@@ -470,6 +471,8 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
470
471
  // Sanitized `unavailable` diagnostics ride the structured shape too;
471
472
  // the plain-text path would drop them (#539).
472
473
  outcome.error.details !== undefined ||
474
+ // So does a conflict's `current`: where things stand is the retry.
475
+ outcome.error.current !== undefined ||
473
476
  [
474
477
  "auth_required",
475
478
  "invalid_args",
@@ -780,7 +783,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
780
783
  },
781
784
  };
782
785
  }
783
- const SEARCH_DESC = `Use top-level search for catalog inspection or approval-required work before call_destructive_tool. Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds direct or program calls; "approvalRequired" finds the fail-closed complement. These filters grant no authority. Empty query browses.`;
786
+ const SEARCH_DESC = `Use top-level search for catalog inspection or approval-required work before call_destructive_tool. Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds tools that run unasked; "approvalRequired" finds the fail-closed complement. These filters grant no authority. Empty query browses.`;
784
787
  const CALL_DESC = 'Call one known-address tool explicitly annotated readOnlyHint: true. Use execute_code for unknown-address, multiple, dependent, or reduced read-only work. Unannotated or write-capable tools fail closed to call_destructive_tool. A truncated result carries a get_result action.';
785
788
  const CALL_DESTRUCTIVE_DESC = "Call any tool not explicitly annotated readOnlyHint: true. Include a short reason for the human reviewer after checking the schema and consequences. The reason grants no authority and is not sent downstream.";
786
789
  const GET_RESULT_DESC = "Page a truncated direct-call result by id and byte offset. A program result is never paged; reduce it inside execute_code. Returns a one-line JSON header (offset, bytes, totalBytes, hasMore, nextAction) and then the page as raw text. maxBytes is an upper bound, clamped to the result's inline cap.";
@@ -868,7 +871,8 @@ const GET_RESULT_INPUT = advertisedSchema(z.object({
868
871
  }));
869
872
  /**
870
873
  * Register the six explicit meta-tools onto an McpServer instance.
871
- * `registerExecuteTool` adds the seventh, `execute_code`. Broad discovery and
874
+ * `registerExecuteTool` adds the seventh, `execute_code`, and
875
+ * `registerResumeTool` the eighth, `resume_execution`. Broad discovery and
872
876
  * multi-call work uses discovery and ordinary JavaScript promises inside a
873
877
  * program, which `execute_code` builds over the same
874
878
  * `CatalogService` and `InvocationService` these handlers use — one shared
@@ -884,6 +888,7 @@ export function registerMetaTools(server, registry, ctx) {
884
888
  credentialHandoffUrl: ctx.credentialHandoffUrl,
885
889
  requestSignal: ctx.requestSignal,
886
890
  defer: ctx.defer,
891
+ approval: ctx.approval,
887
892
  });
888
893
  server.registerTool("skills", {
889
894
  description: describedFor(registry, SKILLS_DESC, "skills"),
@@ -1,6 +1,7 @@
1
- import type { ConnectaBranding } from "./types.js";
1
+ import type { ConnectaBranding, Connector } from "./types.js";
2
2
  import type { ActivityStore, ActivityReadGate, recordToolActivity, recordCatalogDriftActivity } from "./activity.js";
3
3
  import type { RouteContext } from "./routes/shared.js";
4
+ import type { ArtifactRefreshRuntime } from "./artifacts/refresh.js";
4
5
  /** Construction-time UI contract. Core never imports the implementation. */
5
6
  export interface OperatorSurface {
6
7
  readonly branding?: ConnectaBranding;
@@ -17,3 +18,22 @@ export interface ActivityModule {
17
18
  readonly recordTool: typeof recordToolActivity;
18
19
  readonly recordDrift: typeof recordCatalogDriftActivity;
19
20
  }
21
+ /**
22
+ * The artifacts module, created by `artifacts()` from /artifacts. Core appends
23
+ * its one prebuilt connector to the configured set — the same catalog,
24
+ * invocation, admission, and activity paths as any other — and imports none
25
+ * of its implementation.
26
+ */
27
+ export interface ArtifactsModule {
28
+ /** The built-in `artifacts` connector. */
29
+ readonly connector: Connector;
30
+ /**
31
+ * Serve the pages' JSON API (`/artifacts/_api/*`) and the sandboxed frame
32
+ * (`/artifacts/_frame`); null for any other path. The operator UI delegates
33
+ * here and serves the page shells itself, so without `ui` there are no
34
+ * artifact routes at all.
35
+ */
36
+ handle(context: RouteContext): Promise<Response | null>;
37
+ /** Bind the optional refresh runner after core has built its registry and executor. */
38
+ bindRefresh?(runtime: ArtifactRefreshRuntime): void;
39
+ }