@zackbart/connecta 0.27.0 → 0.28.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 (73) hide show
  1. package/AGENTS.md +6 -6
  2. package/CHANGELOG.md +229 -0
  3. package/README.md +23 -24
  4. package/bin/connecta.mjs +1 -5
  5. package/dist/activity.d.ts +5 -4
  6. package/dist/activity.js +1 -10
  7. package/dist/artifacts/connector.js +1 -1
  8. package/dist/artifacts/json.js +10 -0
  9. package/dist/artifacts/operations.js +1 -1
  10. package/dist/artifacts/validate.js +1 -2
  11. package/dist/auth/downstream-oauth.js +147 -28
  12. package/dist/auth/static-oauth.js +504 -0
  13. package/dist/catalog-service.js +1 -1
  14. package/dist/connectors/api-connector.d.ts +168 -0
  15. package/dist/connectors/api-connector.js +127 -0
  16. package/dist/connectors/api.d.ts +8 -71
  17. package/dist/connectors/api.js +10 -99
  18. package/dist/connectors/guarded-fetch.js +37 -3
  19. package/dist/connectors/remote-mcp.js +7 -32
  20. package/dist/execute.js +92 -368
  21. package/dist/executors/quickjs.js +1 -1
  22. package/dist/exempt-writes.js +162 -0
  23. package/dist/index.d.ts +13 -32
  24. package/dist/index.js +14 -47
  25. package/dist/invocation.js +6 -19
  26. package/dist/meta-tools.js +1 -2
  27. package/dist/oauth-partition.js +17 -0
  28. package/dist/operator-ui/generated.js +1 -1
  29. package/dist/operator-ui/model.d.ts +3 -3
  30. package/dist/operator-ui/view.js +6 -4
  31. package/dist/providers/basecamp.d.ts +65 -0
  32. package/dist/providers/basecamp.js +445 -0
  33. package/dist/providers/breeze.d.ts +34 -0
  34. package/dist/providers/breeze.js +1402 -0
  35. package/dist/providers/ccb.d.ts +103 -0
  36. package/dist/providers/ccb.js +1570 -0
  37. package/dist/providers/cloudflare.js +1 -1
  38. package/dist/providers/linear.js +1 -1
  39. package/dist/providers/mixpanel.js +1 -1
  40. package/dist/providers/notion.js +1 -1
  41. package/dist/providers/overflow.d.ts +60 -0
  42. package/dist/providers/overflow.js +1572 -0
  43. package/dist/providers/planning-center.d.ts +59 -0
  44. package/dist/providers/planning-center.js +2039 -0
  45. package/dist/providers/revenuecat.js +1 -1
  46. package/dist/providers/stripe.js +1 -1
  47. package/dist/providers/tithely.d.ts +49 -0
  48. package/dist/providers/tithely.js +1034 -0
  49. package/dist/providers/vercel.js +1 -1
  50. package/dist/routes/mcp.js +5 -21
  51. package/dist/routes/oauth.js +29 -3
  52. package/dist/routes/shared.d.ts +1 -6
  53. package/dist/server.js +0 -4
  54. package/dist/skills.js +8 -23
  55. package/dist/tool-safety.d.ts +1 -1
  56. package/dist/tool-safety.js +1 -1
  57. package/dist/types.d.ts +2 -2
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/documentation/architecture.md +15 -17
  61. package/documentation/auth.md +120 -6
  62. package/documentation/code-mode.md +91 -194
  63. package/documentation/meta-tools.md +38 -41
  64. package/ethos.md +8 -9
  65. package/examples/worker/README.md +10 -14
  66. package/examples/worker/src/d1-activity-row.ts +4 -1
  67. package/examples/worker/src/index.ts +1 -9
  68. package/package.json +26 -2
  69. package/templates/node/AGENTS.md +4 -3
  70. package/templates/node/README.md +3 -4
  71. package/templates/node/package.json +1 -1
  72. package/dist/resumable.js +0 -1323
  73. package/dist/run-journal.js +0 -347
