@cloudhal/mcp-server 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +18 -2
- package/dist/src/analytics-reports.d.ts +164 -0
- package/dist/src/analytics-reports.js +632 -0
- package/dist/src/analytics-tools.d.ts +4 -0
- package/dist/src/analytics-tools.js +597 -0
- package/dist/src/asset-tools.d.ts +5 -0
- package/dist/src/asset-tools.js +685 -0
- package/dist/src/body-tools.d.ts +46 -0
- package/dist/src/body-tools.js +708 -0
- package/dist/src/changes.d.ts +10 -0
- package/dist/src/changes.js +112 -0
- package/dist/src/chrome-tools.d.ts +8 -0
- package/dist/src/chrome-tools.js +207 -0
- package/dist/src/comment-tools.d.ts +9 -0
- package/dist/src/comment-tools.js +743 -0
- package/dist/src/comments.d.ts +184 -0
- package/dist/src/comments.js +408 -0
- package/dist/src/common.d.ts +258 -0
- package/dist/src/common.js +488 -0
- package/dist/src/content-shared.d.ts +50 -0
- package/dist/src/content-shared.js +102 -0
- package/dist/src/design-tools.d.ts +9 -0
- package/dist/src/design-tools.js +349 -0
- package/dist/src/elevation-tools.d.ts +3 -0
- package/dist/src/elevation-tools.js +331 -0
- package/dist/src/entry-terms.d.ts +14 -0
- package/dist/src/entry-terms.js +51 -0
- package/dist/src/entry-tools.d.ts +4 -0
- package/dist/src/entry-tools.js +442 -0
- package/dist/src/http.d.ts +48 -0
- package/dist/src/http.js +101 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +10 -0
- package/dist/src/menu-tools.d.ts +41 -0
- package/dist/src/menu-tools.js +1058 -0
- package/dist/src/page-tools.d.ts +5 -0
- package/dist/src/page-tools.js +311 -0
- package/dist/src/preview-doc.d.ts +9 -0
- package/dist/src/preview-doc.js +56 -0
- package/dist/src/preview-view.d.ts +19 -0
- package/dist/src/preview-view.js +71 -0
- package/dist/src/publish-tools.d.ts +10 -0
- package/dist/src/publish-tools.js +248 -0
- package/dist/src/render/cdp.d.ts +29 -0
- package/dist/src/render/cdp.js +190 -0
- package/dist/src/render/checks/components.d.ts +5 -0
- package/dist/src/render/checks/components.js +95 -0
- package/dist/src/render/checks/content.d.ts +5 -0
- package/dist/src/render/checks/content.js +63 -0
- package/dist/src/render/checks/contrast.d.ts +5 -0
- package/dist/src/render/checks/contrast.js +72 -0
- package/dist/src/render/checks/index.d.ts +1 -0
- package/dist/src/render/checks/index.js +29 -0
- package/dist/src/render/checks/layout.d.ts +5 -0
- package/dist/src/render/checks/layout.js +44 -0
- package/dist/src/render/checks/motion.d.ts +5 -0
- package/dist/src/render/checks/motion.js +66 -0
- package/dist/src/render/checks/navigation.d.ts +5 -0
- package/dist/src/render/checks/navigation.js +78 -0
- package/dist/src/render/checks/prelude.d.ts +1 -0
- package/dist/src/render/checks/prelude.js +64 -0
- package/dist/src/render/limiter.d.ts +11 -0
- package/dist/src/render/limiter.js +29 -0
- package/dist/src/render/page.d.ts +19 -0
- package/dist/src/render/page.js +135 -0
- package/dist/src/render/types.d.ts +89 -0
- package/dist/src/render/types.js +140 -0
- package/dist/src/render-tools.d.ts +14 -0
- package/dist/src/render-tools.js +314 -0
- package/dist/src/render.d.ts +14 -0
- package/dist/src/render.js +33 -0
- package/dist/src/schedule-tools.d.ts +2 -0
- package/dist/src/schedule-tools.js +1596 -0
- package/dist/src/schedules.d.ts +292 -0
- package/dist/src/schedules.js +890 -0
- package/dist/src/schema-transforms.d.ts +14 -0
- package/dist/src/schema-transforms.js +50 -0
- package/dist/src/search-tools.d.ts +2 -0
- package/dist/src/search-tools.js +112 -0
- package/dist/src/server-approvals.d.ts +51 -0
- package/dist/src/server-approvals.js +273 -0
- package/dist/src/server-call.d.ts +28 -0
- package/dist/src/server-call.js +259 -0
- package/dist/src/server-instructions.d.ts +4 -0
- package/dist/src/server-instructions.js +74 -0
- package/dist/src/server-resources.d.ts +4 -0
- package/dist/src/server-resources.js +83 -0
- package/dist/src/server-schema.d.ts +20 -0
- package/dist/src/server-schema.js +82 -0
- package/dist/src/server.d.ts +80 -0
- package/dist/src/server.js +135 -0
- package/dist/src/stdio.d.ts +9 -0
- package/dist/src/stdio.js +18 -0
- package/dist/src/surface-tools.d.ts +4 -0
- package/dist/src/surface-tools.js +357 -0
- package/dist/src/taxonomy-tools.d.ts +16 -0
- package/dist/src/taxonomy-tools.js +1433 -0
- package/dist/src/terms.d.ts +47 -0
- package/dist/src/terms.js +271 -0
- package/dist/src/tools.d.ts +39 -0
- package/dist/src/tools.js +511 -0
- package/dist/src/translation-tools.d.ts +3 -0
- package/dist/src/translation-tools.js +422 -0
- package/package.json +86 -3
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudhal's semantic tool surface, defined independently of the MCP SDK so the same definitions
|
|
3
|
+
* serve stdio, HTTP and tests. Each tool carries: JSON Schema input/output, honest annotations,
|
|
4
|
+
* the scopes the policy engine checks, and a handler over `SiteGraphStore`.
|
|
5
|
+
*
|
|
6
|
+
* Mutations snapshot automatically (reason `agentMutation`, label = tool name) so every agent
|
|
7
|
+
* change is diffable and reversible. `publish` and `rollback` are approval-gated.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
|
|
19
|
+
checkAutoPublish,
|
|
20
|
+
describeAutoCheck,
|
|
21
|
+
describeOwnOnly,
|
|
22
|
+
effectiveMode,
|
|
23
|
+
} from "@cloudhal/agent-runtime";
|
|
24
|
+
import {
|
|
25
|
+
afterRunCall,
|
|
26
|
+
checkRunBudget,
|
|
27
|
+
checkRunCall,
|
|
28
|
+
withinDailyWriteCap,
|
|
29
|
+
} from "@cloudhal/schedules";
|
|
30
|
+
import { analyticsTools } from "./analytics-tools.js";
|
|
31
|
+
import { assetTools } from "./asset-tools.js";
|
|
32
|
+
import { bodyTools } from "./body-tools.js";
|
|
33
|
+
import { chromeTools } from "./chrome-tools.js";
|
|
34
|
+
import { commentTools } from "./comment-tools.js";
|
|
35
|
+
import { ToolError, toToolError } from "./common.js";
|
|
36
|
+
import { designTools } from "./design-tools.js";
|
|
37
|
+
import { elevationTools } from "./elevation-tools.js";
|
|
38
|
+
import { entryTools } from "./entry-tools.js";
|
|
39
|
+
import { menuTools } from "./menu-tools.js";
|
|
40
|
+
import { pageRemovalTools, siteAndPageTools } from "./page-tools.js";
|
|
41
|
+
import { previewDoc } from "./preview-doc.js";
|
|
42
|
+
import { ownOnlyPublish, publishTools } from "./publish-tools.js";
|
|
43
|
+
import { renderTools } from "./render-tools.js";
|
|
44
|
+
import { scheduleTools } from "./schedule-tools.js";
|
|
45
|
+
import { RUN_COUNTS, } from "./schedules.js";
|
|
46
|
+
import { hoistDefs, relaxImageRefs } from "./schema-transforms.js";
|
|
47
|
+
import { searchTools } from "./search-tools.js";
|
|
48
|
+
import { surfaceTools } from "./surface-tools.js";
|
|
49
|
+
import { taxonomyTools } from "./taxonomy-tools.js";
|
|
50
|
+
import { translationTools } from "./translation-tools.js";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
notifyBuild,
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
ToolError,
|
|
62
|
+
|
|
63
|
+
toToolError,
|
|
64
|
+
} from "./common.js";
|
|
65
|
+
export { previewDoc } from "./preview-doc.js";
|
|
66
|
+
export { ownOnlyPublish } from "./publish-tools.js";
|
|
67
|
+
export { hoistDefs, relaxImageRefs } from "./schema-transforms.js";
|
|
68
|
+
|
|
69
|
+
/** All tools, in the deterministic order `tools/list` exposes them (agents see this order). */
|
|
70
|
+
export function defineTools() {
|
|
71
|
+
return [
|
|
72
|
+
...siteAndPageTools(),
|
|
73
|
+
...surfaceTools(),
|
|
74
|
+
...pageRemovalTools(),
|
|
75
|
+
...entryTools(),
|
|
76
|
+
...translationTools(),
|
|
77
|
+
// RFC-0029 §8: find nodes by words (drafts included), or what readers find.
|
|
78
|
+
...searchTools(),
|
|
79
|
+
...bodyTools(),
|
|
80
|
+
// RFC-0021 §2: DESIGN.md
|
|
81
|
+
...designTools(),
|
|
82
|
+
// RFC-0025 §5: the header, footer and brand
|
|
83
|
+
...chromeTools(),
|
|
84
|
+
...renderTools(previewDoc),
|
|
85
|
+
// Taxonomy, term and redirect tools (RFC-0002 §6.2), then menu tools (RFC-0003 §6.1) and
|
|
86
|
+
// media tools (RFC-0004 §6.2), follow the article tools.
|
|
87
|
+
...taxonomyTools(),
|
|
88
|
+
...menuTools(),
|
|
89
|
+
...assetTools(),
|
|
90
|
+
...publishTools(),
|
|
91
|
+
// RFC-0006 §9.3: read-only analytics (`analytics:read`).
|
|
92
|
+
...analyticsTools(),
|
|
93
|
+
// RFC-0007 §11: reader comments (read, advisory labels, reductions; approvals for the rest).
|
|
94
|
+
...commentTools(),
|
|
95
|
+
// RFC-0008 §12: the schedule, releases, gates and automation runs.
|
|
96
|
+
...scheduleTools(),
|
|
97
|
+
...elevationTools(),
|
|
98
|
+
].map((t) => ({
|
|
99
|
+
...t,
|
|
100
|
+
inputSchema: relaxImageRefs(hoistDefs(t.inputSchema)),
|
|
101
|
+
outputSchema: hoistDefs(t.outputSchema),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function toolPolicies(tools ) {
|
|
106
|
+
return tools.map((t) => ({
|
|
107
|
+
name: t.name,
|
|
108
|
+
scopes: t.scopes,
|
|
109
|
+
annotations: t.annotations,
|
|
110
|
+
...(t.requiresApproval ? { requiresApproval: true } : {}),
|
|
111
|
+
...(t.own ? { own: t.own } : {}),
|
|
112
|
+
...(t.classify ? { classify: t.classify } : {}),
|
|
113
|
+
...(t.proposeScopes ? { proposeScopes: t.proposeScopes } : {}),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Policy → audit → handler. Transports call this; it is the only path a tool runs through.
|
|
130
|
+
* `confirmed` is set by the transport once the human said yes (MRTR response or confirm token);
|
|
131
|
+
* `approved` once an out-of-band approval was redeemed — someone who could make the full call
|
|
132
|
+
* decided it (RFC-0005 §6), so an own limit on a publish no longer applies.
|
|
133
|
+
*/
|
|
134
|
+
export async function runTool(
|
|
135
|
+
def ,
|
|
136
|
+
args ,
|
|
137
|
+
ctx ,
|
|
138
|
+
deps
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
,
|
|
144
|
+
) {
|
|
145
|
+
const started = Date.now();
|
|
146
|
+
let decision = deps.policy.evaluate(ctx.principal, def.name, {
|
|
147
|
+
confirmed: deps.confirmed ?? false,
|
|
148
|
+
args,
|
|
149
|
+
});
|
|
150
|
+
const siteId = typeof args.siteId === "string" ? args.siteId : undefined;
|
|
151
|
+
const limit = decision.kind === "deny" ? undefined : decision.limit;
|
|
152
|
+
ctx = {
|
|
153
|
+
...ctx,
|
|
154
|
+
limit,
|
|
155
|
+
confirmed: deps.confirmed === true,
|
|
156
|
+
policy: deps.policy,
|
|
157
|
+
...(deps.audit ? { audit: deps.audit } : {}),
|
|
158
|
+
};
|
|
159
|
+
// Own-only publish (I10): an Author's (or their agent's) publish that carries someone else's
|
|
160
|
+
// work, or structure, waits for someone whose role allows it (cause `role`).
|
|
161
|
+
if (limit?.target === "changeset" && !deps.approved && siteId && decision.kind !== "deny") {
|
|
162
|
+
const check = await ownOnlyPublish(ctx, siteId).catch(() => undefined);
|
|
163
|
+
if (!check?.ownOnly) {
|
|
164
|
+
const why = check ? describeOwnOnly(check) : "the site could not be read";
|
|
165
|
+
decision = {
|
|
166
|
+
kind: "approval-required",
|
|
167
|
+
reason: `your role publishes only your own work, but ${why}`,
|
|
168
|
+
cause: "role",
|
|
169
|
+
limit,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const audited = def.auditArgs ? def.auditArgs(args) : args;
|
|
174
|
+
const base = {
|
|
175
|
+
at: new Date().toISOString(),
|
|
176
|
+
actor: ctx.principal.actor,
|
|
177
|
+
siteId,
|
|
178
|
+
tool: def.name,
|
|
179
|
+
skill: ctx.principal.skill?.name,
|
|
180
|
+
input: audited,
|
|
181
|
+
mode: effectiveMode(ctx.principal),
|
|
182
|
+
...(ctx.principal.actor.kind === "agent" && ctx.principal.elevation
|
|
183
|
+
? { elevation: ctx.principal.elevation }
|
|
184
|
+
: {}),
|
|
185
|
+
// RFC-0008 §5.1: a run's rows carry the run and its automation; release tools the release.
|
|
186
|
+
...(ctx.run ? { runId: ctx.run.run.id, automationId: ctx.run.automation.id } : {}),
|
|
187
|
+
...(typeof args.releaseId === "string" ? { releaseId: args.releaseId } : {}),
|
|
188
|
+
};
|
|
189
|
+
// Inside an automation run (RFC-0008 §11.4–§11.5): the run's site, the taint rule, the budget.
|
|
190
|
+
const run = ctx.run;
|
|
191
|
+
const cls = runCallClass(def, args);
|
|
192
|
+
if (run) {
|
|
193
|
+
const refusal = await runRefusal(def, args, ctx, run, cls, decision);
|
|
194
|
+
if (refusal) {
|
|
195
|
+
await deps.audit?.record({
|
|
196
|
+
...base,
|
|
197
|
+
decision: "deny",
|
|
198
|
+
reason: refusal.message,
|
|
199
|
+
outcome: "error",
|
|
200
|
+
});
|
|
201
|
+
await noteRunCall(ctx, run, def.name, cls, "refused");
|
|
202
|
+
throw refusal;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (decision.kind === "deny") {
|
|
206
|
+
if (run) await noteRunCall(ctx, run, def.name, cls, "refused");
|
|
207
|
+
await deps.audit?.record({
|
|
208
|
+
...base,
|
|
209
|
+
decision: "deny",
|
|
210
|
+
reason: decision.reason,
|
|
211
|
+
outcome: "error",
|
|
212
|
+
});
|
|
213
|
+
return { decision };
|
|
214
|
+
}
|
|
215
|
+
if (def.precheck) {
|
|
216
|
+
try {
|
|
217
|
+
await def.precheck(args, ctx);
|
|
218
|
+
} catch (raw) {
|
|
219
|
+
const e = toToolError(raw);
|
|
220
|
+
await deps.audit?.record({
|
|
221
|
+
...base,
|
|
222
|
+
decision: "deny",
|
|
223
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
224
|
+
outcome: "error",
|
|
225
|
+
});
|
|
226
|
+
throw e;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// `auto` mode: a publish that only adds or edits a few articles goes live without a person.
|
|
230
|
+
let autoCheck ;
|
|
231
|
+
let autoNote ;
|
|
232
|
+
if (
|
|
233
|
+
decision.kind === "approval-required" &&
|
|
234
|
+
decision.autoApprove === "low-risk-publish" &&
|
|
235
|
+
siteId
|
|
236
|
+
) {
|
|
237
|
+
const pending = await ctx.store.pendingChanges(siteId);
|
|
238
|
+
// New raster images used only by the articles in this publish ride along (RFC-0004 §6.3).
|
|
239
|
+
autoCheck = checkAutoPublish(pending, {
|
|
240
|
+
ridingAssets: await ctx.store.ridingAssets(siteId, pending),
|
|
241
|
+
corrections: await ctx.store.correctingEntries(siteId, pending),
|
|
242
|
+
});
|
|
243
|
+
if (autoCheck.lowRisk)
|
|
244
|
+
autoNote = `auto mode: published without approval (${describeAutoCheck(autoCheck)})`;
|
|
245
|
+
else
|
|
246
|
+
decision = {
|
|
247
|
+
...decision,
|
|
248
|
+
reason: `auto mode waits for a person: ${describeAutoCheck(autoCheck)}`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const checked = autoCheck ? { autoCheck } : {};
|
|
252
|
+
// RFC-0008 §10.4: release tools file their own request bound to `rl1:` (or take `auto`'s
|
|
253
|
+
// low-risk path), so their handler runs with the waiting decision instead of the generic gate.
|
|
254
|
+
if (
|
|
255
|
+
decision.kind === "approval-required" &&
|
|
256
|
+
def.handlesApproval &&
|
|
257
|
+
!deps.confirmed &&
|
|
258
|
+
decision.cause !== "role" &&
|
|
259
|
+
((ctx.approvals && ctx.principal.actor.kind === "agent") ||
|
|
260
|
+
decision.autoApprove === "low-risk-release")
|
|
261
|
+
) {
|
|
262
|
+
const pending = decision;
|
|
263
|
+
let effect ;
|
|
264
|
+
const callCtx = {
|
|
265
|
+
...ctx,
|
|
266
|
+
pending,
|
|
267
|
+
report: (e) => {
|
|
268
|
+
effect = { ...effect, ...e };
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
try {
|
|
272
|
+
const result = await def.handler(args, callCtx);
|
|
273
|
+
const waits = (result )?.status === "approval-pending";
|
|
274
|
+
const releaseId =
|
|
275
|
+
(result )?.release?.id ??
|
|
276
|
+
(typeof effect?.releaseId === "string" ? effect.releaseId : undefined);
|
|
277
|
+
await deps.audit?.record({
|
|
278
|
+
...base,
|
|
279
|
+
...(effect ? { input: { ...audited, effect } } : {}),
|
|
280
|
+
...(releaseId ? { releaseId } : {}),
|
|
281
|
+
decision: waits ? "approval-required" : "allow",
|
|
282
|
+
reason: waits
|
|
283
|
+
? pending.reason
|
|
284
|
+
: `auto mode: scheduled without a person (${describeAutoCheck((effect?.autoCheck ) ?? { rule: "low-risk-release", lowRisk: true, code: "low-risk", changes: 0, limit: 20 })})`,
|
|
285
|
+
...(waits ? { cause: pending.cause } : {}),
|
|
286
|
+
outcome: waits ? "pending" : "ok",
|
|
287
|
+
durationMs: Date.now() - started,
|
|
288
|
+
});
|
|
289
|
+
if (run) await noteRunCall(ctx, run, def.name, cls, "allowed");
|
|
290
|
+
return { decision: { kind: "allow" }, result };
|
|
291
|
+
} catch (raw) {
|
|
292
|
+
const e = toToolError(raw);
|
|
293
|
+
await deps.audit?.record({
|
|
294
|
+
...base,
|
|
295
|
+
decision: "allow",
|
|
296
|
+
outcome: "error",
|
|
297
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
298
|
+
durationMs: Date.now() - started,
|
|
299
|
+
});
|
|
300
|
+
if (run)
|
|
301
|
+
await noteRunCall(
|
|
302
|
+
ctx,
|
|
303
|
+
run,
|
|
304
|
+
def.name,
|
|
305
|
+
cls,
|
|
306
|
+
e instanceof ToolError && e.code === "denied" ? "refused" : "allowed",
|
|
307
|
+
);
|
|
308
|
+
throw e;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (decision.kind === "approval-required" && !autoNote) {
|
|
312
|
+
const approvalPrompt = def.approvalPrompt
|
|
313
|
+
? await def.approvalPrompt(args, ctx)
|
|
314
|
+
: decision.reason;
|
|
315
|
+
await deps.audit?.record({
|
|
316
|
+
...base,
|
|
317
|
+
decision: "approval-required",
|
|
318
|
+
reason: decision.reason,
|
|
319
|
+
cause: decision.cause,
|
|
320
|
+
...checked,
|
|
321
|
+
outcome: "pending",
|
|
322
|
+
});
|
|
323
|
+
if (run) await noteRunCall(ctx, run, def.name, cls, "allowed");
|
|
324
|
+
return { decision, approvalPrompt, ...checked };
|
|
325
|
+
}
|
|
326
|
+
// What the call did beyond its arguments (assignment deltas, a governance operation) goes into
|
|
327
|
+
// the audit row's `input.effect` (RFC-0002 §6.4).
|
|
328
|
+
let effect ;
|
|
329
|
+
const callCtx = {
|
|
330
|
+
...ctx,
|
|
331
|
+
report: (e) => {
|
|
332
|
+
effect = { ...effect, ...e };
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
try {
|
|
336
|
+
const result = await def.handler(args, callCtx);
|
|
337
|
+
const snapshotId = (result )?.snapshot?.id;
|
|
338
|
+
const releaseId =
|
|
339
|
+
(result )?.release?.id ??
|
|
340
|
+
(typeof (effect )?.releaseId === "string"
|
|
341
|
+
? (effect ).releaseId
|
|
342
|
+
: undefined);
|
|
343
|
+
await deps.audit?.record({
|
|
344
|
+
...base,
|
|
345
|
+
...(effect ? { input: { ...audited, effect } } : {}),
|
|
346
|
+
...(releaseId ? { releaseId } : {}),
|
|
347
|
+
decision: "allow",
|
|
348
|
+
outcome: "ok",
|
|
349
|
+
snapshotId,
|
|
350
|
+
durationMs: Date.now() - started,
|
|
351
|
+
...(autoNote ? { reason: autoNote } : {}),
|
|
352
|
+
...checked,
|
|
353
|
+
});
|
|
354
|
+
if (run) await noteRunCall(ctx, run, def.name, cls, "allowed");
|
|
355
|
+
return {
|
|
356
|
+
decision: { kind: "allow" },
|
|
357
|
+
result,
|
|
358
|
+
...(autoNote ? { autoApproved: autoNote } : {}),
|
|
359
|
+
...checked,
|
|
360
|
+
};
|
|
361
|
+
} catch (raw) {
|
|
362
|
+
const e = toToolError(raw);
|
|
363
|
+
await deps.audit?.record({
|
|
364
|
+
...base,
|
|
365
|
+
decision: "allow",
|
|
366
|
+
outcome: "error",
|
|
367
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
368
|
+
durationMs: Date.now() - started,
|
|
369
|
+
});
|
|
370
|
+
if (run)
|
|
371
|
+
await noteRunCall(
|
|
372
|
+
ctx,
|
|
373
|
+
run,
|
|
374
|
+
def.name,
|
|
375
|
+
cls,
|
|
376
|
+
e instanceof ToolError && e.code === "denied" ? "refused" : "allowed",
|
|
377
|
+
);
|
|
378
|
+
throw e;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Bookkeeping tools a run may always call: they never count against its budget. */
|
|
383
|
+
const RUN_BOOKKEEPING = new Set(["end_run", "get_mode", "pause_automation", "begin_run"]);
|
|
384
|
+
|
|
385
|
+
/** A call's class inside a run (§11.4): its own `classify`, else read-only or not. */
|
|
386
|
+
function runCallClass(def , args ) {
|
|
387
|
+
const cls = def.classify?.(args);
|
|
388
|
+
// A proposal is a note a person reads before deciding, like a label (RFC-0014 phase 4).
|
|
389
|
+
if (cls === "propose") return "advisory";
|
|
390
|
+
if (cls) return cls;
|
|
391
|
+
return def.annotations.readOnlyHint === true ? "read" : "advisory";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** The classes runs account for (RFC-0008 §11.4): `propose` counts as `advisory`. */
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Why a run may not make this call (RFC-0008 §10.7, §11.4, §11.5), or undefined: another site,
|
|
399
|
+
* `request_elevation` (code `run`), no egress, the taint rule, the run's budget (which ends the run
|
|
400
|
+
* and pauses its automation) and the site's daily cap on automation writes.
|
|
401
|
+
*/
|
|
402
|
+
async function runRefusal(
|
|
403
|
+
def ,
|
|
404
|
+
args ,
|
|
405
|
+
ctx ,
|
|
406
|
+
run ,
|
|
407
|
+
cls ,
|
|
408
|
+
decision ,
|
|
409
|
+
) {
|
|
410
|
+
const siteId = run.automation.siteId;
|
|
411
|
+
if (typeof args.siteId === "string" && args.siteId !== siteId)
|
|
412
|
+
return new ToolError("outside_run", `this run works on site ${siteId} only`);
|
|
413
|
+
if (def.name === "request_elevation")
|
|
414
|
+
return new ToolError(
|
|
415
|
+
"run",
|
|
416
|
+
"a run's autonomy is fixed by its automation (modeCap); request_elevation is not available inside runs",
|
|
417
|
+
);
|
|
418
|
+
const taint = checkRunCall(run.state, { tool: def.name, class: cls });
|
|
419
|
+
if (!taint.allowed) return new ToolError(taint.code, taint.message);
|
|
420
|
+
if (RUN_BOOKKEEPING.has(def.name) || !ctx.schedules) return undefined;
|
|
421
|
+
const store = ctx.schedules.store;
|
|
422
|
+
const now = (ctx.now?.() ?? new Date()).toISOString();
|
|
423
|
+
const writes = decision.kind === "allow" && def.annotations.readOnlyHint !== true;
|
|
424
|
+
const usage = await store.runUsage(siteId, run.run.id);
|
|
425
|
+
const budget = checkRunBudget(
|
|
426
|
+
run.automation.job.budget,
|
|
427
|
+
usage,
|
|
428
|
+
{ startedAt: run.run.startedAt ?? now, now },
|
|
429
|
+
{ write: writes, approvalRequest: decision.kind === "approval-required" },
|
|
430
|
+
);
|
|
431
|
+
if (!budget.ok) {
|
|
432
|
+
await store.endAgentRunAs(siteId, run.run.id, {
|
|
433
|
+
now,
|
|
434
|
+
state: "budget-exceeded",
|
|
435
|
+
error: { code: "budget_exceeded", message: budget.exceeded },
|
|
436
|
+
usage,
|
|
437
|
+
});
|
|
438
|
+
const a = await store.getAutomation(siteId, run.automation.id);
|
|
439
|
+
if (a?.state === "active")
|
|
440
|
+
await store
|
|
441
|
+
.transitionAutomation(siteId, a.id, {
|
|
442
|
+
expectedRev: a.rev,
|
|
443
|
+
to: "auto-paused",
|
|
444
|
+
pausedReason: "budget",
|
|
445
|
+
})
|
|
446
|
+
.catch(() => undefined);
|
|
447
|
+
return new ToolError(
|
|
448
|
+
"budget_exceeded",
|
|
449
|
+
`the run's ${budget.exceeded} budget is used up: the run ended and the automation is paused until a person resumes it`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (writes) {
|
|
453
|
+
const settings = await store.settings(siteId);
|
|
454
|
+
if (
|
|
455
|
+
!withinDailyWriteCap(await store.automationWritesToday(siteId, now), settings.dailyWriteCap)
|
|
456
|
+
)
|
|
457
|
+
return new ToolError(
|
|
458
|
+
"budget_exceeded",
|
|
459
|
+
`daily_write_cap: automations on this site made ${settings.dailyWriteCap} changes today (UTC); try tomorrow or ask a person to raise the cap`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Records a call on its run (§11.4): the taint from the first tainting tool on, the anomaly counters
|
|
467
|
+
* and the usage. More than 50 reductions or 10 refusals in a row end the run (`killed`) and pause
|
|
468
|
+
* the automation.
|
|
469
|
+
*/
|
|
470
|
+
async function noteRunCall(
|
|
471
|
+
ctx ,
|
|
472
|
+
run ,
|
|
473
|
+
tool ,
|
|
474
|
+
cls ,
|
|
475
|
+
result ,
|
|
476
|
+
) {
|
|
477
|
+
const store = ctx.schedules?.store;
|
|
478
|
+
if (!store) return;
|
|
479
|
+
const siteId = run.automation.siteId;
|
|
480
|
+
const now = (ctx.now?.() ?? new Date()).toISOString();
|
|
481
|
+
const next = afterRunCall(run.state, { tool, class: cls }, result, now);
|
|
482
|
+
run.state = next.state;
|
|
483
|
+
const counts = {
|
|
484
|
+
...(run.run.counts ?? {}),
|
|
485
|
+
[RUN_COUNTS.reduce]: next.state.reduceCalls,
|
|
486
|
+
[RUN_COUNTS.refusals]: next.state.consecutiveRefusals,
|
|
487
|
+
};
|
|
488
|
+
const usage = await store.runUsage(siteId, run.run.id);
|
|
489
|
+
await store.recordRunCall(siteId, run.run.id, {
|
|
490
|
+
now,
|
|
491
|
+
...(next.state.taint ? { taint: next.state.taint } : {}),
|
|
492
|
+
counts,
|
|
493
|
+
usage,
|
|
494
|
+
});
|
|
495
|
+
if (!next.kill) return;
|
|
496
|
+
await store.endAgentRunAs(siteId, run.run.id, {
|
|
497
|
+
now,
|
|
498
|
+
state: "killed",
|
|
499
|
+
error: { code: next.kill },
|
|
500
|
+
usage,
|
|
501
|
+
});
|
|
502
|
+
const a = await store.getAutomation(siteId, run.automation.id);
|
|
503
|
+
if (a?.state === "active")
|
|
504
|
+
await store
|
|
505
|
+
.transitionAutomation(siteId, a.id, {
|
|
506
|
+
expectedRev: a.rev,
|
|
507
|
+
to: "auto-paused",
|
|
508
|
+
pausedReason: "failures",
|
|
509
|
+
})
|
|
510
|
+
.catch(() => undefined);
|
|
511
|
+
}
|