toolroll 0.9.45 → 0.9.47

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 (68) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/activity-line.d.ts +26 -0
  3. package/dist/activity-line.js +47 -0
  4. package/dist/api-tokens.d.ts +33 -0
  5. package/dist/api-tokens.js +39 -0
  6. package/dist/audit-cli.d.ts +17 -0
  7. package/dist/audit-cli.js +86 -0
  8. package/dist/browser/workspace.js +25 -25
  9. package/dist/browser-workspace.d.ts +18 -1
  10. package/dist/browser-workspace.js +9 -3
  11. package/dist/builder.js +4 -2
  12. package/dist/cli-http.d.ts +5 -0
  13. package/dist/cli-http.js +12 -2
  14. package/dist/cli.js +7 -1
  15. package/dist/contracts/cli.d.ts +47 -0
  16. package/dist/contracts/cli.js +11 -0
  17. package/dist/contracts/console-api.d.ts +1 -0
  18. package/dist/contracts/console-api.js +1 -0
  19. package/dist/contracts/mcp-oauth.d.ts +53 -0
  20. package/dist/contracts/mcp-oauth.js +40 -0
  21. package/dist/credentials-ui.d.ts +5 -0
  22. package/dist/credentials-ui.js +16 -4
  23. package/dist/export.js +4 -3
  24. package/dist/flow-card-task.d.ts +2 -0
  25. package/dist/flow-card-task.js +57 -0
  26. package/dist/flow-live.d.ts +2 -2
  27. package/dist/flow-live.js +69 -8
  28. package/dist/flows-ui.js +15 -1
  29. package/dist/live.d.ts +14 -0
  30. package/dist/live.js +71 -22
  31. package/dist/mcp-http.d.ts +6 -1
  32. package/dist/mcp-http.js +22 -4
  33. package/dist/mcp-oauth.d.ts +82 -0
  34. package/dist/mcp-oauth.js +479 -0
  35. package/dist/mcp-person.js +1 -1
  36. package/dist/operate-remote-arguments.js +2 -0
  37. package/dist/operate-remote.d.ts +1 -1
  38. package/dist/operate-remote.js +7 -1
  39. package/dist/operate.d.ts +3 -0
  40. package/dist/operate.js +58 -2
  41. package/dist/people-audit-ui.d.ts +14 -0
  42. package/dist/people-audit-ui.js +38 -0
  43. package/dist/planner.js +4 -3
  44. package/dist/public-access.d.ts +50 -0
  45. package/dist/public-access.js +87 -0
  46. package/dist/public-check.d.ts +37 -0
  47. package/dist/public-check.js +170 -0
  48. package/dist/remote-audit.d.ts +79 -0
  49. package/dist/remote-audit.js +161 -0
  50. package/dist/request-budget-ui.d.ts +11 -0
  51. package/dist/request-budget-ui.js +31 -0
  52. package/dist/request-budget.d.ts +96 -0
  53. package/dist/request-budget.js +227 -0
  54. package/dist/serve.d.ts +9 -1
  55. package/dist/serve.js +211 -52
  56. package/dist/setup-guide.d.ts +2 -2
  57. package/dist/store.d.ts +134 -2
  58. package/dist/store.js +270 -7
  59. package/dist/surface.js +19 -2
  60. package/dist/task-activity.d.ts +27 -0
  61. package/dist/task-activity.js +53 -0
  62. package/dist/task-live.d.ts +17 -0
  63. package/dist/task-live.js +144 -0
  64. package/dist/token-notices.d.ts +22 -0
  65. package/dist/token-notices.js +52 -0
  66. package/dist/tokens-cli.d.ts +21 -0
  67. package/dist/tokens-cli.js +184 -0
  68. package/package.json +1 -1
package/dist/store.d.ts CHANGED
@@ -2,6 +2,8 @@ import type { DecisionOption } from "./contracts/decision.js";
2
2
  import { type SavedToolAction, type SavedToolRule } from "./contracts/store-json.js";
3
3
  import type { FlowCardChange, FlowCardFields } from "./contracts/flow-card.js";
4
4
  import { type Actor } from "./actor.js";
5
+ import { type LiveActivityKind } from "./live.js";
6
+ import { type TokenPurpose } from "./api-tokens.js";
5
7
  import { type CriterionMatrixRow, type CriterionJudgementWord } from "./proof.js";
6
8
  import { type ExecutionProfile, type ChainEntry, type UnattendedPermissionMode, type AcceptanceCriterion } from "./scope.js";
7
9
  import { type ProviderRoom, type CandidateTier, type TaskSize, type TaskSizing, type PhaseRoute, type PublicationAuthority, type RouteStamp, type ReadinessLookup, type ReadinessObservation, type RiskLevel, type RouteOverride } from "./phase-routing.js";
@@ -68,6 +70,10 @@ export declare const FLOW_SEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS flow_send
68
70
  /** An owner's lasting choice to approve plans and merges from their paired chat (chat-approval.ts), for all their
69
71
  * projects (scope '*') or one (its path), with the limits they agreed to. Off unless a row says on; a project's own row
70
72
  * outranks the all-projects one. `generation` is the account's at the change: a reset password ends it. */
73
+ /** What a running agent did last (live tasks): one row per run, a fixed kind from live.ts's
74
+ * vocabulary and when — never text, tool names or arguments. Display state, not evidence;
75
+ * a finished run's row goes with the live log's own retention (sweepRunActivity). */
76
+ export declare const RUN_ACTIVITY_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_activity (\n run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,\n kind TEXT NOT NULL CHECK (kind IN ('session','message','edit','command','read','search','lookup','plan','delegate','tool')),\n at TEXT NOT NULL\n);\n";
71
77
  export declare const CHAT_APPROVAL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_approval_setting (\n approver TEXT NOT NULL,\n scope TEXT NOT NULL,\n enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),\n full_access INTEGER NOT NULL DEFAULT 0 CHECK (full_access IN (0,1)),\n attempt_cap_microusd INTEGER CHECK (attempt_cap_microusd IS NULL OR attempt_cap_microusd >= 0),\n generation INTEGER NOT NULL,\n digest TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (approver, scope)\n);\n";
72
78
  /** The second tap on a flow decision in Telegram (telegram-flow.ts): Approve arms Yes and Cancel, bound to the binding,
73
79
  * chat, message and exact visit of the card; Yes decides it once. */
