toolroll 0.9.50 → 0.9.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/backup.d.ts +8 -0
  3. package/dist/backup.js +10 -1
  4. package/dist/browser/workspace.css +1 -1
  5. package/dist/browser/workspace.js +1 -1
  6. package/dist/browser-workspace.d.ts +1 -0
  7. package/dist/database-reclaim.d.ts +4 -0
  8. package/dist/database-reclaim.js +13 -0
  9. package/dist/demo.d.ts +1 -1
  10. package/dist/demo.js +1 -1
  11. package/dist/desktop-update.d.ts +1 -1
  12. package/dist/live-bus.d.ts +3 -3
  13. package/dist/live-bus.js +3 -3
  14. package/dist/mcp-oauth.d.ts +1 -1
  15. package/dist/mcp-oauth.js +3 -2
  16. package/dist/monitoring.js +32 -8
  17. package/dist/notification-delivery.d.ts +19 -0
  18. package/dist/notification-delivery.js +37 -0
  19. package/dist/operate-remote-arguments.js +1 -1
  20. package/dist/operate-remote.js +2 -2
  21. package/dist/operate.d.ts +1 -1
  22. package/dist/operate.js +54 -120
  23. package/dist/process-recovery-settlement.js +1 -0
  24. package/dist/request-budget.d.ts +5 -2
  25. package/dist/request-budget.js +7 -3
  26. package/dist/retention.js +6 -5
  27. package/dist/scope.d.ts +1 -0
  28. package/dist/scope.js +19 -9
  29. package/dist/serve.js +15 -10
  30. package/dist/server/people-tokens.js +1 -2
  31. package/dist/server/remote.js +19 -4
  32. package/dist/server/settings.js +3 -1
  33. package/dist/server/shared.d.ts +1 -0
  34. package/dist/server/shared.js +2 -0
  35. package/dist/sign-in-guard.d.ts +41 -16
  36. package/dist/sign-in-guard.js +147 -24
  37. package/dist/source-key.d.ts +2 -0
  38. package/dist/source-key.js +49 -0
  39. package/dist/store.d.ts +38 -43
  40. package/dist/store.js +255 -134
  41. package/dist/surface.js +1 -2
  42. package/dist/team-http.d.ts +3 -0
  43. package/dist/team-http.js +3 -0
  44. package/dist/toolroll-update.d.ts +14 -3
  45. package/dist/toolroll-update.js +105 -14
  46. package/dist/webhooks.d.ts +10 -22
  47. package/dist/webhooks.js +43 -81
  48. package/dist/work-index.js +2 -2
  49. package/dist/workspace-revision.d.ts +28 -1
  50. package/dist/workspace-revision.js +276 -49
  51. package/package.json +1 -3
@@ -227,6 +227,7 @@ export type BrowserSettingsView = {
227
227
  mode: 'subscription' | 'api-key';
228
228
  set: boolean;
229
229
  }[] | null;
230
+ legacyWebhookWarning?: string;
230
231
  services: {
231
232
  configured: string[];
232
233
  channel: string | null;
@@ -0,0 +1,4 @@
1
+ import type { Database } from "./store.js";
2
+ /** Reclaim whole free pages on databases created without auto_vacuum. Call only after committing writes.
3
+ * A migration needs at least 1 MiB to justify the I/O; retention reclaims whenever more than 25% is free. */
4
+ export declare function reclaimDatabase(db: Database, reason: "migration" | "retention"): boolean;
@@ -0,0 +1,13 @@
1
+ /** Reclaim whole free pages on databases created without auto_vacuum. Call only after committing writes.
2
+ * A migration needs at least 1 MiB to justify the I/O; retention reclaims whenever more than 25% is free. */
3
+ export function reclaimDatabase(db, reason) {
4
+ const free = Number(db.prepare("PRAGMA freelist_count").get()["freelist_count"]);
5
+ const pages = Number(db.prepare("PRAGMA page_count").get()["page_count"]);
6
+ const size = Number(db.prepare("PRAGMA page_size").get()["page_size"]);
7
+ if (reason === "migration" ? free * size < 1024 * 1024 : free * 4 <= pages)
8
+ return false;
9
+ db.exec("VACUUM");
10
+ // VACUUM writes through WAL too: checkpoint so the main file (and its backups) shrinks now.
11
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
12
+ return true;
13
+ }
package/dist/demo.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * - The database is stamped `demo` BEFORE any row exists — an append-only
9
9
  * installation fact with no unset API. Every spending or external-effect
10
10
  * command (tick, watch, build, publish, reconcile, daemon, bridge,
11
- * outbox deliver, intake) fails closed on the stamp, so a kept sandbox
11
+ * intake) fails closed on the stamp, so a kept sandbox
12
12
  * can never be mistaken for real work by a worker pointed at it later.
