@zackbart/connecta 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +387 -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 +80 -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 +478 -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 +217 -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 +653 -0
  28. package/dist/artifacts.d.ts +6 -0
  29. package/dist/artifacts.js +9 -0
  30. package/dist/auth/downstream-oauth.js +397 -48
  31. package/dist/auth/oauth-start-reset.js +20 -0
  32. package/dist/branding.d.ts +87 -0
  33. package/dist/branding.js +170 -1
  34. package/dist/catalog-service.js +39 -29
  35. package/dist/catalog.js +42 -8
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connectors/remote-mcp.js +48 -9
  41. package/dist/errors.d.ts +16 -1
  42. package/dist/errors.js +28 -0
  43. package/dist/execute.js +431 -75
  44. package/dist/executor-admission.d.ts +11 -1
  45. package/dist/executor-admission.js +79 -14
  46. package/dist/executors/quickjs.js +4 -2
  47. package/dist/index.d.ts +62 -6
  48. package/dist/index.js +230 -6
  49. package/dist/invocation.js +56 -10
  50. package/dist/meta-tools.js +7 -2
  51. package/dist/module-contracts.d.ts +21 -1
  52. package/dist/oauth-callback-outcome.js +43 -18
  53. package/dist/operator-ui/generated.js +2 -2
  54. package/dist/operator-ui/model.d.ts +62 -5
  55. package/dist/operator-ui/view.js +372 -28
  56. package/dist/page-styles.js +7 -0
  57. package/dist/program-source.js +116 -0
  58. package/dist/registry.d.ts +42 -6
  59. package/dist/registry.js +164 -60
  60. package/dist/resumable.js +1291 -0
  61. package/dist/routes/activity.js +6 -69
  62. package/dist/routes/actor-labels.js +114 -0
  63. package/dist/routes/credentials.js +27 -5
  64. package/dist/routes/mcp.js +120 -59
  65. package/dist/routes/oauth-management.js +87 -20
  66. package/dist/routes/oauth.js +54 -87
  67. package/dist/routes/operator.js +1 -0
  68. package/dist/routes/shared.d.ts +23 -1
  69. package/dist/routes/shared.js +20 -2
  70. package/dist/routes/ui-data.js +2 -1
  71. package/dist/routes/ui.js +19 -4
  72. package/dist/run-journal.js +347 -0
  73. package/dist/runtime/shared-read.js +106 -0
  74. package/dist/server.js +30 -2
  75. package/dist/skills.js +30 -11
  76. package/dist/timeout.js +26 -0
  77. package/dist/tool-safety.d.ts +35 -0
  78. package/dist/tool-safety.js +31 -0
  79. package/dist/types.d.ts +23 -0
  80. package/dist/ui.d.ts +17 -5
  81. package/dist/ui.js +51 -68
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/documentation/architecture.md +214 -27
  85. package/documentation/auth.md +186 -2
  86. package/documentation/code-mode.md +236 -63
  87. package/documentation/meta-tools.md +55 -29
  88. package/ethos.md +12 -10
  89. package/examples/worker/AGENTS.md +6 -0
  90. package/examples/worker/README.md +174 -10
  91. package/examples/worker/src/d1-activity-row.ts +14 -2
  92. package/examples/worker/src/d1-activity.ts +4 -2
  93. package/examples/worker/src/index.ts +36 -1
  94. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  95. package/examples/worker/wrangler.jsonc +5 -1
  96. package/package.json +6 -2
  97. package/templates/node/AGENTS.md +9 -2
  98. package/templates/node/README.md +31 -3
  99. package/templates/node/package.json +1 -1
  100. package/templates/node/src/index.ts +27 -0
package/ethos.md CHANGED
@@ -5,12 +5,13 @@ Connecta’s scope, refusals, and invariants. Contradictions require a design de
5
5
  ## What this is
6
6
 
7
7
  - **One MCP endpoint.** Agents reach configured connectors through JavaScript,
8
- a required executor, and seven meta-tools, eight planned.
8
+ a required executor, and eight meta-tools.
9
9
  - **Config-as-code.** One tenant and connector set; identities get
10
10
  config-derived views. Maintained providers, `remoteMcp()`, and `api()` share
11
11
  one connector contract.
12
- - **Safe by default.** Programs run only explicitly read-only tools unasked;
13
- others cross `call_destructive_tool` or, planned, pause. The host owns approval.
12
+ - **Safe by default.** Programs run only read-only or config-exempt tools
13
+ unasked; others pause for `resume_execution` or cross
14
+ `call_destructive_tool`. The host owns approval.
14
15
  - **One fetch-native core.** Web APIs on Node and Workers; platform code and
15
16
  optional features behind explicit subpaths; Effect inside, Promises outside.
16
17
  - **Human auth management.** Optional pages show status and payload-free history.
@@ -33,10 +34,11 @@ carries them.
33
34
  | --- | --- | --- |
34
35
  | OpenAPI / GraphQL ingestion | refused | the disease is a document-authored tool nobody chose; hand-written literals, even through a shared factory, are still authorship |
35
36
  | Multi-tenancy / account model | refused | one deployment per tenant; inbound auth owns identity |