@@ -145,7 +151,7 @@ export type SealedLedgerEntry = LedgerEntry & {
145
151
  hash: string;
146
152
  } | null;
147
153
  };
148
- export declare const SCHEMA_VERSION = 110;
154
+ export declare const SCHEMA_VERSION = 113;
149
155
  /**
150
156
  * The migrations `toolroll update` may carry a database through in place: each
151
157
  * one only adds (a table, a column, an accepted value) and leaves every saved row
@@ -155,11 +161,43 @@ export declare const SCHEMA_VERSION = 110;
155
161
  * migrates and does not read this list.
156
162
  *
157
163
  * v110 rebuilds action_ledger only to widen its source check ('api', 'mcp'):
158
- * every row, id and hash-chain link is copied unchanged.
164
+ * every row, id and hash-chain link is copied unchanged. v111 adds nullable
165
+ * api_token columns (project limit, rotation); existing tokens read as before.
166
+ * v112 only adds the two request-budget tables.
167
+ * v113 adds MCP sign-in tables and API-token purpose metadata.
159
168
  */
160
169
  export declare const UPDATE_SAFE_MIGRATIONS: readonly number[];
161
170
  /** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
162
171
  export declare function updateSafeSchema(version: number | null): boolean;
172
+ export type OAuthClientRow = {
173
+ id: string;
174
+ name: string;
175
+ redirectUris: string[];
176
+ createdAt: string;
177
+ };
178
+ export type OAuthCodeRow = {
179
+ client: string;
180
+ account: string;
181
+ generation: number;
182
+ access: "read" | "act";
183
+ projects: string[];
184
+ redirectUri: string;
185
+ resource: string;
186
+ challenge: string;
187
+ expiresAt: string;
188
+ usedAt: string | null;
189
+ token: string | null;
190
+ };
191
+ export type OAuthGrantRow = {
192
+ token: string;
193
+ client: string;
194
+ account: string;
195
+ generation: number;
196
+ projects: string[];
197
+ resource: string;
198
+ accessExpiresAt: string;
199
+ refreshHash: string;
200
+ };
163
201
  export type ApiTokenRow = {
164
202
  id: string;
165
203
  account: string;
@@ -171,6 +209,14 @@ export type ApiTokenRow = {
171
209
  lastUsedAt: string | null;
172
210
  revokedAt: string | null;
173
211
  revokedBy: string | null;
212
+ /** v113: MCP manages its own access-secret rotation. */
213
+ purpose: TokenPurpose;
214
+ /** v111: the projects it is limited to (null: every project its person may use). */
215
+ projects: string[] | null;
216
+ /** v111: the token it replaced, the one that replaced it, and when this one stops after being replaced. */
217
+ replaces: string | null;
218
+ replacedBy: string | null;
219
+ overlapUntil: string | null;
174
220
  };
175
221
  export type WebSessionRow = {
176
222
  idHash: string;
@@ -2591,7 +2637,26 @@ export declare class Store {
2591
2637
  access: "read" | "act";
2592
2638
  expiresAt: string;
2593
2639
  by: string;
2640
+ projects?: readonly string[] | null;
2641
+ purpose?: TokenPurpose;
2594
2642
  }, now: Date): void;
2643
+ /**
2644
+ * v111: replace a live, not-yet-replaced token with a new secret on the same terms (access, projects, end date, so
2645
+ * rotating never extends it). The old one keeps working until `overlapUntil` (never past its own end), then stops;
2646
+ * every check reads that time, and endRotatedApiTokens records the end afterwards. Only the token's own person.
2647
+ */
2648
+ rotateApiToken(oldId: string, replacement: {
2649
+ id: string;
2650
+ secretHash: string;
2651
+ }, by: string, now: Date, overlapMs: number): {
2652
+ ok: true;
2653
+ row: ApiTokenRow;
2654
+ } | {
2655
+ ok: false;
2656
+ reason: "unknown" | "replaced" | "mcp-managed";
2657
+ };
2658
+ /** v111: record the end of each replaced token whose overlap has passed (it already signs no one in). */
2659
+ endRotatedApiTokens(now: Date): number;
2595
2660
  /** A token by id with its kept hash (for the check), or null. */
