toolroll 0.9.41 → 0.9.43
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/CHANGELOG.md +37 -0
- package/dist/chat-actions.js +1 -1
- package/dist/chat-approval.d.ts +45 -0
- package/dist/chat-approval.js +81 -0
- package/dist/chat-channel.js +5 -0
- package/dist/chat-decide.d.ts +18 -6
- package/dist/chat-decide.js +54 -20
- package/dist/chat-flow.js +2 -2
- package/dist/cli.js +4 -2
- package/dist/coding-types.d.ts +2 -24
- package/dist/coding-workspace.js +12 -11
- package/dist/contracts/cli.d.ts +736 -0
- package/dist/contracts/cli.js +223 -0
- package/dist/contracts/coding-activity.d.ts +64 -0
- package/dist/contracts/coding-activity.js +58 -0
- package/dist/contracts/console-api.d.ts +484 -0
- package/dist/contracts/console-api.js +274 -0
- package/dist/contracts/diff-stat.d.ts +58 -0
- package/dist/contracts/diff-stat.js +59 -0
- package/dist/contracts/integration-metadata.d.ts +99 -0
- package/dist/contracts/integration-metadata.js +101 -0
- package/dist/contracts/lead-tools.d.ts +11 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/model-catalog.d.ts +21 -0
- package/dist/contracts/model-catalog.js +14 -0
- package/dist/contracts/native-census.d.ts +42 -0
- package/dist/contracts/native-census.js +36 -0
- package/dist/contracts/observation-request.d.ts +37 -0
- package/dist/contracts/observation-request.js +25 -0
- package/dist/contracts/provider.d.ts +30 -0
- package/dist/contracts/provider.js +15 -0
- package/dist/contracts/recipes.d.ts +166 -0
- package/dist/contracts/recipes.js +52 -0
- package/dist/contracts/retention.d.ts +42 -0
- package/dist/contracts/retention.js +24 -0
- package/dist/contracts/storage.d.ts +23 -0
- package/dist/contracts/storage.js +10 -0
- package/dist/contracts/store-json.d.ts +315 -0
- package/dist/contracts/store-json.js +196 -0
- package/dist/contracts/trigger-payloads.d.ts +369 -0
- package/dist/contracts/trigger-payloads.js +144 -0
- package/dist/contracts/update-journal.d.ts +254 -0
- package/dist/contracts/update-journal.js +155 -0
- package/dist/contracts/verification-view.d.ts +93 -0
- package/dist/contracts/verification-view.js +43 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/desktop-update.d.ts +7 -40
- package/dist/desktop-update.js +36 -8
- package/dist/evidence.d.ts +3 -19
- package/dist/flow-engine.d.ts +2 -0
- package/dist/flow-engine.js +10 -2
- package/dist/flow-triggers.js +61 -60
- package/dist/flows-cli.d.ts +28 -0
- package/dist/flows-cli.js +43 -1
- package/dist/guides.js +2 -0
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +43 -1
- package/dist/mcp-connect.js +37 -22
- package/dist/model-catalog.js +8 -6
- package/dist/observations.d.ts +2 -6
- package/dist/observations.js +13 -9
- package/dist/operate.js +55 -4
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/process-recovery-native.d.ts +2 -13
- package/dist/process-recovery-native.js +8 -5
- package/dist/project-tools.d.ts +6 -24
- package/dist/project-tools.js +22 -18
- package/dist/provider.d.ts +3 -8
- package/dist/provider.js +15 -11
- package/dist/recipes.d.ts +3 -28
- package/dist/recipes.js +79 -52
- package/dist/retention.d.ts +4 -5
- package/dist/retention.js +5 -4
- package/dist/scope.d.ts +4 -2
- package/dist/serve.js +193 -30
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +12 -9
- package/dist/store.d.ts +11 -16
- package/dist/store.js +122 -160
- package/dist/surface.d.ts +6 -54
- package/dist/surface.js +10 -2
- package/dist/teammate-work.js +1 -1
- package/dist/telegram-flow.d.ts +41 -1
- package/dist/telegram-flow.js +96 -12
- package/dist/telegram.js +27 -3
- package/dist/toolroll-update.d.ts +8 -99
- package/dist/toolroll-update.js +28 -18
- package/dist/verification-evidence.js +27 -12
- package/package.json +1 -1
package/dist/mate-tools.js
CHANGED
|
@@ -48,7 +48,9 @@ import { TOOL_CATALOG, discoverTools, projectToolsOf, secretsSetFor, toolCommand
|
|
|
48
48
|
import { deciderOf, durationWords, FLOW_KIND_WORDS, FLOW_TEMPLATES, flowFromSteps, stepsFor, withKeptSteps } from "./flows.js";
|
|
49
49
|
import { parseContract, toModelSchema } from "./contracts/contract.js";
|
|
50
50
|
import { LEAD_TOOL_INPUTS, LEAD_TOOL_READ_INPUTS, LEAD_TOOL_OPTIONS, LEAD_TOOL_OUTPUTS, reportToolOutput } from "./contracts/lead-tools.js";
|
|
51
|
-
import { flowDefinitionOf } from "./flow-engine.js";
|
|
51
|
+
import { flowCardHref, flowDefinitionOf } from "./flow-engine.js";
|
|
52
|
+
import { planInChat } from "./chat-decide.js";
|
|
53
|
+
import { flowDecisionAt } from "./telegram-flow.js";
|
|
52
54
|
import { flowPersonOf } from "./flow-send.js";
|
|
53
55
|
import { flowInsights } from "./flow-insights.js";
|
|
54
56
|
import { describeTrigger, FLOW_TRIGGER_KINDS, triggerConfigOf } from "./flow-triggers.js";
|
|
@@ -491,6 +493,46 @@ const MATE_TOOL_HANDLERS = {
|
|
|
491
493
|
return id === null ? tooMany() : { ok: true, body: { card: id, label: entry.label, action: "open existing control; nothing changed" } };
|
|
492
494
|
},
|
|
493
495
|
},
|
|
496
|
+
offer_approval: {
|
|
497
|
+
description: "When the owner asks to approve one thing (a plan waiting on them, or a flow card waiting at a Person decides step), send that item's own Approve and Send back buttons to their paired chat right away. Their tap there is the approval (two taps, as on any decision card); you never approve. Give task for a plan, or card for a flow card (its number from get_flows). When it can't be approved in chat, the answer says why: say that in one line and open show_control approval (a plan) or the flow, never that you can't approve.",
|
|
498
|
+
handle: (ctx, args) => {
|
|
499
|
+
const pairedHere = ctx.channel === "telegram";
|
|
500
|
+
const paired = ctx.store.handle.prepare("SELECT 1 FROM telegram_binding WHERE approver = ? AND approver_generation = ? AND revoked_at IS NULL LIMIT 1")
|
|
501
|
+
.get(ctx.who.name, ctx.store.accountOf(ctx.who.name)?.generation ?? -1) !== undefined;
|
|
502
|
+
const stamp = ctx.now.getTime();
|
|
503
|
+
if (args["card"] !== undefined) {
|
|
504
|
+
const cardId = Number(args["card"]);
|
|
505
|
+
const card = Number.isSafeInteger(cardId) ? ctx.store.getFlowCard(cardId) : null;
|
|
506
|
+
const flow = card === null ? null : ctx.store.getFlow(card.flow);
|
|
507
|
+
if (card === null || flow === null || !ctx.who.repos.includes(flow.repo))
|
|
508
|
+
return { ok: false, message: "not-found: no such flow card in your projects" };
|
|
509
|
+
const waiting = flowDecisionAt(ctx.store, card.id, card.entry);
|
|
510
|
+
const href = flowCardHref(flow.id, card.id);
|
|
511
|
+
if (waiting === null)
|
|
512
|
+
return { ok: true, body: { offered: false, why: card.state === "active" ? "That card isn't waiting for a decision now." : "That card isn't waiting any more.", open: href } };
|
|
513
|
+
const decider = deciderOf(waiting.stage, waiting.flow);
|
|
514
|
+
if (decider !== null && decider !== ctx.who.name)
|
|
515
|
+
return { ok: true, body: { offered: false, why: `Only ${decider} decides here.`, open: href } };
|
|
516
|
+
if (!pairedHere || !paired)
|
|
517
|
+
return { ok: true, body: { offered: false, why: pairedHere ? "Your chat isn't paired for decisions." : "Buttons go to your paired chat; here, approve it on the card.", open: href } };
|
|
518
|
+
ctx.store.enqueueNotification({ dedupeKey: `flow-decide:${card.id}:${card.entry}:ask:${stamp}`, kind: "flow-decision", pushClass: "attention", recipient: ctx.who.name,
|
|
519
|
+
subject: `${flow.name}: ${card.title} needs your decision`.slice(0, 200), body: `${waiting.stage.title}: approve it, or send it back with a note.`, link: href, source: { project: flow.repo } }, ctx.now);
|
|
520
|
+
return { ok: true, body: { offered: true, item: `flow card ${card.id}`, shown: "Approve and Send back buttons follow your reply in this chat; the owner's tap decides it" } };
|
|
521
|
+
}
|
|
522
|
+
const task = taskIdOf(args);
|
|
523
|
+
if (task === null || admittedRef(ctx, task) === null)
|
|
524
|
+
return notFound();
|
|
525
|
+
const current = ctx.store.taskFamilyOf(task, ctx.who.repos, false)?.current.id ?? task;
|
|
526
|
+
const plan = planInChat(ctx.store, current, ctx.who.name, ctx.now, ctx.evidenceRoot);
|
|
527
|
+
if (!plan.ok)
|
|
528
|
+
return { ok: true, body: { offered: false, task: current, why: plan.why, control: "approval" } };
|
|
529
|
+
if (!pairedHere || !paired)
|
|
530
|
+
return { ok: true, body: { offered: false, task: current, why: pairedHere ? "Your chat isn't paired for decisions." : "Buttons go to your paired chat; here, approve it on its card.", control: "approval" } };
|
|
531
|
+
ctx.store.enqueueNotification({ dedupeKey: `plan-ready:${current}:ask:${stamp}`, kind: "plan-ready", recipient: ctx.who.name, subject: `${current}: plan ready for review`,
|
|
532
|
+
body: "Approve it here, or open it to edit first.", source: { taskRef: ctx.store.lookupRef(current).id } }, ctx.now);
|
|
533
|
+
return { ok: true, body: { offered: true, item: `plan for ${current}`, task: current, shown: "Approve & start follows your reply in this chat; the owner's two taps approve it" } };
|
|
534
|
+
},
|
|
535
|
+
},
|
|
494
536
|
get_result: {
|
|
495
537
|
description: "Read exact execution/run and feedback. Use get_task currentExecution; page nextFeedbackOffset.",
|
|
496
538
|
handle: (ctx, args) => {
|
package/dist/mcp-connect.js
CHANGED
|
@@ -16,6 +16,7 @@ import { homedir } from "node:os";
|
|
|
16
16
|
import { basename } from "node:path";
|
|
17
17
|
import { LOCAL_APPS, addToolTo, catalogTool, localAppOf, projectToolsOf, readToolSecrets, setToolSecrets, testToolOf } from "./project-tools.js";
|
|
18
18
|
import { envValue } from "./names.js";
|
|
19
|
+
import { SCOPE_TOKEN, readAuthorizationServer, readProtectedResource, readRegistration, readTokenResponse } from "./contracts/integration-metadata.js";
|
|
19
20
|
/**
|
|
20
21
|
* Services that connect this way: their servers speak streamable HTTP and
|
|
21
22
|
* advertise registering clients on the spot. Advertising isn't accepting:
|
|
@@ -202,8 +203,7 @@ const GRANTED = "OAUTH_GRANTED_SCOPE";
|
|
|
202
203
|
export const CONNECT_CALLBACK = "/settings/tools/connected";
|
|
203
204
|
const VISIT_MS = 15 * 60_000, RENEW_RETRY_MS = 5 * 60_000;
|
|
204
205
|
const renewTried = new Map();
|
|
205
|
-
/**
|
|
206
|
-
const SCOPE_TOKEN = /^[\x21\x23-\x5b\x5d-\x7e]{1,128}$/;
|
|
206
|
+
/** The most scope text a sign-in address carries (addresses stay well under 8 KB). */
|
|
207
207
|
const SCOPE_PARAM_LIMIT = 6_000;
|
|
208
208
|
const json = async (fetcher, url) => {
|
|
209
209
|
try {
|
|
@@ -238,25 +238,29 @@ export async function discoverSignIn(mcpUrl, fetcher = fetch) {
|
|
|
238
238
|
catch {
|
|
239
239
|
pointer = null;
|
|
240
240
|
}
|
|
241
|
-
const
|
|
241
|
+
const found = (pointer !== null && https(pointer) ? await json(fetcher, pointer) : null)
|
|
242
242
|
?? await json(fetcher, `${mcp.origin}/.well-known/oauth-protected-resource${mcp.pathname === "/" ? "" : mcp.pathname}`)
|
|
243
243
|
?? await json(fetcher, `${mcp.origin}/.well-known/oauth-protected-resource`);
|
|
244
|
-
const
|
|
244
|
+
const resource = found === null ? {} : readProtectedResource(found);
|
|
245
|
+
const issuers = (resource.authorization_servers ?? []).filter(https);
|
|
245
246
|
const issuer = new URL(issuers[0] ?? mcp.origin);
|
|
246
247
|
const path = issuer.pathname === "/" ? "" : issuer.pathname.replace(/\/$/, "");
|
|
247
|
-
const
|
|
248
|
+
const answered = await json(fetcher, `${issuer.origin}/.well-known/oauth-authorization-server${path}`)
|
|
248
249
|
?? await json(fetcher, `${issuer.origin}/.well-known/oauth-authorization-server`)
|
|
249
250
|
?? (path === "" ? null : await json(fetcher, `${issuer.origin}${path}/.well-known/openid-configuration`))
|
|
250
251
|
?? await json(fetcher, `${issuer.origin}/.well-known/openid-configuration`);
|
|
251
|
-
if (
|
|
252
|
+
if (answered === null)
|
|
252
253
|
return null;
|
|
253
|
-
const
|
|
254
|
-
|
|
254
|
+
const meta = readAuthorizationServer(answered);
|
|
255
|
+
const { authorization_endpoint: authorize, token_endpoint: token, registration_endpoint: register } = meta;
|
|
256
|
+
if (!https(authorize) || !https(token) || !https(register))
|
|
257
|
+
return null;
|
|
258
|
+
if (!(meta.code_challenge_methods_supported ?? ["S256"]).includes("S256"))
|
|
255
259
|
return null;
|
|
256
260
|
// Ask for every scope the server lists for this resource: a token missing one the server itself requires is
|
|
257
261
|
// refused after a successful sign-in (PostHog lists 155 and needs user:read, the 140th). Only well-formed scope
|
|
258
262
|
// tokens, and at most what fits a sign-in address.
|
|
259
|
-
const listed =
|
|
263
|
+
const listed = resource.scopes_supported ?? [];
|
|
260
264
|
const scopes = [];
|
|
261
265
|
for (const one of listed) {
|
|
262
266
|
if (scopes.join(" ").length + one.length + 1 > SCOPE_PARAM_LIMIT)
|
|
@@ -264,8 +268,8 @@ export async function discoverSignIn(mcpUrl, fetcher = fetch) {
|
|
|
264
268
|
scopes.push(one);
|
|
265
269
|
}
|
|
266
270
|
// A token is for the resource the server names (Stripe's has no trailing slash), when it names one.
|
|
267
|
-
const named = resource
|
|
268
|
-
return { authorize
|
|
271
|
+
const named = resource.resource;
|
|
272
|
+
return { authorize, token, register, scopes, resource: https(named) ? named : null };
|
|
269
273
|
}
|
|
270
274
|
/**
|
|
271
275
|
* Start connecting a service: find its sign-in, register this console as a
|
|
@@ -287,11 +291,14 @@ export async function startConnect(input, fetcher = fetch, now = Date.now()) {
|
|
|
287
291
|
// Figma's answer (Oct 2026): it registers only apps it has approved. Said before reading a body that may not be JSON.
|
|
288
292
|
if (registered.status === 401 || registered.status === 403)
|
|
289
293
|
return { ok: false, said: `${service.label} doesn't let other apps sign in this way yet.${INSTEAD[service.id] === undefined ? "" : ` ${INSTEAD[service.id]}`}` };
|
|
290
|
-
const body = await registered.json();
|
|
291
|
-
|
|
294
|
+
const body = readRegistration(await registered.json());
|
|
295
|
+
// A `null` answer that says it registered is no answer, as it always read.
|
|
296
|
+
if (registered.ok && body === null)
|
|
297
|
+
return { ok: false, said: `${service.label} couldn't be reached.` };
|
|
298
|
+
if (!registered.ok || body?.client_id === undefined)
|
|
292
299
|
return { ok: false, said: `${service.label} didn't let Toolroll register (HTTP ${registered.status}).` };
|
|
293
300
|
clientId = body.client_id;
|
|
294
|
-
clientSecret =
|
|
301
|
+
clientSecret = body.client_secret ?? null;
|
|
295
302
|
}
|
|
296
303
|
catch {
|
|
297
304
|
return { ok: false, said: `${service.label} couldn't be reached.` };
|
|
@@ -327,9 +334,9 @@ export function connectedSpec(serviceId) {
|
|
|
327
334
|
secrets: [ACCESS, REFRESH, CLIENT, CLIENT_SECRET, TOKEN_URL, EXPIRES, RESOURCE].map(name => ({ name, optional: name !== ACCESS })), about: `${service.label}: ${service.about} Connected by signing in.` };
|
|
328
335
|
}
|
|
329
336
|
const tokensFrom = (body, now) => ({
|
|
330
|
-
access:
|
|
331
|
-
refresh:
|
|
332
|
-
expires:
|
|
337
|
+
access: body.access_token ?? null,
|
|
338
|
+
refresh: body.refresh_token ?? null,
|
|
339
|
+
expires: body.expires_in === undefined ? null : new Date(now + body.expires_in * 1000).toISOString(),
|
|
333
340
|
});
|
|
334
341
|
/**
|
|
335
342
|
* Finish connecting: trade the code for tokens, add the tool to the project
|
|
@@ -347,9 +354,13 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
347
354
|
try {
|
|
348
355
|
const answer = await fetcher(visit.token, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, signal: AbortSignal.timeout(15_000),
|
|
349
356
|
body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: visit.redirect, client_id: visit.clientId, code_verifier: visit.verifier, resource: visit.resource, ...(visit.clientSecret === null ? {} : { client_secret: visit.clientSecret }) }) });
|
|
350
|
-
|
|
357
|
+
const read = readTokenResponse(await answer.json());
|
|
358
|
+
// An answer of `null` reads as none, like one that isn't JSON.
|
|
359
|
+
if (read === null)
|
|
360
|
+
return { ok: false, said: `${service.label} couldn't be reached to finish the sign-in.` };
|
|
361
|
+
body = read;
|
|
351
362
|
if (!answer.ok)
|
|
352
|
-
return { ok: false, said: `${service.label} didn't finish the sign-in (${
|
|
363
|
+
return { ok: false, said: `${service.label} didn't finish the sign-in (${body.error ?? `HTTP ${answer.status}`}).` };
|
|
353
364
|
}
|
|
354
365
|
catch {
|
|
355
366
|
return { ok: false, said: `${service.label} couldn't be reached to finish the sign-in.` };
|
|
@@ -363,7 +374,7 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
363
374
|
return { ok: false, said: added.message };
|
|
364
375
|
}
|
|
365
376
|
// The grant: what the answer says, or what was asked for when it says nothing; never one from an earlier sign-in.
|
|
366
|
-
const granted =
|
|
377
|
+
const granted = body.scopeSaid ? scopeList(body.scope) : scopeList(visit.scope ?? null);
|
|
367
378
|
setToolSecrets(visit.repo, spec.name, { [ACCESS]: tokens.access, [CLIENT]: visit.clientId, [TOKEN_URL]: visit.token, [RESOURCE]: visit.resource, [GRANTED]: granted?.join(" ") ?? "",
|
|
368
379
|
...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }), ...(visit.clientSecret === null ? {} : { [CLIENT_SECRET]: visit.clientSecret }) }, home);
|
|
369
380
|
// The success line names the project it went to: the page and chat may be looking at another.
|
|
@@ -394,7 +405,11 @@ export async function refreshConnections(store, repos, now, options = {}) {
|
|
|
394
405
|
try {
|
|
395
406
|
const answer = await fetcher(values[TOKEN_URL], { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, signal: AbortSignal.timeout(15_000),
|
|
396
407
|
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: values[REFRESH], client_id: values[CLIENT], ...(values[CLIENT_SECRET] === undefined ? {} : { client_secret: values[CLIENT_SECRET] }), ...(values[RESOURCE] === undefined ? {} : { resource: values[RESOURCE] }) }) });
|
|
397
|
-
const body = await answer.json();
|
|
408
|
+
const body = readTokenResponse(await answer.json());
|
|
409
|
+
if (body === null) {
|
|
410
|
+
report.problems.push(`${tool.name}: couldn't reach it to renew its sign-in`);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
398
413
|
const tokens = tokensFrom(body, now.getTime());
|
|
399
414
|
if (!answer.ok || tokens.access === null) {
|
|
400
415
|
report.problems.push(`${tool.name}: its sign-in ran out; connect it again on the Tools page`);
|
|
@@ -402,7 +417,7 @@ export async function refreshConnections(store, repos, now, options = {}) {
|
|
|
402
417
|
}
|
|
403
418
|
// A refresh carries the same grant or a narrower one, and says so when it changes; said badly, it is unknown.
|
|
404
419
|
setToolSecrets(repo, tool.name, { [ACCESS]: tokens.access, ...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }),
|
|
405
|
-
...(
|
|
420
|
+
...(body.scopeSaid ? { [GRANTED]: scopeList(body.scope)?.join(" ") ?? "" } : {}) }, home);
|
|
406
421
|
renewTried.delete(key);
|
|
407
422
|
report.refreshed++;
|
|
408
423
|
}
|
package/dist/model-catalog.js
CHANGED
|
@@ -11,6 +11,7 @@ import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "no
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { delimiter, join } from "node:path";
|
|
13
13
|
import { fetchOpenRouterModels } from "./openrouter-models.js";
|
|
14
|
+
import { codexModelSchema, codexModelsCacheSchema, npmLatestSchema } from "./contracts/model-catalog.js";
|
|
14
15
|
import { readCappedBody } from "./converse.js";
|
|
15
16
|
import { validModelId } from "./provider.js";
|
|
16
17
|
import { activeUpdateWork } from "./desktop-update-gate.js";
|
|
@@ -73,10 +74,11 @@ export function codexCatalog(home = homedir()) {
|
|
|
73
74
|
const path = join(home, ".codex", "models_cache.json");
|
|
74
75
|
if (lstatSync(path).size >= 2_000_000)
|
|
75
76
|
return [];
|
|
76
|
-
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
77
|
-
return (
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
const data = codexModelsCacheSchema.safeParse(JSON.parse(readFileSync(path, "utf8")));
|
|
78
|
+
return (data.success ? data.data.models : []).flatMap(entry => {
|
|
79
|
+
const one = codexModelSchema.safeParse(entry);
|
|
80
|
+
return one.success ? [one.data] : [];
|
|
81
|
+
}).slice(0, 30).map(one => ({ id: one.slug, name: typeof one.display_name === "string" ? one.display_name.slice(0, 80) : one.slug }));
|
|
80
82
|
}
|
|
81
83
|
catch {
|
|
82
84
|
return [];
|
|
@@ -255,8 +257,8 @@ async function latestVersion(pkg, fetcher) {
|
|
|
255
257
|
const body = await readCappedBody(response, 2_000_000);
|
|
256
258
|
if (body === null)
|
|
257
259
|
return null;
|
|
258
|
-
const
|
|
259
|
-
return
|
|
260
|
+
const latest = npmLatestSchema.safeParse(JSON.parse(new TextDecoder().decode(body)));
|
|
261
|
+
return latest.success ? latest.data.version : null;
|
|
260
262
|
}
|
|
261
263
|
catch {
|
|
262
264
|
return null;
|
package/dist/observations.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Runner } from "./builder.js";
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
|
+
import { type ObservationCase } from "./contracts/observation-request.js";
|
|
3
4
|
export declare const OBSERVATION_MAILBOX = "STANDING-ORDERS-OBSERVATIONS.json";
|
|
4
5
|
export declare const OBSERVATION_CAPTURE = "machine focused observations v1";
|
|
5
6
|
export declare const OBSERVATION_FILE = "REVIEW-OBSERVATIONS.json";
|
|
@@ -18,12 +19,7 @@ export type ObservationBrief = {
|
|
|
18
19
|
gateDigest: string;
|
|
19
20
|
};
|
|
20
21
|
export declare function focusedTestCommandSupported(command: string): boolean;
|
|
21
|
-
export type ObservationCase
|
|
22
|
-
criterion: string;
|
|
23
|
-
at: "base" | "head";
|
|
24
|
-
testPath: string;
|
|
25
|
-
testName: string;
|
|
26
|
-
};
|
|
22
|
+
export type { ObservationCase };
|
|
27
23
|
export declare function originalTaskBase(store: Store, taskId: string): string | null;
|
|
28
24
|
/** The existing sealed revision brief identifies this narrower kind of work.
|
|
29
25
|
* A title, agent-authored mailbox, or handoff can never elect gate reuse. */
|
package/dist/observations.js
CHANGED
|
@@ -6,6 +6,7 @@ import { dirname, join } from "node:path";
|
|
|
6
6
|
import { stripVTControlCharacters } from "node:util";
|
|
7
7
|
import { canonicalAcceptance } from "./scope.js";
|
|
8
8
|
import { readVerifiedArtifact, scanForSecrets, storeEvidence } from "./evidence.js";
|
|
9
|
+
import { readObservationRequest } from "./contracts/observation-request.js";
|
|
9
10
|
export const OBSERVATION_MAILBOX = "STANDING-ORDERS-OBSERVATIONS.json";
|
|
10
11
|
export const OBSERVATION_CAPTURE = "machine focused observations v1";
|
|
11
12
|
export const OBSERVATION_FILE = "REVIEW-OBSERVATIONS.json";
|
|
@@ -62,17 +63,20 @@ export function parseObservationCases(raw, ids) {
|
|
|
62
63
|
if (Buffer.byteLength(raw) > 16 * 1024)
|
|
63
64
|
throw Error("The observation request is too large.");
|
|
64
65
|
const parsed = JSON.parse(raw);
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const read = readObservationRequest(parsed);
|
|
67
|
+
// A problem inside one observation is that observation's; anything else is the request's shape.
|
|
68
|
+
if (!read.ok && read.issues.some(issue => !/^observations\[\d+\]/.test(issue.path)))
|
|
67
69
|
throw Error("Use one to four focused test observations.");
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
const bad = () => Error("An observation must name a requested criterion, base/head, one test file and one test name. Shell commands are not accepted.");
|
|
71
|
+
if (!read.ok)
|
|
72
|
+
throw bad();
|
|
73
|
+
for (const item of read.value.observations) {
|
|
74
|
+
if (!ids.includes(item.criterion) || !pathOkay(item.testPath) || !/\.(test|spec)\.[cm]?[jt]sx?$/.test(item.testPath) ||
|
|
75
|
+
item.testName.trim() === "" || item.testName.length > 500 || /[\u0000-\u001f\u007f]/.test(item.testName))
|
|
76
|
+
throw bad();
|
|
75
77
|
}
|
|
78
|
+
// The cases as written: their key order is the receipt's, and two written in different orders are distinct, as before.
|
|
79
|
+
const cases = parsed.observations;
|
|
76
80
|
if (new Set(cases.map(c => JSON.stringify(c))).size !== cases.length || ids.some(id => !cases.some(c => c.criterion === id)))
|
|
77
81
|
throw Error("Every requested criterion needs a distinct observation.");
|
|
78
82
|
return cases;
|
package/dist/operate.js
CHANGED
|
@@ -98,7 +98,8 @@ import { admissionRecorded, admitProject, commandLineActor } from "./project-adm
|
|
|
98
98
|
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
99
99
|
import { createServer as createNetServer } from "node:net";
|
|
100
100
|
import { spawn as spawnChild } from "node:child_process";
|
|
101
|
-
|
|
101
|
+
// Every envelope is checked against its command's schema (logged, never refused), then serialized unchanged.
|
|
102
|
+
import { checkedEnvelopeJson as envelopeJson } from "./contracts/cli.js";
|
|
102
103
|
import { hasDisguisedText, hasForbiddenControls, validateNote } from "./decision.js";
|
|
103
104
|
import { readVerifiedArtifact, readVerifiedReport, storeEvidence } from "./evidence.js";
|
|
104
105
|
import { contractChangesOf, decodePlanContractRecord, describeContractChanges, encodePlannerSource, plannerSourceOf } from "./planner-source.js";
|
|
@@ -203,6 +204,7 @@ import { readPulls } from "./pulls.js";
|
|
|
203
204
|
import { startMaintenance } from "./maintenance.js";
|
|
204
205
|
import { livePin, modelWatchPass } from "./model-catalog.js";
|
|
205
206
|
import { runModelsCommand } from "./models-cli.js";
|
|
207
|
+
import { ALL_PROJECTS, chatApprovalSettings, chatApprovalWords, setChatApproval } from "./chat-approval.js";
|
|
206
208
|
import { updateAdmissionPaused, UPDATE_PAUSED } from "./desktop-update-gate.js";
|
|
207
209
|
import { beads } from "./beads.js";
|
|
208
210
|
import { githubIssues } from "./issues.js";
|
|
@@ -469,6 +471,8 @@ Flows — processes cards move through; the console's rules
|
|
|
469
471
|
toolroll flows script save --repo <path> --name <name> (--file <path in project> | --body <file>)
|
|
470
472
|
--about "<one line>" [--language shell|python|node] [--timeout-minutes <n>]
|
|
471
473
|
toolroll flows card add <id> --title <t> [--description <d>] [--zone <zone>]
|
|
474
|
+
toolroll flows card approve <id> <card> [--option <words|number>]
|
|
475
|
+
toolroll flows card send-back <id> <card> --note <text>
|
|
472
476
|
toolroll flows archive <id>
|
|
473
477
|
writes take --as <you> --token <t> (or the remembered login); create,
|
|
474
478
|
edit, archive and trigger add preview until --yes
|
|
@@ -638,7 +642,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
638
642
|
"max", "cap", "probe", "kind", "expires", "cmd", "since", "repair-model",
|
|
639
643
|
"choose", "note", "max-open-decisions", "max-held-sessions", "name", "days", "publication", "auto-approve", "review-auto", "entries", "port", "host", "allow-host",
|
|
640
644
|
"for", "tick-every", "bridge-every", "reconcile-every", "incarnation",
|
|
641
|
-
"say", "ceiling-usd",
|
|
645
|
+
"say", "ceiling-usd", "cap-usd",
|
|
642
646
|
"token-file", "bin", "poll", "github", "remote", "head-prefix", "password",
|
|
643
647
|
// v102: project rules.
|
|
644
648
|
"not-requester", "protect", "protect-paths",
|
|
@@ -657,7 +661,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
657
661
|
// onboard: the starter flows to switch on.
|
|
658
662
|
"starter",
|
|
659
663
|
// flows: templates, steps, scripts and cards.
|
|
660
|
-
"template", "steps", "about", "language", "timeout-minutes", "body", "description", "zone",
|
|
664
|
+
"template", "steps", "about", "language", "timeout-minutes", "body", "description", "zone", "option",
|
|
661
665
|
"token-env", "after", "repair-max-attempts", "consumer", "batch", "feedback", "source", "view", "cursor", "why", "supersedes", "decision", "sessions", "timeout",
|
|
662
666
|
// pings follow responsibility: replacements.
|
|
663
667
|
"replaced-by", "replaces",
|
|
@@ -670,7 +674,7 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
670
674
|
"json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file", "allow-paid-fallback",
|
|
671
675
|
"clear", "follow", "ready", "all-tasks", "inbound-only", "help", "undo", "anyone", "allow-dispatch", "allow-merge", "merge-delete-branch",
|
|
672
676
|
"no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
|
|
673
|
-
"self-heal", "plan-auto", "chat-approve", "repair-auto", "review-retry-auto", "no-local",
|
|
677
|
+
"self-heal", "plan-auto", "chat-approve", "full-access", "repair-auto", "review-retry-auto", "no-local",
|
|
674
678
|
"html", "csv", "alerts-only",
|
|
675
679
|
// v105: the full export.
|
|
676
680
|
"zip",
|
|
@@ -1087,6 +1091,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1087
1091
|
return proposalsCommand(positional, flags, context);
|
|
1088
1092
|
case "mode":
|
|
1089
1093
|
return modeCommand(positional, flags, context);
|
|
1094
|
+
case "chat-approval":
|
|
1095
|
+
return chatApprovalCommand(positional, flags, context);
|
|
1090
1096
|
case "people":
|
|
1091
1097
|
return peopleCommand(positional, flags, context);
|
|
1092
1098
|
case "keys":
|
|
@@ -5754,6 +5760,51 @@ async function peopleCommand(positional, flags, context) {
|
|
|
5754
5760
|
]);
|
|
5755
5761
|
}
|
|
5756
5762
|
const MODE_ACTIONS = ["set", "show", "revoke"];
|
|
5763
|
+
/** `chat-approval show | on | off [--repo <path>]`: the person's lasting choice to approve plans and merges from their
|
|
5764
|
+
* paired chat (chat-approval.ts), for all their projects or one. On (or wider limits) previews until --yes and takes
|
|
5765
|
+
* the password, like Settings; off is one step. Either way a ledger line. */
|
|
5766
|
+
async function chatApprovalCommand(positional, flags, context) {
|
|
5767
|
+
const { store, write, json, clock } = context;
|
|
5768
|
+
const action = positional[0] ?? "show";
|
|
5769
|
+
const command = `chat-approval ${action}`;
|
|
5770
|
+
if (!["show", "on", "off"].includes(action))
|
|
5771
|
+
return fail(write, json, "chat-approval", "usage", "Use chat-approval show | on | off [--repo <path>] [--full-access] [--cap-usd <n>] [--yes].", EXIT.usage);
|
|
5772
|
+
const acting = await askCredentials(flags, context);
|
|
5773
|
+
if (acting === null)
|
|
5774
|
+
return fail(write, json, command, "unauthenticated", "Approving from chat is your own setting: pass --as <you> --token <your password> (or sign in once with toolroll up).", EXIT.refused);
|
|
5775
|
+
const authed = authenticateApprover(store, acting.name, acting.token);
|
|
5776
|
+
if (!authed.ok)
|
|
5777
|
+
return fail(write, json, command, "unauthenticated", "That name and password aren't an approver's. Nothing was changed.", EXIT.refused);
|
|
5778
|
+
const given = text(flags, "repo");
|
|
5779
|
+
const scope = given === undefined ? ALL_PROJECTS : canonicalProject(given) ?? resolve(given);
|
|
5780
|
+
if (scope !== ALL_PROJECTS && !store.accountCanAccess(acting.name, scope))
|
|
5781
|
+
return fail(write, json, command, "unknown-project", "That isn't one of your projects.", EXIT.refused);
|
|
5782
|
+
const where = scope === ALL_PROJECTS ? "all your projects" : projectName(scope);
|
|
5783
|
+
if (action === "show") {
|
|
5784
|
+
const rows = chatApprovalSettings(store, acting.name).map(one => ({ scope: one.scope === ALL_PROJECTS ? "all" : one.scope, enabled: one.enabled, fullAccess: one.fullAccess, capMicrousd: one.capMicrousd, updatedAt: one.updatedAt }));
|
|
5785
|
+
return succeed(write, json, command, { settings: rows }, () => rows.length === 0 ? ["Approving from chat is off everywhere. Turn it on: toolroll chat-approval on [--repo <path>]"]
|
|
5786
|
+
: rows.map(one => `${one.scope === "all" ? "All projects" : projectName(one.scope)}: ${one.enabled ? `on · ${chatApprovalWords(one)}` : "off"}`));
|
|
5787
|
+
}
|
|
5788
|
+
const now = clock();
|
|
5789
|
+
if (action === "off") {
|
|
5790
|
+
const saved = setChatApproval(store, { approver: acting.name, scope, enabled: false, via: "the command line" }, now);
|
|
5791
|
+
return saved.ok ? succeed(write, json, command, { applied: true, scope: scope === ALL_PROJECTS ? "all" : scope }, () => [saved.said]) : fail(write, json, command, "refused", saved.message, EXIT.refused);
|
|
5792
|
+
}
|
|
5793
|
+
const usd = text(flags, "cap-usd");
|
|
5794
|
+
const cap = usd === undefined ? null : Number(usd);
|
|
5795
|
+
if (cap !== null && !(Number.isFinite(cap) && cap >= 0 && cap <= 10_000))
|
|
5796
|
+
return fail(write, json, command, "usage", "Give --cap-usd in dollars, like 5.", EXIT.usage);
|
|
5797
|
+
const limits = { fullAccess: flag(flags, "full-access"), capMicrousd: cap === null ? null : Math.round(cap * 1_000_000) };
|
|
5798
|
+
const terms = [
|
|
5799
|
+
`Your own taps in your paired chat approve plans and merges in ${where}, until you turn it off: it doesn't expire.`,
|
|
5800
|
+
`Limits: ${chatApprovalWords(limits)}.`,
|
|
5801
|
+
"Plans written for you, protected paths, two-person rules and your organisation's policy still open in Toolroll.",
|
|
5802
|
+
];
|
|
5803
|
+
if (!flag(flags, "yes"))
|
|
5804
|
+
return succeed(write, json, command, { applied: false, scope: scope === ALL_PROJECTS ? "all" : scope, terms }, () => [`Turn on approving from chat for ${where}`, ...terms.map(one => ` ${one}`), "", "Nothing has changed. Add --yes to make this change."]);
|
|
5805
|
+
const saved = setChatApproval(store, { approver: acting.name, scope, enabled: true, limits, via: "the command line" }, now);
|
|
5806
|
+
return saved.ok ? succeed(write, json, command, { applied: true, scope: scope === ALL_PROJECTS ? "all" : scope, terms }, () => [saved.said]) : fail(write, json, command, "refused", saved.message, EXIT.refused);
|
|
5807
|
+
}
|
|
5757
5808
|
async function modeCommand(positional, flags, context) {
|
|
5758
5809
|
const { store, write, json, clock } = context;
|
|
5759
5810
|
const [action] = positional;
|
|
@@ -50,7 +50,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
|
|
|
50
50
|
digest: string;
|
|
51
51
|
approvedAt: string;
|
|
52
52
|
approvedBy: string;
|
|
53
|
-
approvalBasis: "mode" | "password";
|
|
53
|
+
approvalBasis: "chat" | "mode" | "password";
|
|
54
54
|
modeDigest: string | null;
|
|
55
55
|
};
|
|
56
56
|
readonly command: {
|
|
@@ -1,21 +1,11 @@
|
|
|
1
|
+
import { type DarwinProcessIdentity } from "./contracts/native-census.js";
|
|
1
2
|
export type PreparedDarwinProcessCensus = {
|
|
2
3
|
sourcePath: string;
|
|
3
4
|
executablePath: string;
|
|
4
5
|
sourceSha256: string;
|
|
5
6
|
executableSha256: string;
|
|
6
7
|
};
|
|
7
|
-
export type DarwinProcessIdentity
|
|
8
|
-
pid: number;
|
|
9
|
-
ppid: number | null;
|
|
10
|
-
uid: number | null;
|
|
11
|
-
birthMs: number | null;
|
|
12
|
-
uniqueId: string | null;
|
|
13
|
-
parentUniqueId: string | null;
|
|
14
|
-
traced: boolean | null;
|
|
15
|
-
executable: string | null;
|
|
16
|
-
originalParentVersion: number | null;
|
|
17
|
-
pidVersion?: number;
|
|
18
|
-
};
|
|
8
|
+
export type { DarwinProcessIdentity } from "./contracts/native-census.js";
|
|
19
9
|
export type DarwinManagedService = {
|
|
20
10
|
domain: string;
|
|
21
11
|
label: string;
|
|
@@ -169,4 +159,3 @@ export declare function collectDarwinCoalitionSnapshot(args: {
|
|
|
169
159
|
maxAttempts?: number;
|
|
170
160
|
anchorPids?: number[];
|
|
171
161
|
}): Promise<DarwinCoalitionRecoverySnapshot>;
|
|
172
|
-
export {};
|
|
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { hostname, release } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { darwinNativeCensusSchema, darwinProcessIdentitySchema, kernelIdInRange } from "./contracts/native-census.js";
|
|
6
7
|
const digest = (data) => createHash("sha256").update(data).digest("hex");
|
|
7
8
|
const cleanEnv = { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", LC_ALL: "C", TZ: "UTC" };
|
|
8
9
|
/** Facts only. Public KERN_PROC_ALL supplies birth/UID/trace metadata without
|
|
@@ -189,15 +190,17 @@ const nullable = (value, test) => value === null || test(value);
|
|
|
189
190
|
const safeText = (value) => typeof value === "string" && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
|
|
190
191
|
/** Strict parser also strips every field not in the facts contract. */
|
|
191
192
|
export function parseDarwinNativeSnapshot(text) {
|
|
192
|
-
const
|
|
193
|
-
if (!
|
|
193
|
+
const read = darwinNativeCensusSchema.safeParse(JSON.parse(text));
|
|
194
|
+
if (!read.success)
|
|
194
195
|
throw new Error("malformed-native-census");
|
|
196
|
+
const v = read.data;
|
|
195
197
|
const pids = new Set(), ids = new Set();
|
|
196
198
|
const processes = v.processes.map((r) => {
|
|
197
|
-
|
|
199
|
+
const row = darwinProcessIdentitySchema.safeParse(r);
|
|
200
|
+
if (!row.success)
|
|
198
201
|
throw new Error("malformed-process-identity");
|
|
199
|
-
const p =
|
|
200
|
-
if (
|
|
202
|
+
const p = row.data;
|
|
203
|
+
if (pids.has(p.pid) || (p.uniqueId !== null && !kernelIdInRange(p.uniqueId)) || p.uniqueId === "0" || (p.uniqueId !== null && ids.has(p.uniqueId)) || (p.parentUniqueId !== null && !kernelIdInRange(p.parentUniqueId)))
|
|
201
204
|
throw new Error("malformed-process-identity");
|
|
202
205
|
pids.add(p.pid);
|
|
203
206
|
if (p.uniqueId !== null)
|
package/dist/project-tools.d.ts
CHANGED
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
import type { Store, ToolActionInfo } from "./store.js";
|
|
2
|
-
|
|
3
|
-
export type ToolSecret
|
|
4
|
-
name: string;
|
|
5
|
-
optional: boolean;
|
|
6
|
-
};
|
|
7
|
-
export type ToolSpec = {
|
|
8
|
-
name: string;
|
|
9
|
-
transport: "stdio" | "http";
|
|
10
|
-
/** stdio: the program and its arguments (no secrets in either). */
|
|
11
|
-
command: string | null;
|
|
12
|
-
args: string[];
|
|
13
|
-
/** http: the server's address. */
|
|
14
|
-
url: string | null;
|
|
15
|
-
/** Every secret the tool needs. stdio: each is an env variable of the server process. */
|
|
16
|
-
secrets: ToolSecret[];
|
|
17
|
-
/** http: the secret sent as `Authorization: Bearer <value>`. */
|
|
18
|
-
bearer: string | null;
|
|
19
|
-
/** http: header name → the secret sent as its raw value. */
|
|
20
|
-
headerSecrets: Record<string, string>;
|
|
21
|
-
/** What it does, in plain words. */
|
|
22
|
-
about: string;
|
|
23
|
-
};
|
|
2
|
+
import { type ToolSecret, type ToolSpec } from "./contracts/integration-metadata.js";
|
|
3
|
+
export type { ToolSecret, ToolSpec };
|
|
24
4
|
export type ToolTest = {
|
|
25
5
|
at: string;
|
|
26
6
|
ok: boolean;
|
|
@@ -68,7 +48,10 @@ export declare function catalogTool(id: string): (ToolSpec & {
|
|
|
68
48
|
}) | null;
|
|
69
49
|
/** What a build is held to: how the server starts and which secrets it names — never their values or its description. */
|
|
70
50
|
export declare function toolDigest(spec: ToolSpec): string;
|
|
71
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* A tool definition from any source (catalog, console form, the lead, an import, a saved row), checked whole and made
|
|
53
|
+
* the spec its schema (src/contracts/integration-metadata.ts) says. Throws in plain words.
|
|
54
|
+
*/
|
|
72
55
|
export declare function validateToolSpec(input: Record<string, unknown>): ToolSpec;
|
|
73
56
|
/** How the tool starts, in one line a person can check. Never a secret value. */
|
|
74
57
|
export declare function toolCommandLine(spec: ToolSpec): string;
|
|
@@ -234,4 +217,3 @@ export declare function toolStanding(tool: ProjectTool, secretsSet: readonly str
|
|
|
234
217
|
export declare function secretsSetFor(repo: string, tool: ToolSpec, home?: string): string[];
|
|
235
218
|
/** A command line split into program and arguments, honouring "double" and 'single' quotes. */
|
|
236
219
|
export declare function splitCommandLine(line: string): string[];
|
|
237
|
-
export {};
|