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.
- package/CHANGELOG.md +33 -0
- package/dist/activity-line.d.ts +26 -0
- package/dist/activity-line.js +47 -0
- package/dist/api-tokens.d.ts +33 -0
- package/dist/api-tokens.js +39 -0
- package/dist/audit-cli.d.ts +17 -0
- package/dist/audit-cli.js +86 -0
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-workspace.d.ts +18 -1
- package/dist/browser-workspace.js +9 -3
- package/dist/builder.js +4 -2
- package/dist/cli-http.d.ts +5 -0
- package/dist/cli-http.js +12 -2
- package/dist/cli.js +7 -1
- package/dist/contracts/cli.d.ts +47 -0
- package/dist/contracts/cli.js +11 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/mcp-oauth.d.ts +53 -0
- package/dist/contracts/mcp-oauth.js +40 -0
- package/dist/credentials-ui.d.ts +5 -0
- package/dist/credentials-ui.js +16 -4
- package/dist/export.js +4 -3
- package/dist/flow-card-task.d.ts +2 -0
- package/dist/flow-card-task.js +57 -0
- package/dist/flow-live.d.ts +2 -2
- package/dist/flow-live.js +69 -8
- package/dist/flows-ui.js +15 -1
- package/dist/live.d.ts +14 -0
- package/dist/live.js +71 -22
- package/dist/mcp-http.d.ts +6 -1
- package/dist/mcp-http.js +22 -4
- package/dist/mcp-oauth.d.ts +82 -0
- package/dist/mcp-oauth.js +479 -0
- package/dist/mcp-person.js +1 -1
- package/dist/operate-remote-arguments.js +2 -0
- package/dist/operate-remote.d.ts +1 -1
- package/dist/operate-remote.js +7 -1
- package/dist/operate.d.ts +3 -0
- package/dist/operate.js +58 -2
- package/dist/people-audit-ui.d.ts +14 -0
- package/dist/people-audit-ui.js +38 -0
- package/dist/planner.js +4 -3
- package/dist/public-access.d.ts +50 -0
- package/dist/public-access.js +87 -0
- package/dist/public-check.d.ts +37 -0
- package/dist/public-check.js +170 -0
- package/dist/remote-audit.d.ts +79 -0
- package/dist/remote-audit.js +161 -0
- package/dist/request-budget-ui.d.ts +11 -0
- package/dist/request-budget-ui.js +31 -0
- package/dist/request-budget.d.ts +96 -0
- package/dist/request-budget.js +227 -0
- package/dist/serve.d.ts +9 -1
- package/dist/serve.js +211 -52
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +134 -2
- package/dist/store.js +270 -7
- package/dist/surface.js +19 -2
- package/dist/task-activity.d.ts +27 -0
- package/dist/task-activity.js +53 -0
- package/dist/task-live.d.ts +17 -0
- package/dist/task-live.js +144 -0
- package/dist/token-notices.d.ts +22 -0
- package/dist/token-notices.js +52 -0
- package/dist/tokens-cli.d.ts +21 -0
- package/dist/tokens-cli.js +184 -0
- 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 =
|
|
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
|
-
|
|
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
|
-
|
|
9812
|
-
|
|
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",
|