@@ -16,16 +16,12 @@ identifiers (`A1`, `E3`, …) are stable and cited by [Verification](#verificati
16
16
  ## Deploy-time capability
17
17
 
18
18
  The `executor` passed to `createConnecta()` is required. `tools/list` is exactly
19
- eight — `execute_code`, `resume_execution`, `search_tools`, `call_tool`,
20
- `call_destructive_tool`, `authorize_connector`, `get_result`, and `skills`.
21
- Construction fails when the executor is missing, and the removed `surface`
22
- option is rejected rather than ignored
23
- ([#273](https://github.com/zackbart/connecta/issues/273)).
24
-
25
- [Resumable writes](#pausing-and-resuming) need storage with `compareAndSet`:
26
- omitted, `execute.resumableWrites` is on exactly when the storage has it (not
27
- Cloudflare KV, which warns once at startup), `true` without it refuses to
28
- construct, and `/health` reports which.
19
+ seven — `execute_code`, `search_tools`, `call_tool`, `call_destructive_tool`,
20
+ `authorize_connector`, `get_result`, and `skills`. Construction fails when the
21
+ executor is missing, and the removed `surface` option is rejected rather than
22
+ ignored ([#273](https://github.com/zackbart/connecta/issues/273)), as are the
23
+ pause-only `execute.resumableWrites` and `execute.pausedRunTtlSeconds`
24
+ ([#672](https://github.com/zackbart/connecta/issues/672)).
29
25
 
30
26
  On Node, install the optional `quickjs-emscripten` peer and use the package's
31
27
  QuickJS subpath:
@@ -141,33 +137,17 @@ storage carried to the next program, and no request-bound object outliving its
141
137
  request. Within one execution, host calls share one downstream request scope.
142
138
  `S9`'s output observation is host-owned catalog metadata, not guest memory — a
143
139
  later program receives a labeled field/type schema through discovery, never a
144
- prior value or object. A run paused at a write leaves a bounded, expiring
145
- journal (`W3`) for `resume_execution` to replay — host-owned data, not guest
146
- state: the replay gets back exactly the answers the first play got.
140
+ prior value or object.
147
141
 
148
142
  **P5.** Plain JavaScript only; TypeScript syntax is a syntax error. Portable
149
143
  code does not import: QuickJS blocks imports, Dynamic Workers expose the `X5`
150
144
  runtime modules, and neither exposes `require`.
151
145
 
152
- **P6.** A run's clock and randomness are pinned. `Date.now()`, `new Date()`,
153
- and `Date()` read one instant the host takes at run start, and it does not
154
- advance; `Date` with arguments, `Date.parse`, and `Date.UTC` are the native
155
- ones. `Math.random()` is sfc32 seeded from the host's CSPRNG per run. On a
156
- Dynamic Worker (QuickJS has neither, `X5`), `crypto.getRandomValues` and
157
- `randomUUID` draw from the same stream and `performance.now()` stays at 0, on
158
- the objects and their prototypes — so a Workers-only program replays too,
159
- which makes none of them contract (`P2`). An `Intl` formatter asked for "now"
160
- formats the pinned instant, and local time is UTC on both executors (the
161
- QuickJS child runs with `TZ=UTC`). The replacements are trusted prelude,
162
- locked like `Error` (`X11`), and the native constructor's `now` and prototype
163
- `constructor` read the same instant; QuickJS lets `defineProperty` replace a
164
- locked *global* binding, but the replacement is the program's own code. The
165
- reason is replay (`W6`): a program that branched on a moving clock would take
166
- another branch the second time and diverge (`W8`), and pinning every run means
167
- none can tell whether it is replayed. A program cannot time itself; `R7`
168
- measures from the host. The pin is best-effort beyond these: a program that
169
- depends on a source it misses, such as the order Workers timers fire in, is
170
- not portable (`P2`) and fails a replay as divergence.
146
+ **P6** pinned a run's clock and randomness so a paused program could replay;
147
+ it left with replay ([#672](https://github.com/zackbart/connecta/issues/672)).
148
+ `Date.now()` and `Math.random()` are the runtime's own again, and the id stays
149
+ retired. QuickJS still runs with `TZ=UTC`, so local time matches a Dynamic
150
+ Worker's (`X5`).
171
151
 
172
152
  ## Addressing
173
153
 
@@ -263,10 +243,10 @@ integration is ambiguous, because an unscoped search fans out across every
263
243
  configured connector. `safety: "readOnly"` returns exactly the tools
264
244
  `connecta.call` runs unasked; `"approvalRequired"` returns the complementary
265
245
  fail-closed class, false, missing, and contradictory annotations included,
266
- whose calls pause (`W1`) unless config exempts them (`W12`), which a row marks
267
- with `approval: "exempt"` without moving it out of that class; omitted or
268
- `"all"` preserves the complete catalog, as a program that means to write needs.
269
- These filters grant no authority and change no admission decision.
246
+ whose calls a program has refused (`E4`) unless config exempts them (`W12`),
247
+ which a row marks with `approval: "exempt"` without moving it out of that
248
+ class; omitted or `"all"` preserves the complete catalog. These filters grant
249
+ no authority and change no admission decision.
270
250
 
271
251
  **S2.** A requested object schema carries `inputKeys`, `requiredInputKeys`
272
252
  (declared properties only), and `outputKeys`: the names the rendered schema
@@ -335,10 +315,10 @@ is treated as `{}`.
335
315
  **S6.** Every call goes through the same catalog, fail-closed read-only
336
316
  predicate, admission, credential containment, timeout classification, health
337
317
  accounting, and activity recording as an ordinary meta-tool call. The predicate
338
- decides whether a call runs or pauses (`W1`); nothing a program does decides
339
- it. The sandbox is
340
- an additional containment layer, not a second implementation of the boundary,
341
- and nothing a program does widens what it can reach.
318
+ and config decide whether a call runs or is refused (`E4`, `W12`); nothing a
319
+ program does decides it. The sandbox is an additional containment layer, not a
320
+ second implementation of the boundary, and nothing a program does widens what
321
+ it can reach.
342
322
 
343
323
  ### Parallel calls
344
324
 
@@ -413,7 +393,7 @@ caller what to do next, and never invents a cause it was not told.
413
393
  | --- | --- | --- |
414
394
  | `unknown_address` | no connector owns the address | false |
415
395
  | `unknown_tool` | the connector has no such tool | false |
416
- | `destructive_tool_requires_approval` | the tool is not explicitly read-only and the deployment has no resumable writes (`E4`) | false |
396
+ | `destructive_tool_requires_approval` | the tool is neither explicitly read-only nor config-exempt (`E4`) | false |
417
397
  | `auth_required` | the credential is missing, expired, or rejected | false |
418
398
  | `invalid_args` | arguments or discovery bounds were rejected | false |
419
399
  | `not_found` | the downstream answered and the resource is not there — the one code that says skip this id rather than stop, raised only where the provider tells absence from a permission gap | false |
@@ -428,19 +408,7 @@ caller what to do next, and never invents a cause it was not told.
428
408
  | `result_processing_failed` | the result could not be prepared | per message |
429
409
  | `result_too_large` | a discovery response exceeded its byte bound | false |
430
410
  | `budget_exceeded` | the run exhausted a host-call, write, or emitted-output budget | false |
431
- | `execution_paused` | the run already stopped at a write that needs approval; no further call is made (`W1`) | false |
432
- | `pending_write_too_large` | a write's arguments are over 16 KiB serialized, too large to hold for approval (`W2`) | false |
433
- | `journal_too_large` | the run's source and recorded calls are over 4 MiB, too much to pause (`W3`) | false |
434
- | `write_outcome_unknown` | a write was sent and no answer came back; it is never sent again (`W9`) | false |
435
- | `execution_interrupted` | a resumed play sent writes and then ended without pausing, or storage failed under it; the run cannot be resumed (`W5`) | false |
436
- | `execution_diverged` | a replay stopped matching its journal (`W8`) | false |
437
- | `execution_claim_lost` | another `resume_execution` took the run over mid-play (`W5`) | false |
438
- | `approval_mismatch` | `resume_execution` did not repeat the paused write exactly (`W4`) | false |
439
- | `execution_token_stale` | the token names an earlier pause of a run that has moved on (`W4`) | false |
440
- | `execution_in_progress` | another `resume_execution` is playing the run (`W5`) | true |
441
- | `execution_expired` | the paused run outlived `execute.pausedRunTtlSeconds`, or reached another write after it (`W3`) | false |
442
- | `execution_not_found` | no paused run matches the token in this caller's partition and pool (`W3`) | false |
443
- | `resumable_writes_unavailable` | the deployment has no resumable writes, so nothing can be resumed (`W11`) | false |
411
+ | `write_outcome_unknown` | an exempt write was sent and no answer came back; it is never sent again (`W9`) | false |
444
412
 
445
413
  **E3.** `auth_required` carries the same recovery envelope as `call_tool`:
446
414
  `connector`, `operation`, `recovery` (`oauth`, `operator_config`, or
@@ -448,14 +416,15 @@ caller what to do next, and never invents a cause it was not told.
448
416
  sentence. A program cannot recover credentials — only an operator can — so stop
449
417
  and let the failure reach the model.
450
418
 
451
- **E4.** An unannotated, write-capable, or destructive tool never runs unasked:
452
- it pauses the run (`W1`), or, without resumable writes, is refused with
453
- `destructive_tool_requires_approval`, `nextAction` carrying its canonical
454
- address to `call_destructive_tool` plus the original arguments when they fit
455
- the 512-byte echo budget — whole or not at all, since a clipped copy is a
456
- different call. The model's short `reason` grants no authority and never goes
457
- downstream, and generated code can neither mint the capability nor approve its
458
- own write.
419
+ **E4.** An unannotated, write-capable, or destructive tool never runs in a
420
+ program unless config exempts it (`W12`). It is refused with
421
+ `destructive_tool_requires_approval` before validation and before anything is
422
+ sent, `nextAction` carrying its canonical address to `call_destructive_tool`
423
+ plus the original arguments when they fit the 512-byte echo budget — whole or
424
+ not at all, since a clipped copy is a different call. The host's prompt on that
425
+ call is the approval, and the only one: generated code can neither mint the
426
+ capability nor approve its own write, and the model's short `reason` grants no
427
+ authority and never goes downstream.
459
428
 
460
429
  **E5.** Failures of the *execution*, not of a call, never appear inside the
461
430
  guest: admission rejection (`executor_overloaded`, retryable, with
@@ -678,7 +647,7 @@ because connecta enforces them above the sandbox:
678
647
  | Bound | Value |
679
648
  | --- | --- |
680
649
  | Host calls per execution, shared by `search`, `describe`, and `call` | 20 by default, `execute.maxHostCalls` |
681
- | Writes per run, on top of the host calls they also spend (`W10`) | 10 by default, `execute.maxWrites` |
650
+ | Exempt writes per run, on top of the host calls they also spend (`W10`) | 10 by default, `execute.maxWrites` |
682
651
  | Deadline per host call | 15 s, `execute.hostCallTimeoutMs`; one deadline covers catalog resolution, admission, and the connector call |
683
652
  | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
684
653
  | `describe` addresses | ≤ 100 |
@@ -693,10 +662,7 @@ unknown tools, catalog failures, and other pre-dispatch refusals cost what a
693
662
  successful call costs; catching a refusal does not refund it. `search` and
694
663
  `describe` likewise spend on entry. Exhausting the budget fails that call with
695
664
  non-retryable `budget_exceeded` (`E2`) and a message naming the budget. No
696
- connector is reached, and the budget does not refill inside one execution. A
697
- replay (`W6`) spends both budgets again for every call it answers from the
698
- journal, so one play's totals are the run's totals and nothing is counted
699
- twice.
665
+ connector is reached, and the budget does not refill inside one execution.
700
666
 
701
667
  **L5.** The guest is memory-, stack-, and CPU-bounded, and a program that
702
668
  exhausts a bound ends the run with an error instead of degrading the host. The
@@ -738,12 +704,12 @@ refuses that attempt above the invocation path, so it is charged but records no
738
704
  event.
739
705
 
740
706
  **V2.** Each event carries `connectorId`, `toolName`, `address`, `source`,
741
- `outcome` (`success`, `error`, `timeout`, `cancelled`, `paused`, `approved`),
742
- `durationMs`, `attempts`, and `errorCode` when the call *failed* — plus
743
- request, actor, and server identity. `paused` and `approved` name the write a
744
- run stopped at and the one `resume_execution` approved, with zero attempts;
745
- an approval adds `approval: "call" | "tool"`, never the arguments. A resumed
746
- play's live calls carry `source: "resume_execution"`. Typed codes derive an optional `friction`: `tool_not_found`,
707
+ `outcome` (`success`, `error`, `timeout`, `cancelled`), `durationMs`,
708
+ `attempts`, and `errorCode` when the call *failed* — plus request, actor, and
709
+ server identity. Rows written before
710
+ [#672](https://github.com/zackbart/connecta/issues/672) may also carry the
711
+ `paused` and `approved` outcomes, `source: "resume_execution"`, and an
712
+ `approval` scope; readers still render them, and nothing emits them. Typed codes derive an optional `friction`: `tool_not_found`,
747
713
  `schema_retry`, `destructive_reroute`, or `auth_required`. The fifth class,
748
714
  `result_too_large`, cannot reach an `execute_code` event: it belongs to a
749
715
  `call_tool` result too large to return inline, and a program's own return is
@@ -764,114 +730,54 @@ payload-free *by construction* means the event has nowhere to put a payload.
764
730
  distinctive artifact is the program source — exactly what a payload-free history
765
731
  must never keep.
766
732
 
767
- **V5.** A replayed call records nothing: it was recorded when it happened, and
768
- a second event would count one call twice. Nor does a call refused because its
769
- run had already stopped, which was never an attempt.
770
-
771
- ## Pausing and resuming
772
-
773
- A program pauses host-side at its first write needing approval;
774
- `resume_execution` repeats that write to approve it, and the run replays from a
775
- journal to send it and carry on. A paused run is data, not a held program, so a
776
- restart loses nothing
777
- ([#565](https://github.com/zackbart/connecta/issues/565)); `ExecuteResult` and
778
- the executor duties are unchanged.
779
-
780
- **W1.** A call to a tool not explicitly read-only reaches the run's write gate
781
- after validation and before admission, so nobody approves arguments the schema
782
- rejects and a pause costs no permit. Unless an approval covers it, it is not
783
- sent: it rejects `execution_paused`, as does every later host call —
784
- unjournaled, unrecorded — while calls in flight finish and are journaled; one
785
- of those turning unknown fails the run instead (`W9`). The program's result is
786
- discarded, and a write that reaches the gate after it settles is not sent.
787
- Calls are numbered in issue order and a write decides only after every
788
- lower-numbered call has, so the pause point is reproducible.
789
-
790
- **W2.** The pause is a success: `{ paused: { address, args, token, expiresAt,
791
- nextAction, hint } }`, `address` canonical and `args` whole, because the host
792
- shows the human exactly those; `nextAction` is the resume call. Arguments over
793
- 16 KiB stop the run `pending_write_too_large`, nothing journaled; non-object
794
- arguments keep `E4`; a `__proto__` key is refused `invalid_args`, since a layer
795
- between program, host, and human could drop it and approve a different write.
796
-
797
- **W3.** The journal lives in the caller's result storage — the `get_result`
798
- stash's partition and trust level — and records the `/mcp/<pool>`: source,
799
- every host call and its answer, the `P6` clock and seed, and one header, the
800
- only key compared-and-set. Over 4 MiB fails `journal_too_large`. It expires
801
- `execute.pausedRunTtlSeconds` (default 1,800) after the first pause, and later
802
- pauses keep that deadline, so no replay mixes old reads with new. Another
803
- subject or pool finds no run.
804
-
805
- **W4.** `resume_execution` takes `token`, `address`, `args`, optional
806
- `approval`, and a dropped `reason`. Address and args must match the pending
807
- write as JSON, key order aside, with no `__proto__` key; otherwise it returns
808
- `approval_mismatch`. That, `execution_token_stale` (current pause),
809
- `execution_in_progress` (live claim), `execution_expired` (counts if writes
810
- were sent), and `execution_not_found` refuse before the claim, spending nothing.
811
- A repeat returns the ended run's answer past expiry; oversized text or emitted blocks get a completion receipt instead.
812
-
813
- **W5.** An approved write is sent at most once. Resuming claims the run by
814
- compare-and-set; a racing resume gets `execution_in_progress`. Each live write
815
- is first marked `sending` on the header, and a claim found moved sends nothing
816
- further and stops `execution_claim_lost`. The header outlives `expiresAt` while
817
- a claim is live, and by 30 minutes once a write was sent. A storage call over
818
- its deadline (the host call's, at least 5 s) fails: before dispatch nothing is
819
- sent, after it the run stops with the write counted. Replay picks up only at a
820
- pause: a play ended otherwise — executor failure (admission, cancellation, the
821
- `L3` watchdog) or a lapsed claim — returns to its pause only if it sent no
822
- write; a write left `sending` fails `write_outcome_unknown`, and sent writes
823
- fail `execution_interrupted`, the reads after them never journaled.
824
-
825
- **W6.** A replay runs from the top on the journal's clock and seed. A recorded
826
- call — matched by operation, address as written, and canonical arguments, in
827
- first issue order per key — is answered before the rest of the call path: no
828
- catalog, permit, connector, or activity (`V5`). A hit is never the pending
829
- call, which is not journaled. The approved write and everything after it go
830
- live. `emit` is not journaled; only the completing play delivers.
831
-
832
- **W7.** `approval: "call"`, the default, covers that one write; `"tool"` covers
833
- every later call to that canonical address for the run. Approvals live in the
834
- header, a retry's replacing one that sent nothing; nothing else grants one.
835
- Keep an authorized batch in one program; use tool scope for repeated calls to that address, while other addresses still need their own approval.
836
-
837
- **W8.** A replay fails `execution_diverged`, never replayed again, when (a) a
838
- call has no record before the approved write is repeated, (b) the program
839
- finishes without repeating it, or (c) without repeating a write already sent.
840
-
841
- **W9.** A write sent and never answered fails the run `write_outcome_unknown`
842
- wherever it is found — beside a pause, after the program returned, at a
843
- takeover — with its address, its args within the 512-byte echo budget, and
844
- `writes: { succeeded, failed, unknown }`; a failed run is never replayed. Only
845
- a refusal (`auth_required`, `invalid_args`, `not_found`, `rate_limited`,
846
- `conflict`) or the
733
+ **V5.** An exempt write a program issues after it has already returned is not
734
+ sent and records nothing: it was never an attempt (`W9`).
735
+
736
+ ## Writes
737
+
738
+ A program does not ask anyone anything. It runs explicitly read-only tools and
739
+ the writes config exempts from asking, and refuses every other write (`E4`), so
740
+ each consequential call is its own `call_destructive_tool` and the host's
741
+ permission prompt is the one approval there is. Programs once paused at a write
742
+ and resumed by replay
743
+ ([#565](https://github.com/zackbart/connecta/issues/565)); hosts approve one
744
+ call at a time, so under always-allow that prompt approved nothing and cost a
745
+ round trip and a replay, and it went
746
+ ([#672](https://github.com/zackbart/connecta/issues/672)). `W1`–`W8` and `W11`
747
+ belonged to pausing and stay retired; nothing reuses those ids.
748
+
749
+ **W9.** An exempt write the program does not await outlives it: the play stops
750
+ gating once the program settles — a write that reaches the gate after is not
751
+ sent (`V5`) — and waits for writes already dispatched, each within its host-call
752
+ deadline, before the run's scope aborts them. A write sent and never answered
753
+ makes the result `write_outcome_unknown` with
754
+ `writes: { succeeded, failed, unknown }`, whatever the program returned, and it
755
+ is never sent again. Only a refusal (`auth_required`, `invalid_args`,
756
+ `not_found`, `rate_limited`, `conflict`, `input_required_unsupported`) or the
847
757
  tool's own `isError` answers; an `isError` reading as a timeout does not, and
848
- `connector_call_failed` — an oversized body, a refused redirect — may come
849
- after the write landed. Every error from a resumed play carries `writes`, and
850
- once one landed or may have, `nextAction` says to check first; a program that
851
- throws after writing ends the run `failed` with them.
852
-
853
- **W10.** `execute.maxWrites` (default 10) bounds a run's writes beyond the host
854
- calls they spend, checked at the gate — so an over-budget write fails
855
- `budget_exceeded` instead of asking — and spent at dispatch.
758
+ `connector_call_failed` — an oversized body, a refused redirect — may come after
759
+ the write landed. A program that fails after writing carries the same `writes`
760
+ on its error, because it is not a program that is safe to run again.
856
761
 
857
- **W11.** Without resumable writes — storage without `compareAndSet`, or
858
- `execute.resumableWrites: false` — `E4` stands and `resume_execution` answers
859
- `resumable_writes_unavailable`.
762
+ **W10.** `execute.maxWrites` (default 10) bounds a run's exempt writes beyond
763
+ the host calls they spend, checked at the gate after validation and before
764
+ admission, so an over-budget write fails `budget_exceeded` without a permit.
765
+ The host-call budget bounds fan-out; this one bounds how much a program may
766
+ change unasked, which is a different question even for writes a deployment
767
+ called cheap.
860
768
 
861
769
  **W12.** Config may exempt a write from asking, and nothing else may
862
770
  ([#566](https://github.com/zackbart/connecta/issues/566)). `execute.approval`
863
771
  maps connector ids and `connector.tool` addresses to `"never"` or `"ask"`; the
864
772
  address wins over the connector entry, which wins over a connector's own
865
- built-in default (reserved for connectors connecta ships), which wins over
866
- `"ask"`. An exempt call is decided at the same gate as `W1` and dispatches
867
- instead of pausing, with everything else an approved write gets: the write
868
- budget (`W10`), the write-ahead mark (`W5`), the journal, a `V1` event. Without
869
- resumable writes, other writes keep `E4`; an exempt one still finishes after
870
- the program returns, never hiding an unknown outcome (`W9`). Exempt is never
871
- read-only: no read-only tool is reported exempt, discovery keeps an exempt tool
872
- approval-required, `call_tool` refuses it, and no annotation grants it, or a
873
- downstream could exempt itself. Unknown connectors and `api()` addresses its
874
- tools lack refuse to construct; an unserved remote address never matches.
773
+ built-in default (reserved for connectors connecta ships, such as artifacts),
774
+ which wins over `"ask"`. An exempt call is decided at the write gate, after
775
+ validation and before admission, and dispatches with the write budget (`W10`),
776
+ `W9`'s tracking, and a `V1` event. Exempt is never read-only: no read-only tool
777
+ is reported exempt, discovery keeps an exempt tool approval-required,
778
+ `call_tool` refuses it, and no annotation grants it, or a downstream could
779
+ exempt itself. Unknown connectors and `api()` addresses its tools lack refuse
780
+ to construct; an unserved remote address never matches.
875
781
 
876
782
  ## Executor exceptions
877
783
 
@@ -912,7 +818,8 @@ starts has no guest logs to recover.
912
818
 
913
819
  **X5. Leftover authority.** QuickJS blocks imports and has no `fetch`,
914
820
  `process`, timers, `crypto`, or `WebSocket`, and its Node child starts with an
915
- explicit environment holding only `TZ=UTC` (`P6`) rather than inheriting
821
+ explicit environment holding only `TZ=UTC`, so local time matches a Dynamic
822
+ Worker's, rather than inheriting
916
823
  deployment variables or `NODE_OPTIONS`. A Dynamic Worker has those globals plus a non-contract set of
917
824
  runtime builtins through `import()` and `process.getBuiltinModule()`, including
918
825
  `node:path`, `node:crypto`, `node:net`, `node:tls`, `node:dns`, `node:module`,
@@ -957,11 +864,8 @@ none, because that boundary is an isolate-to-isolate call rather than a
957
864
  one tool call therefore fails on Node and may succeed on Workers — reduce inside
958
865
  the program either way (`R1`).
959
866
 
960
- **X12. A paused sandbox.** QuickJS stops: releasing the lease with the child
961
- still running recycles it (`X3`). A Dynamic Worker cannot be interrupted, so a
962
- paused program runs on until it settles or its deadline expires, every host
963
- call failing `execution_paused`. Connecta stops awaiting it at the pause and
964
- discards whatever it returns.
867
+ **X12** described a paused sandbox and left with pausing
868
+ ([#672](https://github.com/zackbart/connecta/issues/672)); the id stays retired.
965
869
 
966
870
  **X11. Typed host rejection.** Both executors rebuild Connecta's authenticated
967
871
  host-failure frame as a thrown guest `Error` (`E1`). The per-run secret stays in
@@ -990,7 +894,6 @@ passing one table is also the check on the executor duties above, with
990
894
  | `P2`, `X5` | `test/guest-api-contract.test.ts` (Dynamic globals plus loader-only filesystem, HTTP, environment, egress, DNS, and local `data:` boundaries), `test/guest-api-contract-quickjs.test.ts` (exact absent globals and blocked imports), `test/quickjs-child-stderr.test.ts` (a child-process environment holding only `TZ=UTC`), `test/deployment-shapes.test.ts` (loader-only Worker construction) |
991
895
  | `P3`, `X9` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` |
992
896
  | `P4` | `test/guest-api-contract.test.ts` (no cross-run leakage), `test/execute.test.ts` (one catalog load per connector per execution) |
993
- | `P6` | `test/guest-api-contract.test.ts` (a frozen clock across host calls, the seeded sfc32 stream against a reference, locked replacements, Workers `crypto` and `performance` pinned directly and through their prototypes, `Intl` "now" and UTC local time on both executors, a fresh pair per run, and a replay drawing the same values), `test/resumable-restart.test.ts` (the same draws after a restart) |
994
897
  | `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (canonical addressing), `test/server.test.ts` (bounded live connector inventory) |
995
898
  | `S1`, `S1a`, `S2` | `test/guest-api-contract.test.ts` (flat page, connector guides, schema keys, unfiltered browse), `test/execute.test.ts` (guide pagination/partial/no-match behavior and `$ref`/`allOf`), `test/meta-tools-search.test.ts` (mixed complete/partial ranking, stable pagination, the two safety classes), `test/typescript-signatures.test.ts` (the TypeScript format over provider and pathological schemas, search/describe parity, observed labeling), `test/typescript-signatures-parse.test.ts` (every rendering parses) |
996
899
  | `S3` | `test/guest-api-contract.test.ts` (typed uncaught bound), `test/execute.test.ts` (count limits, fan-out bound) |
@@ -1000,7 +903,7 @@ passing one table is also the check on the executor duties above, with
1000
903
  | `S8`, `E1`, `X11` | both guest-contract executors (caught call, discovery, utility, budget, removed-function, and forgery cases; typed promise rejections), `test/quickjs-executor.test.ts` (oversized messages, private transport, forged frames) |
1001
904
  | `S9` | `test/result-shapes.test.ts` (value exclusion, bounds, merging, LRU and time expiry, runtime isolation, read-only admission, declared precedence, definition invalidation, unwrapped MCP results, discovery provenance, copy isolation, failure isolation) |
1002
905
  | `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, parallel, and uncaught validation recovery, a conflict's bounded `current`), `test/meta-tools-call.test.ts` (direct, destructive, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
1003
- | `E3`, `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`, destructive reroute), `test/resumable.test.ts` (the refusal without resumable writes) |
906
+ | `E3`, `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`, destructive reroute), `test/program-writes.test.ts` (the refusal's `nextAction`, nothing sent, a caught refusal) |
1004
907
  | `E5` | `test/guest-api-contract.test.ts` (execution-failure channel, in-flight `cancelled`), `test/execute.test.ts` (admission), `test/executor-admission.test.ts`, `test/quickjs-executor.test.ts` (mid-run shutdown) |
1005
908
  | `E6`, `X8` | `test/guest-api-contract.test.ts` (unknown and inherited members, wrapped-message precedence), `test/quickjs-executor.test.ts` |
1006
909
  | `E7` | `test/guest-api-contract.test.ts` (refusals about a `503`-named connector), `test/errors.test.ts` |
@@ -1015,16 +918,9 @@ passing one table is also the check on the executor duties above, with
1015
918
  | `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (shared discovery/call budgets) |
1016
919
  | `L5`, `L7`, `X2` | `test/quickjs-executor.test.ts` (CPU, heap), `test/execute.test.ts` and `test/executor-admission.test.ts` (bounded admission and queue) |
1017
920
  | `L6`, `X10` | `test/quickjs-executor.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message), `test/quickjs-child-stderr.test.ts` (outer reply serialization failure settles the call) |
1018
- | `V1`–`V4` | `test/guest-api-contract.test.ts` (dispatched calls, every refusal class including an address no connector owns, the friction each derives, no event for the execution itself, `paused` and `approved` events), `test/activity.test.ts` (the shared code → friction table, the identity clamp, zero-attempt pauses and approvals) |
1019
- | `V5` | `test/resumable.test.ts` (replay adds only the approval and the live write), `test/guest-api-contract.test.ts` |
1020
- | `W1`, `W2` | `test/guest-api-contract.test.ts` (nothing after a pause gets through, a `__proto__` write refused, on both executors), `test/resumable.test.ts` (the pause shape, reproducible pause points, oversized pending writes, a write gated after the program returned), `test/invocation-pipeline.test.ts` (the gate after validation) |
1021
- | `W3`, `W4` | `test/resumable.test.ts` (header layout, byte bound, expiry, not-found, pool isolation, exact-match refusals that consume nothing, a `__proto__` repetition through the MCP schema, a repeated resume and emitted-content receipt, an expired run's counts) |
1022
- | `W5` | `test/resumable.test.ts` (two resumes racing for a pause, a crashed claimant's `sending` write never sent, a lapsed claim taken over, a claim lost mid-play and the write it sent reported, executor failure returning to the pause, a cut-short or lapsed play that sent writes failing `execution_interrupted`, a claim outliving `expiresAt`, a live or lapsed claim checked before expiry, a failed takeover's answer stored with it, storage that never answers before and after dispatch, a deadline between mark and dispatch) |
1023
- | `W6`, `W7` | `test/guest-api-contract.test.ts` (replay on both executors), `test/resumable.test.ts` (no catalog, connector, or activity traffic for replayed calls; call and tool scope, a retry's scope replacing an unused approval, a hit never taken for the pending call), `test/resumable-restart.test.ts` (across a restart) |
1024
- | `W8`–`W10` | `test/resumable.test.ts` (divergence (a)–(c), each unknown-outcome path never re-sent — beside a concurrent pause, after the program returned, a post-response connector error — a known failure the program handles, the classification table, check-first advice once a write landed, the write budget) |
1025
- | `W11` | `test/code-first-surface.test.ts`, `test/resumable.test.ts` (construction, the default, `/health`, the unavailable answer) |
1026
- | `W12` | `test/resumable.test.ts` (an exempt write runs where the same program otherwise pauses, precedence and a connector default switched off, the write budget, resumable writes off, an unawaited exempt write finished and its unknown outcome reported, journaled and never re-sent, an exempt write's count past expiry, `call_tool` still refusing, search and describe markers, construction refusals), `test/operator-ui-model.test.ts`, `test/browser/operator-ui.spec.ts` (the badge) |
1027
- | `X12` | `test/guest-api-contract.test.ts` (a program that keeps calling after the pause, on both executors) |
921
+ | `V1`–`V4` | `test/guest-api-contract.test.ts` (dispatched calls, every refusal class including an address no connector owns, the friction each derives, no event for the execution itself), `test/activity.test.ts` (the shared code → friction table, the identity clamp, the one-attempt floor), `test/operator-view.test.ts`, `test/d1-activity-example.test.ts` (historical pause and approval rows still render and round-trip) |
922
+ | `V5`, `W9` | `test/program-writes.test.ts` (an unawaited exempt write finished and recorded, its unknown outcome reported, counts on a failed program, the classification table), `test/invocation-pipeline.test.ts` (the gate after validation, an unrecorded refusal) |
923
+ | `W10`, `W12` | `test/program-writes.test.ts` (an exempt write runs and every other write keeps `E4`, the write budget, precedence and a connector default switched off, `call_tool` still refusing, search and describe markers, construction refusals), `test/artifacts-connector.test.ts` (the shipped default and switching it off), `test/operator-ui-model.test.ts`, `test/browser/operator-ui.spec.ts` (the badge) |
1028
924
  | `M1` | `test/guest-api-contract.test.ts` (invalid emits throw catchably, accept nothing), `test/execute-emit.test.ts` (every rejected shape) |
1029
925
  | `M2`, `M3` | `test/guest-api-contract.test.ts` (delivery order, truncated return plus delivered blocks), `test/execute-emit.test.ts` (envelope, `structuredContent`, byte-for-byte no-emit path) |
1030
926
  | `M4` | `test/guest-api-contract.test.ts` (discard is visible), `test/execute-emit.test.ts` (structured and plain paths), `test/quickjs-executor.test.ts` (mid-run shutdown) |
@@ -1035,5 +931,6 @@ passing one table is also the check on the executor duties above, with
1035
931
  | `X3`, `X6` | `test/quickjs-executor.test.ts` (cancels a running child, never-settling await), `test/execute.test.ts` (a wedged executor stops being awaited) |
1036
932
  | `X7` | `P3`'s tests; the Workers superset is deliberately unused |
1037
933
 
1038
- The surface itself is checked by `test/server.test.ts` (the exact eight-tool list) and
1039
- `test/code-first-surface.test.ts` (construction, executor, removed tools, copy, and size).
934
+ The surface itself is checked by `test/server.test.ts` (the exact seven-tool list),
935
+ `test/code-first-surface.test.ts` (construction, executor, removed tools, copy, and size),
936
+ and `test/program-writes.test.ts` (the retired pause options refused at construction).
@@ -2,28 +2,28 @@
2
2
 
3
3
  Connecta keeps one small tool surface in model context and resolves downstream
4
4
  tools behind it. `search_tools` finds addresses, the call tools enforce safety
5
- annotations, `execute_code` runs work as a program that pauses at each write
6
- until `resume_execution` approves it, and `get_result` pages bounded results.
5
+ annotations, `execute_code` runs read-only work as a program, and `get_result`
6
+ pages bounded results.
7
7
 
8
8
  This guide is the contract an MCP client sees. The in-program `connecta.*` API
9
9
  those tools imply belongs to [code mode](./code-mode.md); inbound identity and
10
10
  credential administration belong to [auth](./auth.md).
11
11
 
12
- ## The eight tools
12
+ ## The seven tools
13
13
 
14
- Every deployment requires an executor, so `tools/list` is exactly eight. No
15
- configuration adds a ninth or removes one — not even turning resumable writes
16
- off, which changes what `resume_execution` answers and nothing about whether it
17
- is listed, so a client's cached tool list never depends on the storage behind
18
- the deployment. `execute_code` keeps its read-only hint although its programs
19
- may now reach writes, because no write runs there: a program stops before
20
- sending one, and the write runs only inside the destructive-annotated
21
- `resume_execution` that repeats it ([code mode](./code-mode.md#pausing-and-resuming)).
14
+ Every deployment requires an executor, so `tools/list` is exactly seven. No
15
+ configuration adds an eighth or removes one, so a client's cached tool list
16
+ never depends on the storage behind the deployment. There was an eighth,
17
+ `resume_execution`, while programs paused at writes; it left with pausing
18
+ ([#672](https://github.com/zackbart/connecta/issues/672)). `execute_code`
19
+ keeps its read-only hint because the only writes a program sends are the ones
20
+ the deployment's own config exempted from asking
21
+ ([code mode](./code-mode.md#writes) `W12`) — a decision made in code, not by
22
+ the model, and not by a downstream annotation.
22
23
 
23
24
  | Tool | Arguments | Returns |
24
25
  | --- | --- | --- |
25
- | `execute_code` | `code`, `diagnostics?` | the program's reduced return value, plus a `diagnostics` block when asked; or, when it reached a write, `{ paused: { address, args, token, expiresAt, nextAction, hint } }` |
26
- | `resume_execution` | `token`, `address`, `args`, `approval?: "call" \| "tool"`, `reason?` | what `execute_code` would have returned had the write been approved: the result, the next pause, or a typed failure |
26
+ | `execute_code` | `code`, `diagnostics?` | the program's reduced return value, plus a `diagnostics` block when asked |
27
27
  | `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json" \| "typescript"` | `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`, plus `queryAnalysis` on a partial or failed search |
28
28
  | `call_tool` | `address`, `args?`, `resultMode?: "mcp" \| "value"`, `timeoutMs?`, `diagnostics?` | the downstream result, bounded as [result representation](#result-representation) describes |
29
29
  | `call_destructive_tool` | the same, plus `reason?` | the same |
@@ -52,13 +52,13 @@ credentials, logs, or raw error text.
52
52
 
53
53
  Connecta's own tools carry the annotations it demands of downstream tools:
54
54
  read-only hints on all but `authorize_connector`, which mutates stored auth
55
- state, and the two that send writes, `call_destructive_tool` and
56
- `resume_execution`, both destructive. Otherwise a host that gates on
57
- annotations would prompt for every search, and a connecta aggregated behind
58
- another connecta would be refused by its own policy. `resume_execution` is
59
- where the prompt belongs: its arguments are the exact write, so the human
60
- approves what will be sent rather than a program that might send anything.
61
- Its `reason`, like `call_destructive_tool`'s, is dropped before anything runs.
55
+ state, and `call_destructive_tool`, the one that sends writes, which is
56
+ destructive. Otherwise a host that gates on annotations would prompt for every
57
+ search, and a connecta aggregated behind another connecta would be refused by
58
+ its own policy. `call_destructive_tool` is where the prompt belongs: its
59
+ arguments are the exact write, so the human approves what will be sent rather
60
+ than a program that might send anything. Its `reason` is dropped before
61
+ anything runs.
62
62
 
63
63
  ## Routing between the call surfaces
64
64
 
@@ -78,24 +78,21 @@ use a direct `call_destructive_tool` result and page it with `get_result` after
78
78
  the write runs once. A program return has no page handle; sampling or slicing
79
79
  a write's output there can discard the answer.
80
80
 
81
- Writes take the same route. A program that reaches a tool not explicitly
82
- annotated read-only stops before sending it — unless the deployment's config
83
- exempts that tool, and then it runs unasked — and returns the exact call with a
84
- token; `resume_execution` repeating it is the approval, and the program
85
- replays from a journal to send it and carry on — to its answer, or to its next
86
- write. So "close the stale issues and post a summary" is one program and a few
87
- approvals, not one program and thirty-one top-level calls, and the program's
88
- reasoning survives between them. Keep the entire authorized batch in that
89
- program, including its final post. The default `approval: "call"` covers only
90
- the pending write. Choosing `approval: "tool"` when the batch repeats an
91
- address covers later calls to that tool in this run, so thirty closes cost one
92
- prompt; the summary post still needs its own approval. One known write
93
- still goes straight to `call_destructive_tool`, and top-level discovery stays
94
- for catalog inspection. On a deployment without resumable writes (storage
95
- without `compareAndSet`), a program cannot write at all: the call fails
96
- `destructive_tool_requires_approval`, multi-step writes discover at the top
97
- level and run each step through `call_destructive_tool`, and the MCP
98
- instructions say so.
81
+ Writes do not take that route. A program that reaches a tool not explicitly
82
+ annotated read-only refuses it before validation and before anything is sent —
83
+ unless the deployment's config exempts that tool, and then it runs unasked —
84
+ with `destructive_tool_requires_approval` and a `nextAction` naming
85
+ `call_destructive_tool` and the canonical address. Each write is its own
86
+ top-level call, and the host's permission prompt on it is the only approval
87
+ there is. So "close the stale issues and post a summary" is one program to find
88
+ the issues and one `call_destructive_tool` per write; the program does the
89
+ reading, and the host sees every write it is asked about. Programs once paused
90
+ at a write for a resume call to approve and replay; hosts approve one tool call
91
+ at a time, so under always-allow that prompt approved nothing and cost a round
92
+ trip and a replay
93
+ ([#672](https://github.com/zackbart/connecta/issues/672)). Top-level discovery
94
+ stays for catalog inspection and for finding the address of a write, and the
95
+ MCP instructions say so.
99
96
 
100
97
  The three discovery routes use deliberately different envelopes. These are their
101
98
  smallest successful one-tool shapes:
@@ -133,7 +130,7 @@ discovery. Both take the same arguments.
133
130
  | --- | --- |
134
131
  | `query` | two to four action/object terms; empty or whitespace-only browses |
135
132
  | `connector` | scopes to one id, loading that catalog alone instead of fanning out across every configured connector. Set it when the integration is obvious, omit it when the right one is genuinely ambiguous |
136
- | `safety` | `"readOnly"` for what runs unasked, `"approvalRequired"` for the complementary set that pauses a program or crosses `call_destructive_tool`, omitted or `"all"` for the complete configured catalog. A config-exempt write stays `"approvalRequired"` and its row says `approval: "exempt"`: a program calls it without pausing, and `call_tool` still refuses it ([code mode](./code-mode.md#pausing-and-resuming) `W12`) |
133
+ | `safety` | `"readOnly"` for what runs unasked, `"approvalRequired"` for the complementary set that crosses `call_destructive_tool`, omitted or `"all"` for the complete configured catalog. A config-exempt write stays `"approvalRequired"` and its row says `approval: "exempt"`: a program calls it without asking, and `call_tool` still refuses it ([code mode](./code-mode.md#writes) `W12`) |
137
134
  | `limit` / `offset` | page the ranked results; omit `limit` initially so the default eight-result page stays small |
138
135
  | `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema, `"typescript"` for a function signature ([below](#typescript-signatures)) |
139
136
  | `fullDescriptions` | unabridged tool purposes, at the obvious cost |
@@ -542,8 +539,8 @@ answered. The suggested route follows the route the caller took:
542
539
  the same arguments when the miss happened inside `execute_code`, which has no
543
540
  way to call a tool. `call_tool` reaching an unannotated, write-capable, or
544
541
  destructive tool returns `nextAction` for `call_destructive_tool` with the
545
- canonical address; inside a program the same call pauses, and the pause result
546
- names `resume_execution` instead. Nothing is executed by these records.
542
+ canonical address, and so does the same call inside a program, unless config
543
+ exempts the tool there. Nothing is executed by these records.
547
544
 
548
545
  `connecta.describe` keeps its failures inline instead, so one miss cannot
549
546
  discard the other schemas; each failed entry carries a human `error`, typed