13
13
  * The console banner is decoration; the fence is enforcement.
14
14
  * - The seeded history is SYNTHETIC and does not pretend otherwise: runs
package/dist/demo.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * - The database is stamped `demo` BEFORE any row exists — an append-only
9
9
  * installation fact with no unset API. Every spending or external-effect
10
10
  * command (tick, watch, build, publish, reconcile, daemon, bridge,
11
- * outbox deliver, intake) fails closed on the stamp, so a kept sandbox
11
+ * intake) fails closed on the stamp, so a kept sandbox
12
12
  * can never be mistaken for real work by a worker pointed at it later.
13
13
  * The console banner is decoration; the fence is enforcement.
14
14
  * - The seeded history is SYNTHETIC and does not pretend otherwise: runs
@@ -94,13 +94,13 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
94
94
  export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
95
95
  export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
96
96
  export declare function desktopUpdateStatus(stateDir: string): {
97
- error?: never;
98
97
  active: boolean;
99
98
  phase: string;
100
99
  detail: string;
101
100
  running: boolean;
102
101
  canResume: boolean;
103
102
  canCancel: boolean;
103
+ error?: never;
104
104
  automaticRecovery?: never;
105
105
  recoveryAttempts?: never;
106
106
  recoveryRepairs?: never;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Live views push on write. Nothing here polls once a second.
3
3
  *
4
- * - The bus. The database's own revision triggers (workspace-revision.ts)
5
- * move one counter on every meaningful write. After each in-process
4
+ * - The bus. Every connection's write wrapper (workspace-revision.ts)
5
+ * moves one counter per committed meaningful write. After each in-process
6
6
  * transact() COMMIT, and whenever another process (the worker, the CLI)
7
7
  * writes the database's write-ahead log, the revision is read once; when
8
8
  * it moved, every subscriber hears `{ revision }`. A rolled-back or quiet
@@ -20,7 +20,7 @@
20
20
  * gets one `reload` and ends so its EventSource can reconnect.
21
21
  *
22
22
  * Transport seam: a future WebSocket (live cursors) subscribes to the same
23
- * LiveBus beside these SSE rooms; nothing in the Store or the triggers changes.
23
+ * LiveBus beside these SSE rooms; nothing in the Store or its write wrapper changes.
24
24
  */
25
25
  import { type FSWatcher } from "node:fs";
26
26
  import type { ServerResponse } from "node:http";
package/dist/live-bus.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Live views push on write. Nothing here polls once a second.
3
3
  *
4
- * - The bus. The database's own revision triggers (workspace-revision.ts)
5
- * move one counter on every meaningful write. After each in-process
4
+ * - The bus. Every connection's write wrapper (workspace-revision.ts)
5
+ * moves one counter per committed meaningful write. After each in-process
6
6
  * transact() COMMIT, and whenever another process (the worker, the CLI)
7
7
  * writes the database's write-ahead log, the revision is read once; when
8
8
  * it moved, every subscriber hears `{ revision }`. A rolled-back or quiet
@@ -20,7 +20,7 @@
20
20
  * gets one `reload` and ends so its EventSource can reconnect.
21
21
  *
22
22
  * Transport seam: a future WebSocket (live cursors) subscribes to the same
23
- * LiveBus beside these SSE rooms; nothing in the Store or the triggers changes.
23
+ * LiveBus beside these SSE rooms; nothing in the Store or its write wrapper changes.
24
24
  */
25
25
  import { watch } from "node:fs";
26
26
  import { WORKSPACE_REVISION_KEY } from "./workspace-revision.js";
@@ -62,7 +62,7 @@ export type OAuthHttpOptions = {
62
62
  clock: () => Date;
63
63
  /** This console's canonical origin for the request (the https public address, or this computer's loopback one); null: no sign-in here. */
64
64
  originOf: (request: IncomingMessage) => string | null;
65
- /** Trusted direct peer, or the last hop appended by the console's loopback proxy. */
65
+ /** Trusted direct peer, or the last hop appended by the console's loopback proxy (native IPv6 counted by its /64). */
66
66
  requesterKey: (request: IncomingMessage) => string;
67
67
  /** The browser session signed in by cookie, or null. A bearer never consents. */
68
68
  session: (request: IncomingMessage) => OAuthSession | null;
package/dist/mcp-oauth.js CHANGED
@@ -2,6 +2,7 @@ import { adapterPolicy } from "./server/route-policy.js";
2
2
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
3
3
  import { hashSecret, mintApiToken } from "./api-tokens.js";
4
4
  import { limitWords } from "./request-budget.js";
5
+ import { sourceKey } from "./source-key.js";
5
6
  import { PKCE_CHALLENGE, readOAuthRegistration, readOAuthTokenRequest } from "./contracts/mcp-oauth.js";
6
7
  /** How long each piece lives. A code is spent at once; a grant ends after 30 days and the person consents again. */
7
8
  export const OAUTH_TIMES = { requestMs: 10 * 60_000, codeMs: 60_000, accessSeconds: 3600, grantDays: 30 };
@@ -176,7 +177,7 @@ export function createOAuthHttp(options) {
176
177
  const name = clean(read.value.client_name ?? "") || "MCP client";
177
178
  const now = options.clock();
178
179
  prune(now.getTime());
179
- const kept = store.registerOAuthClient({ id: randomBytes(16).toString("hex"), name, redirectUris: redirects, source: sha256(options.requesterKey(request)) }, now, OAUTH_LIMITS.clients, OAUTH_LIMITS.clientsPerSource, [...waiting.values()].map(one => one.client));
180
+ const kept = store.registerOAuthClient({ id: randomBytes(16).toString("hex"), name, redirectUris: redirects, source: sha256(sourceKey(options.requesterKey(request))) }, now, OAUTH_LIMITS.clients, OAUTH_LIMITS.clientsPerSource, [...waiting.values()].map(one => one.client));
180
181
  if (kept === null) {
181
182
  response.setHeader("retry-after", "600");
182
183
  return oauthError(response, 429, "temporarily_unavailable", "Too many clients are waiting to connect. Try again later.");
@@ -214,7 +215,7 @@ export function createOAuthHttp(options) {
214
215
  const scopes = (params.get("scope") ?? "").split(" ").filter(one => one !== "");
215
216
  if (!scopes.every(one => OAUTH_SCOPES.includes(one)))
216
217
  return fail("The client requested access Toolroll doesn't offer.");
217
- const source = sha256(options.requesterKey(request));
218
+ const source = sha256(sourceKey(options.requesterKey(request)));
218
219
  if (waiting.size >= OAUTH_LIMITS.requests || [...waiting.values()].filter(one => one.source === source).length >= OAUTH_LIMITS.requestsPerSource) {
219
220
  response.setHeader("retry-after", "600");
220
221
  return problemPage(response, 429, "Too many sign-ins waiting", "Try again in ten minutes from your MCP client.");
@@ -24,6 +24,7 @@ export const SIGNATURE_HEADER = "x-standing-orders-signature";
24
24
  const BATCH = 200;
25
25
  const LEASE_MS = 60_000;
26
26
  const TIMEOUT_MS = 10_000;
27
+ const CHECKPOINT_EVERY_MS = 60 * 60_000;
27
28
  /** An audit event: the ledger entry and its seal, as the stream sends it. */
28
29
  export function auditEvent(entry, instance) {
29
30
  return {
@@ -41,15 +42,34 @@ const nanos = (at) => `${BigInt(Date.parse(at)) * 1000000n}`;
41
42
  /** One pass over every destination that's set up. Returns what it sent, per destination. */
42
43
  export async function monitoringPass(store, settings, deps) {
43
44
  const sent = {};
45
+ const heads = [];
46
+ const audit = (delivered) => { if (delivered.head !== null)
47
+ heads.push(delivered.head); return delivered.count; };
44
48
  // A new destination gets the whole audit history; traces start from now.
45
49
  if (settings.webhook !== null)
46
- sent.webhook = await deliver(store, "webhook", targetOf(settings.webhook.url), () => 0, deps, entries => postSigned(settings.webhook, entries, store, deps));
50
+ sent.webhook = audit(await deliver(store, "webhook", targetOf(settings.webhook.url), () => 0, deps, entries => postSigned(settings.webhook, entries, store, deps)));
47
51
  if (settings.folder !== null)
48
- sent.folder = await deliver(store, "folder", targetOf(settings.folder.path), () => 0, deps, async (entries) => { appendFolder(settings.folder.path, entries, deps.instance); return entries.length; });
52
+ sent.folder = audit(await deliver(store, "folder", targetOf(settings.folder.path), () => 0, deps, async (entries) => { appendFolder(settings.folder.path, entries, deps.instance); return entries.length; }));
49
53
  if (settings.traces !== null)
50
- sent.traces = await deliver(store, "traces", targetOf(settings.traces.endpoint), () => store.ledgerHeadId(), deps, entries => postTraces(settings.traces, entries, store, deps));
54
+ sent.traces = (await deliver(store, "traces", targetOf(settings.traces.endpoint), () => store.ledgerHeadId(), deps, entries => postTraces(settings.traces, entries, store, deps))).count;
55
+ checkpointSent(store, heads, (deps.now ?? (() => new Date()))());
51
56
  return sent;
52
57
  }
58
+ /** The furthest head the audit stream delivered, kept as an automatic checkpoint once the whole chain verifies.
59
+ * Automatic checkpoints write no ledger entry, so recording one gives the stream nothing new to send (and checkpoint)
60
+ * next pass. At most one an hour, so a busy stream neither floods the table nor walks the whole chain every pass. */
61
+ function checkpointSent(store, heads, now) {
62
+ const head = heads.reduce((best, one) => best === null || (one !== null && one.through > best.through) ? one : best, null);
63
+ if (head === null)
64
+ return;
65
+ const newest = store.ledgerCheckpoints(1)[0];
66
+ if (newest !== undefined && (newest.through >= head.through || now.getTime() - Date.parse(newest.at) < CHECKPOINT_EVERY_MS))
67
+ return;
68
+ try {
69
+ store.ledgerCheckpoint("system", now, { head });
70
+ }
71
+ catch { /* a busy database: the next delivery tries again */ }
72
+ }
53
73
  /** A destination's name in its status row: a digest of its address (the address itself may carry a key). */
54
74
  export const targetOf = (address) => hex(`standing-orders/monitoring/${address}`, 16);
55
75
  /** Send what's due to one destination: hold it, start it over if it now points elsewhere, read after its cursor,
@@ -57,26 +77,30 @@ export const targetOf = (address) => hex(`standing-orders/monitoring/${address}`
57
77
  async function deliver(store, sink, target, start, deps, send) {
58
78
  const now = deps.now ?? (() => new Date());
59
79
  const at = now();
80
+ const none = { count: 0, head: null };
60
81
  if (!store.holdMonitoring(sink, deps.holder, at, new Date(at.getTime() + LEASE_MS)))
61
- return 0;
82
+ return none;
62
83
  let status = store.monitoringStatus(sink)[0];
63
84
  if (status === undefined || status.target !== target) {
64
85
  store.resetMonitoring(sink, target, start());
65
86
  status = store.monitoringStatus(sink)[0];
66
87
  }
67
88
  if (status.nextTryAt != null && Date.parse(status.nextTryAt) > at.getTime())
68
- return 0;
89
+ return none;
69
90
  const from = status.through;
70
91
  const entries = store.sealedAfter(from, BATCH);
71
92
  if (entries.length === 0)
72
- return 0;
93
+ return none;
94
+ const last = entries[entries.length - 1];
73
95
  try {
74
96
  const count = await send(entries);
75
- return store.monitoringDelivered(sink, deps.holder, target, from, entries[entries.length - 1].id, count, now()) ? count : 0;
97
+ if (!store.monitoringDelivered(sink, deps.holder, target, from, last.id, count, now()))
98
+ return none;
99
+ return { count, head: last.seal === null ? null : { through: last.id, hash: last.seal.hash } };
76
100
  }
77
101
  catch (error) {
78
102
  store.monitoringFailed(sink, deps.holder, target, error instanceof Error ? error.message : String(error), now(), new Date(now().getTime() + backoff(status.failures + 1)));
79
- return 0;
103
+ return none;
80
104
  }
81
105
  }
82
106
  async function post(url, body, headers, deps) {
@@ -0,0 +1,19 @@
1
+ import type { Notification, Store } from "./store.js";
2
+ type Claim = Notification & {
3
+ destination: string;
4
+ generation: number;
5
+ };
6
+ /** URLs and shell commands can contain secrets. Only a destination hash enters saved receipts. */
7
+ export declare function notificationDestination(kind: string, target: string): string;
8
+ /** One-release compatibility for operator-configured, installation-wide senders. Personal notifications never leave here. */
9
+ export declare function claimNotifications(store: Store, destination: string, owner: string, now: Date): Claim[];
10
+ export declare function finalizeNotification(store: Store, row: Claim, owner: string, outcome: {
11
+ ok: true;
12
+ receipt: string | null;
13
+ } | {
14
+ ok: false;
15
+ error: string;
16
+ }, now: Date): boolean;
17
+ /** Recheck after earlier network sends: expiry or resolution must stop this send, not just its receipt. */
18
+ export declare function notificationClaimHeld(store: Store, row: Claim, owner: string, now: Date): boolean;
19
+ export {};
@@ -0,0 +1,37 @@
1
+ import { createHash } from "node:crypto";
2
+ /** URLs and shell commands can contain secrets. Only a destination hash enters saved receipts. */
3
+ export function notificationDestination(kind, target) {
4
+ return `${kind}:${createHash("sha256").update(target).digest("hex")}`;
5
+ }
6
+ /** One-release compatibility for operator-configured, installation-wide senders. Personal notifications never leave here. */
7
+ export function claimNotifications(store, destination, owner, now) {
8
+ return store.transact(() => {
9
+ const db = store.handle, stamp = now.toISOString();
10
+ db.prepare(`INSERT OR IGNORE INTO notification_delivery(notification, destination)
11
+ SELECT id, ? FROM notification WHERE resolved_at IS NULL AND recipient IS NULL`).run(destination);
12
+ const claims = db.prepare(`SELECT d.notification, d.claim_generation FROM notification_delivery d
13
+ JOIN notification n ON n.id = d.notification WHERE d.destination = ? AND n.resolved_at IS NULL AND n.recipient IS NULL
14
+ AND d.delivered_at IS NULL AND (d.receipt IS NULL OR d.receipt NOT LIKE 'skipped:%')
15
+ AND (d.claim_owner IS NULL OR d.claim_expires_at <= ?) ORDER BY n.id`).all(destination, stamp);
16
+ return claims.map(raw => {
17
+ const id = Number(raw["notification"]), generation = Number(raw["claim_generation"]) + 1;
18
+ db.prepare(`UPDATE notification_delivery SET claim_owner = ?, claim_expires_at = ?, claim_generation = ?
19
+ WHERE notification = ? AND destination = ?`).run(owner, new Date(now.getTime() + 60_000).toISOString(), generation, id, destination);
20
+ return { ...store.notificationById(id), destination, generation };
21
+ });
22
+ });
23
+ }
24
+ export function finalizeNotification(store, row, owner, outcome, now) {
25
+ const skipped = outcome.ok && outcome.receipt?.startsWith("skipped:");
26
+ return Number(store.handle.prepare(`UPDATE notification_delivery SET attempts = attempts + 1, last_attempt_at = ?,
27
+ delivered_at = ?, receipt = ?, last_error = ?, claim_owner = NULL, claim_expires_at = NULL
28
+ WHERE notification = ? AND destination = ? AND claim_owner = ? AND claim_generation = ?
29
+ AND claim_expires_at > ? AND delivered_at IS NULL`).run(now.toISOString(), outcome.ok && !skipped ? now.toISOString() : null, outcome.ok ? outcome.receipt : null, outcome.ok ? null : outcome.error, row.id, row.destination, owner, row.generation, now.toISOString()).changes) === 1;
30
+ }
31
+ /** Recheck after earlier network sends: expiry or resolution must stop this send, not just its receipt. */
32
+ export function notificationClaimHeld(store, row, owner, now) {
33
+ return store.handle.prepare(`SELECT 1 FROM notification_delivery d JOIN notification n ON n.id = d.notification
34
+ WHERE d.notification = ? AND d.destination = ? AND d.claim_owner = ? AND d.claim_generation = ?
35
+ AND d.claim_expires_at > ? AND d.delivered_at IS NULL AND n.resolved_at IS NULL AND n.recipient IS NULL`)
36
+ .get(row.id, row.destination, owner, row.generation, now.toISOString()) !== undefined;
37
+ }
@@ -10,7 +10,7 @@ const args = (positionals = '', values = '', references = {}) => ({
10
10
  * digest. Neither dereferences a conversation, task or proposal. Creation IDs are never looked up. */
11
11
  export const REMOTE_ARGUMENTS = new Map([
12
12
  ...['status', 'ready', 'grants', 'gaps', 'sync', 'runner list', 'coordinator list', 'incident list',
13
- 'routine list', 'intake show', 'intake run', 'template list', 'webhook status', 'webhook test', 'review show',
13
+ 'routine list', 'intake show', 'intake run', 'template list', 'webhook status', 'review show',
14
14
  'chat-approval show', 'chat-approval off'].map(row => [row, args()]),
15
15
  ['integrations', args('', 'saved')],
16
16
  // --person and --token-name are names compared inside the reader's own view; --token stays a refused credential.
@@ -46,7 +46,7 @@ export const REMOTE_SCOPES = new Map([
46
46
  ...["list", "show", "add", "refresh", "pause", "resume", "run-now"].map(action => [`routine ${action}`, installation]),
47
47
  ["config show", installation], ["verify show", repo], ["intake show", installation], ["intake run", installation], ["intake preview", installation],
48
48
  ["intake pr-comments", installation], ["template list", installation], ["template show", installation], ["contest show", installation],
49
- ["contest exclude", installation], ["webhook status", installation], ["webhook test", installation], ["review show", installation],
49
+ ["contest exclude", installation], ["webhook status", installation], ["review show", installation],
50
50
  ["chat-approval show", self], ["chat-approval off", self],
51
51
  // The person's own remote actions, or everyone's within their projects for an approver (remote-audit.ts).
52
52
  ["audit", self],
@@ -64,7 +64,7 @@ export const REMOTE_SCOPES = new Map([
64
64
  export const REMOTE_ALL_PROJECTS_ONLY = new Map([
65
65
  // Settings, connections and acts that belong to the installation as a whole, not to any one project.
66
66
  ...["integrations", "grants", "sync", "incident resolve", "routine add", "routine refresh", "routine pause", "routine resume", "routine run-now",
67
- "config show", "intake run", "contest exclude", "webhook status", "webhook test"].map(row => [row, "installation-wide"]),
67
+ "config show", "intake run", "contest exclude", "webhook status"].map(row => [row, "installation-wide"]),
68
68
  // Cross-project lists and reads a limited person is still refused; scoping them is later work.
69
69
  ...["runner list", "coordinator list", "outbox list", "incident list", "routine list", "routine show", "intake show", "intake preview",
70
70
  "intake pr-comments", "template list", "template show", "contest show", "review show"].map(row => [row, "not yet scoped"]),
package/dist/operate.d.ts CHANGED
@@ -124,7 +124,7 @@ export declare const OPERATE_BOOLEAN_FLAGS: ReadonlySet<string>;
124
124
  export declare function parseOperateArgs(argv: readonly string[], ownValues?: ReadonlySet<string>): Args | {
125
125
  error: string;
126
126
  };
127
- /** Route an `operate` command. Returns the process exit code. */
127
+ /** Route a native command and return its exit code. Its password source cannot be selected by an HTTP request. */
128
128
  export declare function runOperate(command: string, argv: readonly string[], write: Write, options?: OperateOptions): Promise<number>;
129
129
  export type { Principal };
130
130
  /**