36
- | Approvals and pauses | planned | a program pauses host-side at its first unapproved write until the destructive-annotated `resume_execution` repeats that exact call, the host's prompt approving it or its tool; an expiring journal replays the run, never re-sending unknown outcomes |
37
+ | Approvals and pauses | accepted | a program pauses host-side at its first unapproved write until the destructive-annotated `resume_execution` repeats it; an expiring journal replays it, never re-sending unknown outcomes ([#565](https://github.com/zackbart/connecta/issues/565)) |
38
+ | Approval exemptions | accepted | config-only, program-only, never read-only elsewhere ([#566](https://github.com/zackbart/connecta/issues/566)) |
37
39
  | Runtime connector registration | refused | config-as-code is the security model |
38
40
  | Optional deployment modules | accepted | typed slots select UI, activity, vault, and inbound auth; core keeps discovery, execution, invocation, and enforcement |
39
- | Artifacts module | planned | a built-in connector, not a meta-tool: team-only sandboxed pages over stored JSON, never calling tools. Immutable versions let writes skip approval. Supersedes [#287](https://github.com/zackbart/connecta/issues/287) on executor's artifacts |
41
+ | Artifacts module | accepted | a built-in connector for team-only sandboxed pages over stored JSON; immutable versions let writes skip approval. Supersedes [#287](https://github.com/zackbart/connecta/issues/287) |
40
42
  | Plugin lifecycle, provider registry, or marketplace | refused | modules are deployment code, not runtime installs; prebuilt connections are imports, discovered in docs ([#297](https://github.com/zackbart/connecta/issues/297)) |
41
43
  | Connecta-issued access tokens | removed | inbound providers authenticate clients; bearer auth stays an optional adapter |
42
44
  | Expanded Notion page create/update options | refused | different workflows, not missing fields; use `api()` ([#408](https://github.com/zackbart/connecta/issues/408)) |
@@ -57,12 +59,12 @@ carries them.
57
59
  | Connector shortcut globals | removed | canonical addresses need no sanitization |
58
60
  | `connecta.batch` | removed | JavaScript promises suffice |
59
61
  | Automatic direct-call retries | removed | callers own retry timing |
60
- | Connector HTTP routes | removed | deployments own custom routes; artifact viewing is the planned exception |
62
+ | Connector HTTP routes | removed | deployments own custom routes; the operator UI's artifact viewer is the exception |
61
63
  | Caller-selected toolkits | removed | config derives every view, grant-gated `/mcp/<pool>` pools included ([#178](https://github.com/zackbart/connecta/issues/178), [#531](https://github.com/zackbart/connecta/issues/531)) |
62
64
  | Proactive credential liveness | removed | fail-at-use is enough ([#179](https://github.com/zackbart/connecta/issues/179)) |
63
65
  | Classic (executor-free) surface | removed | an executor is mandatory ([#273](https://github.com/zackbart/connecta/issues/273)) |
64
66
  | Per-result lexical query coverage | removed | failed a precommitted response-bytes gate ([#323](https://github.com/zackbart/connecta/issues/323)) |
65
- | Stabilized workflows | planned | only as artifact refresh: scheduled read-only programs, shared credentials ([#225](https://github.com/zackbart/connecta/issues/225)) |
67
+ | Stabilized workflows | accepted | only as artifact refresh: deployment-triggered read-only programs, shared credentials ([#225](https://github.com/zackbart/connecta/issues/225), [#564](https://github.com/zackbart/connecta/issues/564)) |
66
68
  | Semantic tool search | gated | keyword search has not been shown to fail ([#27](https://github.com/zackbart/connecta/issues/27)) |
67
69
  | MRTR / `input_required` passthrough | gated | relayable statelessly; no host or downstream emits it yet ([#176](https://github.com/zackbart/connecta/issues/176)) |
68
70
  | Downstream `ttlMs` cache hints | gated | needs refresh-churn evidence ([#206](https://github.com/zackbart/connecta/issues/206)) |
@@ -76,7 +78,7 @@ design decision.
76
78
 
77
79
  - **Fail-closed read-only.** A missing, false, or contradictory annotation never gets the benefit of the doubt.
78
80
  - **Generated code cannot mint capabilities.** Admission, credentials, and classification are enforced below the sandbox.
79
- - **Only explicitly read-only work runs unasked in the sandbox.** Everything else crosses `call_destructive_tool` or, planned, `resume_execution` or a config exemption.
81
+ - **Only explicitly read-only or config-exempt work runs unasked in the sandbox.** Others pause or cross `call_destructive_tool`.
80
82
  - **Nothing request-bound survives a request.** No transport, stream, signal, or awaited promise outlives it; a paused journal is data, not a request.
81
83
  - **A downstream catalog is complete or it is a failure.** A partial one is never cached, persisted, or served.
82
84
  - **Activity is payload-free by construction.** The event type has nowhere to put arguments, results, code, or raw errors.
@@ -84,8 +86,8 @@ design decision.
84
86
  - **Credentials never leave the host.** Encrypted at rest, readable only by the owning connector (and principal, for personal auth), rendered by nothing.
85
87
  - **Import-graph purity.** Nothing reachable from the root entry imports a `node:` builtin or `effect/testing`.
86
88
  - **The published surface is a boundary.** Heavyweight or platform-bound code goes behind an optional-peer subpath; Effect is the one hard dependency, named by no published type.
87
- - **Human routes manage auth, never capability.** Visibility grants use, not administration. Shared and personal auth mutations need separate config-derived permissions, both denied by default, and activity reads their own. Planned: viewing artifacts.
88
- - **Omitted modules do no work.** Core imports no UI bundle, vault, activity, or bearer implementation. OAuth callbacks work without UI.
89
+ - **Human routes manage auth, never capability.** Visibility grants use, not administration. Auth mutations need separate config-derived permissions, denied by default; activity reads its own. Artifact viewing respects connector grants and cannot invoke tools.
90
+ - **Omitted modules do no work.** Core imports no UI bundle, vault, activity, artifacts, or bearer implementation. OAuth callbacks work without UI.
89
91
  - **Status reads do not start authorization.** Only an explicit authorized action starts OAuth, never a page load.
90
92
  - **Structural mistakes throw at construction.** Booting into the wrong shape is worse than not booting.
91
93
 
@@ -37,6 +37,12 @@ This repository is deployment configuration, not a copy of Connecta itself.
37
37
  A configured Connecta bearer cannot cross the Access edge alone. Connecta-issued
38
38
  tokens are removed. UI, vault, and activity use explicit optional imports;
39
39
  auth changes require code-derived shared or personal management permissions.
40
+ - Brand the operator UI in `operatorUi({ branding })`: product and owner names,
41
+ description, favicon, and `theme` (`accent`, `radius`, `fontFamily`,
42
+ `monoFamily`, `colorScheme`). Ask the deployment's owner for their brand
43
+ rather than leaving the default. The commented block above
44
+ `ui: operatorUi()` in `src/index.ts` shows the shape; README "UI and
45
+ encrypted credentials" covers the rest.
40
46
  - After configuration changes, typecheck: inside the connecta repository that
41
47
  is `npm run check:examples`; a copied deployment runs its own `tsc --noEmit`.
42
48
  After deployment, connect both Claude and ChatGPT to `<PUBLIC_URL>/mcp` and
@@ -2,7 +2,7 @@
2
2
 
3
3
  A deployable Worker that aggregates a downstream remote MCP and an in-code HTTP
4
4
  API connector, guarded by Cloudflare Access, with state in a KV namespace. Its
5
- required Worker Loader binding backs the seven-tool surface and requires the
5
+ required Worker Loader binding backs the eight-tool surface and requires the
6
6
  Workers Paid plan.
7
7
 
8
8
  This is also the **starting template for a deployment**: a real deployment
@@ -19,8 +19,17 @@ migrations, and secrets.
19
19
  | `src/d1-storage.ts` | `KVStorage` over D1 with atomic compare-and-set (optional; see below) |
20
20
  | `src/d1-activity.ts` | `ActivityStore` over D1 (deployment-owned; see below) |
21
21
  | `src/d1-activity-row.ts` | the row ↔ event mapping `d1-activity.ts` uses, including friction derived from `error_code` for rows written before that column existed |
22
+ | `src/r2-artifact-blobs.ts` | artifact bodies in an R2 bucket, beside a D1-backed `kvArtifactStore` (optional) |
22
23
  | `wrangler.jsonc` | Worker name, vars, bindings, `compatibility_flags` |
23
24
 
25
+ Keep `enable_request_signal` in `wrangler.jsonc`. On Workers it lets a live
26
+ response's client disconnect abort its request, so connecta cancels the stream
27
+ and returns the admission permit promptly. The total admitted-request bound in
28
+ `admission.requests.maxDurationMs` (default 300,000 ms) also reclaims capacity
29
+ on a later request if workerd ends one without delivering a signal or stream
30
+ cancellation. Set it above the longest authorization, tool call, and response
31
+ delivery your deployment expects to serve.
32
+
24
33
  `cloudflare-kv.ts`, `d1-storage.ts`, and `d1-activity.ts` deliberately live
25
34
  here rather than in the package: storage backends are deployment-owned, so the
26
35
  package ships only the generic `KVStorage` and `ActivityStore` contracts.
@@ -103,11 +112,106 @@ The checked-in `access.dev` block gives `wrangler dev` a local operator
103
112
  identity. Remove the block to test the missing-Access refusal. It has no effect
104
113
  on a deployed Worker's production identity.
105
114
 
115
+ ### Optional resource management with Alchemy
116
+
117
+ Keep this example's Worker on Wrangler. A copied deployment may use
118
+ [Alchemy](https://alchemy.run) to manage its KV namespace, optional D1 databases
119
+ and R2 bucket, and the Access application without adding Alchemy to connecta or
120
+ changing this template. In that deployment, install `alchemy@2.0.0-beta.79`
121
+ and `effect@4.0.0-rc.117` at exact versions. The latter is connecta's own
122
+ Effect pin; check `npm ls effect` for one resolved copy after installation.
123
+ The following `alchemy.run.ts` is a resource stack, not a Worker deployment:
124
+
125
+ ```ts
126
+ import * as Alchemy from "alchemy";
127
+ import * as Cloudflare from "alchemy/Cloudflare";
128
+ import { adopt } from "alchemy/AdoptPolicy";
129
+ import * as Effect from "effect/Effect";
130
+
131
+ // Replace this with the immutable ID of the Wrangler-deployed Worker.
132
+ const workerId = "replace-with-worker-id";
133
+
134
+ export default Alchemy.Stack(
135
+ "ConnectaResources",
136
+ { providers: Cloudflare.providers(), state: Cloudflare.state() },
137
+ Effect.gen(function* () {
138
+ const kv = yield* Cloudflare.KV.Namespace("ConnectaKV", { title: "connecta-kv" }).pipe(adopt(true));
139
+ const activity = yield* Cloudflare.D1.Database("ActivityDB", {
140
+ name: "connecta-activity", migrations: "./migrations/activity",
141
+ }).pipe(adopt(true));
142
+ const storage = yield* Cloudflare.D1.Database("StorageDB", {
143
+ name: "connecta-storage", migrations: "./migrations/storage",
144
+ }).pipe(adopt(true));
145
+ const blobs = yield* Cloudflare.R2.Bucket("ArtifactBlobs", { name: "connecta-artifacts" }).pipe(adopt(true));
146
+ const access = yield* Cloudflare.Access.Application("ConnectaAccess", {
147
+ type: "self_hosted",
148
+ name: "Connecta",
149
+ destinations: [{ type: "worker", workerId }],
150
+ policies: [{ decision: "allow", include: [{ emailDomain: "example.com" }] }],
151
+ oauthConfiguration: {
152
+ enabled: true,
153
+ dynamicClientRegistration: {
154
+ enabled: true,
155
+ allowedUris: [
156
+ "https://claude.ai/api/mcp/auth_callback",
157
+ "https://chatgpt.com/connector_platform_oauth_redirect",
158
+ "https://chatgpt.com/connector/oauth/*",
159
+ ],
160
+ },
161
+ },
162
+ }).pipe(adopt(true));
163
+ return { kvId: kv.namespaceId, activityId: activity.databaseId,
164
+ storageId: storage.databaseId, bucketName: blobs.bucketName,
165
+ accessId: access.applicationId };
166
+ }),
167
+ );
168
+ ```
169
+
170
+ Replace the Worker ID and example Access policy before applying this stack.
171
+ Keep only the resources the deployment uses: `STORAGE_DB` replaces
172
+ `CONNECTA_KV`; `ACTIVITY_DB` and R2 are optional. Copy the output IDs into the
173
+ matching `wrangler.jsonc` bindings. Keep `LOADER` in `worker_loaders` and, when
174
+ enabled, the Browser Rendering binding and cron trigger in the Wrangler-owned
175
+ Worker configuration. Configure the main and artifact hostnames as Wrangler
176
+ custom domain `routes` with `custom_domain: true`. The Worker-level Access
177
+ destination covers both hostnames; verify both against the same application
178
+ after domain setup. Continue to set secrets with `wrangler secret put` and to
179
+ deploy the Worker with Wrangler. After its initial `wrangler deploy`, use
180
+ `wrangler versions upload` and `wrangler versions deploy` for code updates;
181
+ apply route and cron changes through Wrangler too.
182
+
183
+ The `adopt(true)` calls explicitly permit taking over matching existing
184
+ resources; compare their live names, IDs, Access policy, and data before the
185
+ first Alchemy deploy. Remove adoption where a resource is new. Put the D1 SQL
186
+ from this README in the shown migration directories before deploying. With
187
+ `migrations` configured, Alchemy copies an adopted database's Wrangler
188
+ migration history once into `__alchemy_migrations`; the old history stays
189
+ frozen. Review pending SQL before handing migrations over. Remote
190
+ `Cloudflare.state()` bootstraps an account-level state-store Worker backed by a
191
+ Durable Object and Secrets Store entries for its credentials. Retain them:
192
+ deleting the state store loses Alchemy's record of what it owns.
193
+
194
+ Verification for this optional path stops at the API and types. The fenced
195
+ snippet was copied to an isolated `alchemy.run.ts` and passed `tsc --noEmit`
196
+ with TypeScript 5.9.3, Alchemy 2.0.0-beta.79, and Effect 4.0.0-rc.117; no
197
+ Alchemy deploy or adoption was run.
198
+ [Alchemy's Worker resource](https://alchemy.run/cloudflare/compute/workers/)
199
+ can declare `WorkerLoader`, domains, Browser Rendering, and cron wiring, but
200
+ those properties are coupled to its script deploy. Its current full deploy
201
+ still uses the script-upload endpoint; only a conditional gradual rollout uses
202
+ the versions API. A previous full deploy through that endpoint did not provide
203
+ `ctx.access`, so do not transfer Worker ownership until that path is verified
204
+ end to end. The [Access](https://alchemy.run/cloudflare/security/access/),
205
+ [D1](https://alchemy.run/cloudflare/data/d1/), and
206
+ [state-store](https://alchemy.run/state-store/) guides describe the resources
207
+ above; the typecheck does not prove Cloudflare will accept this particular
208
+ Access adoption or preserve production identity.
209
+
106
210
  ### Copied into its own repository
107
211
 
108
212
  The `npm install` above is the connecta repository's, which already has every
109
- dependency this file imports. A copy with its own `package.json` installs three
110
- things, because two of them are not part of connecta and never install with it:
213
+ dependency this file imports. A copy with its own `package.json` installs
214
+ Connecta and the separately installed `@cloudflare/codemode` peer:
111
215
 
112
216
  ```sh
113
217
  npm install @zackbart/connecta @cloudflare/codemode
@@ -125,7 +229,10 @@ the Clerk path in the Node template.
125
229
 
126
230
  Then point an MCP client at `<PUBLIC_URL>/mcp`. The example explicitly enables
127
231
  `ui: operatorUi()`; open `<PUBLIC_URL>/` for Connections and each connection's
128
- authentication controls. There is no separate Credentials or Tokens tab.
232
+ authentication controls. The page labels tool safety, offers fixed repair
233
+ prompts for classified failures, and shows client setup commands for endpoints
234
+ the signed-in person may use. Those commands contain no token. There is no
235
+ separate Credentials or Tokens tab.
129
236
 
130
237
  ## Select optional modules
131
238
 
@@ -166,6 +273,8 @@ Reuse the same key and KV namespace during upgrades. Without the secret, omit
166
273
  the vault; declared credential slots remain unmanageable. Keep the key outside
167
274
  KV because it protects a copied namespace. Credential replacement takes effect
168
275
  on the next call without redeploying; no liveness probe runs in the background.
276
+ With this vault, 0.25.0 seals existing downstream OAuth state on first read.
277
+ Rolling back to 0.24.x then requires reauthorizing those OAuth connectors.
169
278
 
170
279
  The shipped Notion connector uses a deployment-owned static header and echo
171
280
  needs no secret. To exercise vault controls, declare a `credential` slot on an
@@ -186,7 +295,7 @@ below. Omit the module and store wiring to record no history and show no
186
295
  Activity tab. Diagnostics remain independent; `logger: "silent"` suppresses
187
296
  them explicitly.
188
297
 
189
- Verify MCP health and the exact seven tools with:
298
+ Verify MCP health and the exact eight tools with:
190
299
 
191
300
  ```sh
192
301
  CF_ACCESS_CLIENT_ID=… CF_ACCESS_CLIENT_SECRET=… \
@@ -211,7 +320,7 @@ The required Worker Loader binding is checked into `wrangler.jsonc`:
211
320
  ```
212
321
 
213
322
  `src/index.ts` constructs `DynamicWorkerExecutor` with only `env.LOADER` and
214
- serves the seven-tool surface. Do not add `bindings`, `modules`, or
323
+ serves the eight-tool surface. Do not add `bindings`, `modules`, or
215
324
  `globalOutbound`; they grant guest code ambient authority. A copied deployment
216
325
  owns the package install — see
217
326
  [copied into its own repository](#copied-into-its-own-repository).
@@ -219,7 +328,11 @@ owns the package install — see
219
328
  ## Strongly consistent storage (optional)
220
329
 
221
330
  `KVStorage` has an optional atomic `compareAndSet`, for a subsystem that must
222
- claim a key exactly once. **Workers KV cannot provide it.** It is eventually
331
+ claim a key exactly once — resumable writes, where two `resume_execution` calls
332
+ racing for one paused program must send its writes once. **Workers KV cannot
333
+ provide it**, so on KV this example turns resumable writes off
334
+ (`execute: { resumableWrites: false }` in `src/index.ts`) and its programs
335
+ refuse writes. It is eventually
223
336
  consistent: two locations can each read a key as absent and both write, so
224
337
  `cloudflare-kv.ts` declares no `compareAndSet` rather than fake one with a read
225
338
  followed by a write. `src/d1-storage.ts` is a complete `KVStorage` over D1
@@ -256,10 +369,56 @@ of connecta state lives in one store.
256
369
  const storage = d1Storage(env.STORAGE_DB);
257
370
  ```
258
371
 
372
+ Then delete the `execute: { resumableWrites: false }` line: on D1,
373
+ resumable writes default on, and programs pause at a write until
374
+ `resume_execution` approves it.
375
+
259
376
  Switching an existing deployment starts from empty state: downstream OAuth
260
377
  connections must be re-authorized and vault credentials re-entered, because
261
378
  nothing copies them out of the KV namespace.
262
379
 
380
+ ## Artifacts (optional)
381
+
382
+ `artifacts()` from `@zackbart/connecta/artifacts` adds the built-in `artifacts`
383
+ connector: team pages whose data lives in versioned JSON documents. Every write
384
+ commits by compare-and-set, so the store needs the D1 storage above — Workers
385
+ KV is refused at construction. Artifacts share the `connecta_kv` table and need
386
+ no schema of their own. Bodies (page sources and documents) can stay in D1 rows,
387
+ which the default limits keep well under D1's row size, or go to R2 with
388
+ `src/r2-artifact-blobs.ts`:
389
+
390
+ ```ts
391
+ import { artifacts, kvArtifactStore } from "@zackbart/connecta/artifacts";
392
+ import { r2ArtifactBlobs } from "./r2-artifact-blobs.js";
393
+
394
+ createConnecta({
395
+ // …
396
+ publicUrl: env.PUBLIC_URL, // required: artifact links are shared
397
+ artifacts: artifacts({
398
+ store: kvArtifactStore(d1Storage(env.STORAGE_DB), {
399
+ blobs: r2ArtifactBlobs(env.ARTIFACTS_BUCKET), // optional
400
+ }),
401
+ }),
402
+ });
403
+ ```
404
+
405
+ For R2, create the bucket (`wrangler r2 bucket create connecta-artifacts`) and
406
+ add `"r2_buckets": [{ "binding": "ARTIFACTS_BUCKET", "bucket_name":
407
+ "connecta-artifacts" }]` to `wrangler.jsonc` and `ARTIFACTS_BUCKET: R2Bucket`
408
+ to `Env`. Nothing ever deletes a body: versions are immutable, a rollback
409
+ points back at an old one, and a body written by a write that lost a conflict
410
+ stays stored unreferenced.
411
+
412
+ To refresh pages, keep the module in a variable, pass it to `createConnecta`,
413
+ and assign it to `scheduledArtifacts.module` as shown in `src/index.ts`.
414
+ Uncomment the hourly cron in `wrangler.jsonc`. An hourly tick starts at most
415
+ 10 due pages; each page's `manual`, `daily`, or `weekly` schedule lives in its
416
+ versioned refresh configuration. Refresh programs use only shared connectors'
417
+ explicitly read-only tools within the program owner's current grants. Revoking
418
+ refresh or pool access stops future runs. D1's compare-and-set prevents two cron invocations
419
+ from claiming the same page. Failed runs keep the last good data and show a
420
+ stale banner without exposing downstream error text to readers.
421
+
263
422
  ## Activity history (optional)
264
423
 
265
424
  `src/d1-activity.ts` is a complete `ActivityStore` over D1 — keyset paging on
@@ -298,6 +457,7 @@ To enable it:
298
457
  attempts INTEGER NOT NULL,
299
458
  error_code TEXT,
300
459
  friction TEXT,
460
+ approval TEXT,
301
461
  server_name TEXT NOT NULL,
302
462
  server_version TEXT NOT NULL,
303
463
  deployment_id TEXT
@@ -307,15 +467,19 @@ To enable it:
307
467
  ON tool_call_activity (occurred_at_ms DESC, id DESC);
308
468
  ```
309
469
 
310
- **Already have this table?** `actor_namespace` and `friction` were added
311
- after the original example, and `CREATE TABLE IF NOT EXISTS` will not add
312
- them to a table that already exists. Add them as migrations:
470
+ **Already have this table?** `actor_namespace`, `friction`, and `approval`
471
+ were added after the original example, and `CREATE TABLE IF NOT EXISTS`
472
+ will not add them to a table that already exists. Add them as migrations:
313
473
 
314
474
  ```sql
315
475
  ALTER TABLE tool_call_activity ADD COLUMN actor_namespace TEXT;
316
476
  ALTER TABLE tool_call_activity ADD COLUMN friction TEXT;
477
+ ALTER TABLE tool_call_activity ADD COLUMN approval TEXT;
317
478
  ```
318
479
 
480
+ `approval` is set only on an `approved` row — the scope a resumed
481
+ program's approval covered, `call` or `tool` — and is otherwise null.
482
+
319
483
  `friction` is stored rather than derived because one of its classes belongs
320
484
  to a call that *succeeded*: a result too large to return inline is friction
321
485
  for the agent and carries no error code. Rows written before the column keep
@@ -18,8 +18,15 @@ export interface ActivityEvent {
18
18
  | "call_tool"
19
19
  | "call_destructive_tool"
20
20
  | "batch_call"
21
- | "execute_code";
22
- outcome: "success" | "error" | "timeout" | "cancelled";
21
+ | "execute_code"
22
+ | "resume_execution";
23
+ outcome:
24
+ | "success"
25
+ | "error"
26
+ | "timeout"
27
+ | "cancelled"
28
+ | "paused"
29
+ | "approved";
23
30
  durationMs: number;
24
31
  attempts: number;
25
32
  errorCode?: string;
@@ -29,6 +36,7 @@ export interface ActivityEvent {
29
36
  | "destructive_reroute"
30
37
  | "auth_required"
31
38
  | "result_too_large";
39
+ approval?: "call" | "tool";
32
40
  serverName: string;
33
41
  serverVersion: string;
34
42
  deploymentId?: string;
@@ -76,6 +84,8 @@ export interface ActivityRow {
76
84
  attempts: number;
77
85
  error_code: string | null;
78
86
  friction: ActivityEvent["friction"] | null;
87
+ /** Absent from tables created before resumable writes; read as null. */
88
+ approval?: ActivityEvent["approval"] | null;
79
89
  server_name: string;
80
90
  server_version: string;
81
91
  deployment_id: string | null;
@@ -102,6 +112,7 @@ export function activityEventToRow(
102
112
  // friction on a call that succeeded, so it has no error code to derive from
103
113
  // — and `error_code IS NOT NULL` stays an honest count of failures.
104
114
  friction: event.friction ?? null,
115
+ approval: event.approval ?? null,
105
116
  server_name: event.serverName,
106
117
  server_version: event.serverVersion,
107
118
  deployment_id: event.deploymentId ?? null,
@@ -131,6 +142,7 @@ export function activityRowToEvent(
131
142
  attempts: row.attempts,
132
143
  ...(row.error_code ? { errorCode: row.error_code } : {}),
133
144
  ...(friction ? { friction } : {}),
145
+ ...(row.approval ? { approval: row.approval } : {}),
134
146
  serverName: row.server_name,
135
147
  serverVersion: row.server_version,
136
148
  ...(row.deployment_id
@@ -50,8 +50,9 @@ export function d1ActivityStore(db: D1Database): ActivityStore {
50
50
  id, occurred_at_ms, request_id, actor_kind, actor_id,
51
51
  actor_namespace,
52
52
  connector_id, tool_name, source, outcome, duration_ms, attempts,
53
- error_code, friction, server_name, server_version, deployment_id
54
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
53
+ error_code, friction, approval, server_name, server_version,
54
+ deployment_id
55
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
55
56
  )
56
57
  .bind(
57
58
  row.id,
@@ -68,6 +69,7 @@ export function d1ActivityStore(db: D1Database): ActivityStore {
68
69
  row.attempts,
69
70
  row.error_code,
70
71
  row.friction ?? null,
72
+ row.approval ?? null,
71
73
  row.server_name,
72
74
  row.server_version,
73
75
  row.deployment_id,
@@ -8,7 +8,7 @@ import { encryptedCredentialVault } from "@zackbart/connecta/credentials";
8
8
  * by Cloudflare Access, with OAuth/cache state in a KV namespace. Access
9
9
  * authenticates the request before this Worker runs and supplies the trusted
10
10
  * identity through ctx.access. The required Worker Loader binding in
11
- * wrangler.jsonc backs the seven-tool surface.
11
+ * wrangler.jsonc backs the eight-tool surface.
12
12
  *
13
13
  * The operator surface is wired here except for activity history, which needs
14
14
  * a database this example does not create for you: sign-in and the credential
@@ -43,6 +43,8 @@ import {
43
43
  } from "@zackbart/connecta";
44
44
  import { cloudflareAccessAuth } from "@zackbart/connecta/auth/cloudflare-access";
45
45
  import { cloudflareKvStorage } from "./cloudflare-kv.js";
46
+ // import { artifacts, kvArtifactStore } from "@zackbart/connecta/artifacts";
47
+ // import { d1Storage } from "./d1-storage.js";
46
48
  // Activity history, off by default because it needs a D1 database.
47
49
  // import { d1ActivityStore } from "./d1-activity.js";
48
50
 
@@ -67,10 +69,21 @@ interface Env {
67
69
 
68
70
  function build(env: Env) {
69
71
  const storage = cloudflareKvStorage(env.CONNECTA_KV);
72
+ // Optional refreshable pages need the D1 binding, not Workers KV:
73
+ // const artifactModule = artifacts({ store: kvArtifactStore(d1Storage(env.STORAGE_DB)) });
74
+ // scheduledArtifacts.module = artifactModule;
70
75
  return createConnecta({
71
76
  publicUrl: env.PUBLIC_URL,
72
77
  storage,
73
78
  executor: new DynamicWorkerExecutor({ loader: env.LOADER }),
79
+ // Resumable writes — a program pausing at a write until resume_execution
80
+ // approves it — need storage with an atomic compareAndSet, and Workers KV
81
+ // cannot provide one. On KV they are off: programs refuse writes and
82
+ // resume_execution says there is nothing to resume. This line states that
83
+ // choice (without it connecta would warn once at startup). To let programs
84
+ // pause, switch to the D1 store in README.md § "Strongly consistent
85
+ // storage" and delete this line.
86
+ execute: { resumableWrites: false },
74
87
  auth: [
75
88
  // Access owns admission policy. A human identity may use MCP and the
76
89
  // operator pages; a service token may use MCP but cannot mutate operator
@@ -103,7 +116,24 @@ function build(env: Env) {
103
116
  // connector like `notion()`, which declares its own) and the form appears
104
117
  // on the next load.
105
118
  vault: encryptedCredentialVault(storage, env.CREDENTIAL_ENCRYPTION_KEY),
119
+ // Branding is code too: name, owner, description, favicon, and five theme
120
+ // tokens that every other color is mixed from. A value that fails its
121
+ // check falls back to the default, and the startup warning names it.
122
+ // ui: operatorUi({
123
+ // branding: {
124
+ // productName: "Acme Tools",
125
+ // ownerName: "Acme",
126
+ // theme: {
127
+ // accent: "#2f5fe0", // hex only
128
+ // radius: 10, // pixels, or a CSS length such as "0.5rem"
129
+ // fontFamily: "Inter, system-ui, sans-serif",
130
+ // monoFamily: "ui-monospace, monospace",
131
+ // colorScheme: "system", // or "light" | "dark"
132
+ // },
133
+ // },
134
+ // }),
106
135
  ui: operatorUi(),
136
+ // artifacts: artifactModule,
107
137
  identity: { credentialAdministration: () => "all", personalConnection: () => "all" },
108
138
  // Payload-free activity at /activity, off until a database exists to hold
109
139
  // it. Uncomment the `d1_databases` binding in wrangler.jsonc, apply the
@@ -162,6 +192,7 @@ function build(env: Env) {
162
192
  // Lazy per-isolate singleton: reuses the plain-data tool cache. Downstream MCP
163
193
  // clients are request-scoped internally so Worker I/O never crosses requests.
164
194
  let connecta: ReturnType<typeof build> | undefined;
195
+ const scheduledArtifacts: { module?: { runDue(): Promise<unknown> } } = {};
165
196
 
166
197
  export default {
167
198
  // Pass `ctx` through: connecta hands deferred work (activity sinks) to
@@ -175,4 +206,8 @@ export default {
175
206
  connecta ??= build(env);
176
207
  return connecta.fetch(request, env, ctx);
177
208
  },
209
+ async scheduled(_event: ScheduledController, env: Env): Promise<void> {
210
+ connecta ??= build(env);
211
+ await scheduledArtifacts.module?.runDue();
212
+ },
178
213
  };
@@ -0,0 +1,29 @@
1
+ import type { ArtifactBlobStore } from "@zackbart/connecta/artifacts";
2
+
3
+ /**
4
+ * Artifact bodies — page sources, data documents, refresh programs — in an R2
5
+ * bucket, beside a D1-backed `kvArtifactStore` that keeps the heads, versions,
6
+ * and runs. Optional: without it, bodies live in D1 rows, which the default
7
+ * limits keep well under D1's row size. Use it once pages or documents grow,
8
+ * or to keep large bodies out of the database a backup copies.
9
+ *
10
+ * Keys are content addresses, so writing one twice writes the same bytes, and
11
+ * nothing here ever deletes one: versions are immutable, and a rollback points
12
+ * back at an old body rather than copying it.
13
+ */
14
+ export function r2ArtifactBlobs(
15
+ bucket: R2Bucket,
16
+ prefix = "artifacts/blobs/",
17
+ ): ArtifactBlobStore {
18
+ return {
19
+ async put(key, body) {
20
+ await bucket.put(`${prefix}${key}`, body, {
21
+ httpMetadata: { contentType: "text/plain; charset=utf-8" },
22
+ });
23
+ },
24
+ async get(key) {
25
+ const object = await bucket.get(`${prefix}${key}`);
26
+ return object ? object.text() : null;
27
+ },
28
+ };
29
+ }
@@ -3,9 +3,13 @@
3
3
  "name": "connecta-example",
4
4
  "main": "src/index.ts",
5
5
  "compatibility_date": "2025-01-01",
6
- "compatibility_flags": ["nodejs_compat"],
6
+ "compatibility_flags": ["nodejs_compat", "enable_request_signal"],
7
7
  "observability": { "enabled": true },
8
8
 
9
+ // Uncomment with artifacts and D1 in src/index.ts. Every hourly tick runs
10
+ // at most 10 due pages; each page's own daily/weekly schedule lives in data.
11
+ // "triggers": { "crons": ["0 * * * *"] },
12
+
9
13
  // Plain vars. Secrets (CREDENTIAL_ENCRYPTION_KEY, DOWNSTREAM_TOKEN) are set with
10
14
  // `wrangler secret put <NAME>`, not here.
11
15
  "vars": {
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@zackbart/connecta",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
- "description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",
6
+ "description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of eight meta-tools.",
7
7
  "license": "MIT",
8
8
  "engines": {
9
9
  "node": ">=22.0.0"
@@ -110,6 +110,10 @@
110
110
  "./auth/bearer": {
111
111
  "types": "./dist/auth/bearer.d.ts",
112
112
  "import": "./dist/auth/bearer.js"
113
+ },
114
+ "./artifacts": {
115
+ "types": "./dist/artifacts.d.ts",
116
+ "import": "./dist/artifacts.js"
113
117
  }
114
118
  },
115
119
  "scripts": {
@@ -3,8 +3,9 @@
3
3
  This repository is deployment configuration, not a copy of Connecta itself.
4
4
 
5
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.
6
+ - Keep `executor: quickJsExecutor()` for the prescribed eight-tool code-first
7
+ surface. File storage supports resumable writes, so a program pauses at a
8
+ write until `resume_execution` approves it.
8
9
  - Keep credentials in environment variables or an external secret store.
9
10
  Never commit `.env`, `.connecta-state.json`, `.connecta-activity.jsonl`,
10
11
  tokens, or credential values.
@@ -19,6 +20,12 @@ This repository is deployment configuration, not a copy of Connecta itself.
19
20
  alone never grants it. Configured bearer auth is a client option, not a human
20
21
  management identity. Connecta-issued access tokens are removed.
21
22
  `src/file-activity.ts` remains the deployment-owned history store.
23
+ - Brand the operator UI in `operatorUi({ branding })`: product and owner names,
24
+ description, favicon, and `theme` (`accent`, `radius`, `fontFamily`,
25
+ `monoFamily`, `colorScheme`). Ask the deployment's owner for their brand
26
+ rather than leaving the default. The commented block above
27
+ `ui: operatorUi()` in `src/index.ts` shows the shape; README "Select optional
28
+ modules" covers the rest.
22
29
  - Run `npm run typecheck` after configuration changes. With the server running,
23
30
  run `CONNECTA_TOKEN=... npm run doctor` before calling setup complete.
24
31
  - `Dockerfile` and `docker-compose.yml` containerize *this* source; they are