toolroll 0.9.38 → 0.9.40
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 +28 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +19 -19
- package/dist/browser-workspace.d.ts +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/mate-cli.js +4 -1
- package/dist/mate-doors.d.ts +30 -1
- package/dist/mate-doors.js +40 -4
- package/dist/serve.js +86 -15
- package/dist/store.d.ts +5 -0
- package/dist/store.js +14 -0
- package/dist/team-chat-authorization.d.ts +11 -0
- package/dist/team-chat-authorization.js +33 -0
- package/dist/team-contract.d.ts +11 -7
- package/dist/team-runtime.d.ts +3 -5
- package/dist/team-runtime.js +3 -23
- package/package.json +1 -1
|
@@ -100,6 +100,10 @@ export type BrowserConversation = {
|
|
|
100
100
|
resultRunId: number | null;
|
|
101
101
|
/** A project's own thread (v77); absent or null for the lead conversation or a task. */
|
|
102
102
|
project?: string | null;
|
|
103
|
+
/** The lead thread a change in reachable projects replaced (mate arc ruling 9): shown above a divider, read only. */
|
|
104
|
+
previous?: {
|
|
105
|
+
messages: BrowserMessage[];
|
|
106
|
+
} | null;
|
|
103
107
|
};
|
|
104
108
|
/** A page rebuilt as React components (shadcn/ui). The server still renders
|
|
105
109
|
* its HTML as the no-JavaScript fallback; forms post to the same routes. */
|
|
@@ -176,6 +180,8 @@ export type BrowserLimits = {
|
|
|
176
180
|
export type BrowserSettingsView = {
|
|
177
181
|
kind: 'settings';
|
|
178
182
|
said: string | null;
|
|
183
|
+
/** Using it from a phone, under Chat apps (approvers only). */
|
|
184
|
+
phone?: BrowserPhoneCard;
|
|
179
185
|
/** The settings destinations under short headings; a chat app's tile says whether it is connected and gets alerts. */
|
|
180
186
|
groups: BrowserSettingsGroup[];
|
|
181
187
|
theme: 'system' | 'light' | 'dark';
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -125,6 +125,7 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
125
125
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
126
126
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
127
127
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
128
|
+
version?: never;
|
|
128
129
|
active: boolean;
|
|
129
130
|
phase: string;
|
|
130
131
|
detail: string;
|
|
@@ -138,7 +139,6 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
138
139
|
wasRunning?: never;
|
|
139
140
|
backupPath?: never;
|
|
140
141
|
updateId?: never;
|
|
141
|
-
version?: never;
|
|
142
142
|
buildId?: never;
|
|
143
143
|
workDir?: never;
|
|
144
144
|
} | {
|
package/dist/mate-cli.js
CHANGED
|
@@ -316,7 +316,10 @@ export async function runMateCli(input) {
|
|
|
316
316
|
say(done ? `dismissed ${act[2]}` : "that proposal was already acted on");
|
|
317
317
|
}
|
|
318
318
|
else {
|
|
319
|
-
const outcome = confirmMateProposal(store, who, proposal.id, now, { confirm: act[3] !== undefined, via: "cli",
|
|
319
|
+
const outcome = confirmMateProposal(store, who, proposal.id, now, { confirm: act[3] !== undefined, via: "cli", chatProvider: () => {
|
|
320
|
+
const current = store.getChatConfig();
|
|
321
|
+
return current === null ? null : { config: current, key: isDirectChatProvider(current.provider) ? keyFor(current, input.databaseFile, env) : null };
|
|
322
|
+
}, ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }) });
|
|
320
323
|
emit({ ok: outcome.ok, act: "confirm", proposal: proposal.id, ...(outcome.ok ? { said: outcome.said, taskId: outcome.taskId } : { reason: outcome.reason, said: outcome.said }) });
|
|
321
324
|
say(outcome.ok ? outcome.said : outcome.reason === "needs-confirm" ? `${outcome.said}: confirm ${act[2]} yes` : `refused: ${outcome.said}`);
|
|
322
325
|
if (outcome.ok && outcome.kind === "scope" && outcome.taskId !== null)
|
package/dist/mate-doors.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type SharedActionOptions } from './chat-actions.js';
|
|
2
2
|
import { type CoordinatorProposal, type MateProposal, type Store } from "./store.js";
|
|
3
3
|
import type { VerifiedApprover } from "./principal.js";
|
|
4
|
+
import { type TeamChatProviderResolver } from './team-chat-authorization.js';
|
|
4
5
|
export type ProposalKind = MateProposal["kind"];
|
|
5
6
|
export type DoorOutcome = {
|
|
6
7
|
ok: true;
|
|
@@ -14,8 +15,10 @@ export type DoorOutcome = {
|
|
|
14
15
|
reason: DoorRefusal;
|
|
15
16
|
said: string;
|
|
16
17
|
};
|
|
17
|
-
export type DoorRefusal = "standing" | "not-yours" | "ceiling-changed" | "not-pending" | "session-ended" | "turn-not-answered" | "not-confirmable" | "needs-confirm" | "stale" | "claimed" | "not-queued" | "contest-open" | "unknown-task" | "unknown-decision" | "already-answered" | "expired" | "outside-ceiling" | "refused";
|
|
18
|
+
export type DoorRefusal = "standing" | "not-yours" | "ceiling-changed" | "not-pending" | "session-ended" | "turn-not-answered" | "turn-running" | "not-confirmable" | "needs-confirm" | "stale" | "claimed" | "not-queued" | "contest-open" | "unknown-task" | "unknown-decision" | "already-answered" | "expired" | "outside-ceiling" | "refused";
|
|
18
19
|
export type DoorOptions = {
|
|
20
|
+
/** Resolved inside the transaction. Without it, only current subscription terms are available. */
|
|
21
|
+
chatProvider?: TeamChatProviderResolver;
|
|
19
22
|
evidenceRoot?: string;
|
|
20
23
|
/** Only the secure human review endpoint supplies this; never saved or model-authored. */
|
|
21
24
|
actionReview?: SharedActionOptions["review"];
|
|
@@ -36,6 +39,32 @@ export type DoorOptions = {
|
|
|
36
39
|
};
|
|
37
40
|
/** A coordinator proposal lives seven days (§9); the door refuses an older one whatever the sweep did (v3 review, finding 3). */
|
|
38
41
|
export declare const COORDINATOR_PROPOSAL_TTL_MS: number;
|
|
42
|
+
/** The plain reasons a card shows instead of its controls; the door refuses with the same words. */
|
|
43
|
+
export declare const PROPOSAL_CONTRIBUTOR_REASON = "An authorized contributor can act on this proposal.";
|
|
44
|
+
export declare const PROPOSAL_CHAT_REASON = "Enable chat in this conversation before acting on a proposal.";
|
|
45
|
+
export declare const PROPOSAL_WAIT_REASON = "Available when the current reply finishes.";
|
|
46
|
+
/** Unbound claims get the same wall-clock allowance as a provider turn. */
|
|
47
|
+
export declare const TEAM_UNBOUND_CLAIM_MS = 120000;
|
|
48
|
+
export type ProposalGate = {
|
|
49
|
+
ok: true;
|
|
50
|
+
} | {
|
|
51
|
+
ok: false;
|
|
52
|
+
reason: Extract<DoorRefusal, "not-yours" | "session-ended" | "turn-running">;
|
|
53
|
+
said: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Whether `who` may act on a card in `thread` right now: the one gate the
|
|
57
|
+
* cards and the confirm door share. Re-proved from saved state and live provider facts, so
|
|
58
|
+
* every caller (console, CLI, Telegram) reaches the same answer: the thread
|
|
59
|
+
* is theirs (or they contribute to its team conversation), chat is enabled
|
|
60
|
+
* for it under their current generation, and no turn in that thread is
|
|
61
|
+
* live. A team message still marked running after its turn finished is not
|
|
62
|
+
* live; the delivery sweep settles it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function proposalActGate(store: Store, who: {
|
|
65
|
+
name: string;
|
|
66
|
+
generation: number;
|
|
67
|
+
}, thread: number, now: Date, provider?: TeamChatProviderResolver): ProposalGate;
|
|
39
68
|
export declare function confirmMateProposal(store: Store, who: VerifiedApprover, proposalId: number, now: Date, options: DoorOptions): DoorOutcome;
|
|
40
69
|
/** The operator declines the mate's card: `pending → dismissed`. */
|
|
41
70
|
export declare function dismissMateProposal(store: Store, who: VerifiedApprover, proposalId: number, now: Date): boolean;
|
package/dist/mate-doors.js
CHANGED
|
@@ -17,6 +17,9 @@ import { executeSharedAction, sharedActionPayload, sharedActionAllowsChallenge,
|
|
|
17
17
|
import { isCheckLevel, setTaskCheckLevel } from "./check-levels.js";
|
|
18
18
|
import { mateTurnKeepsProposals, verifiedAuthor } from "./store.js";
|
|
19
19
|
import { isVerifiedApprover, reproveApprover } from "./principal.js";
|
|
20
|
+
import { TURN_WALL_CLOCK_MS } from './converse.js';
|
|
21
|
+
import { teamChatAuthorization, subscriptionTeamChatProvider } from './team-chat-authorization.js';
|
|
22
|
+
import { readProjectAccess } from "./project-access.js";
|
|
20
23
|
import { TeamLeads } from './team-leads.js';
|
|
21
24
|
import { fileTaskProposal } from "./proposal.js";
|
|
22
25
|
import { proposeGuarded } from "./scope.js";
|
|
@@ -74,6 +77,38 @@ function recordInTaskChat(store, who, proposal, outcome, via, now) {
|
|
|
74
77
|
const thread = store.openMateThread(who.name, who.ceilingDigest, now, { kind: "task", key: root }).thread;
|
|
75
78
|
store.appendMateMessage({ thread: thread.id, turn: null, role: "assistant", text: `${where} — ${label}: ${said}` }, now);
|
|
76
79
|
}
|
|
80
|
+
/** The plain reasons a card shows instead of its controls; the door refuses with the same words. */
|
|
81
|
+
export const PROPOSAL_CONTRIBUTOR_REASON = "An authorized contributor can act on this proposal.";
|
|
82
|
+
export const PROPOSAL_CHAT_REASON = "Enable chat in this conversation before acting on a proposal.";
|
|
83
|
+
export const PROPOSAL_WAIT_REASON = "Available when the current reply finishes.";
|
|
84
|
+
/** Unbound claims get the same wall-clock allowance as a provider turn. */
|
|
85
|
+
export const TEAM_UNBOUND_CLAIM_MS = TURN_WALL_CLOCK_MS;
|
|
86
|
+
/**
|
|
87
|
+
* Whether `who` may act on a card in `thread` right now: the one gate the
|
|
88
|
+
* cards and the confirm door share. Re-proved from saved state and live provider facts, so
|
|
89
|
+
* every caller (console, CLI, Telegram) reaches the same answer: the thread
|
|
90
|
+
* is theirs (or they contribute to its team conversation), chat is enabled
|
|
91
|
+
* for it under their current generation, and no turn in that thread is
|
|
92
|
+
* live. A team message still marked running after its turn finished is not
|
|
93
|
+
* live; the delivery sweep settles it.
|
|
94
|
+
*/
|
|
95
|
+
export function proposalActGate(store, who, thread, now, provider = () => subscriptionTeamChatProvider(store)) {
|
|
96
|
+
const row = store.getMateThread(thread);
|
|
97
|
+
const shared = row === null ? undefined : store.handle.prepare("SELECT id,projects_json FROM team_conversation WHERE thread=?").get(thread);
|
|
98
|
+
if (row === null || (shared ? !store.canUseTeamMateThread(who.name, who.generation, thread) : row.approver !== who.name))
|
|
99
|
+
return { ok: false, reason: "not-yours", said: PROPOSAL_CONTRIBUTOR_REASON };
|
|
100
|
+
const session = shared ? store.teamMateSession(who.name, thread) : store.activeMateSession(who.name);
|
|
101
|
+
const authorized = shared
|
|
102
|
+
? teamChatAuthorization(store, who, { id: String(shared['id']), threadId: thread, projects: [...(readProjectAccess(shared['projects_json']) ?? [])] }, provider()).enabled
|
|
103
|
+
: session !== null && session.endedAt === null && session.approverGeneration === who.generation;
|
|
104
|
+
if (!authorized) {
|
|
105
|
+
return { ok: false, reason: "session-ended", said: PROPOSAL_CHAT_REASON };
|
|
106
|
+
}
|
|
107
|
+
const live = store.handle.prepare(`SELECT 1 FROM mate_turn WHERE thread=? AND state IN ('queued','running')
|
|
108
|
+
UNION ALL SELECT 1 FROM team_message q JOIN team_conversation c ON c.id=q.conversation LEFT JOIN mate_turn t ON t.id=q.turn_id
|
|
109
|
+
WHERE c.thread=? AND q.status='running' AND ((q.turn_id IS NULL AND q.claimed_at>?) OR t.state IN ('queued','running')) LIMIT 1`).get(thread, thread, new Date(now.getTime() - TEAM_UNBOUND_CLAIM_MS).toISOString());
|
|
110
|
+
return live === undefined ? { ok: true } : { ok: false, reason: "turn-running", said: PROPOSAL_WAIT_REASON };
|
|
111
|
+
}
|
|
77
112
|
export function confirmMateProposal(store, who, proposalId, now, options) {
|
|
78
113
|
if (!isVerifiedApprover(who))
|
|
79
114
|
return { ok: false, kind: null, reason: "standing", said: "your approver standing changed — sign in again" };
|
|
@@ -89,10 +124,11 @@ export function confirmMateProposal(store, who, proposalId, now, options) {
|
|
|
89
124
|
const shared = thread !== null && store.handle.prepare("SELECT id,lead FROM team_conversation WHERE thread=?").get(thread.id);
|
|
90
125
|
if (thread === null || (shared ? !store.canUseTeamMateThread(who.name, who.generation, thread.id) : thread.approver !== who.name))
|
|
91
126
|
return { ok: false, kind: null, reason: "not-yours", said: "no such proposal" };
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
127
|
+
// The card's own gate: chat enabled for this thread and no live turn in it.
|
|
128
|
+
const gate = proposalActGate(store, who, thread.id, now, options.chatProvider);
|
|
129
|
+
if (!gate.ok)
|
|
130
|
+
return { ok: false, kind: proposal.kind, reason: gate.reason, said: gate.said };
|
|
131
|
+
const session = (shared ? store.teamMateSession(who.name, thread.id) : store.activeMateSession(who.name));
|
|
96
132
|
if (proposal.ceilingDigest !== who.ceilingDigest || session.ceilingDigest !== who.ceilingDigest) {
|
|
97
133
|
return { ok: false, kind: proposal.kind, reason: "ceiling-changed", said: "the admitted projects changed since this was proposed — it cannot be confirmed" };
|
|
98
134
|
}
|
package/dist/serve.js
CHANGED
|
@@ -272,7 +272,7 @@ import { checkTeamsCredentials, clearTeamsCredentials, loadTeamsCredentials, sav
|
|
|
272
272
|
import { verifyApproverByPassword, verifyApproverStanding } from "./principal.js";
|
|
273
273
|
import { runMateTurn, MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
274
274
|
import { replyHtmlInline, shapeReply } from "./reply-shape.js";
|
|
275
|
-
import { confirmCoordinatorProposal, confirmMateProposal, dismissCoordinatorProposal, dismissMateProposal } from "./mate-doors.js";
|
|
275
|
+
import { confirmCoordinatorProposal, confirmMateProposal, dismissCoordinatorProposal, dismissMateProposal, proposalActGate, PROPOSAL_WAIT_REASON } from "./mate-doors.js";
|
|
276
276
|
import { envValue } from "./names.js";
|
|
277
277
|
import { cachedRelease, isNewer, runnerVersions, setUpdateChecks, updateChecksOff } from "./releases.js";
|
|
278
278
|
import { installMethod } from "./install-method.js";
|
|
@@ -739,11 +739,25 @@ export function createDecisionServer(options) {
|
|
|
739
739
|
const connectVisits = new Map();
|
|
740
740
|
/** Flows open in a browser (v88): who's here, and a nudge when one changes. */
|
|
741
741
|
const flowRooms = createFlowRooms(flow => flowFingerprint(store, flow));
|
|
742
|
+
const teamChatProvider = () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; };
|
|
742
743
|
const team = createTeamRuntime({ store, repos: codingProjects, evidenceRoot, clock, workspaceRevision,
|
|
743
744
|
...(options.chatFetcher ? { fetcher: options.chatFetcher } : {}),
|
|
744
|
-
provider:
|
|
745
|
+
provider: teamChatProvider,
|
|
745
746
|
...(options.subscriptionChatRunner ? { subscriptionRunner: options.subscriptionChatRunner } : {}),
|
|
746
747
|
});
|
|
748
|
+
// Only browser sessions receive HTML cards. The CLI keeps its summary contract.
|
|
749
|
+
const teamBrowserReply = (reply, actor, csrf) => {
|
|
750
|
+
const snapshot = reply.snapshot;
|
|
751
|
+
if (!snapshot?.selected || !snapshot.proposals)
|
|
752
|
+
return reply;
|
|
753
|
+
return { ...reply, snapshot: { ...snapshot, proposals: snapshot.proposals.map(summary => {
|
|
754
|
+
const proposal = store.getMateProposal(summary.id);
|
|
755
|
+
if (!proposal || proposal.thread !== snapshot.selected.threadId)
|
|
756
|
+
return summary;
|
|
757
|
+
const decision = proposal.kind === 'answer' && typeof proposal.payload['decision'] === 'number' ? store.getDecision(proposal.payload['decision']) : null;
|
|
758
|
+
return { ...summary, card: teamProposalCardParts(store, actor, proposal, snapshot, csrf, decision, clock(), teamChatProvider).card };
|
|
759
|
+
}) } };
|
|
760
|
+
};
|
|
747
761
|
const teamEndpoint = (request, response) => handleTeamHttp(request, response, {
|
|
748
762
|
authenticate: request => {
|
|
749
763
|
const who = identify(request, request.method === 'POST');
|
|
@@ -773,7 +787,11 @@ export function createDecisionServer(options) {
|
|
|
773
787
|
return false;
|
|
774
788
|
return request.headers['x-csrf-token'] === who.session.csrf;
|
|
775
789
|
},
|
|
776
|
-
execute:
|
|
790
|
+
execute: async (actor, input) => {
|
|
791
|
+
const reply = await team.execute(actor, input);
|
|
792
|
+
const who = identify(request, false);
|
|
793
|
+
return who?.via === 'cookie' ? teamBrowserReply(reply, actor, who.session.csrf) : reply;
|
|
794
|
+
}, cursor: team.cursor, streams: teamStreams,
|
|
777
795
|
});
|
|
778
796
|
const sessionEndpoint = createSessionEndpoint({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo) });
|
|
779
797
|
/** The enumerable admission list for roll-up SQL: repos-only ceilings
|
|
@@ -3585,11 +3603,11 @@ export function createDecisionServer(options) {
|
|
|
3585
3603
|
if (who.via !== "cookie")
|
|
3586
3604
|
return refuse(response, who, 403, "chat is a browser surface — it keeps your drafts in the session");
|
|
3587
3605
|
if (url.searchParams.get('private') !== '1' && !url.searchParams.has('task') && !url.searchParams.has('result') && !url.searchParams.has('project')) {
|
|
3588
|
-
const reply = await team.execute({ name: who.name, generation: who.session.generation }, {
|
|
3606
|
+
const reply = teamBrowserReply(await team.execute({ name: who.name, generation: who.session.generation }, {
|
|
3589
3607
|
operation: url.searchParams.has('conversation') ? 'show' : 'list',
|
|
3590
3608
|
args: { ...(url.searchParams.has('conversation') ? { conversationId: url.searchParams.get('conversation') } : {}),
|
|
3591
3609
|
...(url.searchParams.has('lead') ? { leadId: url.searchParams.get('lead') } : {}) },
|
|
3592
|
-
});
|
|
3610
|
+
}), { name: who.name, generation: who.session.generation }, who.session.csrf);
|
|
3593
3611
|
if (!reply.ok || !reply.snapshot)
|
|
3594
3612
|
return refuse(response, who, 403, reply.message, '/chat?private=1');
|
|
3595
3613
|
if (url.searchParams.has('proposal')) {
|
|
@@ -3599,11 +3617,7 @@ export function createDecisionServer(options) {
|
|
|
3599
3617
|
return refuse(response, who, 404, 'This proposal is unavailable.', '/chat');
|
|
3600
3618
|
const back = '/chat?conversation=' + encodeURIComponent(selected.id);
|
|
3601
3619
|
const decision = proposal.kind === 'answer' && typeof proposal.payload['decision'] === 'number' ? store.getDecision(proposal.payload['decision']) : null;
|
|
3602
|
-
const
|
|
3603
|
-
const canAct = reply.snapshot.canSend && reply.snapshot.chatAuthorization?.enabled === true;
|
|
3604
|
-
let card = mateProposalCard(proposal, who.session.csrf, !canAct || active, decision, back);
|
|
3605
|
-
if (!canAct)
|
|
3606
|
-
card = card.replace('Available when the current turn finishes.', reply.snapshot.canSend ? 'Enable chat in this conversation before acting on a proposal.' : 'An authorized contributor can act on this proposal.');
|
|
3620
|
+
const card = teamProposalCardParts(store, { name: who.name, generation: who.session.generation }, proposal, reply.snapshot, who.session.csrf, decision, clock(), teamChatProvider).html;
|
|
3607
3621
|
return sendScreen(response, 200, screen('Review action', `<p><a href="${escape(back)}">Back to conversation</a></p>` + card, { chrome: chromeFor(null, 'chat', undefined, 'all') }));
|
|
3608
3622
|
}
|
|
3609
3623
|
if (reply.snapshot.leads.length > 0 || url.searchParams.has('conversation') || url.searchParams.get('team') === '1') {
|
|
@@ -4290,7 +4304,7 @@ export function createDecisionServer(options) {
|
|
|
4290
4304
|
})(), (() => {
|
|
4291
4305
|
const chosen = store.notificationPreference(who.name);
|
|
4292
4306
|
return { mode: chosen.mode, digestAt: chosen.digestAt, screenshots: chosen.screenshots, projects: notificationProjects(store, who.name) };
|
|
4293
|
-
})(), telegramTrouble(store, loadBotToken(process.env, options.telegramTokenFile))));
|
|
4307
|
+
})(), telegramTrouble(store, loadBotToken(process.env, options.telegramTokenFile)), who.via === "cookie" ? phoneSetup(who) ?? null : null));
|
|
4294
4308
|
}
|
|
4295
4309
|
if (url.pathname === "/push/key") {
|
|
4296
4310
|
// Session-gated: the key is not secret, but strangers get nothing.
|
|
@@ -4464,12 +4478,19 @@ export function createDecisionServer(options) {
|
|
|
4464
4478
|
return first !== undefined && String(first["id"]) === rootId;
|
|
4465
4479
|
}
|
|
4466
4480
|
/** After the first Ready result, once (onboarding): the phone, by a chat app or the console over Tailscale. */
|
|
4481
|
+
/** Chat's one-line pointer to the phone setup: after the first Ready result, until put away or Telegram is paired. */
|
|
4467
4482
|
function phoneCard(who, now) {
|
|
4468
4483
|
if (who.role !== "approver" || store.isDemo() || store.firstSuccessAt(now) === null || store.installationFact(PHONE_CARD_FACT) !== null)
|
|
4469
4484
|
return undefined;
|
|
4470
4485
|
const botId = options.telegramTokenFile === undefined ? null : loadBotToken(process.env, options.telegramTokenFile)?.botId ?? null;
|
|
4471
4486
|
if (botId !== null && store.liveTelegramBindings(botId).length > 0)
|
|
4472
4487
|
return undefined;
|
|
4488
|
+
return phoneSetup(who);
|
|
4489
|
+
}
|
|
4490
|
+
/** The phone setup itself (Settings → Chat apps): a chat app, or this console over Tailscale. */
|
|
4491
|
+
function phoneSetup(who) {
|
|
4492
|
+
if (who.role !== "approver" || store.isDemo())
|
|
4493
|
+
return undefined;
|
|
4473
4494
|
const port = servedPort() ?? 4180;
|
|
4474
4495
|
const address = server.address();
|
|
4475
4496
|
const bound = typeof address === "object" && address !== null ? address.address : "127.0.0.1";
|
|
@@ -4509,7 +4530,11 @@ export function createDecisionServer(options) {
|
|
|
4509
4530
|
const ask = last?.role === "assistant" && last.turn !== null && store.getMateTurn(last.turn)?.approver === who.name ? store.mateAskOpen(last.turn, now) : null;
|
|
4510
4531
|
// Answered questions stay readable above the answer; only their buttons go.
|
|
4511
4532
|
const asks = new Map(messages.flatMap(one => { const asked = one.role === "assistant" && one.turn !== null ? store.mateAsk(one.turn) : null; return asked === null ? [] : [[asked.turn, asked]]; }));
|
|
4533
|
+
// The thread a ceiling change replaced (ruling 9): read after the open, display only.
|
|
4534
|
+
const replaced = store.replacedLeadThread(opened.thread);
|
|
4535
|
+
const previousProposals = replaced === null ? [] : store.listMateProposals(replaced.id);
|
|
4512
4536
|
return {
|
|
4537
|
+
previous: replaced === null ? null : { messages: store.listMateMessages(replaced.id, 40), proposals: previousProposals, decisions: decisionsFor(store, previousProposals) },
|
|
4513
4538
|
messages,
|
|
4514
4539
|
ask,
|
|
4515
4540
|
asks,
|
|
@@ -9621,7 +9646,7 @@ export function createDecisionServer(options) {
|
|
|
9621
9646
|
noteMate(who.session.csrf, null, "that proposal was already acted on");
|
|
9622
9647
|
return redirect(response, chatReturnWithLatest(back));
|
|
9623
9648
|
}
|
|
9624
|
-
const outcome = confirmMateProposal(store, principal, id, now, { confirm: body.get("confirm") === "yes", via: "web", evidenceRoot, held: options.attended?.coordinator, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
|
|
9649
|
+
const outcome = confirmMateProposal(store, principal, id, now, { chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", evidenceRoot, held: options.attended?.coordinator, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
|
|
9625
9650
|
if (cardJson) {
|
|
9626
9651
|
if (!outcome.ok)
|
|
9627
9652
|
return cardAnswer(outcome.reason === "standing" ? 403 : outcome.reason === "not-yours" ? 404 : 409, false, outcome.said);
|
|
@@ -15406,6 +15431,8 @@ ${THEME_DARK}
|
|
|
15406
15431
|
min-height: min(32rem, 48vh); padding: .25rem;
|
|
15407
15432
|
}
|
|
15408
15433
|
.thread .msg { max-width: 48rem; line-height: 1.65; overflow-wrap: anywhere; }
|
|
15434
|
+
.chat-previous-divider { display: flex; align-items: center; gap: .6rem; margin: 1rem 0; color: var(--so-muted); font-size: .8rem; }
|
|
15435
|
+
.chat-previous-divider::before, .chat-previous-divider::after { content: ""; flex: 1; border-top: 1px solid var(--so-line); }
|
|
15409
15436
|
.thread .msg p { margin: .3rem 0; }
|
|
15410
15437
|
.thread .msg.op {
|
|
15411
15438
|
align-self: flex-end; max-width: min(82%, 40rem); padding: .75rem 1rem;
|
|
@@ -18250,7 +18277,7 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
18250
18277
|
`</div>`;
|
|
18251
18278
|
}
|
|
18252
18279
|
else if (view.state === "pending") {
|
|
18253
|
-
acts = `<p class="meta proposal-wait"
|
|
18280
|
+
acts = `<p class="meta proposal-wait">${escape(PROPOSAL_WAIT_REASON)}</p>`;
|
|
18254
18281
|
}
|
|
18255
18282
|
else if (view.state === "confirmed") {
|
|
18256
18283
|
const filed = outcome !== null && typeof outcome.taskId === "string" ? outcome.taskId : null;
|
|
@@ -18299,7 +18326,7 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
18299
18326
|
],
|
|
18300
18327
|
primary,
|
|
18301
18328
|
dismissable: pending && view.kind !== "control",
|
|
18302
|
-
note: view.state === "pending" && inert ?
|
|
18329
|
+
note: view.state === "pending" && inert ? PROPOSAL_WAIT_REASON
|
|
18303
18330
|
: pending && view.kind === "cancel" ? "Cancelling is confirmed on the task itself."
|
|
18304
18331
|
: view.state === "dismissed" ? "Dismissed." : view.state === "expired" ? "Expired — this conversation moved on." : null,
|
|
18305
18332
|
};
|
|
@@ -18311,6 +18338,18 @@ function mateProposalCard(proposal, csrf, inert, decision, returnTo = null) {
|
|
|
18311
18338
|
function mateProposalCardParts(proposal, csrf, inert, decision, returnTo = null) {
|
|
18312
18339
|
return proposalCardParts({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: true }, actionBase: "/chat/proposal" }, csrf, inert, decision, returnTo);
|
|
18313
18340
|
}
|
|
18341
|
+
/** Review and inline team cards share the same server-decided controls and reasons. */
|
|
18342
|
+
function teamProposalCardParts(store, actor, proposal, snapshot, csrf, decision, now, provider) {
|
|
18343
|
+
const gate = proposalActGate(store, actor, proposal.thread, now, provider);
|
|
18344
|
+
const reason = !gate.ok ? gate.said : null;
|
|
18345
|
+
const back = '/chat?conversation=' + encodeURIComponent(snapshot.selected.id);
|
|
18346
|
+
const parts = mateProposalCardParts(proposal, csrf, reason !== null, decision, back);
|
|
18347
|
+
if (reason !== null && reason !== PROPOSAL_WAIT_REASON && proposal.state === 'pending') {
|
|
18348
|
+
parts.html = parts.html.replace(PROPOSAL_WAIT_REASON, reason);
|
|
18349
|
+
parts.card = { ...parts.card, note: reason };
|
|
18350
|
+
}
|
|
18351
|
+
return parts;
|
|
18352
|
+
}
|
|
18314
18353
|
function coordinatorProposalCard(proposal, csrf, now, decision, returnTo = null) {
|
|
18315
18354
|
return proposalCard({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: false, name: proposal.name, ago: relativeAge(proposal.createdAt, now) }, actionBase: "/proposals" }, csrf, false, decision, returnTo);
|
|
18316
18355
|
}
|
|
@@ -18330,6 +18369,24 @@ function coordinatorProposalsSection(proposals, decisions, csrf, now, heading =
|
|
|
18330
18369
|
proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
|
|
18331
18370
|
`</div>`);
|
|
18332
18371
|
}
|
|
18372
|
+
const REPLACED_THREAD_DIVIDER = "New conversation — the projects I can reach changed";
|
|
18373
|
+
/** The replaced thread as the React conversation renders it: words and card outcomes, no buttons. */
|
|
18374
|
+
function replacedBrowserMessages(previous, csrf) {
|
|
18375
|
+
return mateBrowserMessages({ ...previous, pending: null, ask: null, asks: new Map() }, csrf, null).map(message => ({
|
|
18376
|
+
...message, cardsHtml: "",
|
|
18377
|
+
cards: (message.cards ?? []).map(card => ({ ...card, primary: null, dismissable: false })),
|
|
18378
|
+
}));
|
|
18379
|
+
}
|
|
18380
|
+
/** The replaced thread for the server-rendered page: read only, then the divider. */
|
|
18381
|
+
function replacedThreadHtml(previous) {
|
|
18382
|
+
if (previous == null)
|
|
18383
|
+
return "";
|
|
18384
|
+
return `<section class="thread chat-previous" aria-label="Earlier conversation" data-previous-thread>` +
|
|
18385
|
+
previous.messages.map(one => one.role === "operator"
|
|
18386
|
+
? `<div class="msg op" data-previous-message="${one.id}"><p style="white-space:pre-wrap">${escape(one.text)}</p></div>`
|
|
18387
|
+
: `<div class="msg mate" data-previous-message="${one.id}">${renderChatText(one.text)}</div>`).join("") +
|
|
18388
|
+
`<p class="chat-previous-divider" role="separator" data-thread-divider>${escape(REPLACED_THREAD_DIVIDER)}</p></section>`;
|
|
18389
|
+
}
|
|
18333
18390
|
/** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
|
|
18334
18391
|
* "Something else" moves to the composer. Nothing shows while a reply is running. */
|
|
18335
18392
|
function mateAskHtml(ask, csrf, target, composer, open = true) {
|
|
@@ -18378,6 +18435,7 @@ function mateChatVersion(rows) {
|
|
|
18378
18435
|
const focus = rows.focusTask;
|
|
18379
18436
|
const facts = [
|
|
18380
18437
|
rows.messages.map(one => [one.id, one.role, one.turn]),
|
|
18438
|
+
rows.previous == null ? null : rows.previous.messages.map(one => one.id),
|
|
18381
18439
|
rows.proposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
18382
18440
|
rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
18383
18441
|
[...rows.decisions.values()].map(one => [one.id, one.state, one.choice ?? null]),
|
|
@@ -18504,6 +18562,7 @@ function matePage(chrome, data) {
|
|
|
18504
18562
|
}
|
|
18505
18563
|
const lastMessage = data.messages.at(-1);
|
|
18506
18564
|
const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
|
|
18565
|
+
conversation.push(replacedThreadHtml(data.previous));
|
|
18507
18566
|
conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
|
|
18508
18567
|
conversation.push(
|
|
18509
18568
|
// The New update action (package 2): hidden until a live update lands
|
|
@@ -18524,6 +18583,7 @@ function matePage(chrome, data) {
|
|
|
18524
18583
|
messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
18525
18584
|
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
|
|
18526
18585
|
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|
|
18586
|
+
...(data.previous == null ? {} : { previous: { messages: replacedBrowserMessages(data.previous, data.csrf) } }),
|
|
18527
18587
|
},
|
|
18528
18588
|
focus: data.focusTask === null ? null : { id: data.focusTask.id, title: data.focusTask.title,
|
|
18529
18589
|
html: taskChatLiveRegion(data.focusTask, data.csrf, requestContext.getStore()?.workspaceRead === true, data.pending !== null) },
|
|
@@ -24057,7 +24117,7 @@ function notificationProjects(store, account) {
|
|
|
24057
24117
|
.map(one => ({ repo: one.path, name: one.name, muted: muted.has(one.path) }))
|
|
24058
24118
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
24059
24119
|
}
|
|
24060
|
-
function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null, firstResult = null, chatNotices = null, telegramFailing = false) {
|
|
24120
|
+
function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null, firstResult = null, chatNotices = null, telegramFailing = false, phone = null) {
|
|
24061
24121
|
const permissionCard = permissionDefault === null
|
|
24062
24122
|
? ""
|
|
24063
24123
|
: [
|
|
@@ -24283,10 +24343,12 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
24283
24343
|
workers,
|
|
24284
24344
|
updates,
|
|
24285
24345
|
firstResult,
|
|
24346
|
+
...(phone === null ? {} : { phone }),
|
|
24286
24347
|
};
|
|
24287
24348
|
return screen("Settings", [
|
|
24288
24349
|
"<h1>Settings</h1>",
|
|
24289
24350
|
settingsTiles(view.groups),
|
|
24351
|
+
phone === null ? "" : phoneSetupHtml(phone),
|
|
24290
24352
|
appearanceCard(csrf),
|
|
24291
24353
|
permissionCard,
|
|
24292
24354
|
qualityCard,
|
|
@@ -24312,6 +24374,15 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
24312
24374
|
`</details>`,
|
|
24313
24375
|
].join("\n"), { chrome, workspace: { view }, functional: { script: SETTINGS_AUTOSAVE_SCRIPT + (pushScript ?? ""), ...(pushScript === null ? {} : { fetches: true }) } });
|
|
24314
24376
|
}
|
|
24377
|
+
/** The phone setup for the server-rendered Settings page: the same choices as the React card. */
|
|
24378
|
+
function phoneSetupHtml(phone) {
|
|
24379
|
+
const [first, ...others] = phone.chatApps;
|
|
24380
|
+
return `<section class="card" id="phone" aria-labelledby="phone-card-title" data-phone-card><h2 id="phone-card-title">Use it from your phone</h2>` +
|
|
24381
|
+
`<p>${first === undefined ? "" : `<a href="${escape(first.href)}">Pair ${escape(first.label)}</a>`}${others.length === 0 ? "" : ` · or ${others.map(one => `<a href="${escape(one.href)}">${escape(one.label)}</a>`).join(", ")}`}</p>` +
|
|
24382
|
+
`<p class="meta" data-phone-tailnet>${phone.tailnet === null ? `Or install <a href="https://tailscale.com/download">Tailscale</a> on this computer and your phone, then reload this page for the address.`
|
|
24383
|
+
: phone.tailnet.restart === null ? `Or on your phone open <code>${escape(phone.tailnet.address)}</code> and sign in.`
|
|
24384
|
+
: `Or start Toolroll with <code>${escape(phone.tailnet.restart)}</code>, then on your phone open <code>${escape(phone.tailnet.address)}</code> and sign in.`}</p></section>`;
|
|
24385
|
+
}
|
|
24315
24386
|
/** Every worker that is not retired: its capacity, and the tasks it holds now. */
|
|
24316
24387
|
function settingsWorkers(store, now) {
|
|
24317
24388
|
const claims = store.liveClaims(null, now);
|
package/dist/store.d.ts
CHANGED
|
@@ -6323,6 +6323,11 @@ export declare class Store {
|
|
|
6323
6323
|
};
|
|
6324
6324
|
/** The approver's live thread for one scope, read only — null when none is open. */
|
|
6325
6325
|
liveMateThreadFor(approver: string, scope?: MateThreadScope): MateThread | null;
|
|
6326
|
+
/** The lead thread a ceiling change closed just before `current` (ruling 9),
|
|
6327
|
+
* read only and for display only: it never continues and never reaches the
|
|
6328
|
+
* model. Null when there is none, when it was ended or revoked (its text is
|
|
6329
|
+
* gone), or when it closed under the same ceiling. */
|
|
6330
|
+
replacedLeadThread(current: MateThread): MateThread | null;
|
|
6326
6331
|
/** The task a paired chat is talking about (v78), or null for the lead conversation. */
|
|
6327
6332
|
chatFocus(surface: string, binding: number): string | null;
|
|
6328
6333
|
setChatFocus(surface: string, binding: number, task: string | null, now: Date): void;
|
package/dist/store.js
CHANGED
|
@@ -19275,6 +19275,20 @@ export class Store {
|
|
|
19275
19275
|
const row = this.db.prepare("SELECT * FROM mate_thread WHERE approver = ? AND scope_kind = ? AND scope_key IS ? AND closed_at IS NULL AND NOT EXISTS (SELECT 1 FROM team_conversation tc WHERE tc.thread=mate_thread.id) ORDER BY id DESC LIMIT 1").get(approver, scope.kind, scope.kind === "lead" ? null : scope.key);
|
|
19276
19276
|
return row === undefined ? null : readMateThread(row);
|
|
19277
19277
|
}
|
|
19278
|
+
/** The lead thread a ceiling change closed just before `current` (ruling 9),
|
|
19279
|
+
* read only and for display only: it never continues and never reaches the
|
|
19280
|
+
* model. Null when there is none, when it was ended or revoked (its text is
|
|
19281
|
+
* gone), or when it closed under the same ceiling. */
|
|
19282
|
+
replacedLeadThread(current) {
|
|
19283
|
+
if (current.scope.kind !== "lead")
|
|
19284
|
+
return null;
|
|
19285
|
+
const row = this.db.prepare(`SELECT * FROM mate_thread WHERE approver = ? AND scope_kind = 'lead' AND scope_key IS NULL AND id < ? AND closed_at IS NOT NULL
|
|
19286
|
+
AND NOT EXISTS (SELECT 1 FROM team_conversation tc WHERE tc.thread=mate_thread.id) ORDER BY id DESC LIMIT 1`).get(current.approver, current.id);
|
|
19287
|
+
if (row === undefined || String(row["ceiling_digest"]) === current.ceilingDigest)
|
|
19288
|
+
return null;
|
|
19289
|
+
const previous = readMateThread(row);
|
|
19290
|
+
return this.db.prepare("SELECT 1 FROM mate_message WHERE thread = ? LIMIT 1").get(previous.id) === undefined ? null : previous;
|
|
19291
|
+
}
|
|
19278
19292
|
/** The task a paired chat is talking about (v78), or null for the lead conversation. */
|
|
19279
19293
|
chatFocus(surface, binding) {
|
|
19280
19294
|
const row = this.db.prepare("SELECT task FROM chat_focus WHERE surface = ? AND binding = ?").get(surface, binding);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Store, ChatConfig } from './store.js';
|
|
2
|
+
import type { TeamActor, TeamChatAuthorization, TeamSnapshot } from './team-contract.js';
|
|
3
|
+
export type TeamChatProvider = {
|
|
4
|
+
config: ChatConfig;
|
|
5
|
+
key: string | null;
|
|
6
|
+
};
|
|
7
|
+
export type TeamChatProviderResolver = () => TeamChatProvider | null;
|
|
8
|
+
/** Subscription-only surfaces can resolve current terms from saved settings.
|
|
9
|
+
* Direct API callers must supply their live key; a stored grant is never proof of it. */
|
|
10
|
+
export declare function subscriptionTeamChatProvider(store: Store): TeamChatProvider | null;
|
|
11
|
+
export declare function teamChatAuthorization(store: Store, actor: TeamActor, conversation: Pick<NonNullable<TeamSnapshot['selected']>, 'id' | 'threadId' | 'projects'> | null, live: TeamChatProvider | null): TeamChatAuthorization;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The live grant shared by team execution, proposal cards and confirmation doors. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { ceilingDigestOf } from './principal.js';
|
|
4
|
+
import { credentialKeyOf, isDirectChatProvider, priceForConfig, subscriptionCredentialKey } from './converse.js';
|
|
5
|
+
const hash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
6
|
+
/** Subscription-only surfaces can resolve current terms from saved settings.
|
|
7
|
+
* Direct API callers must supply their live key; a stored grant is never proof of it. */
|
|
8
|
+
export function subscriptionTeamChatProvider(store) {
|
|
9
|
+
const config = store.getChatConfig();
|
|
10
|
+
return config && !isDirectChatProvider(config.provider) ? { config, key: null } : null;
|
|
11
|
+
}
|
|
12
|
+
export function teamChatAuthorization(store, actor, conversation, live) {
|
|
13
|
+
const config = live?.config;
|
|
14
|
+
const empty = { enabled: false, provider: config?.provider ?? null, model: config?.model ?? null,
|
|
15
|
+
dailyTurns: config?.dailyTurns ?? 0, weeklyCeilingUsd: null, conversationCeilingUsd: null, termsDigest: '' };
|
|
16
|
+
if (!conversation)
|
|
17
|
+
return empty;
|
|
18
|
+
if (!live || !config)
|
|
19
|
+
return { ...empty, waitingReason: 'Choose a chat provider in Settings.' };
|
|
20
|
+
const direct = isDirectChatProvider(config.provider);
|
|
21
|
+
if (direct && (live.key === null || priceForConfig(config) === null))
|
|
22
|
+
return { ...empty, waitingReason: 'The configured chat connection is unavailable. Open Settings.' };
|
|
23
|
+
const credential = direct ? credentialKeyOf(config.provider, live.key) : subscriptionCredentialKey(config.provider);
|
|
24
|
+
const session = store.teamMateSession(actor.name, conversation.threadId);
|
|
25
|
+
const ceiling = direct ? (session ? session.ceilingMicrousd / 1_000_000 : Math.min(5, config.weeklyCeilingMicrousd / 1_000_000)) : null;
|
|
26
|
+
const termsDigest = hash({ version: 1, actor: actor.name, generation: actor.generation, conversation: conversation.id,
|
|
27
|
+
projects: conversation.projects, credential, provider: config.provider, model: config.model,
|
|
28
|
+
daily: config.dailyTurns, weekly: config.weeklyCeilingMicrousd, priceIn: config.priceInMicrousd, priceOut: config.priceOutMicrousd, ceiling });
|
|
29
|
+
const enabled = session !== null && session.approverGeneration === actor.generation && session.endedAt === null
|
|
30
|
+
&& session.credentialKey === credential && session.ceilingDigest === ceilingDigestOf(conversation.projects) && session.termsDigest === termsDigest;
|
|
31
|
+
return { enabled, provider: config.provider, model: config.model, dailyTurns: config.dailyTurns,
|
|
32
|
+
weeklyCeilingUsd: direct ? config.weeklyCeilingMicrousd / 1_000_000 : null, conversationCeilingUsd: ceiling, termsDigest };
|
|
33
|
+
}
|
package/dist/team-contract.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WorkIndexPage } from './work-index.js';
|
|
2
|
+
import type { BrowserActionCard } from './browser-workspace.js';
|
|
2
3
|
/** Shared central-team transport contract. Requests never establish actor identity. */
|
|
3
4
|
export type TeamActor = {
|
|
4
5
|
name: string;
|
|
@@ -52,6 +53,15 @@ export type TeamChatAuthorization = {
|
|
|
52
53
|
termsDigest: string;
|
|
53
54
|
waitingReason?: string;
|
|
54
55
|
};
|
|
56
|
+
/** Browser cards extend the existing CLI summary; both refer to the same saved proposal. */
|
|
57
|
+
export type TeamProposal = {
|
|
58
|
+
id: number;
|
|
59
|
+
turnId: number;
|
|
60
|
+
title: string;
|
|
61
|
+
state: string;
|
|
62
|
+
href: string;
|
|
63
|
+
card?: BrowserActionCard;
|
|
64
|
+
};
|
|
55
65
|
export type TeamSnapshot = {
|
|
56
66
|
leads: TeamLead[];
|
|
57
67
|
conversations: TeamConversation[];
|
|
@@ -67,13 +77,7 @@ export type TeamSnapshot = {
|
|
|
67
77
|
accounts: string[];
|
|
68
78
|
chatAuthorization?: TeamChatAuthorization;
|
|
69
79
|
tasks?: WorkIndexPage;
|
|
70
|
-
proposals?:
|
|
71
|
-
id: number;
|
|
72
|
-
turnId: number;
|
|
73
|
-
title: string;
|
|
74
|
-
state: string;
|
|
75
|
-
href: string;
|
|
76
|
-
}[];
|
|
80
|
+
proposals?: TeamProposal[];
|
|
77
81
|
};
|
|
78
82
|
export declare const TEAM_OPERATIONS: readonly ['list', 'show', 'create-lead', 'update-lead', 'create-conversation', 'member', 'send', 'edit', 'withdraw', 'read', 'authorize', 'follow', 'stop', 'transfer'];
|
|
79
83
|
export type TeamOperation = typeof TEAM_OPERATIONS[number];
|
package/dist/team-runtime.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import type { Store
|
|
1
|
+
import type { Store } from './store.js';
|
|
2
2
|
import type { WorkspaceRevision } from './workspace-revision.js';
|
|
3
3
|
import { TeamLeads } from './team-leads.js';
|
|
4
4
|
import type { TeamActor, TeamExecute } from './team-contract.js';
|
|
5
5
|
import { type MateTurnInput } from './mate.js';
|
|
6
|
+
import { type TeamChatProviderResolver } from './team-chat-authorization.js';
|
|
6
7
|
export type TeamRuntimeOptions = {
|
|
7
8
|
store: Store;
|
|
8
9
|
repos: () => readonly string[];
|
|
9
10
|
evidenceRoot: string;
|
|
10
|
-
provider:
|
|
11
|
-
config: ChatConfig;
|
|
12
|
-
key: string | null;
|
|
13
|
-
} | null;
|
|
11
|
+
provider: TeamChatProviderResolver;
|
|
14
12
|
subscriptionRunner?: MateTurnInput['subscriptionRunner'];
|
|
15
13
|
fetcher?: typeof fetch;
|
|
16
14
|
clock?: () => Date;
|
package/dist/team-runtime.js
CHANGED
|
@@ -4,9 +4,10 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { deliverTeamUpdates, startTeamUpdates } from './team-updates.js';
|
|
5
5
|
import { workIndexPage } from './work-index.js';
|
|
6
6
|
import { TeamLeads } from './team-leads.js';
|
|
7
|
-
import {
|
|
7
|
+
import { verifyApproverStanding } from './principal.js';
|
|
8
8
|
import { credentialKeyOf, isDirectChatProvider, priceForConfig, subscriptionCredentialKey, mateWorstCaseForPrice } from './converse.js';
|
|
9
9
|
import { runMateTurn } from './mate.js';
|
|
10
|
+
import { teamChatAuthorization } from './team-chat-authorization.js';
|
|
10
11
|
import { updateAdmissionPaused } from './desktop-update-gate.js';
|
|
11
12
|
const hash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
12
13
|
const response = (ok, code, message) => ({ version: 1, ok, code, message });
|
|
@@ -20,28 +21,7 @@ export function createTeamRuntime(options) {
|
|
|
20
21
|
const waiting = new Map();
|
|
21
22
|
const pages = new Map();
|
|
22
23
|
let timer = null, closed = false, passing = false, lastDelivery = 0;
|
|
23
|
-
|
|
24
|
-
const live = options.provider(), config = live?.config, conversation = snapshot.selected;
|
|
25
|
-
const empty = { enabled: false, provider: config?.provider ?? null, model: config?.model ?? null,
|
|
26
|
-
dailyTurns: config?.dailyTurns ?? 0, weeklyCeilingUsd: null, conversationCeilingUsd: null, termsDigest: '' };
|
|
27
|
-
if (!conversation)
|
|
28
|
-
return empty;
|
|
29
|
-
if (!live || !config)
|
|
30
|
-
return { ...empty, waitingReason: 'Choose a chat provider in Settings.' };
|
|
31
|
-
const direct = isDirectChatProvider(config.provider);
|
|
32
|
-
if (direct && (live.key === null || priceForConfig(config) === null))
|
|
33
|
-
return { ...empty, waitingReason: 'The configured chat connection is unavailable. Open Settings.' };
|
|
34
|
-
const credential = direct ? credentialKeyOf(config.provider, live.key) : subscriptionCredentialKey(config.provider);
|
|
35
|
-
const session = store.teamMateSession(actor.name, conversation.threadId);
|
|
36
|
-
const ceiling = direct ? (session ? session.ceilingMicrousd / 1_000_000 : Math.min(5, config.weeklyCeilingMicrousd / 1_000_000)) : null;
|
|
37
|
-
const termsDigest = hash({ version: 1, actor: actor.name, generation: actor.generation, conversation: conversation.id,
|
|
38
|
-
projects: conversation.projects, credential, provider: config.provider, model: config.model,
|
|
39
|
-
daily: config.dailyTurns, weekly: config.weeklyCeilingMicrousd, priceIn: config.priceInMicrousd, priceOut: config.priceOutMicrousd, ceiling });
|
|
40
|
-
const enabled = session !== null && session.approverGeneration === actor.generation && session.endedAt === null
|
|
41
|
-
&& session.credentialKey === credential && session.ceilingDigest === ceilingDigestOf(conversation.projects) && session.termsDigest === termsDigest;
|
|
42
|
-
return { enabled, provider: config.provider, model: config.model, dailyTurns: config.dailyTurns,
|
|
43
|
-
weeklyCeilingUsd: direct ? config.weeklyCeilingMicrousd / 1_000_000 : null, conversationCeilingUsd: ceiling, termsDigest };
|
|
44
|
-
}
|
|
24
|
+
const authorization = (actor, snapshot) => teamChatAuthorization(store, actor, snapshot.selected, options.provider());
|
|
45
25
|
function admission(actor, id) {
|
|
46
26
|
const conversation = domain.access(actor, id, 'contributor').conversation;
|
|
47
27
|
const grant = authorization(actor, { selected: conversation }), live = options.provider();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.40",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|