@zackbart/connecta 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -5
- package/CHANGELOG.md +271 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +76 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +449 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +194 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +652 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/catalog-service.js +39 -29
- package/dist/catalog.js +42 -8
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connectors/remote-mcp.js +16 -2
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +427 -70
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +79 -14
- package/dist/executors/quickjs.js +4 -2
- package/dist/index.d.ts +62 -6
- package/dist/index.js +230 -6
- package/dist/invocation.js +56 -10
- package/dist/meta-tools.js +7 -2
- package/dist/module-contracts.d.ts +21 -1
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +62 -5
- package/dist/operator-ui/view.js +94 -5
- package/dist/program-source.js +116 -0
- package/dist/registry.d.ts +42 -6
- package/dist/registry.js +148 -47
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +6 -69
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +27 -5
- package/dist/routes/mcp.js +116 -59
- package/dist/routes/oauth-management.js +22 -9
- package/dist/routes/operator.js +1 -0
- package/dist/routes/shared.d.ts +23 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +2 -1
- package/dist/routes/ui.js +13 -4
- package/dist/run-journal.js +347 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/server.js +26 -0
- package/dist/skills.js +27 -8
- package/dist/timeout.js +26 -0
- package/dist/tool-safety.d.ts +35 -0
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +18 -0
- package/dist/ui.d.ts +12 -5
- package/dist/ui.js +32 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +196 -19
- package/documentation/auth.md +83 -2
- package/documentation/code-mode.md +221 -48
- package/documentation/meta-tools.md +44 -25
- package/ethos.md +12 -10
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +174 -10
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/index.ts +36 -1
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +5 -1
- package/package.json +6 -2
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
// The built-in `artifacts` connector: reads are explicitly read-only, writes
|
|
2
|
+
// are not — so discovery, `call_tool`, and the guide treat them as writes —
|
|
3
|
+
// but every write is a new immutable version anyone can roll back, so the
|
|
4
|
+
// connector's own default exempts them from approval inside programs
|
|
5
|
+
// (`approval: "never"`), and `execute.approval` can switch that off.
|
|
6
|
+
//
|
|
7
|
+
// Who made each version comes from the caller core attached to the context,
|
|
8
|
+
// never from arguments.
|
|
9
|
+
import { api } from "../connectors/api.js";
|
|
10
|
+
import { callerOf } from "../connector-caller.js";
|
|
11
|
+
import { ConnectorCallError } from "../errors.js";
|
|
12
|
+
import { buildFrameDocument, frameCsp } from "./document.js";
|
|
13
|
+
import { buildGuide, GUIDE_SUMMARY } from "./guide.js";
|
|
14
|
+
import { invalidContent, } from "./operations.js";
|
|
15
|
+
import { scriptSafeJson } from "./json.js";
|
|
16
|
+
import { freshnessOf } from "./refresh.js";
|
|
17
|
+
import { validateWithContext } from "./validate.js";
|
|
18
|
+
const RENDER_DEADLINE_MS = 20_000;
|
|
19
|
+
const MAX_RENDER_FINDINGS = 10;
|
|
20
|
+
const MAX_RENDER_FINDING_CHARS = 300;
|
|
21
|
+
const READ = { readOnlyHint: true };
|
|
22
|
+
const WRITE = {
|
|
23
|
+
readOnlyHint: false,
|
|
24
|
+
destructiveHint: false,
|
|
25
|
+
idempotentHint: false,
|
|
26
|
+
openWorldHint: false,
|
|
27
|
+
};
|
|
28
|
+
const id = {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "The artifact's id: its URL slug, e.g. q3-bugs.",
|
|
31
|
+
minLength: 1,
|
|
32
|
+
maxLength: 64,
|
|
33
|
+
};
|
|
34
|
+
const kind = { type: "string", enum: ["html", "markdown"] };
|
|
35
|
+
const version = (description) => ({
|
|
36
|
+
type: "integer",
|
|
37
|
+
minimum: 1,
|
|
38
|
+
description,
|
|
39
|
+
});
|
|
40
|
+
const documentValues = {
|
|
41
|
+
type: "object",
|
|
42
|
+
description: "Documents by name, each mapped straight to its JSON value (no wrapper): " +
|
|
43
|
+
'{ "data": { "rows": [...] } } is read by the page as artifact.data.data.rows.',
|
|
44
|
+
additionalProperties: true,
|
|
45
|
+
};
|
|
46
|
+
/** Bound what a hook said: at most 10 findings of 300 characters. */
|
|
47
|
+
function renderFindings(values) {
|
|
48
|
+
if (!Array.isArray(values))
|
|
49
|
+
return [];
|
|
50
|
+
return values
|
|
51
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
52
|
+
.slice(0, MAX_RENDER_FINDINGS)
|
|
53
|
+
.map((value) => value.length > MAX_RENDER_FINDING_CHARS
|
|
54
|
+
? `${value.slice(0, MAX_RENDER_FINDING_CHARS - 1)}…`
|
|
55
|
+
: value);
|
|
56
|
+
}
|
|
57
|
+
/** Run a hook under its 20-second deadline and the call's own signal. Never throws. */
|
|
58
|
+
export async function runRenderCheck(hook, input, callSignal) {
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
let timer;
|
|
61
|
+
const onAbort = () => controller.abort(callSignal?.reason);
|
|
62
|
+
callSignal?.addEventListener("abort", onAbort, { once: true });
|
|
63
|
+
const deadline = new Promise((_, reject) => {
|
|
64
|
+
timer = setTimeout(() => {
|
|
65
|
+
const reason = new Error("The render check did not answer within 20 seconds.");
|
|
66
|
+
controller.abort(reason);
|
|
67
|
+
reject(reason);
|
|
68
|
+
}, RENDER_DEADLINE_MS);
|
|
69
|
+
});
|
|
70
|
+
const aborted = new Promise((_, reject) => {
|
|
71
|
+
controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true });
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
const result = (await Promise.race([
|
|
75
|
+
hook({ ...input, signal: controller.signal }),
|
|
76
|
+
deadline,
|
|
77
|
+
aborted,
|
|
78
|
+
]));
|
|
79
|
+
if (result?.ok === true) {
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
warnings: renderFindings(result.warnings).map((message) => ({
|
|
83
|
+
code: "W_RENDER",
|
|
84
|
+
severity: "warning",
|
|
85
|
+
message: `Render check: ${message}`,
|
|
86
|
+
})),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (result?.ok === false) {
|
|
90
|
+
const errors = renderFindings(result.errors);
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
errors: (errors.length ? errors : ["the page failed to render"]).map((message) => ({
|
|
94
|
+
code: "E_RENDER",
|
|
95
|
+
severity: "error",
|
|
96
|
+
message: `Render check: ${message}`,
|
|
97
|
+
})),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { ok: false, unavailable: "The render check answered with something other than { ok }." };
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
unavailable: "The render check failed to run or did not answer in time; nothing was saved. Retry the write.",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
if (timer !== undefined)
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
callSignal?.removeEventListener("abort", onAbort);
|
|
112
|
+
// Settle the race's losers so neither rejects unhandled.
|
|
113
|
+
deadline.catch(() => { });
|
|
114
|
+
aborted.catch(() => { });
|
|
115
|
+
controller.abort();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** The page's `window.artifact` minus data, for a head. */
|
|
119
|
+
function globalFor(artifactId, head, view, documents, snapshot) {
|
|
120
|
+
return {
|
|
121
|
+
id: artifactId,
|
|
122
|
+
title: head.title,
|
|
123
|
+
view: { version: view.version },
|
|
124
|
+
documents: Object.fromEntries(Object.entries(documents).map(([name, record]) => [
|
|
125
|
+
name,
|
|
126
|
+
{ version: record.version, updatedAt: record.at },
|
|
127
|
+
])),
|
|
128
|
+
snapshot,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function failure(result) {
|
|
132
|
+
return new ConnectorCallError(result.code, result.message, result.current ? { current: result.current } : {});
|
|
133
|
+
}
|
|
134
|
+
const warningsOf = (warnings) => warnings.length ? { warnings: warnings.map((issue) => issue.message) } : {};
|
|
135
|
+
const history = (records) => records.map((record) => ({
|
|
136
|
+
version: record.version,
|
|
137
|
+
by: record.by,
|
|
138
|
+
at: record.at,
|
|
139
|
+
op: record.op,
|
|
140
|
+
...(record.restoredFrom !== undefined ? { restoredFrom: record.restoredFrom } : {}),
|
|
141
|
+
...(record.runId !== undefined ? { runId: record.runId } : {}),
|
|
142
|
+
...(record.removed ? { removed: true } : {}),
|
|
143
|
+
}));
|
|
144
|
+
export function artifactsConnector(options) {
|
|
145
|
+
const ops = options.operations;
|
|
146
|
+
const refresh = options.refresh;
|
|
147
|
+
const csp = frameCsp(options.allowlist);
|
|
148
|
+
const hook = options.renderCheck;
|
|
149
|
+
const actorOf = (ctx) => callerOf(ctx)?.identity.actor ?? { kind: "unknown" };
|
|
150
|
+
const originOf = (ctx) => options.origin ?? new URL(ctx.baseUrl).origin;
|
|
151
|
+
const urlOf = (ctx, artifactId) => `${originOf(ctx)}/artifacts/${artifactId}`;
|
|
152
|
+
const snapshotUrlOf = (ctx, artifactId, view, documents) => {
|
|
153
|
+
const pins = Object.entries(documents)
|
|
154
|
+
.filter(([, record]) => !record.removed)
|
|
155
|
+
// Names are identifiers and versions digits, so the pins need no escaping.
|
|
156
|
+
.map(([name, record]) => `d=${name}:${record.version}`);
|
|
157
|
+
return `${urlOf(ctx, artifactId)}/v/${view}${pins.length ? `?${pins.join("&")}` : ""}`;
|
|
158
|
+
};
|
|
159
|
+
const renderForSignal = (signal) => hook
|
|
160
|
+
? async ({ id: artifactId, head, source, data }) => {
|
|
161
|
+
const verdict = await runRenderCheck(hook, {
|
|
162
|
+
document: buildFrameDocument({
|
|
163
|
+
kind: head.kind,
|
|
164
|
+
source,
|
|
165
|
+
global: globalFor(artifactId, head, head.view, liveOf(head), false),
|
|
166
|
+
data,
|
|
167
|
+
}),
|
|
168
|
+
csp,
|
|
169
|
+
kind: head.kind,
|
|
170
|
+
}, signal);
|
|
171
|
+
if (verdict.ok)
|
|
172
|
+
return verdict;
|
|
173
|
+
if ("unavailable" in verdict) {
|
|
174
|
+
return { ok: false, code: "unavailable", message: verdict.unavailable };
|
|
175
|
+
}
|
|
176
|
+
const validation = { ok: false, errors: verdict.errors, warnings: [] };
|
|
177
|
+
return invalidContent("The page", validation);
|
|
178
|
+
}
|
|
179
|
+
: undefined;
|
|
180
|
+
const renderFor = (ctx) => renderForSignal(ctx.signal);
|
|
181
|
+
refresh.setRender(renderForSignal);
|
|
182
|
+
const unwrap = (result) => {
|
|
183
|
+
if (!result.ok)
|
|
184
|
+
throw failure(result);
|
|
185
|
+
return result;
|
|
186
|
+
};
|
|
187
|
+
const connector = api("artifacts", {
|
|
188
|
+
title: "Artifacts",
|
|
189
|
+
description: "Team pages over stored data: publish, edit, and share HTML or Markdown pages whose numbers live in versioned JSON documents.",
|
|
190
|
+
usageGuide: {
|
|
191
|
+
content: buildGuide({
|
|
192
|
+
allowlist: options.allowlist,
|
|
193
|
+
limits: options.limits,
|
|
194
|
+
renderCheck: hook !== undefined,
|
|
195
|
+
}),
|
|
196
|
+
summary: GUIDE_SUMMARY,
|
|
197
|
+
},
|
|
198
|
+
tools: [
|
|
199
|
+
{
|
|
200
|
+
name: "list_artifacts",
|
|
201
|
+
description: "List artifacts in id order, with each one's url; query searches titles and ids.",
|
|
202
|
+
annotations: READ,
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
query: { type: "string", maxLength: 200, description: "Case-insensitive title or id substring." },
|
|
207
|
+
includeArchived: { type: "boolean", description: "Include archived artifacts. Default false." },
|
|
208
|
+
limit: { type: "integer", minimum: 1, maximum: 100, description: "Default 50." },
|
|
209
|
+
cursor: { type: "string", maxLength: 64, description: "nextCursor from the previous page." },
|
|
210
|
+
},
|
|
211
|
+
additionalProperties: false,
|
|
212
|
+
},
|
|
213
|
+
outputSchema: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: {
|
|
216
|
+
artifacts: {
|
|
217
|
+
type: "array",
|
|
218
|
+
items: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
id: { type: "string" },
|
|
222
|
+
title: { type: "string" },
|
|
223
|
+
kind: { type: "string" },
|
|
224
|
+
viewVersion: { type: "integer" },
|
|
225
|
+
revision: { type: "integer" },
|
|
226
|
+
updatedAt: { type: "string" },
|
|
227
|
+
updatedBy: { type: "object" },
|
|
228
|
+
archived: { type: "boolean" },
|
|
229
|
+
url: { type: "string" },
|
|
230
|
+
freshness: { type: "object" },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
nextCursor: { type: "string" },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
handler: async (args, ctx) => {
|
|
238
|
+
const listed = unwrap(await ops.list({
|
|
239
|
+
limit: args.limit ?? 50,
|
|
240
|
+
...(args.query !== undefined ? { query: args.query } : {}),
|
|
241
|
+
...(args.includeArchived ? { includeArchived: true } : {}),
|
|
242
|
+
...(args.cursor !== undefined ? { cursor: args.cursor } : {}),
|
|
243
|
+
}));
|
|
244
|
+
return {
|
|
245
|
+
artifacts: listed.items.map(({ id: artifactId, head }) => ({
|
|
246
|
+
id: artifactId,
|
|
247
|
+
title: head.title,
|
|
248
|
+
kind: head.kind,
|
|
249
|
+
viewVersion: head.view.version,
|
|
250
|
+
revision: head.revision,
|
|
251
|
+
updatedAt: head.updatedAt,
|
|
252
|
+
updatedBy: head.updatedBy,
|
|
253
|
+
archived: head.archived,
|
|
254
|
+
url: urlOf(ctx, artifactId),
|
|
255
|
+
freshness: freshnessOf(head),
|
|
256
|
+
})),
|
|
257
|
+
...(listed.nextCursor !== undefined ? { nextCursor: listed.nextCursor } : {}),
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "get_artifact",
|
|
263
|
+
description: "Read an artifact: its view source and history, document versions, revision, url, and snapshotUrl.",
|
|
264
|
+
annotations: READ,
|
|
265
|
+
inputSchema: {
|
|
266
|
+
type: "object",
|
|
267
|
+
required: ["id"],
|
|
268
|
+
properties: {
|
|
269
|
+
id,
|
|
270
|
+
version: version("A past view version. Default: the current one."),
|
|
271
|
+
includeSource: { type: "boolean", description: "Return the view source. Default true." },
|
|
272
|
+
},
|
|
273
|
+
additionalProperties: false,
|
|
274
|
+
},
|
|
275
|
+
outputSchema: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
id: { type: "string" },
|
|
279
|
+
title: { type: "string" },
|
|
280
|
+
kind: { type: "string" },
|
|
281
|
+
revision: { type: "integer" },
|
|
282
|
+
archived: { type: "boolean" },
|
|
283
|
+
url: { type: "string" },
|
|
284
|
+
snapshotUrl: { type: "string" },
|
|
285
|
+
view: { type: "object" },
|
|
286
|
+
documents: { type: "array" },
|
|
287
|
+
history: { type: "array" },
|
|
288
|
+
freshness: { type: "object" },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
handler: async (args, ctx) => {
|
|
292
|
+
const got = unwrap(await ops.get(args.id, {
|
|
293
|
+
...(args.version !== undefined ? { version: args.version } : {}),
|
|
294
|
+
includeSource: args.includeSource !== false,
|
|
295
|
+
}));
|
|
296
|
+
const live = liveOf(got.head);
|
|
297
|
+
return {
|
|
298
|
+
id: args.id,
|
|
299
|
+
title: got.head.title,
|
|
300
|
+
kind: got.head.kind,
|
|
301
|
+
revision: got.head.revision,
|
|
302
|
+
archived: got.head.archived,
|
|
303
|
+
url: urlOf(ctx, args.id),
|
|
304
|
+
snapshotUrl: snapshotUrlOf(ctx, args.id, got.view.version, live),
|
|
305
|
+
view: {
|
|
306
|
+
version: got.view.version,
|
|
307
|
+
...(got.source !== undefined ? { source: got.source } : {}),
|
|
308
|
+
by: got.view.by,
|
|
309
|
+
at: got.view.at,
|
|
310
|
+
op: got.view.op,
|
|
311
|
+
...(got.view.restoredFrom !== undefined ? { restoredFrom: got.view.restoredFrom } : {}),
|
|
312
|
+
},
|
|
313
|
+
documents: Object.entries(live).map(([name, record]) => ({
|
|
314
|
+
name,
|
|
315
|
+
version: record.version,
|
|
316
|
+
bytes: record.bytes,
|
|
317
|
+
by: record.by,
|
|
318
|
+
at: record.at,
|
|
319
|
+
})),
|
|
320
|
+
history: history(got.history),
|
|
321
|
+
freshness: freshnessOf(got.head),
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "get_refresh",
|
|
327
|
+
description: "Read an artifact's refresh program, schedule, freshness, and recent run history.",
|
|
328
|
+
annotations: READ,
|
|
329
|
+
inputSchema: { type: "object", required: ["id"], properties: { id }, additionalProperties: false },
|
|
330
|
+
handler: async (args) => {
|
|
331
|
+
const got = unwrap(await ops.refreshConfig(args.id));
|
|
332
|
+
return {
|
|
333
|
+
id: args.id,
|
|
334
|
+
freshness: freshnessOf(got.head),
|
|
335
|
+
...(got.program !== undefined ? { program: got.program } : {}),
|
|
336
|
+
runs: await ops.store.runs(args.id, 20),
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "set_refresh",
|
|
342
|
+
description: "Attach or replace a read-only refresh program for one data document. Pass the current refresh baseVersion (0 if absent).",
|
|
343
|
+
annotations: WRITE,
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: "object", required: ["id", "document", "program", "schedule", "baseVersion"],
|
|
346
|
+
properties: {
|
|
347
|
+
id,
|
|
348
|
+
document: { type: "string", minLength: 1, maxLength: 64 },
|
|
349
|
+
program: { type: "string" },
|
|
350
|
+
schedule: { type: "string", enum: ["manual", "daily", "weekly"] },
|
|
351
|
+
baseVersion: { type: "integer", minimum: 0 },
|
|
352
|
+
},
|
|
353
|
+
additionalProperties: false,
|
|
354
|
+
},
|
|
355
|
+
handler: async (args, ctx) => {
|
|
356
|
+
const owner = callerOf(ctx);
|
|
357
|
+
if (!owner)
|
|
358
|
+
throw new ConnectorCallError("unavailable", "Refresh requires an admitted caller identity.");
|
|
359
|
+
const saved = unwrap(await ops.setRefresh({
|
|
360
|
+
id: args.id, document: args.document, program: args.program,
|
|
361
|
+
schedule: args.schedule, baseVersion: args.baseVersion, by: actorOf(ctx),
|
|
362
|
+
owner,
|
|
363
|
+
}));
|
|
364
|
+
return { id: args.id, freshness: freshnessOf(saved.head) };
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "run_refresh",
|
|
369
|
+
description: "Trigger one configured refresh now. Only shared connectors' explicitly read-only tools may run.",
|
|
370
|
+
annotations: WRITE,
|
|
371
|
+
inputSchema: { type: "object", required: ["id"], properties: { id }, additionalProperties: false },
|
|
372
|
+
handler: async (args, ctx) => refresh.run(args.id, { manual: actorOf(ctx) }),
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "get_document",
|
|
376
|
+
description: "Read one data document's value, at its current or a past version, with its history.",
|
|
377
|
+
annotations: READ,
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: "object",
|
|
380
|
+
required: ["id", "name"],
|
|
381
|
+
properties: {
|
|
382
|
+
id,
|
|
383
|
+
name: { type: "string", minLength: 1, maxLength: 64 },
|
|
384
|
+
version: version("A past version. Default: the current one."),
|
|
385
|
+
},
|
|
386
|
+
additionalProperties: false,
|
|
387
|
+
},
|
|
388
|
+
outputSchema: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
name: { type: "string" },
|
|
392
|
+
version: { type: "integer" },
|
|
393
|
+
value: {},
|
|
394
|
+
by: { type: "object" },
|
|
395
|
+
at: { type: "string" },
|
|
396
|
+
history: { type: "array" },
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
handler: async (args) => {
|
|
400
|
+
const got = unwrap(await ops.getDocument(args.id, args.name, args.version));
|
|
401
|
+
return {
|
|
402
|
+
name: args.name,
|
|
403
|
+
version: got.record.version,
|
|
404
|
+
value: got.value,
|
|
405
|
+
by: got.record.by,
|
|
406
|
+
at: got.record.at,
|
|
407
|
+
...(got.record.runId !== undefined ? { runId: got.record.runId } : {}),
|
|
408
|
+
...(got.record.removed ? { removed: true } : {}),
|
|
409
|
+
history: history(got.history),
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "validate_artifact",
|
|
415
|
+
description: "Check a page and sample documents exactly as a save would, without saving. Errors name a line and the fix.",
|
|
416
|
+
annotations: READ,
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: "object",
|
|
419
|
+
required: ["kind", "source"],
|
|
420
|
+
properties: {
|
|
421
|
+
kind,
|
|
422
|
+
source: { type: "string" },
|
|
423
|
+
documents: documentValues,
|
|
424
|
+
},
|
|
425
|
+
additionalProperties: false,
|
|
426
|
+
},
|
|
427
|
+
outputSchema: {
|
|
428
|
+
type: "object",
|
|
429
|
+
properties: {
|
|
430
|
+
ok: { type: "boolean" },
|
|
431
|
+
errors: { type: "array" },
|
|
432
|
+
warnings: { type: "array" },
|
|
433
|
+
errorsOmitted: { type: "integer" },
|
|
434
|
+
renderCheck: { type: "object" },
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
handler: async (args, ctx) => {
|
|
438
|
+
const documents = args.documents ?? {};
|
|
439
|
+
const validation = validateWithContext({ kind: args.kind, source: args.source, documents, title: "Preview" }, ops.context);
|
|
440
|
+
if (!validation.ok || !hook)
|
|
441
|
+
return validation;
|
|
442
|
+
const data = {};
|
|
443
|
+
const records = {};
|
|
444
|
+
const at = new Date().toISOString();
|
|
445
|
+
for (const [name, value] of Object.entries(documents)) {
|
|
446
|
+
data[name] = scriptSafeJson(value);
|
|
447
|
+
records[name] = { version: 1, bytes: 0, by: { kind: "preview" }, at, op: "create" };
|
|
448
|
+
}
|
|
449
|
+
const verdict = await runRenderCheck(hook, {
|
|
450
|
+
document: buildFrameDocument({
|
|
451
|
+
kind: args.kind,
|
|
452
|
+
source: args.source,
|
|
453
|
+
global: {
|
|
454
|
+
id: "preview",
|
|
455
|
+
title: "Preview",
|
|
456
|
+
view: { version: 1 },
|
|
457
|
+
documents: Object.fromEntries(Object.keys(records).map((name) => [name, { version: 1, updatedAt: at }])),
|
|
458
|
+
snapshot: false,
|
|
459
|
+
},
|
|
460
|
+
data,
|
|
461
|
+
}),
|
|
462
|
+
csp,
|
|
463
|
+
kind: args.kind,
|
|
464
|
+
}, ctx.signal);
|
|
465
|
+
if ("unavailable" in verdict) {
|
|
466
|
+
throw new ConnectorCallError("unavailable", verdict.unavailable);
|
|
467
|
+
}
|
|
468
|
+
return verdict.ok
|
|
469
|
+
? {
|
|
470
|
+
...validation,
|
|
471
|
+
warnings: [...validation.warnings, ...verdict.warnings],
|
|
472
|
+
renderCheck: { ok: true },
|
|
473
|
+
}
|
|
474
|
+
: {
|
|
475
|
+
...validation,
|
|
476
|
+
ok: false,
|
|
477
|
+
errors: verdict.errors,
|
|
478
|
+
renderCheck: { ok: false },
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
name: "create_artifact",
|
|
484
|
+
description: "Publish a new page. Put its numbers in documents (the page's script reads each as " +
|
|
485
|
+
"window.artifact.data.<name>), not in the HTML. Validate it first.",
|
|
486
|
+
annotations: WRITE,
|
|
487
|
+
inputSchema: {
|
|
488
|
+
type: "object",
|
|
489
|
+
required: ["id", "title", "kind", "source"],
|
|
490
|
+
properties: {
|
|
491
|
+
id,
|
|
492
|
+
title: { type: "string", minLength: 1, maxLength: 1000 },
|
|
493
|
+
kind,
|
|
494
|
+
source: { type: "string" },
|
|
495
|
+
documents: documentValues,
|
|
496
|
+
},
|
|
497
|
+
additionalProperties: false,
|
|
498
|
+
},
|
|
499
|
+
handler: async (args, ctx) => {
|
|
500
|
+
const render = renderFor(ctx);
|
|
501
|
+
const created = unwrap(await ops.create({
|
|
502
|
+
id: args.id,
|
|
503
|
+
title: args.title,
|
|
504
|
+
kind: args.kind,
|
|
505
|
+
source: args.source,
|
|
506
|
+
...(args.documents !== undefined ? { documents: args.documents } : {}),
|
|
507
|
+
by: actorOf(ctx),
|
|
508
|
+
...(render ? { render } : {}),
|
|
509
|
+
}));
|
|
510
|
+
return {
|
|
511
|
+
id: args.id,
|
|
512
|
+
revision: created.head.revision,
|
|
513
|
+
url: urlOf(ctx, args.id),
|
|
514
|
+
view: { version: created.head.view.version },
|
|
515
|
+
documents: Object.fromEntries(Object.entries(created.head.documents).map(([name, record]) => [name, record.version])),
|
|
516
|
+
...warningsOf(created.warnings),
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: "update_artifact",
|
|
522
|
+
description: "Replace a page's whole source (and optionally its title). For small edits use patch_artifact.",
|
|
523
|
+
annotations: WRITE,
|
|
524
|
+
inputSchema: {
|
|
525
|
+
type: "object",
|
|
526
|
+
required: ["id", "baseVersion", "source"],
|
|
527
|
+
properties: {
|
|
528
|
+
id,
|
|
529
|
+
baseVersion: version("The view version you read; a newer one fails with conflict."),
|
|
530
|
+
source: { type: "string" },
|
|
531
|
+
title: { type: "string", minLength: 1, maxLength: 1000 },
|
|
532
|
+
},
|
|
533
|
+
additionalProperties: false,
|
|
534
|
+
},
|
|
535
|
+
handler: async (args, ctx) => {
|
|
536
|
+
const render = renderFor(ctx);
|
|
537
|
+
const updated = unwrap(await ops.update({
|
|
538
|
+
id: args.id,
|
|
539
|
+
baseVersion: args.baseVersion,
|
|
540
|
+
source: args.source,
|
|
541
|
+
...(args.title !== undefined ? { title: args.title } : {}),
|
|
542
|
+
by: actorOf(ctx),
|
|
543
|
+
...(render ? { render } : {}),
|
|
544
|
+
}));
|
|
545
|
+
return {
|
|
546
|
+
id: args.id,
|
|
547
|
+
revision: updated.head.revision,
|
|
548
|
+
view: { version: updated.head.view.version },
|
|
549
|
+
url: urlOf(ctx, args.id),
|
|
550
|
+
...warningsOf(updated.warnings),
|
|
551
|
+
};
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: "patch_artifact",
|
|
556
|
+
description: "Edit a page by exact find-and-replace: each find must match once, all apply at once, or nothing changes.",
|
|
557
|
+
annotations: WRITE,
|
|
558
|
+
inputSchema: {
|
|
559
|
+
type: "object",
|
|
560
|
+
required: ["id", "baseVersion", "edits"],
|
|
561
|
+
properties: {
|
|
562
|
+
id,
|
|
563
|
+
baseVersion: version("The view version you read; a newer one fails with conflict."),
|
|
564
|
+
edits: {
|
|
565
|
+
type: "array",
|
|
566
|
+
minItems: 1,
|
|
567
|
+
maxItems: 50,
|
|
568
|
+
items: {
|
|
569
|
+
type: "object",
|
|
570
|
+
required: ["find", "replace"],
|
|
571
|
+
properties: {
|
|
572
|
+
find: { type: "string", minLength: 1, description: "Exact text occurring once in the current source." },
|
|
573
|
+
replace: { type: "string" },
|
|
574
|
+
},
|
|
575
|
+
additionalProperties: false,
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
additionalProperties: false,
|
|
580
|
+
},
|
|
581
|
+
handler: async (args, ctx) => {
|
|
582
|
+
const render = renderFor(ctx);
|
|
583
|
+
const patched = unwrap(await ops.patch({
|
|
584
|
+
id: args.id,
|
|
585
|
+
baseVersion: args.baseVersion,
|
|
586
|
+
edits: args.edits,
|
|
587
|
+
by: actorOf(ctx),
|
|
588
|
+
...(render ? { render } : {}),
|
|
589
|
+
}));
|
|
590
|
+
return {
|
|
591
|
+
id: args.id,
|
|
592
|
+
revision: patched.head.revision,
|
|
593
|
+
view: { version: patched.head.view.version },
|
|
594
|
+
url: urlOf(ctx, args.id),
|
|
595
|
+
...warningsOf(patched.warnings),
|
|
596
|
+
};
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
name: "set_documents",
|
|
601
|
+
description: "Set, create (baseVersion 0), or remove (value null) data documents, atomically.",
|
|
602
|
+
annotations: WRITE,
|
|
603
|
+
inputSchema: {
|
|
604
|
+
type: "object",
|
|
605
|
+
required: ["id", "documents"],
|
|
606
|
+
properties: {
|
|
607
|
+
id,
|
|
608
|
+
documents: {
|
|
609
|
+
type: "object",
|
|
610
|
+
minProperties: 1,
|
|
611
|
+
maxProperties: 16,
|
|
612
|
+
additionalProperties: {
|
|
613
|
+
type: "object",
|
|
614
|
+
required: ["baseVersion", "value"],
|
|
615
|
+
properties: {
|
|
616
|
+
baseVersion: {
|
|
617
|
+
type: "integer",
|
|
618
|
+
minimum: 0,
|
|
619
|
+
description: "The document's current version; 0 to create it.",
|
|
620
|
+
},
|
|
621
|
+
value: { description: "Any JSON value; null removes the document." },
|
|
622
|
+
},
|
|
623
|
+
additionalProperties: false,
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
additionalProperties: false,
|
|
628
|
+
},
|
|
629
|
+
handler: async (args, ctx) => {
|
|
630
|
+
const render = renderFor(ctx);
|
|
631
|
+
const set = unwrap(await ops.setDocuments({
|
|
632
|
+
id: args.id,
|
|
633
|
+
documents: args.documents,
|
|
634
|
+
by: actorOf(ctx),
|
|
635
|
+
...(render ? { render } : {}),
|
|
636
|
+
}));
|
|
637
|
+
return {
|
|
638
|
+
id: args.id,
|
|
639
|
+
revision: set.head.revision,
|
|
640
|
+
documents: Object.fromEntries(Object.keys(args.documents).map((name) => {
|
|
641
|
+
const record = set.head.documents[name];
|
|
642
|
+
return [name, { version: record?.version ?? 0, ...(record?.removed ? { removed: true } : {}) }];
|
|
643
|
+
})),
|
|
644
|
+
...warningsOf(set.warnings),
|
|
645
|
+
};
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: "rollback_artifact",
|
|
650
|
+
description: "Restore an earlier view or document version as a new version; history is never rewritten.",
|
|
651
|
+
annotations: WRITE,
|
|
652
|
+
inputSchema: {
|
|
653
|
+
type: "object",
|
|
654
|
+
required: ["id", "target", "version", "baseVersion"],
|
|
655
|
+
properties: {
|
|
656
|
+
id,
|
|
657
|
+
target: { type: "string", enum: ["view", "document"] },
|
|
658
|
+
name: { type: "string", description: 'The document, when target is "document".' },
|
|
659
|
+
version: version("The version to restore."),
|
|
660
|
+
baseVersion: version("The current version of that view or document."),
|
|
661
|
+
},
|
|
662
|
+
additionalProperties: false,
|
|
663
|
+
},
|
|
664
|
+
handler: async (args, ctx) => {
|
|
665
|
+
const render = renderFor(ctx);
|
|
666
|
+
const rolled = unwrap(await ops.rollback({
|
|
667
|
+
id: args.id,
|
|
668
|
+
target: args.target,
|
|
669
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
670
|
+
version: args.version,
|
|
671
|
+
baseVersion: args.baseVersion,
|
|
672
|
+
by: actorOf(ctx),
|
|
673
|
+
...(render ? { render } : {}),
|
|
674
|
+
}));
|
|
675
|
+
const record = args.target === "view" ? rolled.head.view : rolled.head.documents[args.name ?? ""];
|
|
676
|
+
return {
|
|
677
|
+
id: args.id,
|
|
678
|
+
revision: rolled.head.revision,
|
|
679
|
+
target: args.target,
|
|
680
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
681
|
+
version: record?.version,
|
|
682
|
+
restoredFrom: args.version,
|
|
683
|
+
...warningsOf(rolled.warnings),
|
|
684
|
+
};
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: "archive_artifact",
|
|
689
|
+
description: "Archive an artifact: hidden from the library, writes refused, restorable.",
|
|
690
|
+
annotations: WRITE,
|
|
691
|
+
inputSchema: {
|
|
692
|
+
type: "object",
|
|
693
|
+
required: ["id", "baseRevision"],
|
|
694
|
+
properties: { id, baseRevision: version("The artifact's current revision.") },
|
|
695
|
+
additionalProperties: false,
|
|
696
|
+
},
|
|
697
|
+
handler: async (args, ctx) => {
|
|
698
|
+
const archived = unwrap(await ops.setArchived({ id: args.id, baseRevision: args.baseRevision, archived: true, by: actorOf(ctx) }));
|
|
699
|
+
return { id: args.id, revision: archived.head.revision, archived: true };
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: "restore_artifact",
|
|
704
|
+
description: "Restore an archived artifact.",
|
|
705
|
+
annotations: WRITE,
|
|
706
|
+
inputSchema: {
|
|
707
|
+
type: "object",
|
|
708
|
+
required: ["id", "baseRevision"],
|
|
709
|
+
properties: { id, baseRevision: version("The artifact's current revision.") },
|
|
710
|
+
additionalProperties: false,
|
|
711
|
+
},
|
|
712
|
+
handler: async (args, ctx) => {
|
|
713
|
+
const restored = unwrap(await ops.setArchived({ id: args.id, baseRevision: args.baseRevision, archived: false, by: actorOf(ctx) }));
|
|
714
|
+
return { id: args.id, revision: restored.head.revision, archived: false };
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
],
|
|
718
|
+
});
|
|
719
|
+
return { ...connector, approval: "never" };
|
|
720
|
+
}
|
|
721
|
+
/** Every live document record on a head. */
|
|
722
|
+
function liveOf(head) {
|
|
723
|
+
return Object.fromEntries(Object.entries(head.documents).filter(([, record]) => !record.removed));
|
|
724
|
+
}
|