@zackbart/connecta 0.26.3 → 0.28.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.
- package/AGENTS.md +11 -11
- package/CHANGELOG.md +128 -1
- package/README.md +15 -19
- package/bin/connecta.mjs +10 -6
- package/dist/access-tokens.js +13 -2
- package/dist/activity.d.ts +5 -4
- package/dist/activity.js +1 -10
- package/dist/artifacts/connector.d.ts +3 -0
- package/dist/artifacts/connector.js +6 -0
- package/dist/artifacts/document.js +6 -0
- package/dist/artifacts/json.js +10 -0
- package/dist/artifacts/module.js +7 -1
- package/dist/artifacts/operations.d.ts +3 -1
- package/dist/artifacts/operations.js +69 -11
- package/dist/artifacts/refresh.d.ts +2 -0
- package/dist/artifacts/refresh.js +3 -2
- package/dist/artifacts/routes.js +4 -15
- package/dist/artifacts/validate.js +2 -4
- package/dist/auth/downstream-oauth.js +95 -36
- package/dist/catalog-service.js +1 -1
- package/dist/connector-scope.d.ts +2 -0
- package/dist/connector-scope.js +27 -0
- package/dist/connectors/api.js +8 -2
- package/dist/connectors/remote-mcp.js +57 -17
- package/dist/credentials.js +2 -1
- package/dist/execute.js +107 -368
- package/dist/executors/quickjs.js +1 -1
- package/dist/exempt-writes.js +162 -0
- package/dist/index.d.ts +12 -31
- package/dist/index.js +15 -47
- package/dist/invocation.js +6 -19
- package/dist/meta-tools.js +1 -2
- package/dist/oauth-partition.js +32 -0
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +3 -3
- package/dist/operator-ui/view.js +6 -4
- package/dist/providers/notion.js +4 -1
- package/dist/providers/vercel.js +13 -9
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +58 -25
- package/dist/routes/credentials.js +16 -8
- package/dist/routes/mcp.js +5 -21
- package/dist/routes/oauth.js +70 -64
- package/dist/routes/shared.d.ts +1 -6
- package/dist/runtime/run.js +7 -1
- package/dist/server.js +0 -4
- package/dist/skills.js +8 -23
- package/dist/storage/file.js +57 -20
- package/dist/storage/memory.js +8 -1
- package/dist/tool-safety.d.ts +1 -1
- package/dist/tool-safety.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +51 -14
- package/documentation/auth.md +7 -4
- package/documentation/code-mode.md +91 -194
- package/documentation/meta-tools.md +38 -41
- package/ethos.md +6 -7
- package/examples/worker/README.md +14 -17
- package/examples/worker/src/d1-activity-row.ts +4 -1
- package/examples/worker/src/d1-storage.ts +27 -5
- package/examples/worker/src/index.ts +1 -9
- package/package.json +4 -4
- package/templates/node/AGENTS.md +4 -3
- package/templates/node/README.md +3 -4
- package/templates/node/package.json +2 -2
- package/dist/resumable.js +0 -1291
- package/dist/run-journal.js +0 -347
package/AGENTS.md
CHANGED
|
@@ -4,13 +4,13 @@ This is the canonical instruction file for coding agents. `CLAUDE.md` is a
|
|
|
4
4
|
symlink to this file so every agent works from the same conventions.
|
|
5
5
|
|
|
6
6
|
A single MCP endpoint aggregating downstream connectors (remote MCP servers and
|
|
7
|
-
plain HTTP APIs) behind
|
|
7
|
+
plain HTTP APIs) behind seven meta-tools, `execute_code` among them. Every
|
|
8
8
|
deployment configures an executor, and agents reach connectors by writing
|
|
9
9
|
JavaScript against it. One fetch-native core on Effect behind a Promise API,
|
|
10
|
-
running on both Node and Cloudflare Workers. A program
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
exists.
|
|
10
|
+
running on both Node and Cloudflare Workers. A program runs read-only and
|
|
11
|
+
config-exempt tools; every other write is its own `call_destructive_tool` call,
|
|
12
|
+
so the host's permission prompt is the only approval. The code and the guides
|
|
13
|
+
describe what exists.
|
|
14
14
|
|
|
15
15
|
- **[`ethos.md`](./ethos.md) is the constitution.** It states what connecta is
|
|
16
16
|
and isn't, and its decisions table carries a verdict for every shape already
|
|
@@ -74,7 +74,7 @@ pins the generated deployment to the CLI package's exact version, restores the
|
|
|
74
74
|
template `.gitignore` and the `CLAUDE.md` symlink, and refuses to merge into an
|
|
75
75
|
existing path.
|
|
76
76
|
`connecta doctor` verifies a running deployment's health, executor, and exact
|
|
77
|
-
|
|
77
|
+
seven-tool surface. The template carries its own `Dockerfile` and
|
|
78
78
|
`docker-compose.yml`, so the generated project is the container: setup changes
|
|
79
79
|
must keep the root README, the template (source, container files, and README),
|
|
80
80
|
and the `scripts/check-package.mjs` smoke — which builds and runs that
|
|
@@ -141,14 +141,14 @@ an unclassified, double-classified, stale, or reasonless entry.
|
|
|
141
141
|
Promise-based and name no Effect type: a deployment author should never need
|
|
142
142
|
a second async paradigm to write a connector, so convert at the boundary,
|
|
143
143
|
not in the caller. Effect is a hard dependency pinned to one exact version,
|
|
144
|
-
never a range, and
|
|
145
|
-
both resolves one Effect rather than two.
|
|
146
|
-
|
|
144
|
+
never a range, and compatible with Alchemy's peer requirement, so a deployment
|
|
145
|
+
that uses both resolves one Effect rather than two. Modules tagged
|
|
146
|
+
`@stability unstable` are allowed behind subpaths: they can break in minor releases,
|
|
147
147
|
so an upgrade is its own deliberate pull request, never a drive-by in another
|
|
148
148
|
change. `effect/testing` stays out of `src/` (see import-graph purity). The
|
|
149
149
|
MCP edges do not move with the core — `@modelcontextprotocol/server` upward,
|
|
150
|
-
the SDK client downstream
|
|
151
|
-
|
|
150
|
+
the SDK client downstream. Replacing either requires separate proof of the
|
|
151
|
+
wire contracts and request lifetimes; the stable core release is not that proof.
|
|
152
152
|
- **Style.** There is no formatter. Match the surrounding code. The docs voice
|
|
153
153
|
is precise, occasionally wry, and always explains *why* — don't flatten it
|
|
154
154
|
into boilerplate.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,134 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 0.28.0 — 2026-10-04
|
|
6
|
+
|
|
7
|
+
This minor release removes program pauses ([#672](https://github.com/zackbart/connecta/issues/672)).
|
|
8
|
+
`execute_code` programs run explicitly read-only tools and the writes
|
|
9
|
+
`execute.approval` exempts, and every other write is its own
|
|
10
|
+
`call_destructive_tool` call, so the host's permission prompt on that call is
|
|
11
|
+
the only approval there is. Hosts approve one tool call at a time, and under
|
|
12
|
+
always-allow or bypass modes the agent called `resume_execution` itself: each
|
|
13
|
+
pause cost a round trip and a full replay and protected nothing. What breaks:
|
|
14
|
+
`resume_execution` is gone, so the surface is seven tools again, and a host
|
|
15
|
+
allow-list or cached tool list naming it should drop it.
|
|
16
|
+
`execute.resumableWrites` and `execute.pausedRunTtlSeconds` now refuse to
|
|
17
|
+
construct with an error naming
|
|
18
|
+
the removal — delete them, including the Worker example's
|
|
19
|
+
`execute: { resumableWrites: false }` line. A program's non-exempt write now
|
|
20
|
+
fails `destructive_tool_requires_approval` with a `nextAction` for
|
|
21
|
+
`call_destructive_tool`, exactly as it did on deployments without resumable
|
|
22
|
+
writes, and a run never returns `{ paused: … }`. `Date.now()` and
|
|
23
|
+
`Math.random()` inside a program are the runtime's own again rather than pinned
|
|
24
|
+
per run. A deployment that sets neither option and whose programs only read
|
|
25
|
+
can ignore all of this: approval exemptions, `execute.maxWrites`, storage
|
|
26
|
+
contracts, and the D1 activity schema are unchanged, and any paused runs left
|
|
27
|
+
in storage expire on their own TTL. The Node template now pins 0.28.0.
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- `resume_execution`, the run journal, deterministic replay, compare-and-set
|
|
32
|
+
run claims, and the pinned clock and randomness (code-mode `P6`) that
|
|
33
|
+
existed only so a replay could match. `W1`–`W8`, `W11`, `P6`, and `X12` are
|
|
34
|
+
retired clause ids.
|
|
35
|
+
- The pause-only error codes: `execution_paused`, `pending_write_too_large`,
|
|
36
|
+
`journal_too_large`, `execution_interrupted`, `execution_diverged`,
|
|
37
|
+
`execution_claim_lost`, `approval_mismatch`, `execution_token_stale`,
|
|
38
|
+
`execution_in_progress`, `execution_expired`, `execution_not_found`, and
|
|
39
|
+
`resumable_writes_unavailable`. `write_outcome_unknown` stays, for an exempt
|
|
40
|
+
write that was sent and never answered.
|
|
41
|
+
- `resumableWrites` from `/health`, and the "resumable writes off" note from
|
|
42
|
+
`connecta doctor`, which now verifies the exact seven-tool surface.
|
|
43
|
+
- The pause and resume agent eval tasks.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- The MCP instructions, the `execute_code` description, and the `usage` skill
|
|
48
|
+
describe the read-only program route: reads and config-exempt writes in a
|
|
49
|
+
program, every other write through `call_destructive_tool`. The description
|
|
50
|
+
states the exempt-write budget.
|
|
51
|
+
- `execute.maxWrites` now bounds only config-exempt writes, the one kind a
|
|
52
|
+
program can still send. Its default stays 10.
|
|
53
|
+
- Activity no longer emits `paused` or `approved` outcomes, the
|
|
54
|
+
`resume_execution` source, or an `approval` scope. The types keep them as
|
|
55
|
+
read-only history, the operator UI still renders older rows (a historical
|
|
56
|
+
pause now reads "Paused for approval"), and the D1 example still stores and
|
|
57
|
+
reads the `approval` column.
|
|
58
|
+
- QuickJS still runs its child with `TZ=UTC`, now for parity with a Dynamic
|
|
59
|
+
Worker's local time rather than for replay.
|
|
60
|
+
|
|
61
|
+
## 0.27.0 — 2026-10-01
|
|
62
|
+
|
|
63
|
+
This minor release moves the runtime to stable Effect v4 and fixes execution,
|
|
64
|
+
OAuth, storage, and operator recovery. Deployment APIs and the eight MCP tools
|
|
65
|
+
keep their existing contracts, and the Node template now pins 0.27.0.
|
|
66
|
+
Development requires TypeScript 5.9.3 or newer within 5.x.
|
|
67
|
+
|
|
68
|
+
Duplicate API tool names now fail at construction. Memory, file, and D1 storage
|
|
69
|
+
refuse TTLs that cannot produce finite expirations. Artifact quotas reserve 8 KiB
|
|
70
|
+
for frame metadata, reducing the available content allowance by 4 KiB, and apply
|
|
71
|
+
to historical snapshots too. Artifact render checks use the deployment's viewer
|
|
72
|
+
theme.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- Pin Effect to `4.0.0`, replacing `4.0.0-rc.117`, and align the optional
|
|
77
|
+
Alchemy setup instructions with the stable pin.
|
|
78
|
+
- Require TypeScript 5.9.3 or newer within 5.x for repository and Node-template
|
|
79
|
+
development, matching Effect v4's TypeScript 5.9 minimum.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- Refuse already-aborted deadline operations and preserve explicit `null`
|
|
84
|
+
cancellation reasons (#660) (#661).
|
|
85
|
+
- Return structured failures for malformed executor replies (#662).
|
|
86
|
+
- Close callback and credential-test connector scopes after their final use,
|
|
87
|
+
keeping cleanup bounded and best-effort (#663) (#664).
|
|
88
|
+
- Prevent concurrent OAuth state handoffs from replacing another owner on
|
|
89
|
+
storage that supports atomic claims (#665).
|
|
90
|
+
- Show retryable Activity errors for unreadable pages and discard superseded
|
|
91
|
+
pagination responses (#666) (#667).
|
|
92
|
+
- Reject invalid TTLs in memory and D1 storage before mutation, and match NUL
|
|
93
|
+
bytes literally in D1 list prefixes (#668) (#669).
|
|
94
|
+
- Enforce catalog count and byte ceilings even when discovery is invalidated
|
|
95
|
+
while the listing is in flight (#644).
|
|
96
|
+
- Retain OAuth cleanup lineage after uncertain storage commits, preserve
|
|
97
|
+
concurrent token rotation and revocation during issuer migration, and isolate
|
|
98
|
+
refresh coordination by credential owner (#645) (#646) (#648).
|
|
99
|
+
- Return `null` for absent prototype-named credential fields (#647).
|
|
100
|
+
- Reserve enough artifact frame metadata for valid document names and titles,
|
|
101
|
+
enforce current quotas on historical snapshots, and reject refresh scan
|
|
102
|
+
cursors that fail to advance (#649) (#658) (#650).
|
|
103
|
+
- Reconcile connector state after failed operator mutations, ignore older data
|
|
104
|
+
reloads, and keep malformed collection responses recoverable (#651) (#652)
|
|
105
|
+
(#653).
|
|
106
|
+
- Keep committed file snapshots consistent after rename, quarantine malformed
|
|
107
|
+
entries, and reject TTLs that cannot produce finite expirations (#654) (#655)
|
|
108
|
+
(#656).
|
|
109
|
+
- Reject unreadable successful responses and malformed build-log streams from
|
|
110
|
+
named Vercel tools, preserving raw API response bodies and no-content
|
|
111
|
+
mutations (#657).
|
|
112
|
+
- Keep queued catalog mutations alive across Worker requests while preserving
|
|
113
|
+
cancellation and storage ordering (#623).
|
|
114
|
+
- Release failed access-token reservations before lookup publication, close
|
|
115
|
+
failed OAuth callback transports, and persist accepted rotating refresh tokens
|
|
116
|
+
after owner cancellation without reviving disconnected generations (#624)
|
|
117
|
+
(#625) (#626).
|
|
118
|
+
- Count historical writes across divergent resumptions and bound resumed write
|
|
119
|
+
answers by the journal limit, retaining truthful outcomes for writes already
|
|
120
|
+
sent (#627) (#628).
|
|
121
|
+
- Roll back rejected file-storage mutations, preserve prototype-named keys,
|
|
122
|
+
expire entries at their TTL boundary, and quarantine invalid snapshot roots
|
|
123
|
+
without losing their original bytes (#629) (#630) (#631) (#632).
|
|
124
|
+
- Ignore superseded artifact-list responses, preserve newly created tokens when
|
|
125
|
+
an older list finishes, and reload a confined artifact shell when signing in
|
|
126
|
+
again (#633) (#634) (#635).
|
|
127
|
+
- Advance artifact listings through empty continuation pages, refuse already
|
|
128
|
+
cancelled render checks, and validate Markdown with the viewer's theme across
|
|
129
|
+
writes and refreshes (#636) (#637) (#638).
|
|
130
|
+
- Prevent doctor redirects from forwarding Cloudflare Access credentials,
|
|
131
|
+
reject unreadable successful Notion responses, and reject duplicate API tool
|
|
132
|
+
names before classification and dispatch can disagree (#639) (#640) (#641).
|
|
6
133
|
|
|
7
134
|
## 0.26.3 — 2026-09-28
|
|
8
135
|
|
package/README.md
CHANGED
|
@@ -10,19 +10,18 @@ writing code instead of loading a thousand tool definitions.
|
|
|
10
10
|
You ask your agent a question that touches a service — Linear, Stripe, an
|
|
11
11
|
internal API, anything you have connected. Here is what happens:
|
|
12
12
|
|
|
13
|
-
1. The agent talks to one endpoint, yours, and sees
|
|
13
|
+
1. The agent talks to one endpoint, yours, and sees seven tools. Always seven,
|
|
14
14
|
no matter how many services sit behind it.
|
|
15
15
|
2. It writes a short JavaScript program. Connecta runs it in a sandbox next to
|
|
16
16
|
your integrations. The program can search for tools, call them, chain the
|
|
17
17
|
calls, and shape the result.
|
|
18
18
|
3. Only the answer comes back into the agent's context — not raw pages of
|
|
19
19
|
API output.
|
|
20
|
-
4. If the
|
|
21
|
-
|
|
22
|
-
write
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
can also go straight through `call_destructive_tool`.
|
|
20
|
+
4. If the agent wants to change something — create, update, delete — it
|
|
21
|
+
cannot do that from a program, unless your config exempts that tool. The
|
|
22
|
+
program refuses the write before anything is sent and hands back the exact
|
|
23
|
+
call; the agent makes it through `call_destructive_tool`, one visible call
|
|
24
|
+
your MCP client can put in front of you first.
|
|
26
25
|
|
|
27
26
|
Credentials never leave the server. The program never sees them, and neither
|
|
28
27
|
does the agent.
|
|
@@ -31,9 +30,9 @@ does the agent.
|
|
|
31
30
|
flowchart TB
|
|
32
31
|
Client["Your MCP client<br/>Claude, Cursor, …"]
|
|
33
32
|
|
|
34
|
-
subgraph Connecta["Connecta — one endpoint,
|
|
35
|
-
Sandbox["execute_code<br/>the agent's program runs here<br/>
|
|
36
|
-
Explicit["
|
|
33
|
+
subgraph Connecta["Connecta — one endpoint, seven tools, your credentials"]
|
|
34
|
+
Sandbox["execute_code<br/>the agent's program runs here<br/>read-only tools, plus writes<br/>your config exempts"]
|
|
35
|
+
Explicit["call_destructive_tool<br/>one visible call per write<br/>your client can ask you first"]
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
Integrations["The integrations you chose<br/>Linear · Stripe · Notion · Vercel · your HTTP API · any MCP server"]
|
|
@@ -41,7 +40,6 @@ flowchart TB
|
|
|
41
40
|
Client -->|"one connection"| Sandbox
|
|
42
41
|
Client --> Explicit
|
|
43
42
|
Sandbox -->|"reads"| Integrations
|
|
44
|
-
Sandbox -.->|"paused write"| Explicit
|
|
45
43
|
Explicit -->|"writes"| Integrations
|
|
46
44
|
```
|
|
47
45
|
|
|
@@ -82,13 +80,11 @@ Fifty issues in, one small object out. Your context window notices.
|
|
|
82
80
|
notice also points the agent to reduce or search the result inside a
|
|
83
81
|
program. Discovery can show compact schemas, exact JSON Schema, or a
|
|
84
82
|
TypeScript signature to read while writing JavaScript.
|
|
85
|
-
- **Keep writes deliberate.** Only tools marked read-only run in a program
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
never sent twice. Config — and only config — can exempt a cheap,
|
|
91
|
-
reversible write from asking, per tool or per connector.
|
|
83
|
+
- **Keep writes deliberate.** Only tools marked read-only run in a program.
|
|
84
|
+
Every other write is a separate, visible `call_destructive_tool` call your
|
|
85
|
+
client can gate, so the host's permission prompt is the one approval there
|
|
86
|
+
is. Config — and only config — can exempt a cheap, reversible write from
|
|
87
|
+
asking, per tool or per connector, and then a program may make it.
|
|
92
88
|
- **Run it on Node or Cloudflare Workers.** The core is shared; each deployment
|
|
93
89
|
supplies its platform's executor and storage. The Node template also runs
|
|
94
90
|
unchanged in Docker.
|
|
@@ -122,7 +118,7 @@ and why.
|
|
|
122
118
|
|
|
123
119
|
Setup is written for an agent. Point yours at [`AGENTS.md`](./AGENTS.md) and
|
|
124
120
|
ask it to set up a Connecta deployment; the
|
|
125
|
-
[documentation](./documentation/) covers the architecture, the
|
|
121
|
+
[documentation](./documentation/) covers the architecture, the seven tools,
|
|
126
122
|
code mode, and inbound auth if you want to go deeper. When upgrading an
|
|
127
123
|
existing deployment, each [changelog](./CHANGELOG.md) release opens with what
|
|
128
124
|
breaks and what a deployment can ignore.
|
package/bin/connecta.mjs
CHANGED
|
@@ -132,10 +132,18 @@ const DOCTOR_TIMEOUT_MS = 10_000;
|
|
|
132
132
|
|
|
133
133
|
async function doctorFetch(url, init = {}) {
|
|
134
134
|
try {
|
|
135
|
-
|
|
135
|
+
const response = await fetch(url, {
|
|
136
136
|
...init,
|
|
137
|
+
redirect: "manual",
|
|
137
138
|
signal: AbortSignal.timeout(DOCTOR_TIMEOUT_MS),
|
|
138
139
|
});
|
|
140
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
141
|
+
await response.body?.cancel().catch(() => {});
|
|
142
|
+
throw new Error(
|
|
143
|
+
`HTTP ${response.status} redirect refused: doctor sends authentication only to the configured deployment URL.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return response;
|
|
139
147
|
} catch (error) {
|
|
140
148
|
if (
|
|
141
149
|
error instanceof Error &&
|
|
@@ -249,7 +257,6 @@ async function doctor() {
|
|
|
249
257
|
"call_tool",
|
|
250
258
|
"execute_code",
|
|
251
259
|
"get_result",
|
|
252
|
-
"resume_execution",
|
|
253
260
|
"search_tools",
|
|
254
261
|
"skills",
|
|
255
262
|
];
|
|
@@ -302,10 +309,7 @@ async function doctor() {
|
|
|
302
309
|
console.log(
|
|
303
310
|
`Connecta doctor passed: ${health.connectors} connector(s), ` +
|
|
304
311
|
`${executorName ? `${executorName} executed` : "code executed"}, ` +
|
|
305
|
-
"prescribed
|
|
306
|
-
// resume_execution is listed either way; whether programs can write is
|
|
307
|
-
// what an operator cannot tell from the tool list, so say when not.
|
|
308
|
-
(health.resumableWrites === false ? ", resumable writes off" : "") +
|
|
312
|
+
"prescribed seven-tool surface" +
|
|
309
313
|
(drifted.length > 0
|
|
310
314
|
? `, catalog drift on ${drifted.length} connector(s).`
|
|
311
315
|
: "."),
|
package/dist/access-tokens.js
CHANGED
|
@@ -191,8 +191,19 @@ export class AccessTokenManager {
|
|
|
191
191
|
? {}
|
|
192
192
|
: { principal: { ...createdBy } }),
|
|
193
193
|
};
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
try {
|
|
195
|
+
await this.updateActive(record.id, true);
|
|
196
|
+
await this.storage.set(recordKey(record.id), JSON.stringify(record));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// No lookup write has started, so this secret can never authenticate.
|
|
200
|
+
// A failed record or reservation write may still have committed: revoke
|
|
201
|
+
// whatever metadata exists and release the reservation even on a miss.
|
|
202
|
+
const revoked = await this.revoke(record.id, record.createdBy);
|
|
203
|
+
if (!revoked)
|
|
204
|
+
await this.updateActive(record.id, false);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
196
207
|
// A failed lookup write may have committed. Keep its reservation and
|
|
197
208
|
// metadata, so a manager can revoke it without ever returning the secret.
|
|
198
209
|
await this.storage.set(lookupKey(hash), JSON.stringify({ version: 1, id: record.id }));
|
package/dist/activity.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ActivityModule } from "./module-contracts.js";
|
|
|
2
2
|
import type { CatalogDriftCounts, Logger } from "./types.js";
|
|
3
3
|
export type ActivityCallSource = "call_tool" | "call_destructive_tool" | "batch_call" | "execute_code" | "resume_execution";
|
|
4
4
|
export type ActivityOutcome = "success" | "error" | "timeout" | "cancelled" | "paused" | "approved";
|
|
5
|
-
/**
|
|
5
|
+
/** Read-only history: how much one `resume_execution` approved (#672). */
|
|
6
6
|
export type ActivityApproval = "call" | "tool";
|
|
7
7
|
export type AgentFriction = "tool_not_found" | "schema_retry" | "destructive_reroute" | "auth_required" | "result_too_large";
|
|
8
8
|
export { agentFrictionForCode } from "./activity-friction.js";
|
|
@@ -50,8 +50,9 @@ export interface ToolCallActivityEvent {
|
|
|
50
50
|
*/
|
|
51
51
|
friction?: AgentFriction;
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* Read-only history: set only on an `approved` event written before issue
|
|
54
|
+
* #672, the scope the approval covered. An enum, like everything else
|
|
55
|
+
* here — the approved arguments were never recorded.
|
|
55
56
|
*/
|
|
56
57
|
approval?: ActivityApproval;
|
|
57
58
|
serverName: string;
|
|
@@ -149,7 +150,7 @@ export interface ActivityRequestContext {
|
|
|
149
150
|
defer?: (promise: Promise<unknown>) => void;
|
|
150
151
|
logger: Logger;
|
|
151
152
|
}
|
|
152
|
-
export type ActivityEventInput = Pick<ToolCallActivityEvent, "connectorId" | "toolName" | "address" | "source" | "outcome" | "durationMs" | "attempts" | "errorCode" | "friction"
|
|
153
|
+
export type ActivityEventInput = Pick<ToolCallActivityEvent, "connectorId" | "toolName" | "address" | "source" | "outcome" | "durationMs" | "attempts" | "errorCode" | "friction">;
|
|
153
154
|
/**
|
|
154
155
|
* Best-effort by design: activity storage can never change a tool result.
|
|
155
156
|
* Workers attach async sinks to waitUntil; synchronous sinks such as Analytics
|
package/dist/activity.js
CHANGED
|
@@ -49,18 +49,9 @@ export function recordToolActivity(context, input) {
|
|
|
49
49
|
source: input.source,
|
|
50
50
|
outcome: input.outcome,
|
|
51
51
|
durationMs: Math.max(0, Math.trunc(input.durationMs)),
|
|
52
|
-
|
|
53
|
-
// events that honestly carry zero attempts. Every other outcome keeps its
|
|
54
|
-
// floor of one, as it always has.
|
|
55
|
-
attempts: input.outcome === "paused" || input.outcome === "approved"
|
|
56
|
-
? 0
|
|
57
|
-
: Math.max(1, Math.trunc(input.attempts)),
|
|
52
|
+
attempts: Math.max(1, Math.trunc(input.attempts)),
|
|
58
53
|
...(input.errorCode ? { errorCode: input.errorCode } : {}),
|
|
59
54
|
...(friction ? { friction } : {}),
|
|
60
|
-
...(input.outcome === "approved" &&
|
|
61
|
-
(input.approval === "call" || input.approval === "tool")
|
|
62
|
-
? { approval: input.approval }
|
|
63
|
-
: {}),
|
|
64
55
|
serverName: context.serverInfo.name,
|
|
65
56
|
serverVersion: context.serverInfo.version,
|
|
66
57
|
...(context.deploymentId
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ResolvedTheme } from "../branding.js";
|
|
1
2
|
import type { Connector } from "../types.js";
|
|
2
3
|
import { type ArtifactOperations } from "./operations.js";
|
|
3
4
|
import { type ArtifactRefreshService } from "./refresh.js";
|
|
@@ -50,6 +51,8 @@ export interface ArtifactsConnectorOptions {
|
|
|
50
51
|
renderCheck?: ArtifactRenderCheck;
|
|
51
52
|
/** Where pages are served, when not the deployment's own origin. */
|
|
52
53
|
origin?: string;
|
|
54
|
+
/** Resolved deployment theme, available after the module binds to core. */
|
|
55
|
+
theme?: () => ResolvedTheme | undefined;
|
|
53
56
|
}
|
|
54
57
|
export declare function artifactsConnector(options: ArtifactsConnectorOptions): Connector;
|
|
55
58
|
export {};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { api } from "../connectors/api.js";
|
|
10
10
|
import { callerOf } from "../connector-caller.js";
|
|
11
11
|
import { ConnectorCallError } from "../errors.js";
|
|
12
|
+
import { resolveTheme } from "../branding.js";
|
|
12
13
|
import { buildFrameDocument, frameCsp } from "./document.js";
|
|
13
14
|
import { buildGuide, GUIDE_SUMMARY } from "./guide.js";
|
|
14
15
|
import { invalidContent, } from "./operations.js";
|
|
@@ -56,6 +57,9 @@ function renderFindings(values) {
|
|
|
56
57
|
}
|
|
57
58
|
/** Run a hook under its 20-second deadline and the call's own signal. Never throws. */
|
|
58
59
|
export async function runRenderCheck(hook, input, callSignal) {
|
|
60
|
+
if (callSignal?.aborted) {
|
|
61
|
+
return { ok: false, unavailable: "The render check's call was cancelled; nothing was saved." };
|
|
62
|
+
}
|
|
59
63
|
const controller = new AbortController();
|
|
60
64
|
let timer;
|
|
61
65
|
const onAbort = () => controller.abort(callSignal?.reason);
|
|
@@ -162,6 +166,7 @@ export function artifactsConnector(options) {
|
|
|
162
166
|
document: buildFrameDocument({
|
|
163
167
|
kind: head.kind,
|
|
164
168
|
source,
|
|
169
|
+
theme: options.theme?.() ?? resolveTheme(),
|
|
165
170
|
global: globalFor(artifactId, head, head.view, liveOf(head), false),
|
|
166
171
|
data,
|
|
167
172
|
}),
|
|
@@ -450,6 +455,7 @@ export function artifactsConnector(options) {
|
|
|
450
455
|
document: buildFrameDocument({
|
|
451
456
|
kind: args.kind,
|
|
452
457
|
source: args.source,
|
|
458
|
+
theme: options.theme?.() ?? resolveTheme(),
|
|
453
459
|
global: {
|
|
454
460
|
id: "preview",
|
|
455
461
|
title: "Preview",
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
import { scanHtml } from "./html-scan.js";
|
|
9
9
|
import { scriptSafeJson } from "./json.js";
|
|
10
10
|
import { markdownPage, MarkdownNestingError } from "./markdown.js";
|
|
11
|
+
/**
|
|
12
|
+
* Reserve for the injected global and Markdown theme beyond source and data.
|
|
13
|
+
* Sixteen 64-character document names appear twice, alongside timestamps and
|
|
14
|
+
* versions, and a 160-character title can expand sixfold when script-escaped.
|
|
15
|
+
*/
|
|
16
|
+
export const FRAME_OVERHEAD_BYTES = 8192;
|
|
11
17
|
/**
|
|
12
18
|
* The frame's Content-Security-Policy. `sandbox allow-scripts` without
|
|
13
19
|
* `allow-same-origin` gives the page an opaque origin — no cookies, no
|
package/dist/artifacts/json.js
CHANGED
|
@@ -66,3 +66,13 @@ export function jsonProblem(value, maxDepth) {
|
|
|
66
66
|
}
|
|
67
67
|
return undefined;
|
|
68
68
|
}
|
|
69
|
+
const encoder = new TextEncoder();
|
|
70
|
+
/** A string's size in UTF-8 bytes: what every artifact limit counts. */
|
|
71
|
+
export function utf8Bytes(text) {
|
|
72
|
+
return encoder.encode(text).byteLength;
|
|
73
|
+
}
|
|
74
|
+
/** SHA-256 as lowercase hex: the content address of a stored body. */
|
|
75
|
+
export async function sha256Hex(text) {
|
|
76
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(text));
|
|
77
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
78
|
+
}
|
package/dist/artifacts/module.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// artifacts(): the typed module a deployment passes as `ConnectaConfig.artifacts`.
|
|
2
|
+
import { resolveBranding } from "../branding.js";
|
|
2
3
|
import { artifactsConnector } from "./connector.js";
|
|
3
4
|
import { ArtifactOperations } from "./operations.js";
|
|
4
5
|
import { artifactRoutes } from "./routes.js";
|
|
@@ -43,17 +44,22 @@ export function artifacts(options) {
|
|
|
43
44
|
limits,
|
|
44
45
|
});
|
|
45
46
|
const refresh = new ArtifactRefreshService(operations);
|
|
47
|
+
let theme;
|
|
46
48
|
const connector = artifactsConnector({
|
|
47
49
|
operations,
|
|
48
50
|
refresh,
|
|
49
51
|
allowlist,
|
|
50
52
|
limits,
|
|
53
|
+
theme: () => theme,
|
|
51
54
|
...(options.renderCheck ? { renderCheck: options.renderCheck } : {}),
|
|
52
55
|
});
|
|
53
56
|
return Object.freeze({
|
|
54
57
|
connector,
|
|
55
58
|
handle: artifactRoutes({ operations, allowlist }),
|
|
56
|
-
bindRefresh: (runtime) =>
|
|
59
|
+
bindRefresh: (runtime) => {
|
|
60
|
+
refresh.bind(runtime);
|
|
61
|
+
theme = resolveBranding(runtime.branding).theme;
|
|
62
|
+
},
|
|
57
63
|
refresh: (id, by) => refresh.run(id, { manual: by }),
|
|
58
64
|
runDue: () => refresh.runDue(),
|
|
59
65
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResolvedTheme } from "../branding.js";
|
|
1
2
|
import { type CheckContext } from "./validate.js";
|
|
2
3
|
import type { ArtifactActor, ArtifactHeadRecord, ArtifactIssue, ArtifactKind, ArtifactStore, ArtifactValidation, ArtifactVersionRecord, ArtifactRefreshSchedule, ArtifactRunRecord } from "./types.js";
|
|
3
4
|
type ArtifactFailureCode = "conflict" | "invalid_args" | "not_found" | "unavailable";
|
|
@@ -164,10 +165,11 @@ export declare class ArtifactOperations {
|
|
|
164
165
|
page(id: string, pin?: {
|
|
165
166
|
view?: number;
|
|
166
167
|
documents?: Record<string, number>;
|
|
167
|
-
}): Promise<Result<{
|
|
168
|
+
}, theme?: ResolvedTheme): Promise<Result<{
|
|
168
169
|
head: ArtifactHeadRecord;
|
|
169
170
|
view: ArtifactVersionRecord;
|
|
170
171
|
source: string;
|
|
172
|
+
document: string;
|
|
171
173
|
documents: Record<string, {
|
|
172
174
|
record: ArtifactVersionRecord;
|
|
173
175
|
value: unknown;
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// Nothing here throws for a caller's mistake. Operations answer
|
|
15
15
|
// `{ ok: false, code, message }`, and the connector turns that into its typed
|
|
16
16
|
// error; a throw means storage failed.
|
|
17
|
-
import {
|
|
17
|
+
import { buildFrameDocument, FRAME_OVERHEAD_BYTES } from "./document.js";
|
|
18
|
+
import { sha256Hex, utf8Bytes } from "./json.js";
|
|
18
19
|
import { markdownPage, MarkdownNestingError } from "./markdown.js";
|
|
19
20
|
import { ARTIFACT_ID, checkDocument, checkDocumentTotals, checkView, finish, validDocumentName, } from "./validate.js";
|
|
20
21
|
/** Physical head moves a write absorbs before giving up as busy. */
|
|
@@ -23,8 +24,6 @@ const MAX_SWAP_ATTEMPTS = 8;
|
|
|
23
24
|
const HISTORY_LIMIT = 20;
|
|
24
25
|
/** Heads one listing call may examine while searching. */
|
|
25
26
|
const LIST_SCAN_LIMIT = 1000;
|
|
26
|
-
/** What the viewer adds around a page beyond its source and documents. */
|
|
27
|
-
const FRAME_OVERHEAD_BYTES = 4096;
|
|
28
27
|
/** Tombstones keep version numbers, so distinct names need their own bound. */
|
|
29
28
|
const DOCUMENT_NAMES_LIMIT = 64;
|
|
30
29
|
const fail = (code, message, extra = {}) => ({ ok: false, code, message, ...extra });
|
|
@@ -827,7 +826,7 @@ export class ArtifactOperations {
|
|
|
827
826
|
return { ok: true, head: current.head, record, value, history };
|
|
828
827
|
}
|
|
829
828
|
/** The page a viewer renders: a view version and exactly the documents asked for (all current ones by default). */
|
|
830
|
-
async page(id, pin = {}) {
|
|
829
|
+
async page(id, pin = {}, theme) {
|
|
831
830
|
const badId = this.#checkId(id);
|
|
832
831
|
if (badId)
|
|
833
832
|
return badId;
|
|
@@ -841,7 +840,10 @@ export class ArtifactOperations {
|
|
|
841
840
|
const wanted = pin.documents
|
|
842
841
|
? Object.entries(pin.documents)
|
|
843
842
|
: liveDocuments(head).map(([name, record]) => [name, record.version]);
|
|
844
|
-
|
|
843
|
+
// Historical streams were each valid when written, but their selected
|
|
844
|
+
// versions may never have coexisted. Bound the selection before loading
|
|
845
|
+
// its bodies, then check the actual bodies against today's configuration.
|
|
846
|
+
const selected = [];
|
|
845
847
|
for (const [name, version] of wanted) {
|
|
846
848
|
const badName = this.#checkDocumentName(name);
|
|
847
849
|
if (badName)
|
|
@@ -849,11 +851,58 @@ export class ArtifactOperations {
|
|
|
849
851
|
const record = await this.#version(id, `doc:${name}`, head.documents[name], version);
|
|
850
852
|
if (!record)
|
|
851
853
|
return this.#notFound(id);
|
|
852
|
-
if (record.removed)
|
|
853
|
-
|
|
854
|
-
|
|
854
|
+
if (!record.removed)
|
|
855
|
+
selected.push([name, record]);
|
|
856
|
+
}
|
|
857
|
+
const { limits } = this.#context;
|
|
858
|
+
const tooMany = checkDocumentTotals(selected.length, 0, limits);
|
|
859
|
+
if (tooMany)
|
|
860
|
+
return invalidContent("The snapshot", finish({ errors: [tooMany], warnings: [] }));
|
|
861
|
+
const source = await this.#body(view.body);
|
|
862
|
+
const sourceBytes = utf8Bytes(source);
|
|
863
|
+
if (sourceBytes > limits.sourceBytes) {
|
|
864
|
+
return invalidContent("The snapshot", finish({ errors: [{
|
|
865
|
+
code: "E_TOO_LARGE", severity: "error",
|
|
866
|
+
message: `The view is ${sourceBytes} bytes; the limit is ${limits.sourceBytes}.`,
|
|
867
|
+
}], warnings: [] }));
|
|
855
868
|
}
|
|
856
|
-
|
|
869
|
+
const documents = {};
|
|
870
|
+
const data = {};
|
|
871
|
+
let dataBytes = 0;
|
|
872
|
+
for (const [name, record] of selected) {
|
|
873
|
+
const value = JSON.parse(await this.#body(record.body));
|
|
874
|
+
const checked = checkDocument(name, value, limits);
|
|
875
|
+
if (checked.error)
|
|
876
|
+
return invalidContent("The snapshot", finish({ errors: [checked.error], warnings: [] }));
|
|
877
|
+
dataBytes += checked.bytes;
|
|
878
|
+
const totals = checkDocumentTotals(selected.length, dataBytes, limits);
|
|
879
|
+
if (totals)
|
|
880
|
+
return invalidContent("The snapshot", finish({ errors: [totals], warnings: [] }));
|
|
881
|
+
documents[name] = { record, value };
|
|
882
|
+
data[name] = checked.text;
|
|
883
|
+
}
|
|
884
|
+
const tooLarge = () => invalidContent("The snapshot", finish({ errors: [{
|
|
885
|
+
code: "E_TOO_LARGE", severity: "error",
|
|
886
|
+
message: `The rendered page exceeds the current limit of ${limits.renderedBytes} bytes.`,
|
|
887
|
+
}], warnings: [] }));
|
|
888
|
+
// For HTML, source plus data is a lower bound before frame construction.
|
|
889
|
+
// Markdown can shrink or expand, so its final document decides the bound.
|
|
890
|
+
if (dataBytes + (head.kind === "html" ? sourceBytes : 0) > limits.renderedBytes)
|
|
891
|
+
return tooLarge();
|
|
892
|
+
const rendered = buildFrameDocument({
|
|
893
|
+
kind: head.kind, source, ...(theme ? { theme } : {}),
|
|
894
|
+
global: {
|
|
895
|
+
id, title: head.title, view: { version: view.version },
|
|
896
|
+
documents: Object.fromEntries(selected.map(([name, record]) => [name, {
|
|
897
|
+
version: record.version, updatedAt: record.at,
|
|
898
|
+
}])),
|
|
899
|
+
snapshot: pin.view !== undefined || pin.documents !== undefined,
|
|
900
|
+
},
|
|
901
|
+
data,
|
|
902
|
+
});
|
|
903
|
+
if (utf8Bytes(rendered) > limits.renderedBytes)
|
|
904
|
+
return tooLarge();
|
|
905
|
+
return { ok: true, head, view, source, document: rendered, documents };
|
|
857
906
|
}
|
|
858
907
|
async list(options) {
|
|
859
908
|
if (options.cursor !== undefined && !ARTIFACT_ID.test(options.cursor)) {
|
|
@@ -864,9 +913,11 @@ export class ArtifactOperations {
|
|
|
864
913
|
let after = options.cursor;
|
|
865
914
|
let scanned = 0;
|
|
866
915
|
for (;;) {
|
|
916
|
+
const limit = Math.min(100, LIST_SCAN_LIMIT - scanned);
|
|
917
|
+
const previous = after;
|
|
867
918
|
const page = await this.#store.heads({
|
|
868
919
|
...(after === undefined ? {} : { after }),
|
|
869
|
-
limit
|
|
920
|
+
limit,
|
|
870
921
|
});
|
|
871
922
|
for (const item of page.heads) {
|
|
872
923
|
scanned++;
|
|
@@ -885,8 +936,15 @@ export class ArtifactOperations {
|
|
|
885
936
|
}
|
|
886
937
|
if (page.next === undefined)
|
|
887
938
|
return { ok: true, items };
|
|
939
|
+
if (previous !== undefined && page.next <= previous) {
|
|
940
|
+
return fail("unavailable", "Artifact head scan did not advance; retry the listing.");
|
|
941
|
+
}
|
|
942
|
+
// Listed keys can disappear before the store reads them. Its cursor
|
|
943
|
+
// still consumed the whole page, including those missing head records.
|
|
944
|
+
after = page.next;
|
|
945
|
+
scanned += Math.max(0, limit - page.heads.length);
|
|
888
946
|
if (scanned >= LIST_SCAN_LIMIT) {
|
|
889
|
-
return
|
|
947
|
+
return { ok: true, items, nextCursor: after };
|
|
890
948
|
}
|
|
891
949
|
}
|
|
892
950
|
}
|
|
@@ -28,6 +28,8 @@ export interface ArtifactRefreshRuntime {
|
|
|
28
28
|
isError?: boolean;
|
|
29
29
|
}>;
|
|
30
30
|
claimMs: number;
|
|
31
|
+
/** UI branding shared by the viewer and every render check. */
|
|
32
|
+
branding?: import("../types.js").ConnectaBranding;
|
|
31
33
|
}
|
|
32
34
|
export type RefreshOutcome = {
|
|
33
35
|
status: "skipped";
|