2596
2661
  apiTokenSecret(id: string): {
2597
2662
  row: ApiTokenRow;
@@ -2602,6 +2667,62 @@ export declare class Store {
2602
2667
  /** Kept at most once a minute: a busy script doesn't write on every request. */
2603
2668
  touchApiToken(id: string, now: Date): void;
2604
2669
  revokeApiToken(id: string, by: string, now: Date, why?: string): boolean;
2670
+ /** A registered client, or null. */
2671
+ oauthClient(id: string): OAuthClientRow | null;
2672
+ /** Expire unused registrations, retaining clients with a live code, grant or in-process consent. */
2673
+ pruneOAuthClients(now: Date, pending?: readonly string[]): void;
2674
+ /**
2675
+ * Unused registrations expire after a day. Admission has a global and per-source bound; it never evicts a live
2676
+ * registration. Repeating metadata from the same source returns its existing registration without spending a slot.
2677
+ */
2678
+ registerOAuthClient(client: {
2679
+ id: string;
2680
+ name: string;
2681
+ redirectUris: string[];
2682
+ source: string;
2683
+ }, now: Date, cap: number, perSourceCap: number, pending?: readonly string[]): OAuthClientRow | null;
2684
+ /** Keep a one-time code by its hash. */
2685
+ saveOAuthCode(hash: string, code: Omit<OAuthCodeRow, "usedAt" | "token">): void;
2686
+ /**
2687
+ * Spend a code: the first caller gets it (`taken`), once, before it expires. A code spent before answers `used`
2688
+ * with the token it made, so that token can be ended; anything else is null.
2689
+ */
2690
+ takeOAuthCode(hash: string, now: Date): {
2691
+ taken: OAuthCodeRow;
2692
+ } | {
2693
+ used: string | null;
2694
+ } | null;
2695
+ /** Make a grant: its API token (created and recorded as any other) and its binding, together; the code notes the token. */
2696
+ createOAuthGrant(code: string, token: {
2697
+ id: string;
2698
+ account: string;
2699
+ name: string;
2700
+ secretHash: string;
2701
+ access: "read" | "act";
2702
+ expiresAt: string;
2703
+ }, grant: Omit<OAuthGrantRow, "token">, now: Date): void;
2704
+ /** The grant an API token signs in for, or null when the token is an ordinary one. */
2705
+ oauthGrant(token: string): OAuthGrantRow | null;
2706
+ /**
2707
+ * Classify, validate and rotate under one write transaction. Every issued hash remains in the family, so any
2708
+ * older secret revokes even a concurrent winner. An unknown guess or the wrong client cannot end the family.
2709
+ */
2710
+ rotateOAuthGrant(token: string, ask: {
2711
+ client: string;
2712
+ resource: string;
2713
+ refreshHash: string;
2714
+ scope?: string;
2715
+ }, next: {
2716
+ refreshHash: string;
2717
+ accessHash: string;
2718
+ accessExpiresAt: string;
2719
+ }, now: Date, projectsFor: (account: string) => readonly string[]): {
2720
+ ok: true;
2721
+ access: "read" | "act";
2722
+ } | {
2723
+ ok: false;
2724
+ reason: "invalid_grant" | "invalid_scope";
2725
+ };
2605
2726
  saveWebSession(session: WebSessionRow): void;
2606
2727
  webSession(idHash: string): WebSessionRow | null;
2607
2728
  webSessions(account: string | null): WebSessionRow[];
@@ -5485,6 +5606,17 @@ export declare class Store {
5485
5606
  * page and focused chat render live, including once the run that wrote
5486
5607
  * it has ended. */
5487
5608
  latestCheckpointForTask(taskRef: number): RunCheckpoint | null;
5609
+ /** What the agent did last on this run (live tasks): one fixed kind and when, replacing the last. */
5610
+ recordRunActivity(run: number, kind: LiveActivityKind, now: Date): void;
5611
+ /** A run's last recorded activity and its newest progress checkpoint's time, in one read. */
5612
+ runActivity(run: number): {
5613
+ kind: LiveActivityKind | null;
5614
+ at: string | null;
5615
+ progressAt: string | null;
5616
+ };
5617
+ /** The live log's own retention: a finished run's activity goes once older than `retainMs`, and beyond the newest
5618
+ * `bound` finished runs; a live run's always stays. */
5619
+ sweepRunActivity(now: Date, retainMs: number, bound: number): number;
5488
5620
  /** One run's full checkpoint history, oldest first. */
5489
5621
  checkpointHistory(run: number): RunCheckpoint[];
5490
5622
  /** The newest scout run's report for a task (v34), when one exists. */
package/dist/store.js CHANGED
@@ -12,6 +12,8 @@ import { cardOutputsForStore, cardOutputsFromStore } from "./contracts/stage-out
12
12
  import { chatControlHref, chatResultHref } from "./chat-controls.js";
13
13
  import { actorLabel, currentActor, leadSecretMatches, mintLeadToken, parseLeadToken } from "./actor.js";
14
14
  import { scanForSecrets } from "./evidence.js";
15
+ import { LIVE_ACTIVITY_KINDS } from "./live.js";
16
+ import { tokenPurpose, tokenRotationProblem } from "./api-tokens.js";
15
17
  /**
16
18
  * The database: a small task store, and the operational overlay beside it.
17
19
  *
@@ -69,6 +71,7 @@ import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
69
71
  import { RECIPE_SCHEMA } from "./recipes.js";
70
72
  import { POLICY_SCHEMA, agentRefusal, approvalRefusal, attendedRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
71
73
  import { PROVIDER_AUTH_SCHEMA } from "./provider-auth.js";
74
+ import { REQUEST_BUDGET_SCHEMA } from "./request-budget.js";
72
75
  import { REVIEW_SCHEMA, reviewSwitchWords } from "./review-switch.js";
73
76
  import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, teammateFilers, usd } from "./spend.js";
74
77
  import { DEFAULT_PERIODS, RETENTION_SCHEMA, periodWords, widenRetentionSchema } from "./retention.js";
@@ -357,6 +360,16 @@ CREATE INDEX IF NOT EXISTS telegram_flow_choice_visit ON telegram_flow_choice (c
357
360
  /** An owner's lasting choice to approve plans and merges from their paired chat (chat-approval.ts), for all their
358
361
  * projects (scope '*') or one (its path), with the limits they agreed to. Off unless a row says on; a project's own row
359
362
  * outranks the all-projects one. `generation` is the account's at the change: a reset password ends it. */
363
+ /** What a running agent did last (live tasks): one row per run, a fixed kind from live.ts's
364
+ * vocabulary and when — never text, tool names or arguments. Display state, not evidence;
365
+ * a finished run's row goes with the live log's own retention (sweepRunActivity). */
366
+ export const RUN_ACTIVITY_SCHEMA = `
367
+ CREATE TABLE IF NOT EXISTS run_activity (
368
+ run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,
369
+ kind TEXT NOT NULL CHECK (kind IN ('session','message','edit','command','read','search','lookup','plan','delegate','tool')),
370
+ at TEXT NOT NULL
371
+ );
372
+ `;
360
373
  export const CHAT_APPROVAL_SCHEMA = `
361
374
  CREATE TABLE IF NOT EXISTS chat_approval_setting (
362
375
  approver TEXT NOT NULL,
@@ -564,7 +577,10 @@ const REGISTERED_AT_FILING = "again, as it was filed";
564
577
  // v108 keeps sign-in pauses: one incident per provider whose sign-in stopped working (provider_auth_pause).
565
578
  // v109 remembers which sign-in pause parked a task (task_ref.auth_wait_pause), so reviews and other runs outside the tick wait for it too.
566
579
  // v110 lets the action ledger record commands a person ran on this server with their API token (source 'api', or 'mcp' from their agent).
567
- export const SCHEMA_VERSION = 110;
580
+ // v111 keeps an API token's project limit and its rotation: the token that replaced it, and when the old one stops.
581
+ // v112 keeps request budgets for API tokens: owner overrides (request_budget_limit) and saved usage (request_budget_usage).
582
+ // v113 keeps MCP sign-ins (OAuth): registered clients, one-time codes, and each grant's binding to its API token.
583
+ export const SCHEMA_VERSION = 113;
568
584
  /**
569
585
  * The migrations `toolroll update` may carry a database through in place: each
570
586
  * one only adds (a table, a column, an accepted value) and leaves every saved row
@@ -574,9 +590,12 @@ export const SCHEMA_VERSION = 110;
574
590
  * migrates and does not read this list.
575
591
  *
576
592
  * v110 rebuilds action_ledger only to widen its source check ('api', 'mcp'):
577
- * every row, id and hash-chain link is copied unchanged.
593
+ * every row, id and hash-chain link is copied unchanged. v111 adds nullable
594
+ * api_token columns (project limit, rotation); existing tokens read as before.
595
+ * v112 only adds the two request-budget tables.
596
+ * v113 adds MCP sign-in tables and API-token purpose metadata.
578
597
  */
579
- export const UPDATE_SAFE_MIGRATIONS = Object.freeze([110]);
598
+ export const UPDATE_SAFE_MIGRATIONS = Object.freeze([110, 111, 112, 113]);
580
599
  /** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
581
600
  export function updateSafeSchema(version) {
582
601
  if (version === null || !Number.isSafeInteger(version) || version <= 0 || version > SCHEMA_VERSION)
@@ -643,6 +662,58 @@ CREATE TABLE IF NOT EXISTS web_session (
643
662
  );
644
663
  CREATE INDEX IF NOT EXISTS web_session_account ON web_session (account);
645
664
  `;
665
+ /**
666
+ * v113: MCP sign-in (OAuth 2.1, mcp-oauth.ts). A registered client, a one-time code (its hash, a minute at most), and
667
+ * each grant: the ordinary API token it signs in with (revoked like any other), the account generation and exact
668
+ * projects the person chose, and every issued refresh-secret hash (any replay ends the whole family).
669
+ */
670
+ const OAUTH_SCHEMA = `
671
+ CREATE TABLE IF NOT EXISTS oauth_client (
672
+ id TEXT PRIMARY KEY,
673
+ name TEXT NOT NULL,
674
+ redirect_json TEXT NOT NULL,
675
+ created_at TEXT NOT NULL,
676
+ source_hash TEXT NOT NULL
677
+ );
678
+ CREATE INDEX IF NOT EXISTS oauth_client_source ON oauth_client (source_hash);
679
+ CREATE TABLE IF NOT EXISTS oauth_code (
680
+ hash TEXT PRIMARY KEY,
681
+ client TEXT NOT NULL REFERENCES oauth_client(id),
682
+ account TEXT NOT NULL,
683
+ generation INTEGER NOT NULL,
684
+ access TEXT NOT NULL CHECK (access IN ('read','act')),
685
+ projects_json TEXT NOT NULL,
686
+ redirect_uri TEXT NOT NULL,
687
+ resource TEXT NOT NULL,
688
+ challenge TEXT NOT NULL,
689
+ expires_at TEXT NOT NULL,
690
+ used_at TEXT,
691
+ token TEXT
692
+ );
693
+ CREATE TABLE IF NOT EXISTS oauth_grant (
694
+ token TEXT PRIMARY KEY REFERENCES api_token(id),
695
+ client TEXT NOT NULL REFERENCES oauth_client(id),
696
+ account TEXT NOT NULL,
697
+ generation INTEGER NOT NULL,
698
+ projects_json TEXT NOT NULL,
699
+ resource TEXT NOT NULL,
700
+ access_expires_at TEXT NOT NULL,
701
+ renew_hash TEXT NOT NULL
702
+ );
703
+ CREATE INDEX IF NOT EXISTS oauth_grant_client ON oauth_grant (client);
704
+ CREATE TABLE IF NOT EXISTS oauth_refresh (
705
+ token TEXT NOT NULL REFERENCES oauth_grant(token),
706
+ hash TEXT NOT NULL,
707
+ PRIMARY KEY (token, hash)
708
+ );
709
+ `;
710
+ const stringList = (raw) => { try {
711
+ const value = JSON.parse(String(raw));
712
+ return Array.isArray(value) && value.every((one) => typeof one === "string") ? value : [];
713
+ }
714
+ catch {
715
+ return [];
716
+ } };
646
717
  /** v100: which account each identity-provider identity (issuer + subject) signs in as. */
647
718
  const SSO_SCHEMA = `
648
719
  CREATE TABLE IF NOT EXISTS sso_identity (
@@ -4013,6 +4084,14 @@ function initializeStore(db, file) {
4013
4084
  db.exec(TEAM_SCHEMA);
4014
4085
  db.exec(SSO_SCHEMA);
4015
4086
  db.exec(CREDENTIALS_SCHEMA);
4087
+ // v111: a token's project limit (null: every project its person may use) and its rotation.
4088
+ addColumn(db, "api_token", "projects_json", "TEXT");
4089
+ addColumn(db, "api_token", "replaces", "TEXT");
4090
+ addColumn(db, "api_token", "replaced_by", "TEXT");
4091
+ addColumn(db, "api_token", "overlap_until", "TEXT");
4092
+ // v113: MCP sign-in (OAuth), and whether a token is an ordinary one or an MCP sign-in's.
4093
+ db.exec(OAUTH_SCHEMA);
4094
+ addColumn(db, "api_token", "purpose", "TEXT NOT NULL DEFAULT 'api' CHECK (purpose IN ('api','mcp'))");
4016
4095
  db.exec(APPROVAL_SCHEMA);
4017
4096
  db.exec(LEDGER_CHAIN_SCHEMA);
4018
4097
  db.exec(MONITORING_SCHEMA);
@@ -4022,12 +4101,16 @@ function initializeStore(db, file) {
4022
4101
  db.exec(CHAT_DECIDE_SCHEMA);
4023
4102
  // An owner's lasting chat-approval setting (no version bump: additive only; absent means off).
4024
4103
  db.exec(CHAT_APPROVAL_SCHEMA);
4104
+ // What a running agent did last (no version bump: additive only; absent means no word yet).
4105
+ db.exec(RUN_ACTIVITY_SCHEMA);
4025
4106
  db.exec(TELEGRAM_FLOW_CONFIRM_SCHEMA);
4026
4107
  // A flow's "Send to me" and "Person chooses" zones (no version bump: additive only).
4027
4108
  db.exec(FLOW_SEND_SCHEMA);
4028
4109
  db.exec(SPEND_SCHEMA);
4029
4110
  // Sign-in pauses: one row per incident of a provider's sign-in no longer working.
4030
4111
  db.exec(PROVIDER_AUTH_SCHEMA);
4112
+ // v112: request budgets for API tokens (request-budget.ts).
4113
+ db.exec(REQUEST_BUDGET_SCHEMA);
4031
4114
  // The sign-in pause the dispatch gate last left this task waiting on (null: none), for the work index.
4032
4115
  addColumn(db, "task_ref", "auth_wait_pause", "INTEGER REFERENCES provider_auth_pause(id)");
4033
4116
  // Sized routing (no version bump: additive, and a wider tier check whose rows carry over): how big each task is,
@@ -5547,7 +5630,20 @@ function rebuildExact(db, table, oldDdl, targetDdl, columns) {
5547
5630
  function readApiToken(row) {
5548
5631
  const text = (key) => row[key] == null ? null : String(row[key]);
5549
5632
  return { id: String(row["id"]), account: String(row["account"]), name: String(row["name"]), access: row["access"] === "act" ? "act" : "read", createdAt: String(row["created_at"]), createdBy: String(row["created_by"]),
5550
- expiresAt: String(row["expires_at"]), lastUsedAt: text("last_used_at"), revokedAt: text("revoked_at"), revokedBy: text("revoked_by") };
5633
+ expiresAt: String(row["expires_at"]), lastUsedAt: text("last_used_at"), revokedAt: text("revoked_at"), revokedBy: text("revoked_by"),
5634
+ purpose: tokenPurpose(row["purpose"]), projects: tokenProjectsOf(row["projects_json"]), replaces: text("replaces"), replacedBy: text("replaced_by"), overlapUntil: text("overlap_until") };
5635
+ }
5636
+ /** A token's stored project limit. Anything unreadable limits it to no project at all: never wider. */
5637
+ function tokenProjectsOf(value) {
5638
+ if (value == null)
5639
+ return null;
5640
+ try {
5641
+ const parsed = JSON.parse(String(value));
5642
+ return Array.isArray(parsed) && parsed.every(one => typeof one === "string") ? parsed : [];
5643
+ }
5644
+ catch {
5645
+ return [];
5646
+ }
5551
5647
  }
5552
5648
  function readWebSession(row) {
5553
5649
  return { idHash: String(row["id_hash"]), account: String(row["account"]), csrf: String(row["csrf"]), role: row["role"] === "viewer" ? "viewer" : "approver", generation: Number(row["generation"]),
@@ -9808,12 +9904,45 @@ export class Store {
9808
9904
  // ---- API tokens and browser sessions (v101) ------------------------------
9809
9905
  createApiToken(token, now) {
9810
9906
  this.transact(() => {
9811
- this.db.prepare("INSERT INTO api_token (id, account, name, secret_hash, access, created_at, created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
9812
- .run(token.id, token.account, token.name, token.secretHash, token.access, now.toISOString(), token.by, token.expiresAt);
9907
+ const projects = token.projects ?? null;
9908
+ this.db.prepare("INSERT INTO api_token (id, account, name, secret_hash, access, created_at, created_by, expires_at, projects_json, purpose) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
9909
+ .run(token.id, token.account, token.name, token.secretHash, token.access, now.toISOString(), token.by, token.expiresAt, projects === null ? null : JSON.stringify(projects), token.purpose ?? "api");
9813
9910
  this.recordAction({ at: now.toISOString(), actor: token.by, repo: null, taskId: null, runId: null, action: `API token created: ${token.name}`, outcome: token.access, source: "access",
9814
- detail: `for ${token.account}, until ${token.expiresAt.slice(0, 10)}` });
9911
+ detail: `for ${token.account}, until ${token.expiresAt.slice(0, 10)}${projects === null ? "" : ` · ${projects.length === 0 ? "no projects" : projects.join(", ")}`} · id ${token.id}` });
9815
9912
  });
9816
9913
  }
9914
+ /**
9915
+ * v111: replace a live, not-yet-replaced token with a new secret on the same terms (access, projects, end date, so
9916
+ * rotating never extends it). The old one keeps working until `overlapUntil` (never past its own end), then stops;
9917
+ * every check reads that time, and endRotatedApiTokens records the end afterwards. Only the token's own person.
9918
+ */
9919
+ rotateApiToken(oldId, replacement, by, now, overlapMs) {
9920
+ return this.transact(() => {
9921
+ const old = this.apiTokenSecret(oldId)?.row ?? null;
9922
+ const at = now.getTime();
9923
+ if (old === null || old.account !== by || old.revokedAt !== null || !(Date.parse(old.expiresAt) > at) || old.overlapUntil !== null && !(Date.parse(old.overlapUntil) > at))
9924
+ return { ok: false, reason: "unknown" };
9925
+ if (tokenRotationProblem(old) !== null || this.oauthGrant(oldId) !== null)
9926
+ return { ok: false, reason: "mcp-managed" };
9927
+ if (old.replacedBy !== null)
9928
+ return { ok: false, reason: "replaced" };
9929
+ const stamp = now.toISOString();
9930
+ const until = new Date(Math.min(at + Math.max(0, overlapMs), Date.parse(old.expiresAt))).toISOString();
9931
+ this.db.prepare("INSERT INTO api_token (id, account, name, secret_hash, access, created_at, created_by, expires_at, projects_json, replaces) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
9932
+ .run(replacement.id, old.account, old.name, replacement.secretHash, old.access, stamp, by, old.expiresAt, old.projects === null ? null : JSON.stringify(old.projects), old.id);
9933
+ this.db.prepare("UPDATE api_token SET replaced_by = ?, overlap_until = ? WHERE id = ? AND replaced_by IS NULL").run(replacement.id, until, old.id);
9934
+ this.recordAction({ at: stamp, actor: by, repo: null, taskId: null, runId: null, action: `API token rotated: ${old.name}`, outcome: old.access, source: "access",
9935
+ detail: `${old.account}'s · id ${old.id} → ${replacement.id}, the old one stops ${until.slice(0, 16).replace("T", " ")} UTC` });
9936
+ return { ok: true, row: this.apiTokenSecret(replacement.id).row };
9937
+ });
9938
+ }
9939
+ /** v111: record the end of each replaced token whose overlap has passed (it already signs no one in). */
9940
+ endRotatedApiTokens(now) {
9941
+ const due = this.db.prepare("SELECT id, overlap_until FROM api_token WHERE revoked_at IS NULL AND overlap_until IS NOT NULL AND overlap_until <= ?").all(now.toISOString());
9942
+ for (const row of due)
9943
+ this.revokeApiToken(String(row["id"]), "system", new Date(String(row["overlap_until"])), "replaced; its overlap ended");
9944
+ return due.length;
9945
+ }
9817
9946
  /** A token by id with its kept hash (for the check), or null. */
