toolroll 0.9.72 → 0.9.74
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 +108 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-fence.d.ts +5 -0
- package/dist/agent-fence.js +42 -7
- package/dist/agent-turns.js +5 -0
- package/dist/agents-live.d.ts +31 -0
- package/dist/agents-live.js +43 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -30
- package/dist/browser-workspace.d.ts +6 -0
- package/dist/builder.js +1 -1
- package/dist/chat-actions.js +13 -8
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +170 -4
- package/dist/chat-quiet.js +3 -3
- package/dist/check-report.js +2 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +17 -0
- package/dist/commit-extract.js +82 -4
- package/dist/contracts/console-api.d.ts +3 -3
- package/dist/contracts/console-api.js +4 -4
- package/dist/contracts/release-terms.d.ts +7 -1
- package/dist/contracts/release-terms.js +6 -2
- package/dist/deploy-gate.js +5 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.js +35 -2
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +20 -19
- package/dist/first-run.d.ts +17 -1
- package/dist/first-run.js +38 -0
- package/dist/flow-gallery-ui.js +4 -2
- package/dist/flow-gallery.d.ts +3 -0
- package/dist/flow-gallery.js +6 -2
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/guides.js +10 -1
- package/dist/invoke.d.ts +13 -0
- package/dist/invoke.js +21 -12
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/knowledge-ui.d.ts +4 -1
- package/dist/knowledge-ui.js +10 -5
- package/dist/lead-progress.d.ts +4 -1
- package/dist/lead-progress.js +3 -1
- package/dist/lead.js +4 -0
- package/dist/live-bus.d.ts +2 -0
- package/dist/live-bus.js +1 -0
- package/dist/live-children-read.d.ts +52 -0
- package/dist/live-children-read.js +333 -0
- package/dist/live-children.d.ts +73 -0
- package/dist/live-children.js +318 -0
- package/dist/live-detail.d.ts +4 -3
- package/dist/live-detail.js +9 -5
- package/dist/live-input.js +7 -8
- package/dist/live-main-read.d.ts +19 -0
- package/dist/live-main-read.js +91 -0
- package/dist/live-open.d.ts +28 -0
- package/dist/live-open.js +125 -0
- package/dist/live-records.d.ts +10 -1
- package/dist/live-records.js +16 -5
- package/dist/live.d.ts +54 -22
- package/dist/live.js +218 -69
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/operate.js +3 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/project-tools.js +3 -1
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- package/dist/release-adapters.d.ts +8 -1
- package/dist/release-adapters.js +84 -14
- package/dist/release-authority.d.ts +6 -0
- package/dist/release-authority.js +9 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +3 -0
- package/dist/release-carry.js +23 -4
- package/dist/release-cli.js +36 -3
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -0
- package/dist/release.d.ts +11 -2
- package/dist/release.js +82 -6
- package/dist/repository-context-ui.js +2 -1
- package/dist/run-efficiency.d.ts +3 -3
- package/dist/run-efficiency.js +20 -8
- package/dist/scope.d.ts +7 -0
- package/dist/scope.js +22 -5
- package/dist/scout.d.ts +3 -0
- package/dist/scout.js +5 -3
- package/dist/serve.js +34 -23
- package/dist/server/agents-live-lead.d.ts +47 -0
- package/dist/server/agents-live-lead.js +116 -0
- package/dist/server/agents-live-room.d.ts +23 -0
- package/dist/server/agents-live-room.js +88 -0
- package/dist/server/chat.js +84 -30
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- package/dist/server/flows.js +40 -10
- package/dist/server/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +81 -1
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +8 -4
- package/dist/server/render-pages.js +55 -50
- package/dist/server/render-settings.js +1 -1
- package/dist/server/render-tasks.js +10 -3
- package/dist/server/route-table.js +5 -1
- package/dist/server/runtime.d.ts +1 -1
- package/dist/server/settings.js +29 -8
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +59 -41
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +34 -1
- package/dist/store.js +81 -2
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subagent-memory.d.ts +1 -0
- package/dist/subagent-memory.js +1 -1
- package/dist/subagent-tools.d.ts +22 -6
- package/dist/subagent-tools.js +44 -8
- package/dist/subagent-work.d.ts +2 -0
- package/dist/subagent-work.js +5 -2
- package/dist/subagents-ui.js +6 -4
- package/dist/subscription-chat.js +5 -3
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- package/package.json +1 -1
|
@@ -211,6 +211,10 @@ export type BrowserTasksView = {
|
|
|
211
211
|
newTask: BrowserLink;
|
|
212
212
|
/** v105: subscription windows and monthly budgets, one tile each (an instance operator's; null when there are none). */
|
|
213
213
|
limits: BrowserLimits | null;
|
|
214
|
+
/** The stored-state filter (Any state, Queued, Running, Done, Failed, Cancelled), each a real link starting on page one. */
|
|
215
|
+
states?: (BrowserLink & {
|
|
216
|
+
active: boolean;
|
|
217
|
+
})[];
|
|
214
218
|
};
|
|
215
219
|
/** One limit: a plan's usage window ("Claude · 5-hour, 48%") or a monthly budget ("shop · Budget, $4.20 of $10"). */
|
|
216
220
|
export type BrowserTaskGroup = WorkIndexGroup;
|
|
@@ -1445,6 +1449,8 @@ export type BrowserAgentsView = {
|
|
|
1445
1449
|
selected: BrowserAgentDetail | null;
|
|
1446
1450
|
/** Shown when nothing is on the board. */
|
|
1447
1451
|
empty: string | null;
|
|
1452
|
+
/** The Live grid (?view=live): every running agent's live output, watching only. */
|
|
1453
|
+
live?: boolean;
|
|
1448
1454
|
};
|
|
1449
1455
|
export type BrowserView = BrowserTasksView | BrowserSettingsView | BrowserTaskView | BrowserProjectsView | BrowserResultView | BrowserFlowView | BrowserAgentsView;
|
|
1450
1456
|
export type BrowserWorkspace = {
|
package/dist/builder.js
CHANGED
|
@@ -1240,7 +1240,7 @@ export async function build(store, request) {
|
|
|
1240
1240
|
// Every streaming transport emits events now (peek); a file that cannot
|
|
1241
1241
|
// open is a null, and a null never costs a build.
|
|
1242
1242
|
// What the agent did last rides the same stream into one row (live tasks).
|
|
1243
|
-
const liveLog = withAgentTurns(withRunActivity(openLiveLog(root, request.runId, liveLogContext(store, taskRef, worktree)), store, request.runId, clock), (state, at) => { if (attemptKey !== null)
|
|
1243
|
+
const liveLog = withAgentTurns(withRunActivity(openLiveLog(root, request.runId, liveLogContext(store, request.runId, taskRef, worktree, clock())), store, request.runId, clock), (state, at) => { if (attemptKey !== null)
|
|
1244
1244
|
recordAgentTurn(store, { run: request.runId, attemptKey, state, at }); }, clock, (open, at) => { if (attemptKey !== null)
|
|
1245
1245
|
noteOpenTool(store, request.runId, attemptKey, open, at); });
|
|
1246
1246
|
// The retry base (steering fix for run 1465's proof): the SAME pinned
|
package/dist/chat-actions.js
CHANGED
|
@@ -36,7 +36,7 @@ import { requestUndo, undoFor } from "./subagent-week.js";
|
|
|
36
36
|
import { kitInstalled, kitOf, KITS, setUpKitNow } from "./kits.js";
|
|
37
37
|
import { starterFlowOf, starterOf, startersFor, starterTerms, STARTER_FLOWS, switchOnStarter } from "./flow-starters.js";
|
|
38
38
|
import { answerSubagentQuestion } from "./subagent-work.js";
|
|
39
|
-
import { callWords, checkRule, defaultRule, grantListed, revokeTool, ruleWords, setToolRules } from "./subagent-tools.js";
|
|
39
|
+
import { callWords, checkRule, defaultRule, grantListed, revokeTool, ruleLoosens, ruleWords, setToolRules } from "./subagent-tools.js";
|
|
40
40
|
import { aboutYouOf, checkAboutYouLine, saveAboutYou, withAboutYouLine } from "./lead-about.js";
|
|
41
41
|
import { CHAT_ACTION_FIELDS, readChatActionRequest, readSharedAction, SHARED_ACTION_VERSION } from "./contracts/chat-actions.js";
|
|
42
42
|
export { CHAT_ACTION_FIELDS };
|
|
@@ -441,12 +441,10 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
441
441
|
throw Error(`${project} has no tool called ${tool}.${named.length === 0 ? " Add it on the Tools page first." : ` Its tools: ${named.join(", ")}.`}${uses}`);
|
|
442
442
|
if (grant !== null)
|
|
443
443
|
throw Error(`${name} can already use ${tool}.`);
|
|
444
|
-
|
|
445
|
-
if (actions.length === 0)
|
|
444
|
+
if ((found.lastTest?.tools ?? []).length === 0)
|
|
446
445
|
throw Error(`${tool} hasn't said what it can do yet. Test it on the Tools page first.`);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
terms.push(...actions.map(one => `${one}: ${defaultRule({ name: one, readOnly: false }).use === "free" ? "does it" : "asks you first"}`), `${name} asks for each call on its turn; Toolroll makes it by these rules, and every call is kept on the card. Change a rule any time.`);
|
|
446
|
+
// Giving a subagent a tool takes the person's password (chat-management.md §0a.3), which never goes in chat.
|
|
447
|
+
throw Error(`Giving ${name} a tool takes your password. Open ${name}'s page in Toolroll, then Tools → Let ${name} use ${tool}.`);
|
|
450
448
|
}
|
|
451
449
|
else if (change === "revoke") {
|
|
452
450
|
if (grant === null)
|
|
@@ -463,6 +461,10 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
463
461
|
const checked = checkRule(grant, action, { use: input["use"], limit, undo: input["undoWith"] ?? grant.rules[action]?.undo ?? "" });
|
|
464
462
|
if (!checked.ok)
|
|
465
463
|
throw Error(checked.said);
|
|
464
|
+
// Letting it do more on its own takes the person's password (chat-management.md §0a.3), never a card.
|
|
465
|
+
const info = grant.actions.find(one => one.name === action);
|
|
466
|
+
if (ruleLoosens(grant.rules[action] ?? defaultRule(info), checked.rule))
|
|
467
|
+
throw Error(`Letting ${name} do more with ${action} on its own takes your password. Change the rule on ${name}'s page in Toolroll.`);
|
|
466
468
|
state = { rules: JSON.stringify(grant.rules) };
|
|
467
469
|
title = `${name}: ${action} — ${ruleWords(checked.rule)}`;
|
|
468
470
|
terms.push(`${tool} → ${action}: ${ruleWords(checked.rule)}.`, `Was: ${ruleWords(grant.rules[action] ?? { use: "ask" })}.`, ...(checked.rule.undo === undefined ? [] : [`A person can undo it with ${checked.rule.undo}, called with the same input.`]));
|
|
@@ -1168,7 +1170,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1168
1170
|
}
|
|
1169
1171
|
else if (payload.operation === "kit_setup") {
|
|
1170
1172
|
const kit = kitOf(String(req["kit"]));
|
|
1171
|
-
const made = setUpKitNow(store, kit, repo, actor, now, null
|
|
1173
|
+
const made = setUpKitNow(store, kit, repo, actor, now, null);
|
|
1172
1174
|
if (!made.ok)
|
|
1173
1175
|
throw Error(made.said);
|
|
1174
1176
|
return { ok: true, taskId: null, said: `${made.said} Try it with a sample card from its page.`, href: `/kits/${kit.id}?repo=${encodeURIComponent(repo)}` };
|
|
@@ -1431,7 +1433,10 @@ function runSubagentAction(store, payload, actor, now) {
|
|
|
1431
1433
|
if ((grant === null ? "" : JSON.stringify(grant.rules)) !== payload.state["rules"])
|
|
1432
1434
|
throw Error("Someone changed its tools since. Ask for a fresh proposal.");
|
|
1433
1435
|
const found = projectToolsOf(store, mate.repo).find(one => one.name === tool);
|
|
1434
|
-
|
|
1436
|
+
// A grant takes the person's password on the subagent's page (chat-management.md §0a.3): never from a card.
|
|
1437
|
+
if (req["change"] === "grant")
|
|
1438
|
+
throw Error("Giving a subagent a tool takes your password. Do it on its page in Toolroll.");
|
|
1439
|
+
const done = req["change"] === "grant" ? found === undefined ? { ok: false, said: `There's no tool called ${tool} any more.` } : grantListed(store, mate, found, null, actor, now, {})
|
|
1435
1440
|
: req["change"] === "revoke" ? revokeTool(store, mate, tool, actor, now)
|
|
1436
1441
|
: setToolRules(store, mate, tool, { [String(req["action"])]: { use: req["use"], limit: req["limitField"] === undefined || req["limitField"] === null || req["limitField"] === "" ? null : { field: req["limitField"], over: req["limitOver"] },
|
|
1437
1442
|
undo: req["undoWith"] ?? grant?.rules[String(req["action"])]?.undo ?? "" } }, actor, now);
|
package/dist/chat-continuity.js
CHANGED
|
@@ -125,6 +125,8 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
125
125
|
if(got===null)return;
|
|
126
126
|
var data=got.data||{};
|
|
127
127
|
if(data.ok===true){
|
|
128
|
+
// A conversation shown before it existed (session 0) is started by its first send: that is this one.
|
|
129
|
+
if(session==='0'&&typeof data.session==='number'&&data.session>0)adopt(String(data.session));
|
|
128
130
|
if(String(data.session)!==session){changed();return;}
|
|
129
131
|
say(data.queued===true?'Queued. It goes to the lead after the reply in progress.':'Sent. Waiting for the reply…');later(300);return;
|
|
130
132
|
}
|
|
@@ -273,6 +275,9 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
273
275
|
}
|
|
274
276
|
return true;
|
|
275
277
|
}
|
|
278
|
+
// The conversation this page's first send started: its binding and its draft move to it (a reload restores from there).
|
|
279
|
+
function adopt(started){session=started;document.querySelectorAll('form[action="/chat"] [name="request-session"]').forEach(function(bound){bound.value=started;});
|
|
280
|
+
try{sessionStorage.removeItem(key);}catch(e){}key='standing-orders:chat-draft:'+session+':'+task;save();}
|
|
276
281
|
function later(ms){if(stale)return;clearTimeout(timer);timer=setTimeout(check,ms);}
|
|
277
282
|
async function check(){
|
|
278
283
|
if(polling||stale)return;
|
|
@@ -289,6 +294,11 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
289
294
|
if(!r.ok)throw new Error('connection');
|
|
290
295
|
var data=await r.json();
|
|
291
296
|
if(!valid(data))throw new Error('response');
|
|
297
|
+
// Not started yet (session 0): only this send's own receipt in the started conversation proves it is the one.
|
|
298
|
+
if(session==='0'&&typeof data.session==='number'&&data.session>0&&asked&&data.received===true)adopt(String(data.session));
|
|
299
|
+
// A poll that lands while the first send is on its way decides nothing: that send's answer (or the poll it
|
|
300
|
+
// asks for afterwards, with its request) says whether the started conversation is this page's.
|
|
301
|
+
if(session==='0'&&sending&&data.session!==null&&String(data.session)!=='0'){later(1000);return;}
|
|
292
302
|
if(data.session===null||String(data.session)!==session){changed();return;}
|
|
293
303
|
// A late answer for another lens never lands here; the poll goes on.
|
|
294
304
|
if(data.task!==task){later(5000);return;}
|
package/dist/chat-decide.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { DECIDE_ACTS } from "./contracts/chat-callback-rows.js";
|
|
|
2
2
|
import { type ChatApprovalLimits } from "./chat-approval.js";
|
|
3
3
|
import { type VerifiedApprover } from "./principal.js";
|
|
4
4
|
import type { Scope } from "./scope.js";
|
|
5
|
+
import { type ReleaseCard } from "./release-card.js";
|
|
5
6
|
import type { Store } from "./store.js";
|
|
6
7
|
/** The tokens live in chat_decide_action and the open "What should change?" in chat_decide_prompt (store.ts). */
|
|
7
8
|
export type DecideAct = (typeof DECIDE_ACTS)[number];
|
|
@@ -59,11 +60,17 @@ type ActSpec = {
|
|
|
59
60
|
act: DecideAct;
|
|
60
61
|
run: number | null;
|
|
61
62
|
digest: string;
|
|
63
|
+
label?: string;
|
|
62
64
|
};
|
|
63
|
-
/** What a card offers: an optional replacement text (the plan card) and rows of acts and links
|
|
65
|
+
/** What a card offers: an optional replacement text (the plan card) and rows of acts and links; a release card also
|
|
66
|
+
* sends its full terms as a file with it (`file`), where the chat app can. */
|
|
64
67
|
export type DecideOffer = {
|
|
65
68
|
text: string | null;
|
|
66
69
|
rows: Array<Array<ActSpec | DecideLink>>;
|
|
70
|
+
file?: {
|
|
71
|
+
name: string;
|
|
72
|
+
text: string;
|
|
73
|
+
};
|
|
67
74
|
};
|
|
68
75
|
/** What lets this person's chat on `channel` approve on this repository now: their lasting chat-approval setting
|
|
69
76
|
* (chat-approval.ts), or else a live signed mode carrying the chatApprove term, signed by them, that names this chat
|
|
@@ -127,12 +134,24 @@ export declare function hasLiveDecideTokens(store: Store, channel: DecideChannel
|
|
|
127
134
|
export declare function isDecideToken(store: Store, token: string): boolean;
|
|
128
135
|
export type DecideTapOutcome = {
|
|
129
136
|
ack: string;
|
|
137
|
+
/** A release card's full terms, sent as a file before its Approve is offered (chat apps that limit a message). */
|
|
138
|
+
file?: {
|
|
139
|
+
name: string;
|
|
140
|
+
text: string;
|
|
141
|
+
};
|
|
130
142
|
/** The card repainted: its text and buttons now. */
|
|
131
143
|
edit?: {
|
|
132
144
|
text: string;
|
|
133
145
|
rows: DecideButton[][];
|
|
134
146
|
tokens: string[];
|
|
135
147
|
};
|
|
148
|
+
/** With `file`, where the chat app carries the terms only as a file: the card instead of `edit` when the file didn't
|
|
149
|
+
* arrive. No Approve, a fresh Continue to send it again; the channel drops whichever set it doesn't show. */
|
|
150
|
+
unsent?: {
|
|
151
|
+
text: string;
|
|
152
|
+
rows: DecideButton[][];
|
|
153
|
+
tokens: string[];
|
|
154
|
+
};
|
|
136
155
|
/** Ask "What should change?": the person's next message is the feedback. `id` names the prompt, so the channel can
|
|
137
156
|
* record the message it sent (a reply to anything else is not the feedback). */
|
|
138
157
|
prompt?: {
|
|
@@ -152,6 +171,33 @@ export type ChatMerge = {
|
|
|
152
171
|
repo: string;
|
|
153
172
|
via: string;
|
|
154
173
|
};
|
|
174
|
+
/** Where a release card is tapped: the person's binding and the chat the tap came from. */
|
|
175
|
+
export type ReleaseSeat = Pick<DecideSeat, "channel" | "binding" | "chat" | "approver">;
|
|
176
|
+
/**
|
|
177
|
+
* Whether a release card's tap comes from the person's own private chat with Toolroll (chat-management.md §0a.5), from
|
|
178
|
+
* the binding's own record, failing closed: Telegram, a private chat (a positive id, which Telegram makes equal to the
|
|
179
|
+
* user's own id) that is the binding's chat and its user's; Slack, a direct-message channel (D…) that is the binding's.
|
|
180
|
+
* Any other chat app, a missing chat or binding, or another person's binding is not.
|
|
181
|
+
*/
|
|
182
|
+
export declare function privateReleaseChat(store: Store, seat: ReleaseSeat): boolean;
|
|
183
|
+
/**
|
|
184
|
+
* A deploy gate this person may approve from this chat now, through its release card: every plain-plan rule
|
|
185
|
+
* (planInChat: their Approve from chat setting and limits, no coordinator-, lead- or scout-written text, organisation
|
|
186
|
+
* policy, protected work), never a nightly check, only in their private chat, and the card as the live records say
|
|
187
|
+
* it now (chat-management.md §0a.5).
|
|
188
|
+
*/
|
|
189
|
+
export declare function releaseInChat(store: Store, taskId: string, approver: string, now: Date, channel: DecideChannel,
|
|
190
|
+
/** The seat a tap came from, checked strictly; "offer" only where the card is drawn for the binding's own chat (no act). */
|
|
191
|
+
seat: ReleaseSeat | "offer", root?: string): {
|
|
192
|
+
ok: true;
|
|
193
|
+
card: ReleaseCard;
|
|
194
|
+
modeDigest: string;
|
|
195
|
+
source: "setting" | "mode";
|
|
196
|
+
scope: Scope;
|
|
197
|
+
} | {
|
|
198
|
+
ok: false;
|
|
199
|
+
why: string;
|
|
200
|
+
};
|
|
155
201
|
/** The links a card keeps once its acts are spent or stale: where to look, never a token. */
|
|
156
202
|
export declare function linksFor(target: DecideTarget): DecideButton[][];
|
|
157
203
|
/** Where a plan card's task is now, for a card whose buttons no longer act: the family's current version, named only
|
package/dist/chat-decide.js
CHANGED
|
@@ -28,7 +28,8 @@ import { verifyApproverStanding } from "./principal.js";
|
|
|
28
28
|
import { pullRequestFollowOf } from "./pull-request-flow.js";
|
|
29
29
|
import { requestResultChanges } from "./result-actions.js";
|
|
30
30
|
import { revisionSourceOf } from "./result-review.js";
|
|
31
|
-
import { APPROVAL_PROOF, approvalOf, approvalProofDetail, quarantinedAuthor } from "./scope.js";
|
|
31
|
+
import { APPROVAL_PROOF, approvalOf, approvalProofDetail, deployGateScope, quarantinedAuthor } from "./scope.js";
|
|
32
|
+
import { releaseCardOf } from "./release-card.js";
|
|
32
33
|
import { phoneText } from "./telegram-status.js";
|
|
33
34
|
/** The words, said once. */
|
|
34
35
|
export const DECIDE_LABELS = {
|
|
@@ -44,6 +45,7 @@ export const DECIDE_LABELS = {
|
|
|
44
45
|
cancel: "Cancel",
|
|
45
46
|
};
|
|
46
47
|
const OFFER_TTL_MS = 7 * 86_400_000;
|
|
48
|
+
const CHANNEL_NAMES = { telegram: "Telegram", slack: "Slack", discord: "Discord", teams: "Teams" };
|
|
47
49
|
const CONFIRM_TTL_MS = 10 * 60_000;
|
|
48
50
|
const PROMPT_TTL_MS = 30 * 60_000;
|
|
49
51
|
export const WHAT_SHOULD_CHANGE = "What should change?";
|
|
@@ -262,6 +264,30 @@ export function decideOffer(store, target, who, now, channel, root) {
|
|
|
262
264
|
const plan = planInChat(store, target.taskId, who.name, now, channel, root);
|
|
263
265
|
if (!plan.ok)
|
|
264
266
|
return null;
|
|
267
|
+
const release = deployGateScope(plan.scope);
|
|
268
|
+
// Checks beyond the sandbox seal only on the person's password (store.ts sealScopeApproval): the card says so and
|
|
269
|
+
// links to the approval in Toolroll, and offers no Continue or Approve that would dead-end. A release or a plan.
|
|
270
|
+
if (passwordOnlySeal(plan.scope)) {
|
|
271
|
+
let head;
|
|
272
|
+
try {
|
|
273
|
+
head = release ? releaseCardOf(store, plan.scope, plan.digest, { profile: permissionPlainWords }).short.split("\n").slice(0, 3)
|
|
274
|
+
: planCardText(store, target.taskId, plan.scope, root, channel).split("\n").slice(0, 1);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
head = [title(store, target.taskId)];
|
|
278
|
+
}
|
|
279
|
+
return { text: [...head, "", needsPassword(plan.scope)].join("\n"), rows: [[{ label: "Approve in Toolroll", path: chatControlHref("approval", target.taskId) }]] };
|
|
280
|
+
}
|
|
281
|
+
// A deploy gate is approved from chat only through its release card (chat-management.md §0a.5).
|
|
282
|
+
if (release) {
|
|
283
|
+
const release = releaseInChat(store, target.taskId, who.name, now, channel, "offer", root);
|
|
284
|
+
if (!release.ok)
|
|
285
|
+
return null;
|
|
286
|
+
const card = release.card;
|
|
287
|
+
return { text: channel === "telegram" ? card.short : `${card.short}\n\n${card.full}`,
|
|
288
|
+
rows: [[{ act: "approve", run: null, digest: releaseDigest(card), label: "Continue" }], [{ label: DECIDE_LABELS.edit, path: chatControlHref("approval", target.taskId) }, { act: "not-now", run: null, digest: releaseDigest(card) }]],
|
|
289
|
+
...(channel === "telegram" ? { file: { name: card.fileName, text: card.full } } : {}) };
|
|
290
|
+
}
|
|
265
291
|
return { text: planCardText(store, target.taskId, plan.scope, root, channel),
|
|
266
292
|
rows: [[{ act: "approve", run: null, digest: plan.digest }], [{ label: DECIDE_LABELS.edit, path: chatControlHref("approval", target.taskId) }, { act: "not-now", run: null, digest: plan.digest }]] };
|
|
267
293
|
}
|
|
@@ -320,7 +346,7 @@ export function mintDecideButtons(store, seat, target, offer, now, message = nul
|
|
|
320
346
|
return { label: one.label, link: one };
|
|
321
347
|
const token = mint(store, seat, message, one, target.taskId, "offer", now);
|
|
322
348
|
tokens.push(token);
|
|
323
|
-
return { label: DECIDE_LABELS[one.act], token };
|
|
349
|
+
return { label: one.label ?? DECIDE_LABELS[one.act], token };
|
|
324
350
|
}));
|
|
325
351
|
return { rows, tokens };
|
|
326
352
|
}
|
|
@@ -350,6 +376,66 @@ export function hasLiveDecideTokens(store, channel, chat, message, now) {
|
|
|
350
376
|
export function isDecideToken(store, token) {
|
|
351
377
|
return token.startsWith("d:") && store.handle.prepare("SELECT 1 FROM chat_decide_action WHERE token = ?").get(token) !== undefined;
|
|
352
378
|
}
|
|
379
|
+
/** A release card's acts carry its card digest (release-card.ts), never a plan's. */
|
|
380
|
+
const RELEASE_DIGEST = "release:";
|
|
381
|
+
/** Why a password-only scope (passwordOnlySeal) isn't approved from chat, for a release or a plan. */
|
|
382
|
+
const needsPassword = (scope) => `This ${deployGateScope(scope) ? "release" : "plan"}'s checks run outside the sandbox, so it's approved in Toolroll with your password.`;
|
|
383
|
+
/** Whether only a person's password ceremony seals this scope (store.ts sealScopeApproval): checks that reach beyond the
|
|
384
|
+
* sandbox (sign-ins, or none at all) or extra macOS services, sealed into its profile. */
|
|
385
|
+
const passwordOnlySeal = (scope) => {
|
|
386
|
+
const profile = scope.profile;
|
|
387
|
+
return profile != null && (profile["checkAccess"] !== undefined || profile["machLookup"] !== undefined);
|
|
388
|
+
};
|
|
389
|
+
const releaseDigest = (card) => `${RELEASE_DIGEST}${card.digest}`;
|
|
390
|
+
/**
|
|
391
|
+
* Whether a release card's tap comes from the person's own private chat with Toolroll (chat-management.md §0a.5), from
|
|
392
|
+
* the binding's own record, failing closed: Telegram, a private chat (a positive id, which Telegram makes equal to the
|
|
393
|
+
* user's own id) that is the binding's chat and its user's; Slack, a direct-message channel (D…) that is the binding's.
|
|
394
|
+
* Any other chat app, a missing chat or binding, or another person's binding is not.
|
|
395
|
+
*/
|
|
396
|
+
export function privateReleaseChat(store, seat) {
|
|
397
|
+
const row = store.handle.prepare("SELECT member, channel, approver FROM chat_binding WHERE provider = ? AND id = ? AND revoked IS NULL").get(seat.channel, seat.binding);
|
|
398
|
+
if (row === undefined || String(row["approver"]) !== seat.approver || seat.chat !== String(row["channel"]))
|
|
399
|
+
return false;
|
|
400
|
+
if (seat.channel === "telegram")
|
|
401
|
+
return /^[1-9][0-9]*$/.test(seat.chat) && seat.chat === String(row["member"]);
|
|
402
|
+
if (seat.channel === "slack")
|
|
403
|
+
return /^D[A-Z0-9]+$/.test(seat.chat);
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* A deploy gate this person may approve from this chat now, through its release card: every plain-plan rule
|
|
408
|
+
* (planInChat: their Approve from chat setting and limits, no coordinator-, lead- or scout-written text, organisation
|
|
409
|
+
* policy, protected work), never a nightly check, only in their private chat, and the card as the live records say
|
|
410
|
+
* it now (chat-management.md §0a.5).
|
|
411
|
+
*/
|
|
412
|
+
export function releaseInChat(store, taskId, approver, now, channel,
|
|
413
|
+
/** The seat a tap came from, checked strictly; "offer" only where the card is drawn for the binding's own chat (no act). */
|
|
414
|
+
seat, root) {
|
|
415
|
+
const plan = planInChat(store, taskId, approver, now, channel, root);
|
|
416
|
+
if (!plan.ok)
|
|
417
|
+
return plan;
|
|
418
|
+
if (!deployGateScope(plan.scope))
|
|
419
|
+
return { ok: false, why: "This plan isn't a release." };
|
|
420
|
+
if (passwordOnlySeal(plan.scope))
|
|
421
|
+
return { ok: false, why: needsPassword(plan.scope) };
|
|
422
|
+
const ref = store.lookupRef(taskId);
|
|
423
|
+
if (ref === null || nightlyTaskRef(store, ref.id))
|
|
424
|
+
return { ok: false, why: NIGHTLY_CHECK_ONLY };
|
|
425
|
+
if (seat !== "offer" && (seat.channel !== channel || !privateReleaseChat(store, seat)))
|
|
426
|
+
return { ok: false, why: "A release is approved only in your private chat with Toolroll." };
|
|
427
|
+
let card;
|
|
428
|
+
try {
|
|
429
|
+
card = releaseCardOf(store, plan.scope, plan.digest, { profile: permissionPlainWords });
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return { ok: false, why: "This release's terms can't be shown here, so you approve it in Toolroll." };
|
|
433
|
+
}
|
|
434
|
+
// What changed is part of what's approved: a list that couldn't be read (or was refused as rewritten history) is no card.
|
|
435
|
+
if (plan.scope.releaseTerms != null && card.changes.commits === null)
|
|
436
|
+
return { ok: false, why: "This release's list of changes couldn't be read, so you approve it in Toolroll." };
|
|
437
|
+
return { ok: true, card, modeDigest: plan.modeDigest, source: plan.source, scope: plan.scope };
|
|
438
|
+
}
|
|
353
439
|
const QUESTIONS = {
|
|
354
440
|
accept: name => `Accept and finish "${name}"?`,
|
|
355
441
|
retry: name => `Retry "${name}"? It queues another attempt.`,
|
|
@@ -366,6 +452,24 @@ function targetOf(row) {
|
|
|
366
452
|
}
|
|
367
453
|
}
|
|
368
454
|
/** Why the card's act no longer applies, or null while it still does — checked at the offer and again at the yes. */
|
|
455
|
+
/**
|
|
456
|
+
* A release card's terms, then its Approve (Continue, and a release card refreshed after it changed): the full terms
|
|
457
|
+
* (a file on Telegram, where a message is limited; inline elsewhere), Approve and Cancel bound to this card's digest,
|
|
458
|
+
* and on Telegram the card to show instead when the file didn't arrive: no Approve, a Continue to send it again.
|
|
459
|
+
*/
|
|
460
|
+
function releaseContinue(store, seat, message, taskId, name, card, now, ack, lead) {
|
|
461
|
+
const spec = { act: "approve", run: null, digest: releaseDigest(card) };
|
|
462
|
+
const yes = mint(store, seat, message, spec, taskId, "yes", now);
|
|
463
|
+
const cancel = mint(store, seat, message, spec, taskId, "cancel", now);
|
|
464
|
+
const text = `${seat.channel === "telegram" ? card.short : `${card.short}\n\n${card.full}`}${lead === null ? "" : `\n\n${lead}`}`;
|
|
465
|
+
// Telegram carries the terms only as the file: Approve shows only once that file has arrived (telegram.ts).
|
|
466
|
+
const again = seat.channel === "telegram" ? mint(store, seat, message, spec, taskId, "offer", now) : null;
|
|
467
|
+
return { ack, file: { name: card.fileName, text: card.full },
|
|
468
|
+
edit: { text: `${text}\n\nApprove release "${name}"? This approves exactly these terms${seat.channel === "telegram" ? `, every one in ${card.fileName}` : ""}.`,
|
|
469
|
+
rows: [[{ label: "Approve", token: yes }, { label: DECIDE_LABELS.cancel, token: cancel }]], tokens: [yes, cancel] },
|
|
470
|
+
...(again === null ? {} : { unsent: { text: `${text}\n\nThe terms file didn't arrive, so Approve isn't shown. Tap Continue to send it again.`,
|
|
471
|
+
rows: [[{ label: "Continue", token: again }]], tokens: [again] } }) };
|
|
472
|
+
}
|
|
369
473
|
function staleWhy(store, row, who, now, channel, root) {
|
|
370
474
|
switch (row.act) {
|
|
371
475
|
case "accept":
|
|
@@ -384,9 +488,21 @@ function staleWhy(store, row, who, now, channel, root) {
|
|
|
384
488
|
}
|
|
385
489
|
case "approve":
|
|
386
490
|
case "not-now": {
|
|
491
|
+
if (row.digest.startsWith(RELEASE_DIGEST)) {
|
|
492
|
+
// The button's own seat as it was minted: its binding's chat, checked against the binding's record.
|
|
493
|
+
const release = releaseInChat(store, row.taskId, who.name, now, channel, { channel, binding: row.binding ?? -1, chat: row.chat ?? "", approver: who.name }, root);
|
|
494
|
+
if (!release.ok)
|
|
495
|
+
return release.why;
|
|
496
|
+
return releaseDigest(release.card) === row.digest ? null : "This release changed while you were reading.";
|
|
497
|
+
}
|
|
387
498
|
const plan = planInChat(store, row.taskId, who.name, now, channel, root);
|
|
388
499
|
if (!plan.ok)
|
|
389
500
|
return plan.why;
|
|
501
|
+
// A deploy gate's plain plan card never approves it: only its release card does.
|
|
502
|
+
if (deployGateScope(plan.scope))
|
|
503
|
+
return "This is a release: approve it from its release card.";
|
|
504
|
+
if (passwordOnlySeal(plan.scope))
|
|
505
|
+
return needsPassword(plan.scope);
|
|
390
506
|
return plan.digest === row.digest ? null : "The plan changed since this card was sent.";
|
|
391
507
|
}
|
|
392
508
|
case "merge": {
|
|
@@ -427,7 +543,7 @@ export function planWhereNow(store, taskId, who, now, root) {
|
|
|
427
543
|
}
|
|
428
544
|
return { said: `"${name}" is under way.`, rows: [] };
|
|
429
545
|
}
|
|
430
|
-
const QUESTION_STARTS = ["Accept and finish \"", "Retry \"", "Approve and start \"", "Merge \""];
|
|
546
|
+
const QUESTION_STARTS = ["Accept and finish \"", "Retry \"", "Approve and start \"", "Merge \"", "Approve release "];
|
|
431
547
|
/** The card's words without a question this module appended. */
|
|
432
548
|
function cardBody(shown) {
|
|
433
549
|
const at = shown.lastIndexOf("\n\n");
|
|
@@ -484,8 +600,24 @@ export function applyDecideTap(store, seat, input) {
|
|
|
484
600
|
return { ack: "Cancelled.", edit: { text: offer.text ?? body, ...minted } };
|
|
485
601
|
}
|
|
486
602
|
const why = staleWhy(store, row, who, now, seat.channel, input.root);
|
|
487
|
-
if (why !== null)
|
|
603
|
+
if (why !== null) {
|
|
604
|
+
// A release that changed while it was read: the new card, with fresh buttons, so the person reads it again.
|
|
605
|
+
// Through the same gate as Continue: the new terms go first (as a file where the chat app limits a message), and
|
|
606
|
+
// Approve shows only once they arrived; else a Continue to send them again. A release that is now password-only
|
|
607
|
+
// gets its card with the link to Toolroll.
|
|
608
|
+
if (row.digest.startsWith(RELEASE_DIGEST) && row.act === "approve" && why === "This release changed while you were reading.") {
|
|
609
|
+
spend();
|
|
610
|
+
const release = releaseInChat(store, row.taskId, who.name, now, seat.channel, seat, input.root);
|
|
611
|
+
if (release.ok)
|
|
612
|
+
return releaseContinue(store, seat, input.message, row.taskId, name, release.card, now, why, `${why} Here it is now.`);
|
|
613
|
+
const offer = decideOffer(store, target, who, now, seat.channel, input.root);
|
|
614
|
+
if (offer !== null && offer.file === undefined) {
|
|
615
|
+
const minted = mintDecideButtons(store, seat, target, offer, now, input.message);
|
|
616
|
+
return { ack: why, edit: { text: `${offer.text ?? body}\n\n${why} Here it is now.`, ...minted } };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
488
619
|
return stale(why);
|
|
620
|
+
}
|
|
489
621
|
if (row.phase === "offer") {
|
|
490
622
|
if (row.act === "changes") {
|
|
491
623
|
const id = openDecidePrompt(store, seat, row.taskId, row.run ?? 0, row.digest, now);
|
|
@@ -496,6 +628,14 @@ export function applyDecideTap(store, seat, input) {
|
|
|
496
628
|
return { ack: "Okay, it waits.", edit: { text: `${body}\n\nNot now. It waits for you in Tasks.`,
|
|
497
629
|
rows: [[{ label: DECIDE_LABELS.edit, link: { label: DECIDE_LABELS.edit, path: chatControlHref("approval", row.taskId) } }]], tokens: [] } };
|
|
498
630
|
}
|
|
631
|
+
// A release card's Continue: the card again from the live records, its full terms as a file, then Approve.
|
|
632
|
+
if (row.act === "approve" && row.digest.startsWith(RELEASE_DIGEST)) {
|
|
633
|
+
const release = releaseInChat(store, row.taskId, who.name, now, seat.channel, seat, input.root);
|
|
634
|
+
if (!release.ok)
|
|
635
|
+
return stale(release.why);
|
|
636
|
+
spend();
|
|
637
|
+
return releaseContinue(store, seat, input.message, row.taskId, name, release.card, now, "Read the terms, then approve.", null);
|
|
638
|
+
}
|
|
499
639
|
// The first tap asks; only the yes acts, bound to exactly what this button was.
|
|
500
640
|
spend();
|
|
501
641
|
const spec = { act: row.act, run: row.run, digest: row.digest };
|
|
@@ -521,9 +661,35 @@ export function applyDecideTap(store, seat, input) {
|
|
|
521
661
|
return retried.ok ? done("✓ Queued again. Existing approvals and holds still apply.") : notDone(retried.message);
|
|
522
662
|
}
|
|
523
663
|
case "approve": {
|
|
664
|
+
if (row.digest.startsWith(RELEASE_DIGEST)) {
|
|
665
|
+
const release = releaseInChat(store, row.taskId, who.name, now, seat.channel, seat, input.root);
|
|
666
|
+
if (!release.ok)
|
|
667
|
+
return notDone(release.why);
|
|
668
|
+
const cardDigest = release.card.digest;
|
|
669
|
+
// The seal knows which card it came from: a chat seal of a deploy gate needs this evidence (store.ts).
|
|
670
|
+
if (!store.sealScopeApproval(row.taskId, who.name, now, {}, { kind: "chat", modeDigest: release.modeDigest, card: "release", cardDigest }))
|
|
671
|
+
return notDone("the release couldn't be approved from chat. Open it in Toolroll.");
|
|
672
|
+
const sealedDigest = store.getScope(row.taskId)?.approvedDigest ?? "";
|
|
673
|
+
store.recordAction({ at: now.toISOString(), actor: who.name, repo: ref.repo, taskId: row.taskId, runId: null, action: APPROVAL_PROOF, outcome: "chat", source: "request",
|
|
674
|
+
detail: JSON.stringify({ v: 1, at: now.toISOString(), generation: store.accountOf(who.name)?.generation ?? 0, digest: sealedDigest, card: "release", cardDigest }) });
|
|
675
|
+
const where = `${CHANNEL_NAMES[seat.channel]} · chat binding #${seat.binding}`;
|
|
676
|
+
store.recordAction({ at: now.toISOString(), actor: who.name, repo: ref.repo, taskId: row.taskId, runId: null, action: "release approved in chat", outcome: "approved",
|
|
677
|
+
source: "request", detail: `via ${where} · ${release.source} ${release.modeDigest} · card ${cardDigest}` });
|
|
678
|
+
// Said everywhere the person hears from Toolroll (their other chats and push), naming the chat it came from.
|
|
679
|
+
const terms = release.scope.releaseTerms ?? null;
|
|
680
|
+
store.enqueueNotification({ dedupeKey: `release-approved-chat:${row.taskId}:${cardDigest}`, kind: "release-approved", recipient: who.name, pushClass: "attention",
|
|
681
|
+
subject: `Release${terms === null ? "" : ` ${terms.packageVersion}`} approved from ${CHANNEL_NAMES[seat.channel]}`,
|
|
682
|
+
body: `You approved "${name}"${terms === null ? "" : ` (${terms.approvedSha.slice(0, 7)}, pull request #${terms.pr})`} from ${CHANNEL_NAMES[seat.channel]}. If that wasn't you, stop it in Toolroll now.`,
|
|
683
|
+
link: chatControlHref("task", row.taskId), source: { taskRef: ref.id } }, now);
|
|
684
|
+
return done("✓ Release approved. It deploys when its check passes and someone runs the release.");
|
|
685
|
+
}
|
|
524
686
|
const plan = planInChat(store, row.taskId, who.name, now, seat.channel, input.root);
|
|
525
687
|
if (!plan.ok)
|
|
526
688
|
return notDone(plan.why);
|
|
689
|
+
if (deployGateScope(plan.scope))
|
|
690
|
+
return notDone("This is a release: approve it from its release card.");
|
|
691
|
+
if (passwordOnlySeal(plan.scope))
|
|
692
|
+
return notDone(needsPassword(plan.scope));
|
|
527
693
|
// The owner's own yes, sealed as theirs ("chat"), never as a mode's: it stands like the console's after the mode or setting ends.
|
|
528
694
|
if (!store.sealScopeApproval(row.taskId, who.name, now, {}, { kind: "chat", modeDigest: plan.modeDigest }))
|
|
529
695
|
return notDone("the plan couldn't be approved from chat. Open it in Toolroll.");
|
package/dist/chat-quiet.js
CHANGED
|
@@ -101,7 +101,7 @@ export function quietCardView(store, taskRefs, now, root, viewer) {
|
|
|
101
101
|
const heading = `${lines.length} tasks`;
|
|
102
102
|
const projects = [...new Set(lines.map(one => projectLabel(one.project)))];
|
|
103
103
|
const text = [heading, ...lines.map(one => `${one.icon} ${phoneText(one.title, 60)} · ${one.status}`), "", projects.join(", ")].join("\n");
|
|
104
|
-
return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/
|
|
104
|
+
return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/work" } };
|
|
105
105
|
}
|
|
106
106
|
/** Facts that carry their own controls or must stand alone (a security alert, a decision, a flow card, a
|
|
107
107
|
* screenshot, an acceptance packet) are never folded into a batch. */
|
|
@@ -176,7 +176,7 @@ export function finishedView(store, batch, now, root, viewer) {
|
|
|
176
176
|
return null;
|
|
177
177
|
const text = batchLine(lines.map(one => one.fact));
|
|
178
178
|
if (lines.length > 1)
|
|
179
|
-
return { text, entities: [], link: { label: "Open", path: "/
|
|
179
|
+
return { text, entities: [], link: { label: "Open", path: "/work" } };
|
|
180
180
|
const target = lines[0].target ?? null;
|
|
181
181
|
return { text, entities: [], link: lines[0].link, ...(lines[0].also.length === 0 ? {} : { also: lines[0].also }), ...(target === null ? {} : { target }) };
|
|
182
182
|
}
|
|
@@ -242,7 +242,7 @@ export function enqueueEveningDigests(store, now, root) {
|
|
|
242
242
|
if (body === null)
|
|
243
243
|
return;
|
|
244
244
|
if (store.enqueueNotification({ dedupeKey: `digest:evening:${person.account}:${day}`, kind: "evening-digest", subject: "Evening digest", body,
|
|
245
|
-
link: "/
|
|
245
|
+
link: "/work", source: { installation: true }, recipient: person.account }, now))
|
|
246
246
|
queued++;
|
|
247
247
|
});
|
|
248
248
|
}
|
package/dist/check-report.js
CHANGED
|
@@ -127,7 +127,8 @@ export function prepareCheckAttempt(store, root, input, now) {
|
|
|
127
127
|
planDigest: plan === null ? null : payloadDigest(plan), snapshotDigest: snapshot === null ? null : payloadDigest(snapshot),
|
|
128
128
|
inventoryDigest: payloadDigest(inventory), contextDigest: payloadDigest(context), nonceHash: sha256(nonce),
|
|
129
129
|
}, now);
|
|
130
|
-
|
|
130
|
+
// The command's own limit rides along, so the check ends itself (logs and report printed) before the plane stops it.
|
|
131
|
+
const stdin = JSON.stringify({ nonce, mode, plan, watch: [...(input.watch ?? [])], limitMs: input.command.timeoutMs });
|
|
131
132
|
return { runId: input.runId, nonce, stdin, mode, inventory, context, plan, snapshot, watch: [...(input.watch ?? [])] };
|
|
132
133
|
}
|
|
133
134
|
/** Seal one JSON payload as a structured-output artifact under its capture label. */
|
package/dist/coding-provider.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createContainer, currentContainment } from "./containment.js";
|
|
|
10
10
|
import { processMayBeAlive } from "./process-liveness.js";
|
|
11
11
|
import { observeProcessTree, sampleProcessTree, stopProcessTree } from "./process-tree.js";
|
|
12
12
|
import { isOwnEnv } from "./names.js";
|
|
13
|
+
import { isProtectedEnvName } from "./protected-storage.js";
|
|
13
14
|
import { sleep } from "./sleep.js";
|
|
14
15
|
/** A response from the server, distinct from an ambiguous transport failure. */
|
|
15
16
|
export class CodingProviderRequestError extends Error {
|
|
@@ -54,8 +55,9 @@ export function codingEnvironment() {
|
|
|
54
55
|
// Preserve native Codex auth, HOME, PATH, CODEX_HOME and project config.
|
|
55
56
|
// A parent desktop session's control pipe and identity do not belong to
|
|
56
57
|
// this independent native client. Keep install/runtime paths such as
|
|
57
|
-
// CODEX_MCP_NODE_PATH. Service credentials stay with the web service
|
|
58
|
-
|
|
58
|
+
// CODEX_MCP_NODE_PATH. Service credentials stay with the web service; no chat or vault variable reaches it
|
|
59
|
+
// (protected-storage.ts).
|
|
60
|
+
if (isOwnEnv(key) || isProtectedEnvName(key) || /^CODEX_APP_TOOLS_/.test(key) || invokingSession.has(key) || key === "NODE_OPTIONS" || key === "NODE_PATH")
|
|
59
61
|
delete env[key];
|
|
60
62
|
}
|
|
61
63
|
return env;
|
package/dist/commit-extract.d.ts
CHANGED
|
@@ -60,6 +60,12 @@ export type Verified<T> = {
|
|
|
60
60
|
ok: false;
|
|
61
61
|
message: string;
|
|
62
62
|
};
|
|
63
|
+
/** Which Git runs and with what environment: the release path's fixed absolute Git and its own environment, or by
|
|
64
|
+
* default the checkout Git environment's. The hardening settings are added over either. */
|
|
65
|
+
export type GitRunner = {
|
|
66
|
+
git?: string;
|
|
67
|
+
env?: NodeJS.ProcessEnv;
|
|
68
|
+
};
|
|
63
69
|
/** One commit, hashed here and parsed from its own bytes. */
|
|
64
70
|
export declare function readVerifiedCommit(gitDir: string, sha: string): Verified<VerifiedCommit>;
|
|
65
71
|
/**
|
|
@@ -91,4 +97,15 @@ export declare function verifiedBlobs(gitDir: string, tree: string, paths: reado
|
|
|
91
97
|
id: string;
|
|
92
98
|
bytes: Buffer;
|
|
93
99
|
}>>;
|
|
100
|
+
/**
|
|
101
|
+
* The commits reachable from `to` and not from `from`, as Git lists them (newest first), each read from its own verified
|
|
102
|
+
* bytes: what a release card says changed (release-card.ts). Git only names them. Each is hashed here and its parents
|
|
103
|
+
* checked against Git's listing; every commit the range stops at (a parent outside it) must be reached from `from`
|
|
104
|
+
* through verified parents within `reach` commits, so history a build rewrote around the objects can't shorten the
|
|
105
|
+
* list. A shallow boundary, grafts or replacement refs refuse (verifiedReader), as does a range longer than `max`.
|
|
106
|
+
*/
|
|
107
|
+
export declare function verifiedRange(gitDir: string, from: string, to: string, options?: GitRunner & {
|
|
108
|
+
max?: number;
|
|
109
|
+
reach?: number;
|
|
110
|
+
}): Verified<VerifiedCommit[]>;
|
|
94
111
|
export {};
|