9818
9947
  apiTokenSecret(id) {
9819
9948
  const row = this.db.prepare("SELECT * FROM api_token WHERE id = ?").get(id);
@@ -9837,6 +9966,119 @@ export class Store {
9837
9966
  return true;
9838
9967
  });
9839
9968
  }
9969
+ // ---- MCP sign-in (v113, mcp-oauth.ts) ---------------------------------------------------------------------------
9970
+ /** A registered client, or null. */
9971
+ oauthClient(id) {
9972
+ const row = this.db.prepare("SELECT * FROM oauth_client WHERE id = ?").get(id);
9973
+ return row === undefined ? null : { id: String(row["id"]), name: String(row["name"]), redirectUris: stringList(row["redirect_json"]), createdAt: String(row["created_at"]) };
9974
+ }
9975
+ /** Expire unused registrations, retaining clients with a live code, grant or in-process consent. */
9976
+ pruneOAuthClients(now, pending = []) {
9977
+ this.transact(() => {
9978
+ const stale = new Date(now.getTime() - 86_400_000).toISOString();
9979
+ this.db.prepare("DELETE FROM oauth_code WHERE expires_at <= ?").run(now.toISOString());
9980
+ this.db.prepare(`DELETE FROM oauth_client WHERE created_at <= ? AND id NOT IN (SELECT client FROM oauth_grant)
9981
+ AND id NOT IN (SELECT client FROM oauth_code) AND id NOT IN (SELECT value FROM json_each(?))`).run(stale, JSON.stringify(pending));
9982
+ });
9983
+ }
9984
+ /**
9985
+ * Unused registrations expire after a day. Admission has a global and per-source bound; it never evicts a live
9986
+ * registration. Repeating metadata from the same source returns its existing registration without spending a slot.
9987
+ */
9988
+ registerOAuthClient(client, now, cap, perSourceCap, pending = []) {
9989
+ return this.transact(() => {
9990
+ const redirectJson = JSON.stringify(client.redirectUris);
9991
+ this.pruneOAuthClients(now, pending);
9992
+ const same = this.db.prepare("SELECT id FROM oauth_client WHERE name = ? AND redirect_json = ? AND source_hash = ?").get(client.name, redirectJson, client.source);
9993
+ if (same !== undefined)
9994
+ return this.oauthClient(String(same["id"]));
9995
+ const unused = "FROM oauth_client WHERE id NOT IN (SELECT client FROM oauth_grant)";
9996
+ if (Number(this.db.prepare(`SELECT COUNT(*) AS n ${unused}`).get()?.["n"] ?? 0) >= cap
9997
+ || Number(this.db.prepare(`SELECT COUNT(*) AS n ${unused} AND source_hash = ?`).get(client.source)?.["n"] ?? 0) >= perSourceCap)
9998
+ return null;
9999
+ this.db.prepare("INSERT INTO oauth_client (id, name, redirect_json, created_at, source_hash) VALUES (?, ?, ?, ?, ?)").run(client.id, client.name, redirectJson, now.toISOString(), client.source);
10000
+ return this.oauthClient(client.id);
10001
+ });
10002
+ }
10003
+ /** Keep a one-time code by its hash. */
10004
+ saveOAuthCode(hash, code) {
10005
+ this.db.prepare("INSERT INTO oauth_code (hash, client, account, generation, access, projects_json, redirect_uri, resource, challenge, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
10006
+ .run(hash, code.client, code.account, code.generation, code.access, JSON.stringify(code.projects), code.redirectUri, code.resource, code.challenge, code.expiresAt);
10007
+ }
10008
+ /**
10009
+ * Spend a code: the first caller gets it (`taken`), once, before it expires. A code spent before answers `used`
10010
+ * with the token it made, so that token can be ended; anything else is null.
10011
+ */
10012
+ takeOAuthCode(hash, now) {
10013
+ return this.transact(() => {
10014
+ const row = this.db.prepare("SELECT * FROM oauth_code WHERE hash = ?").get(hash);
10015
+ if (row === undefined)
10016
+ return null;
10017
+ if (row["used_at"] !== null)
10018
+ return { used: row["token"] === null ? null : String(row["token"]) };
10019
+ this.db.prepare("UPDATE oauth_code SET used_at = ? WHERE hash = ? AND used_at IS NULL").run(now.toISOString(), hash);
10020
+ if (String(row["expires_at"]) <= now.toISOString())
10021
+ return null;
10022
+ return { taken: { client: String(row["client"]), account: String(row["account"]), generation: Number(row["generation"]), access: row["access"] === "act" ? "act" : "read", projects: stringList(row["projects_json"]),
10023
+ redirectUri: String(row["redirect_uri"]), resource: String(row["resource"]), challenge: String(row["challenge"]), expiresAt: String(row["expires_at"]), usedAt: null, token: null } };
10024
+ });
10025
+ }
10026
+ /** Make a grant: its API token (created and recorded as any other) and its binding, together; the code notes the token. */
10027
+ createOAuthGrant(code, token, grant, now) {
10028
+ this.transact(() => {
10029
+ this.createApiToken({ ...token, by: token.account, projects: grant.projects, purpose: "mcp" }, now);
10030
+ this.db.prepare("INSERT INTO oauth_grant (token, client, account, generation, projects_json, resource, access_expires_at, renew_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
10031
+ .run(token.id, grant.client, grant.account, grant.generation, JSON.stringify(grant.projects), grant.resource, grant.accessExpiresAt, grant.refreshHash);
10032
+ this.db.prepare("INSERT INTO oauth_refresh (token, hash) VALUES (?, ?)").run(token.id, grant.refreshHash);
10033
+ this.db.prepare("UPDATE oauth_code SET token = ? WHERE hash = ?").run(token.id, code);
10034
+ });
10035
+ }
10036
+ /** The grant an API token signs in for, or null when the token is an ordinary one. */
10037
+ oauthGrant(token) {
10038
+ const row = this.db.prepare("SELECT * FROM oauth_grant WHERE token = ?").get(token);
10039
+ return row === undefined ? null : { token: String(row["token"]), client: String(row["client"]), account: String(row["account"]), generation: Number(row["generation"]), projects: stringList(row["projects_json"]),
10040
+ resource: String(row["resource"]), accessExpiresAt: String(row["access_expires_at"]), refreshHash: String(row["renew_hash"]) };
10041
+ }
10042
+ /**
10043
+ * Classify, validate and rotate under one write transaction. Every issued hash remains in the family, so any
10044
+ * older secret revokes even a concurrent winner. An unknown guess or the wrong client cannot end the family.
10045
+ */
10046
+ rotateOAuthGrant(token, ask, next, now, projectsFor) {
10047
+ return this.transact(() => {
10048
+ const invalid = { ok: false, reason: "invalid_grant" };
10049
+ const grant = this.oauthGrant(token), kept = this.apiTokenSecret(token)?.row;
10050
+ if (grant === null || kept === undefined || grant.client !== ask.client || grant.resource !== ask.resource)
10051
+ return invalid;
10052
+ const issued = this.db.prepare("SELECT 1 FROM oauth_refresh WHERE token = ? AND hash = ?").get(token, ask.refreshHash);
10053
+ if (issued === undefined)
10054
+ return invalid;
10055
+ if (grant.refreshHash !== ask.refreshHash) {
10056
+ this.revokeApiToken(token, "toolroll", now, "its refresh secret was used twice");
10057
+ return invalid;
10058
+ }
10059
+ if (kept.revokedAt !== null || !(Date.parse(kept.expiresAt) > now.getTime()))
10060
+ return invalid;
10061
+ const account = this.accountOf(grant.account);
10062
+ if (account === null || account.revokedAt !== null || account.generation !== grant.generation || (kept.access === "act" && account.role !== "approver")) {
10063
+ this.revokeApiToken(token, "toolroll", now, "the person's sign-in changed");
10064
+ return invalid;
10065
+ }
10066
+ const mine = projectsFor(grant.account);
10067
+ if (!grant.projects.some(project => mine.includes(project) && (kept.projects === null || kept.projects.includes(project)))) {
10068
+ this.revokeApiToken(token, "toolroll", now, "its projects are no longer the person's");
10069
+ return invalid;
10070
+ }
10071
+ if (ask.scope !== undefined && !ask.scope.split(" ").every(one => one === kept.access || one === "read"))
10072
+ return { ok: false, reason: "invalid_scope" };
10073
+ const moved = this.db.prepare("UPDATE oauth_grant SET renew_hash = ?, access_expires_at = ? WHERE token = ? AND renew_hash = ?")
10074
+ .run(next.refreshHash, next.accessExpiresAt, token, ask.refreshHash);
10075
+ if (Number(moved.changes) !== 1)
10076
+ return invalid;
10077
+ this.db.prepare("UPDATE api_token SET secret_hash = ? WHERE id = ? AND revoked_at IS NULL").run(next.accessHash, token);
10078
+ this.db.prepare("INSERT INTO oauth_refresh (token, hash) VALUES (?, ?)").run(token, next.refreshHash);
10079
+ return { ok: true, access: kept.access };
10080
+ });
10081
+ }
9840
10082
  saveWebSession(session) {
9841
10083
  this.db.prepare(`INSERT INTO web_session (id_hash, account, csrf, role, generation, created_at, last_seen, project, project_revision, sso_at, agent, address) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
9842
10084
  ON CONFLICT(id_hash) DO UPDATE SET last_seen = excluded.last_seen, project = excluded.project, project_revision = excluded.project_revision, sso_at = excluded.sso_at`)
@@ -17294,6 +17536,27 @@ export class Store {
17294
17536
  const row = this.db.prepare("SELECT * FROM run_checkpoint WHERE task_ref = ? ORDER BY id DESC LIMIT 1").get(taskRef);
17295
17537
  return row === undefined ? null : readRunCheckpoint(row);
17296
17538
  }
17539
+ /** What the agent did last on this run (live tasks): one fixed kind and when, replacing the last. */
17540
+ recordRunActivity(run, kind, now) {
17541
+ this.db.prepare("INSERT INTO run_activity (run, kind, at) VALUES (?, ?, ?) ON CONFLICT (run) DO UPDATE SET kind = excluded.kind, at = excluded.at")
17542
+ .run(run, kind, now.toISOString());
17543
+ }
17544
+ /** A run's last recorded activity and its newest progress checkpoint's time, in one read. */
17545
+ runActivity(run) {
17546
+ const row = this.db.prepare(`SELECT (SELECT kind FROM run_activity WHERE run = ?1) AS kind, (SELECT at FROM run_activity WHERE run = ?1) AS at,
17547
+ (SELECT created_at FROM run_checkpoint WHERE run = ?1 ORDER BY id DESC LIMIT 1) AS progress_at`).get(run);
17548
+ const kind = typeof row["kind"] === "string" && LIVE_ACTIVITY_KINDS.includes(row["kind"]) ? row["kind"] : null;
17549
+ return { kind, at: kind === null ? null : String(row["at"]), progressAt: row["progress_at"] == null ? null : String(row["progress_at"]) };
17550
+ }
17551
+ /** The live log's own retention: a finished run's activity goes once older than `retainMs`, and beyond the newest
17552
+ * `bound` finished runs; a live run's always stays. */
17553
+ sweepRunActivity(now, retainMs, bound) {
17554
+ const finished = "run IN (SELECT id FROM run WHERE outcome IS NOT NULL)";
17555
+ const aged = this.db.prepare(`DELETE FROM run_activity WHERE at < ? AND ${finished}`).run(new Date(now.getTime() - retainMs).toISOString()).changes;
17556
+ const over = this.db.prepare(`DELETE FROM run_activity WHERE ${finished} AND run NOT IN (SELECT run FROM run_activity WHERE ${finished} ORDER BY at DESC, run DESC LIMIT ?)`)
17557
+ .run(Math.max(0, Math.floor(bound))).changes;
17558
+ return Number(aged) + Number(over);
17559
+ }
17297
17560
  /** One run's full checkpoint history, oldest first. */
17298
17561
  checkpointHistory(run) {
17299
17562
  return this.db.prepare("SELECT * FROM run_checkpoint WHERE run = ? ORDER BY id ASC").all(run).map(readRunCheckpoint);
package/dist/surface.js CHANGED
@@ -135,6 +135,16 @@ const GUIDE_ROWS = [
135
135
  { invocation: "brief", synopsis: "with a saved central profile, --lead <id> --conversation <id> reads messages, proposals and saved work; --request-id inspects your saved receipt without resending; --local uses local DB catch-up, where --history selects the older operational report", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
136
136
  { invocation: "gaps", synopsis: "requirement gaps blocking dispatch", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag, repoFlag] },
137
137
  { invocation: "grants", synopsis: "list authority grants", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
138
+ { invocation: "audit", synopsis: "what people did on this server with their API tokens, newest first: when, who, which token, from the CLI (api) or an agent (mcp), the command or tool, the project and the outcome (ok, refused, error); an approver sees everyone in their projects, anyone else only themselves",
139
+ audience: "agent", agentMayInvoke: true, mutation: "none",
140
+ flags: [jsonFlag, dbFlag,
141
+ { name: "person", takesValue: true, meaning: "only this person's actions" },
142
+ { name: "token", takesValue: true, meaning: "only actions with the token of this name (on the server's own machine)" },
143
+ { name: "token-name", takesValue: true, meaning: "the same, where --token would be a credential (remotely)" },
144
+ { name: "source", takesValue: true, meaning: "api (the CLI) or mcp (an agent)" },
145
+ { name: "since", takesValue: true, meaning: "only the last N days, as Nd (7d)" },
146
+ { name: "limit", takesValue: true, meaning: "actions per page (default 20, at most 100)" },
147
+ { name: "cursor", takesValue: true, meaning: "the nextCursor of the previous page" }] },
138
148
  { invocation: "sync", synopsis: "refresh external-tracker mirrors and deliver write-backs — safe to run; fails closed", audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
139
149
  flags: [jsonFlag, dbFlag, repoFlag], notableReasons: ["no-op", "external"] },
140
150
  { invocation: "task add", synopsis: "queue work", audience: "agent", agentMayInvoke: true, mutation: "keyed",
@@ -377,6 +387,10 @@ const GUIDE_ROWS = [
377
387
  operator("people invite", "mint a single-use sign-in link for one person — their powers are pinned when you mint, never after"),
378
388
  operator("people projects", "set a person's project access — selected project, all projects, or no projects"),
379
389
  operator("people revoke", "end a person's access — their sessions, invites, and signed modes end with them; history stays"),
390
+ operator("tokens create", "make one of your API tokens: --name, --access read|act, --days 30|90|365, optional --projects a,b; your password at the prompt or --password-stdin; the token is shown once"),
391
+ operatorRead("tokens list", "your API tokens: name, access, projects, made, last used, expiry — never a secret; your password at the prompt or --password-stdin"),
392
+ operator("tokens revoke", "end one of your API tokens now, by name or id; your password at the prompt or --password-stdin"),
393
+ operator("tokens rotate", "replace one of your API tokens on the same terms; the old one works 10 more minutes (--overlap), then stops; the new one is shown once"),
380
394
  operatorRead("mode show", "the repository's operating mode, in full — or 'locked' when none is signed"),
381
395
  operator("mode set", "sign a per-repository operating mode (password ceremony; standard or hands-off, always expiring)"),
382
396
  operator("publish merge", "say yes to ONE waiting merge — it fires only when CI is seen green on the exact commit you authorized"),
@@ -411,6 +425,7 @@ const GUIDE_ROWS = [
411
425
  operator("publish unblock", "lift a repair's merge hold"),
412
426
  operator("publish rearm", "re-arm a refused merge after fixing the named cause"),
413
427
  operator("serve", "the operations console (HTTP)"),
428
+ operatorRead("serve check-public", "is this server ready on its --public-url? checks the URL, --allow-host, the console on --port, the certificate, HSTS and the proxy, and names each problem; sends no credentials"),
414
429
  operator("watch", "the unattended loop, kept running"),
415
430
  operator("up", "console + worker + browser, one command"),
416
431
  operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), offer the starter flows (--starter <ids>), and print the handoff"),
@@ -436,7 +451,7 @@ const REMOTE_NO = [
436
451
  // Workers, the loop and the services that run here.
437
452
  "claim", "heartbeat", "release", "reap", "tick", "build", "reconcile",
438
453
  "runner register", "runner retire", "runner bind", "runner capacity", "coordinator mint", "coordinator revoke", "mcp", "outbox deliver", "peek",
439
- "serve", "watch", "up", "onboard", "daemon", "bridge", "setup show", "setup clear",
454
+ "serve", "serve check-public", "watch", "up", "onboard", "daemon", "bridge", "setup show", "setup clear",
440
455
  // Provider keys and model tools on this machine.
441
456
  "providers", "keys status", "keys set", "keys clear", "keys verify", "keys auth", "models check", "models update", "models watch",
442
457
  // Publication and release.
@@ -452,11 +467,13 @@ const REMOTE_STEP_UP = [
452
467
  "proposals", "decide", "task regate", "task approve", "task accept", "task merge", "run settle", "routine approve", "knowledge apply", "memory apply",
453
468
  "flows card approve", "flows card send-back", "flows script approve",
454
469
  "approver list", "approver add", "people list", "people invite", "people projects", "people revoke",
470
+ // A person's own API tokens: a password step-up on the server's machine, or the console. Never with a token.
471
+ "tokens create", "tokens list", "tokens revoke", "tokens rotate",
455
472
  "mode show", "mode set", "mode revoke", "chat-approval on", "review on", "review off", "config set", "config clear",
456
473
  "verify set", "verify clear", "verify level", "project checks", "project concurrency", "intake grant", "intake clear", "webhook primary",
457
474
  ];
458
475
  const REMOTE_YES = [
459
- "status", "integrations", "ready", "gaps", "grants", "sync",
476
+ "status", "integrations", "ready", "gaps", "grants", "sync", "audit",
460
477
  "task add", "task ask", "task checks", "task add-tests", "task list", "task show", "check-progress", "task wait", "task complete", "task revise",
461
478
  "task state", "task block", "task unblock", "task next", "task steer", "task assign", "task scope", "task plan", "task hold", "task unhold", "task require",
462
479
  "task requeue", "task review", "task repair", "task route", "task reopen", "task stop", "task resume",