toolroll 0.9.63 → 0.9.67
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/README.md +33 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +117 -2
- package/dist/agent-fence.js +439 -12
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +39 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +228 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +10 -1
- package/dist/builder.js +87 -24
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +13 -4
- package/dist/contracts/console-api.js +13 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +35 -0
- package/dist/invoke.js +347 -28
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +269 -0
- package/dist/live-terminal.js +686 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +335 -19
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +18 -0
- package/dist/project-delete-ui.js +60 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release-adapters.d.ts +6 -0
- package/dist/release-adapters.js +70 -3
- package/dist/release.d.ts +10 -1
- package/dist/release.js +34 -15
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-follow-ups.js +3 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +183 -5
- package/dist/scope.js +256 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +248 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +16 -2
- package/dist/server/chrome.js +63 -11
- package/dist/server/console.css +107 -42
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +32 -4
- package/dist/server/render-pages.js +83 -13
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +111 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +47 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +142 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +327 -78
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +440 -15
- package/dist/store.js +1072 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +14 -0
- package/dist/tool-broker.js +67 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +15 -2
package/dist/telegram.js
CHANGED
|
@@ -46,6 +46,10 @@ import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
|
|
|
46
46
|
import { envValue } from "./names.js";
|
|
47
47
|
import { applyProposalTap, processTelegramConversations, phoneLinkButton, replyContextFor, telegramConversationRepos, telegramRequestId, tooLongText, whichTaskText, } from "./telegram-lead.js";
|
|
48
48
|
import { askOf, parseTelegramUpdate, pickOf, readTelegramButtonData, readTelegramUpdate, telegramButton } from "./contracts/telegram-callback.js";
|
|
49
|
+
/** Read the enrolled project list on demand. No callback means no task data,
|
|
50
|
+
* never an implicit all-database ceiling. Shared by pass and embedded follower. */
|
|
51
|
+
/** A delivery its person began watching after it was claimed: held or settled at the final fence (cockpit §3). */
|
|
52
|
+
const WATCHED_SINCE_CLAIM = "Held: its person is watching this agent now";
|
|
49
53
|
/** The environment name — and therefore the name the builder strips from agents (both names; see names.ts). */
|
|
50
54
|
export { TELEGRAM_TOKEN_ENV as TOKEN_ENV, TELEGRAM_TOKEN_ENVS as TOKEN_ENVS } from "./names.js";
|
|
51
55
|
/** BotFather's shape: numeric bot id, colon, secret. */
|
|
@@ -732,6 +736,9 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
732
736
|
const blocked = [];
|
|
733
737
|
const preflight = (await readAccess()) ?? channelProblem();
|
|
734
738
|
for (const row of group) {
|
|
739
|
+
// Watched since the claim (cockpit §3): held for later or settled quietly, never sent now, never a failure.
|
|
740
|
+
if (preflight === null && store.telegramWatchFence(row, binding, owner, clock()) !== "send")
|
|
741
|
+
continue;
|
|
735
742
|
const problem = preflight ?? store.telegramDeliveryProblem(row, binding, owner, projects, clock(), [...rows.map(one => one.id), row.id]);
|
|
736
743
|
if (problem === null)
|
|
737
744
|
rows.push(row);
|
|
@@ -747,7 +754,18 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
747
754
|
if (rows.length === 0)
|
|
748
755
|
continue;
|
|
749
756
|
const ids = rows.map(row => row.id);
|
|
750
|
-
|
|
757
|
+
// Once any part of this delivery has reached Telegram it is committed: a watch opening between its parts never
|
|
758
|
+
// releases it (that would send the parts already accepted again). Every transport call below goes through `wire`.
|
|
759
|
+
let sentAny = false;
|
|
760
|
+
const wire = async (...args) => {
|
|
761
|
+
const answer = await transport(...args);
|
|
762
|
+
if (answer.ok === true)
|
|
763
|
+
sentAny = true;
|
|
764
|
+
return answer;
|
|
765
|
+
};
|
|
766
|
+
// `beforeSend`: the fence before a transport call also asks the watch — until something of this delivery went out;
|
|
767
|
+
// the one after a confirmed send never does, so a message Telegram accepted is recorded as delivered.
|
|
768
|
+
const fence = (beforeSend = !sentAny) => {
|
|
751
769
|
const channel = channelProblem();
|
|
752
770
|
if (channel !== null)
|
|
753
771
|
return channel;
|
|
@@ -755,6 +773,10 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
755
773
|
const problem = store.telegramDeliveryProblem(row, binding, owner, projects, clock(), ids);
|
|
756
774
|
if (problem !== null)
|
|
757
775
|
return problem;
|
|
776
|
+
// The watch, at the last moment before the network: a held row's claim is released (its finalize is then a
|
|
777
|
+
// no-op), so nothing is spent on it.
|
|
778
|
+
if (beforeSend && store.telegramWatchFence(row, binding, owner, clock()) !== "send")
|
|
779
|
+
return WATCHED_SINCE_CLAIM;
|
|
758
780
|
}
|
|
759
781
|
return null;
|
|
760
782
|
};
|
|
@@ -763,7 +785,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
763
785
|
const problem = (await readAccess()) ?? fence();
|
|
764
786
|
if (problem !== null)
|
|
765
787
|
return { ok: false, error: problem };
|
|
766
|
-
const sent = await send(
|
|
788
|
+
const sent = await send(wire, binding.chatId, text, keyboard, entities, silent);
|
|
767
789
|
if (!sent.ok) {
|
|
768
790
|
if (sent.retryAfter !== undefined)
|
|
769
791
|
store.deferTelegram(botId, new Date(clock().getTime() + sent.retryAfter * 1_000).toISOString());
|
|
@@ -774,7 +796,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
774
796
|
for (const row of messageRows)
|
|
775
797
|
store.recordTelegramMessage(row, binding, sent.messageId, clock());
|
|
776
798
|
// Keep the old message's history but never acknowledge a replaced pairing.
|
|
777
|
-
const after = (await readAccess()) ?? fence();
|
|
799
|
+
const after = (await readAccess()) ?? fence(false);
|
|
778
800
|
return after === null ? sent : { ok: false, error: after };
|
|
779
801
|
};
|
|
780
802
|
const imageSender = async (row) => {
|
|
@@ -803,7 +825,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
803
825
|
// No await between the access fence, file validation and upload.
|
|
804
826
|
let answer;
|
|
805
827
|
try {
|
|
806
|
-
answer = await
|
|
828
|
+
answer = await wire("sendDocument", { chat_id: binding.chatId, caption: `${resultTaskLabel(row.taskId)} · result #${row.run} · screenshot for acceptance` }, undefined, {
|
|
807
829
|
field: "document", bytes: verified.bytes, contentType: verified.format === "png" ? "image/png" : "image/jpeg", fileName: resultImageFileName(row.taskId, row.run, artifact, verified.format),
|
|
808
830
|
});
|
|
809
831
|
}
|
|
@@ -819,7 +841,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
819
841
|
return { ok: false, error: answer.uncertain || (answer.ok && messageId == null) ? "Screenshot delivery is unconfirmed; retry may duplicate it" : "Telegram did not accept the screenshot" };
|
|
820
842
|
}
|
|
821
843
|
store.recordTelegramMessage(row, binding, String(messageId), clock());
|
|
822
|
-
const after = (await readAccess()) ?? fence();
|
|
844
|
+
const after = (await readAccess()) ?? fence(false);
|
|
823
845
|
return after === null ? { ok: true, receipt: receiptFor(botId, binding.chatId, String(messageId)) } : { ok: false, error: after };
|
|
824
846
|
};
|
|
825
847
|
/** Screenshots with a result (result-shots.ts): photos, several as one album, re-proved before every call. Each is
|
|
@@ -846,10 +868,10 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
846
868
|
let answer;
|
|
847
869
|
try {
|
|
848
870
|
answer = batch.length > 1
|
|
849
|
-
? await
|
|
871
|
+
? await wire("sendMediaGroup", { chat_id: binding.chatId, media: batch.map((one, index) => ({ type: "photo", media: `attach://shot${index}`, ...(index === 0 || one.numbered === true ? { caption: one.caption } : {}) })) }, undefined, upload(batch[0], "shot0"), batch.slice(1).map((one, index) => upload(one, `shot${index + 1}`)))
|
|
850
872
|
: batch[0].photo
|
|
851
|
-
? await
|
|
852
|
-
: await
|
|
873
|
+
? await wire("sendPhoto", { chat_id: binding.chatId, caption: batch[0].caption }, undefined, upload(batch[0], "photo"))
|
|
874
|
+
: await wire("sendDocument", { chat_id: binding.chatId, caption: batch[0].caption }, undefined, upload(batch[0], "document"));
|
|
853
875
|
}
|
|
854
876
|
catch {
|
|
855
877
|
answer = { ok: false, uncertain: true };
|
|
@@ -893,7 +915,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
893
915
|
const keyboard = button === null ? [] : [button];
|
|
894
916
|
if (messageId === null)
|
|
895
917
|
return sender(card.text, keyboard, rows, card.entities);
|
|
896
|
-
const edited = await editProgress(
|
|
918
|
+
const edited = await editProgress(wire, binding.chatId, messageId, card.text, keyboard, card.entities);
|
|
897
919
|
if (!edited.ok) {
|
|
898
920
|
if (edited.retryAfter !== undefined)
|
|
899
921
|
store.deferTelegram(botId, new Date(clock().getTime() + edited.retryAfter * 1000).toISOString());
|
|
@@ -905,7 +927,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
905
927
|
}
|
|
906
928
|
if (!onlyExisting)
|
|
907
929
|
store.recordTelegramMessage(row, binding, messageId, clock());
|
|
908
|
-
const after = (await readAccess()) ?? fence();
|
|
930
|
+
const after = (await readAccess()) ?? fence(false);
|
|
909
931
|
return after === null ? edited : { ok: false, error: after };
|
|
910
932
|
};
|
|
911
933
|
/** Quiet mode: the task's one card (or its group's), created silently and then only ever edited. */
|
|
@@ -929,11 +951,11 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
929
951
|
return { ok: true, messageId: card.message };
|
|
930
952
|
}
|
|
931
953
|
if (card.message !== null) {
|
|
932
|
-
const edited = await editProgress(
|
|
954
|
+
const edited = await editProgress(wire, binding.chatId, card.message, view.text, keyboard, view.entities);
|
|
933
955
|
if (edited.ok) {
|
|
934
956
|
store.setChatCardMessage(card.id, card.message, shown);
|
|
935
957
|
store.recordTelegramMessage(fact, binding, card.message, clock());
|
|
936
|
-
const after = (await readAccess()) ?? fence();
|
|
958
|
+
const after = (await readAccess()) ?? fence(false);
|
|
937
959
|
return after === null ? edited : { ok: false, error: after };
|
|
938
960
|
}
|
|
939
961
|
if (edited.retryAfter !== undefined)
|
|
@@ -1003,14 +1025,14 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
1003
1025
|
if (problem !== null)
|
|
1004
1026
|
return { ok: false, error: problem };
|
|
1005
1027
|
mintFor();
|
|
1006
|
-
const edited = await editProgress(
|
|
1028
|
+
const edited = await editProgress(wire, binding.chatId, batch.message, text, keyboard);
|
|
1007
1029
|
if (edited.ok) {
|
|
1008
1030
|
// The repaint landed: the old words' buttons retire and the new ones ride the message.
|
|
1009
1031
|
retireDecideTokens(store, "telegram", binding.chatId, batch.message, clock());
|
|
1010
1032
|
placeDecideTokens(store, tokens, batch.message);
|
|
1011
1033
|
store.setChatBatchMessage(batch.id, batch.message, shown);
|
|
1012
1034
|
store.recordTelegramMessage(fact, binding, batch.message, clock());
|
|
1013
|
-
const after = (await readAccess()) ?? fence();
|
|
1035
|
+
const after = (await readAccess()) ?? fence(false);
|
|
1014
1036
|
return after === null ? { ok: true, receipt: receiptFor(botId, binding.chatId, batch.message) } : { ok: false, error: after };
|
|
1015
1037
|
}
|
|
1016
1038
|
// A failed edit: the buttons it carried never act, and the card's old ones keep working.
|
|
@@ -1049,10 +1071,10 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
1049
1071
|
const problem = (await readAccess()) ?? fence();
|
|
1050
1072
|
if (problem !== null)
|
|
1051
1073
|
return { ok: false, error: problem };
|
|
1052
|
-
const edited = await editProgress(
|
|
1074
|
+
const edited = await editProgress(wire, binding.chatId, earlier, words, keyboard);
|
|
1053
1075
|
if (edited.ok) {
|
|
1054
1076
|
store.recordTelegramMessage(fact, binding, earlier, clock());
|
|
1055
|
-
const after = (await readAccess()) ?? fence();
|
|
1077
|
+
const after = (await readAccess()) ?? fence(false);
|
|
1056
1078
|
return after === null ? { ok: true, receipt: receiptFor(botId, binding.chatId, earlier) } : { ok: false, error: after };
|
|
1057
1079
|
}
|
|
1058
1080
|
if (edited.retryAfter !== undefined)
|
|
@@ -1082,7 +1104,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
1082
1104
|
? await deliverDigest(botId, binding, sender, rows, digest.lastSentAt, clock, taskId => chatTitle(store, taskId))
|
|
1083
1105
|
: await deliverOne(store, botId, binding, sender, rows[0], clock, phoneOrigin, evidenceRoot, projects);
|
|
1084
1106
|
}
|
|
1085
|
-
const finalProblem = outcome.ok ? (await readAccess()) ?? fence() : null;
|
|
1107
|
+
const finalProblem = outcome.ok ? (await readAccess()) ?? fence(false) : null;
|
|
1086
1108
|
const settled = finalProblem === null ? outcome : { ok: false, error: finalProblem };
|
|
1087
1109
|
// A skipped screenshot settles its row but nothing reached the phone.
|
|
1088
1110
|
const skipped = settled.ok && settled.receipt !== null && settled.receipt.startsWith("skipped:");
|
|
@@ -1748,6 +1770,8 @@ function applyMessage(context, update, effects) {
|
|
|
1748
1770
|
* message that carried several tasks (never guessed). Everything hostile is
|
|
1749
1771
|
* silence, as for every other inbound shape.
|
|
1750
1772
|
*/
|
|
1773
|
+
/** At most this many of a chat's messages wait for the lead at once (the personal lead queue's bound). */
|
|
1774
|
+
export const TELEGRAM_MAX_WAITING = 10;
|
|
1751
1775
|
function applyConversation(context, update, effects) {
|
|
1752
1776
|
const { store, botId, transport, clock, report } = context;
|
|
1753
1777
|
const message = update.message;
|
|
@@ -1812,6 +1836,12 @@ function applyConversation(context, update, effects) {
|
|
|
1812
1836
|
context_ = focusContextFor(focused);
|
|
1813
1837
|
}
|
|
1814
1838
|
}
|
|
1839
|
+
// The lead queue's bound, per chat: ten messages wait at most; the next is answered at once instead of queued.
|
|
1840
|
+
if (store.telegramMessagesWaiting(binding.id) >= TELEGRAM_MAX_WAITING) {
|
|
1841
|
+
say(`${TELEGRAM_MAX_WAITING} messages are already waiting for the lead. Wait for a reply, then send this again.`);
|
|
1842
|
+
report.ignored++;
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1815
1845
|
store.enqueueTelegramConversation({
|
|
1816
1846
|
binding, updateId: update.update_id, messageId: String(message.message_id), replyTo,
|
|
1817
1847
|
request: telegramRequestId(botId, binding.id, update.update_id), text, context: context_, taskId, sourceRun,
|
|
@@ -2248,6 +2278,10 @@ function applyCallback(context, update, effects) {
|
|
|
2248
2278
|
return;
|
|
2249
2279
|
}
|
|
2250
2280
|
const focused = store.chatFocus("telegram", binding.id);
|
|
2281
|
+
if (store.telegramMessagesWaiting(binding.id) >= TELEGRAM_MAX_WAITING) {
|
|
2282
|
+
ack(`${TELEGRAM_MAX_WAITING} messages are already waiting. Wait for a reply.`);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2251
2285
|
store.enqueueTelegramConversation({
|
|
2252
2286
|
binding, updateId: update.update_id, messageId: String(message.message_id), replyTo: null,
|
|
2253
2287
|
request: telegramRequestId(botId, binding.id, update.update_id), text: option,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Database } from "./store.js";
|
|
2
|
+
import type { TerminalLedger } from "./live-terminal.js";
|
|
3
|
+
export declare function terminalCatalogPath(orders: Database): string | null;
|
|
4
|
+
/** The console's ledger: creates the catalog, drops rows of consoles that are gone, and writes its own. */
|
|
5
|
+
export declare function openTerminalLedger(orders: Database, alive?: (pid: number) => boolean): TerminalLedger | null;
|
|
6
|
+
export declare function installTerminalUpdateGate(orders: Database, id: string): void;
|
|
7
|
+
export declare function removeTerminalUpdateGate(orders: Database, id: string): void;
|
|
8
|
+
/** Terminals starting or running in a console that is still alive: active work an update waits for. */
|
|
9
|
+
export declare function activeTerminalUpdateWork(orders: Database, alive?: (pid: number) => boolean): {
|
|
10
|
+
terminals: number;
|
|
11
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live terminals, written down for the updater (docs/plans/live-terminal.md, review round 2 finding 7). Terminals live
|
|
3
|
+
* in the console's memory, but an update is decided by another process, so each one is a row in a small catalog
|
|
4
|
+
* beside the database (`<db>.terminals.sqlite`), as the coding workspace keeps its own:
|
|
5
|
+
*
|
|
6
|
+
* - a start inserts its row before any work, and an update's gate (a trigger) refuses that insert while the update
|
|
7
|
+
* owns admission: no terminal starts during a drain;
|
|
8
|
+
* - a row is `starting` or `running` until the terminal's sweep ends, then deleted — or kept as `uncertain` when the
|
|
9
|
+
* sweep couldn't prove its processes gone, until the terminal is removed; the updater counts rows whose console is
|
|
10
|
+
* still alive as active work, so "update when idle" waits for running and unproved terminals;
|
|
11
|
+
* - rows of a console that is gone are stale (its programs went with it, or are swept at its next start).
|
|
12
|
+
*/
|
|
13
|
+
import { chmodSync, lstatSync } from "node:fs";
|
|
14
|
+
import { processMayBeAlive } from "./process-liveness.js";
|
|
15
|
+
import { sqliteRuntime } from "./sqlite-runtime.js";
|
|
16
|
+
const gatePrefix = "so_terminal_update_";
|
|
17
|
+
const PAUSED = "Toolroll is updating. Running terminals keep going; new ones can start after the update.";
|
|
18
|
+
const SCHEMA = "CREATE TABLE IF NOT EXISTS live_terminal (id TEXT PRIMARY KEY, console_pid INTEGER NOT NULL, state TEXT NOT NULL CHECK (state IN ('starting','running','uncertain')), at TEXT NOT NULL)";
|
|
19
|
+
export function terminalCatalogPath(orders) {
|
|
20
|
+
const main = orders.prepare("PRAGMA database_list").all().find(row => row["name"] === "main")?.["file"];
|
|
21
|
+
return typeof main === "string" && main !== "" ? `${main}.terminals.sqlite` : null;
|
|
22
|
+
}
|
|
23
|
+
function exists(file) {
|
|
24
|
+
try {
|
|
25
|
+
return lstatSync(file).isFile();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function open(file, readOnly) {
|
|
32
|
+
if (exists(file) && lstatSync(file).isSymbolicLink())
|
|
33
|
+
throw Error("The terminal catalog is linked, not a regular database.");
|
|
34
|
+
const db = new (sqliteRuntime().DatabaseSync)(file, { readOnly });
|
|
35
|
+
db.exec("PRAGMA busy_timeout=1000");
|
|
36
|
+
return db;
|
|
37
|
+
}
|
|
38
|
+
/** The updater's read: never creates the catalog. */
|
|
39
|
+
function withCatalog(orders, absent, readOnly, action) {
|
|
40
|
+
const file = terminalCatalogPath(orders);
|
|
41
|
+
if (file === null || !exists(file))
|
|
42
|
+
return absent;
|
|
43
|
+
const db = open(file, readOnly);
|
|
44
|
+
try {
|
|
45
|
+
return action(db);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
db.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** The console's ledger: creates the catalog, drops rows of consoles that are gone, and writes its own. */
|
|
52
|
+
export function openTerminalLedger(orders, alive = pid => processMayBeAlive(pid, false)) {
|
|
53
|
+
const file = terminalCatalogPath(orders);
|
|
54
|
+
if (file === null)
|
|
55
|
+
return null;
|
|
56
|
+
const db = open(file, false);
|
|
57
|
+
chmodSync(file, 0o600);
|
|
58
|
+
db.exec(SCHEMA);
|
|
59
|
+
for (const row of db.prepare("SELECT DISTINCT console_pid FROM live_terminal").all()) {
|
|
60
|
+
const pid = Number(row["console_pid"]);
|
|
61
|
+
if (pid !== process.pid && !alive(pid))
|
|
62
|
+
db.prepare("DELETE FROM live_terminal WHERE console_pid=?").run(pid);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
reserve(id) {
|
|
66
|
+
// Fail closed even where this catalog had no gate to install (it didn't exist yet when the update began): the main
|
|
67
|
+
// database's own admission pause refuses too.
|
|
68
|
+
if (orders.prepare("SELECT 1 FROM sqlite_master WHERE type='trigger' AND name GLOB 'so_desktop_update_*' LIMIT 1").get() !== undefined)
|
|
69
|
+
throw new Error(PAUSED);
|
|
70
|
+
try {
|
|
71
|
+
db.prepare("INSERT INTO live_terminal (id, console_pid, state, at) VALUES (?, ?, 'starting', ?)").run(id, process.pid, new Date().toISOString());
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new Error(error instanceof Error && error.message.includes(PAUSED) ? PAUSED : "No terminal can start right now (its record couldn't be written).");
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
running(id) { db.prepare("UPDATE live_terminal SET state='running', at=? WHERE id=?").run(new Date().toISOString(), id); },
|
|
78
|
+
// A sweep that couldn't prove its processes gone stays written down: the updater counts it while this console lives.
|
|
79
|
+
release(id, cleanup) {
|
|
80
|
+
try {
|
|
81
|
+
if (cleanup === "uncertain")
|
|
82
|
+
db.prepare("UPDATE live_terminal SET state='uncertain', at=? WHERE id=?").run(new Date().toISOString(), id);
|
|
83
|
+
else
|
|
84
|
+
db.prepare("DELETE FROM live_terminal WHERE id=?").run(id);
|
|
85
|
+
}
|
|
86
|
+
catch { /* the next start drops it as stale */ }
|
|
87
|
+
},
|
|
88
|
+
forget(id) { try {
|
|
89
|
+
db.prepare("DELETE FROM live_terminal WHERE id=?").run(id);
|
|
90
|
+
}
|
|
91
|
+
catch { /* stale later */ } },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function gateStatement(id) {
|
|
95
|
+
if (!/^[a-f0-9-]{36}$/.test(id))
|
|
96
|
+
throw Error("Invalid update identity.");
|
|
97
|
+
return `CREATE TRIGGER ${gatePrefix}start BEFORE INSERT ON live_terminal BEGIN SELECT RAISE(ABORT, '${PAUSED} [${id}]'); END`;
|
|
98
|
+
}
|
|
99
|
+
function changeGate(db, id, install) {
|
|
100
|
+
const wanted = gateStatement(id);
|
|
101
|
+
db.exec("BEGIN IMMEDIATE");
|
|
102
|
+
try {
|
|
103
|
+
db.exec(SCHEMA);
|
|
104
|
+
const rows = db.prepare("SELECT name, sql FROM sqlite_master WHERE type='trigger' AND name GLOB 'so_terminal_update_*'").all();
|
|
105
|
+
if (rows.some(row => String(row["sql"]) !== wanted))
|
|
106
|
+
throw Error("Another or unrecognized update owns terminal admission. Its gate was preserved.");
|
|
107
|
+
if (install && rows.length === 0)
|
|
108
|
+
db.exec(wanted);
|
|
109
|
+
if (!install)
|
|
110
|
+
for (const row of rows)
|
|
111
|
+
db.exec(`DROP TRIGGER "${String(row["name"])}"`);
|
|
112
|
+
db.exec("COMMIT");
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
db.exec("ROLLBACK");
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function installTerminalUpdateGate(orders, id) { withCatalog(orders, undefined, false, db => changeGate(db, id, true)); }
|
|
120
|
+
export function removeTerminalUpdateGate(orders, id) { withCatalog(orders, undefined, false, db => changeGate(db, id, false)); }
|
|
121
|
+
/** Terminals starting or running in a console that is still alive: active work an update waits for. */
|
|
122
|
+
export function activeTerminalUpdateWork(orders, alive = pid => processMayBeAlive(pid, false)) {
|
|
123
|
+
return withCatalog(orders, { terminals: 0 }, true, db => {
|
|
124
|
+
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='live_terminal'").get())
|
|
125
|
+
return { terminals: 0 };
|
|
126
|
+
const rows = db.prepare("SELECT console_pid FROM live_terminal").all();
|
|
127
|
+
return { terminals: rows.filter(row => alive(Number(row["console_pid"]))).length };
|
|
128
|
+
});
|
|
129
|
+
}
|
package/dist/text-limits.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ export declare const TEXT_LIMITS: {
|
|
|
68
68
|
readonly triggerState: 40;
|
|
69
69
|
readonly triggerWebhookTitle: 120;
|
|
70
70
|
readonly triggerWebhookField: 80;
|
|
71
|
+
/** v126: a Sentry or Vercel project (or Vercel team) a signed webhook is scoped to. */
|
|
72
|
+
readonly triggerWebhookScope: 120;
|
|
71
73
|
readonly triggerFolder: 100;
|
|
72
74
|
readonly triggerSender: 300;
|
|
73
75
|
readonly triggerSubject: 100;
|
package/dist/text-limits.js
CHANGED
|
@@ -68,6 +68,8 @@ export const TEXT_LIMITS = {
|
|
|
68
68
|
triggerState: 40,
|
|
69
69
|
triggerWebhookTitle: 120,
|
|
70
70
|
triggerWebhookField: 80,
|
|
71
|
+
/** v126: a Sentry or Vercel project (or Vercel team) a signed webhook is scoped to. */
|
|
72
|
+
triggerWebhookScope: 120,
|
|
71
73
|
triggerFolder: 100,
|
|
72
74
|
triggerSender: 300,
|
|
73
75
|
triggerSubject: 100,
|
package/dist/tool-broker.d.ts
CHANGED
|
@@ -11,7 +11,13 @@ export type BrokerOptions = {
|
|
|
11
11
|
omitEnv?: readonly string[];
|
|
12
12
|
/** Toolroll's own settings for a local server (the run's isolated database), passed as they are. */
|
|
13
13
|
pass?: Readonly<Record<string, string>>;
|
|
14
|
+
/** Where a local server starts: a folder the build can't write (never its checkout). */
|
|
14
15
|
cwd?: string;
|
|
16
|
+
/** Folders the build writes (its checkout, its caches): a server whose command, or a path it's given, resolves inside
|
|
17
|
+
* one is never started, since it would run the build's own bytes outside the build's limits. */
|
|
18
|
+
untrusted?: readonly string[];
|
|
19
|
+
/** Why a local server may not start unwrapped (no fence for it here), read when it would; null when it may. */
|
|
20
|
+
unwrapped?: () => string | null;
|
|
15
21
|
/** How a local server is started inside its run's fence, read when it starts (the fence is fixed after the tools). */
|
|
16
22
|
wrap?: () => ((file: string, args: readonly string[]) => {
|
|
17
23
|
file: string;
|
|
@@ -22,6 +28,8 @@ export type ToolBroker = {
|
|
|
22
28
|
/** The launch's token: the agent's only key to its tools. */
|
|
23
29
|
token: string;
|
|
24
30
|
url: (name: string) => string;
|
|
31
|
+
/** Its loopback port: the one a confined network leaves reachable for its agent (agent-fence.ts NetworkFence). */
|
|
32
|
+
port: number;
|
|
25
33
|
/** The local servers running now (the fence tests read them). */
|
|
26
34
|
pids: () => number[];
|
|
27
35
|
/** Revoke the token, stop every server, close the port. */
|
|
@@ -33,5 +41,11 @@ export declare const SERVER_ENV: readonly string[];
|
|
|
33
41
|
/** A local server's environment: the allowlisted names from `base` (and its LC_* locale), less `omit`, then `pass`,
|
|
34
42
|
* then the tool's own values. */
|
|
35
43
|
export declare function serverEnv(base: NodeJS.ProcessEnv, omit: readonly string[], pass: Readonly<Record<string, string>>, values: Readonly<Record<string, string>>): NodeJS.ProcessEnv;
|
|
44
|
+
/**
|
|
45
|
+
* The first of a server's command and path-like arguments that resolves inside an untrusted folder (a build's checkout
|
|
46
|
+
* or caches), or null. The command is resolved as spawn would: a path against `cwd`, a bare name through PATH; an
|
|
47
|
+
* argument counts when it names an existing path (relative to `cwd`) or is written as one (`./x`, `../x`, `/x`).
|
|
48
|
+
*/
|
|
49
|
+
export declare function workspaceBytes(command: string, args: readonly string[], cwd: string | undefined, path: string | undefined, untrusted: readonly string[]): string | null;
|
|
36
50
|
/** Start one launch's broker on a loopback port. */
|
|
37
51
|
export declare function startToolBroker(tools: readonly BrokeredTool[], options?: BrokerOptions): Promise<ToolBroker>;
|
package/dist/tool-broker.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
import { spawn } from "node:child_process";
|
|
21
21
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
22
22
|
import { createServer } from "node:http";
|
|
23
|
+
import { accessSync, constants, existsSync, realpathSync } from "node:fs";
|
|
24
|
+
import { delimiter, isAbsolute, resolve, sep } from "node:path";
|
|
23
25
|
const BODY_LIMIT = 4 * 1024 * 1024;
|
|
24
26
|
const ANSWER_LIMIT = 32 * 1024 * 1024;
|
|
25
27
|
const STOP_GRACE_MS = 2_000;
|
|
@@ -59,6 +61,54 @@ export function serverEnv(base, omit, pass, values) {
|
|
|
59
61
|
delete env[name];
|
|
60
62
|
return Object.assign(env, pass, values);
|
|
61
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The first of a server's command and path-like arguments that resolves inside an untrusted folder (a build's checkout
|
|
66
|
+
* or caches), or null. The command is resolved as spawn would: a path against `cwd`, a bare name through PATH; an
|
|
67
|
+
* argument counts when it names an existing path (relative to `cwd`) or is written as one (`./x`, `../x`, `/x`).
|
|
68
|
+
*/
|
|
69
|
+
export function workspaceBytes(command, args, cwd, path, untrusted) {
|
|
70
|
+
if (untrusted.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
const base = cwd ?? process.cwd();
|
|
73
|
+
// Canonical on-disk names, folded where the filesystem ignores case (macOS, Windows), so `/Users/a/CHECKOUT` is the checkout.
|
|
74
|
+
const fold = process.platform === "darwin" || process.platform === "win32" ? (one) => one.toLowerCase() : (one) => one;
|
|
75
|
+
const real = (one) => { try {
|
|
76
|
+
return fold(realpathSync.native(one));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return fold(resolve(one));
|
|
80
|
+
} };
|
|
81
|
+
const roots = untrusted.map(real);
|
|
82
|
+
const within = (one) => { const at = real(one); return roots.some(root => at === root || at.startsWith(`${root}${sep}`)); };
|
|
83
|
+
// A root's name anywhere in an argument (inline code, a list of paths) counts, however it's spelled.
|
|
84
|
+
const mentions = (arg) => { const text = fold(arg); return roots.some(root => text.includes(root)) || untrusted.some(root => text.includes(fold(root))); };
|
|
85
|
+
const located = command.includes("/") || command.includes(sep) ? resolve(base, command)
|
|
86
|
+
: (path ?? "").split(delimiter).filter(Boolean).map(dir => resolve(base, dir, command)).find(one => { try {
|
|
87
|
+
accessSync(one, constants.X_OK);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
} }) ?? null;
|
|
93
|
+
if (located !== null && within(located))
|
|
94
|
+
return located;
|
|
95
|
+
// A relative path once meant "in the checkout" (servers started there): it is refused rather than read elsewhere.
|
|
96
|
+
if (!isAbsolute(command) && (command.includes("/") || command.includes(sep)))
|
|
97
|
+
return `${command} (a relative path)`;
|
|
98
|
+
for (const arg of args) {
|
|
99
|
+
if (mentions(arg))
|
|
100
|
+
return `${arg} (names what this build writes)`;
|
|
101
|
+
// A flag's value (`--config=x`, `-c=x`) is checked as its own argument.
|
|
102
|
+
for (const piece of [arg, ...(arg.startsWith("-") && arg.includes("=") ? [arg.slice(arg.indexOf("=") + 1)] : [])]) {
|
|
103
|
+
if (piece.startsWith("./") || piece.startsWith("../"))
|
|
104
|
+
return `${piece} (a relative path)`;
|
|
105
|
+
const candidate = resolve(base, piece);
|
|
106
|
+
if ((isAbsolute(piece) || existsSync(candidate)) && within(candidate))
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
62
112
|
/** A local server, started on first use and spoken to over its stdin and stdout. */
|
|
63
113
|
class LocalServer {
|
|
64
114
|
tool;
|
|
@@ -68,6 +118,7 @@ class LocalServer {
|
|
|
68
118
|
buffer = "";
|
|
69
119
|
next = 1;
|
|
70
120
|
pending = new Map();
|
|
121
|
+
refusal = null;
|
|
71
122
|
constructor(tool, options) {
|
|
72
123
|
this.tool = tool;
|
|
73
124
|
this.options = options;
|
|
@@ -75,7 +126,19 @@ class LocalServer {
|
|
|
75
126
|
pid() { return this.child?.pid ?? null; }
|
|
76
127
|
start() {
|
|
77
128
|
const env = serverEnv(this.options.env ?? process.env, this.options.omitEnv ?? [], this.options.pass ?? {}, this.tool.values);
|
|
129
|
+
const planted = workspaceBytes(this.tool.spec.command, this.tool.spec.args, this.options.cwd, env["PATH"], this.options.untrusted ?? []);
|
|
130
|
+
if (planted !== null) {
|
|
131
|
+
this.refusal = `The tool's server would run ${planted}, inside what this build can write, so it was not started.`;
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
78
134
|
const wrap = this.options.wrap?.() ?? null;
|
|
135
|
+
if (wrap === null) {
|
|
136
|
+
const why = this.options.unwrapped?.() ?? null;
|
|
137
|
+
if (why !== null) {
|
|
138
|
+
this.refusal = why;
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
79
142
|
const { file, args } = wrap === null ? { file: this.tool.spec.command, args: [...this.tool.spec.args] } : wrap(this.tool.spec.command, this.tool.spec.args);
|
|
80
143
|
const child = spawn(file, args, { env, ...(this.options.cwd === undefined ? {} : { cwd: this.options.cwd }), stdio: ["pipe", "pipe", "ignore"], detached: process.platform !== "win32" });
|
|
81
144
|
this.child = child;
|
|
@@ -131,8 +194,9 @@ class LocalServer {
|
|
|
131
194
|
// A new session (a reconnect) starts a fresh server: a server answers initialize once.
|
|
132
195
|
if (message.method === "initialize" && this.initialized)
|
|
133
196
|
this.stop();
|
|
134
|
-
if (this.child === null)
|
|
135
|
-
this.start
|
|
197
|
+
if (this.child === null && this.start() === null) {
|
|
198
|
+
return Promise.resolve(isRequest(message) ? rpcError(message.id, this.refusal ?? "The tool could not start.") : null);
|
|
199
|
+
}
|
|
136
200
|
if (message.method === "initialize")
|
|
137
201
|
this.initialized = true;
|
|
138
202
|
if (!isRequest(message)) {
|
|
@@ -357,6 +421,7 @@ export async function startToolBroker(tools, options = {}) {
|
|
|
357
421
|
return {
|
|
358
422
|
token,
|
|
359
423
|
url: name => `http://127.0.0.1:${port}/mcp/${name}`,
|
|
424
|
+
port,
|
|
360
425
|
pids: () => [...local.values()].map(one => one.pid()).filter((pid) => pid !== null),
|
|
361
426
|
close: () => {
|
|
362
427
|
revoked = true;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and a one-time "What's new" card afterwards. The work itself is the
|
|
5
5
|
* `toolroll update` command (toolroll-update.ts), started as its own job.
|
|
6
6
|
*/
|
|
7
|
-
import { html, postForm, replaceMarkup } from "./html.js";
|
|
7
|
+
import { html, postForm, replaceMarkup, reauth } from "./html.js";
|
|
8
8
|
import { STEP_WORDS, UPDATE_STEPS, runtimeUpdateTerminal } from "./toolroll-update.js";
|
|
9
9
|
const clock = (iso) => { const d = new Date(iso); return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; };
|
|
10
10
|
export const UPDATES_CSS = `.updates{max-width:640px;min-width:0;overflow-wrap:anywhere}.updates .card{margin:0 0 16px}.updates h2{margin:0;font-size:1.0625rem}.updates .card>p{margin:4px 0 0}` +
|
|
@@ -69,7 +69,7 @@ export function updatesHtml(view, notice) {
|
|
|
69
69
|
: latest === null ? html `<h2>Toolroll ${view.current}</h2><p class="meta">Couldn’t check for a newer release: ${"problem" in view.latest ? view.latest.problem : ""}</p>`
|
|
70
70
|
: html `<h2>Toolroll ${view.current} is up to date</h2>`;
|
|
71
71
|
const form = newer && !["npx", "source", "desktop"].includes(view.method.kind)
|
|
72
|
-
? postForm("/settings/updates", html `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label
|
|
72
|
+
? postForm("/settings/updates", html `${reauth(html `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<div class="update-actions"><button type="submit" name="when" value="now" class="primary">Update now</button><button type="submit" name="when" value="when-idle">When idle</button><button type="submit" name="when" value="tonight">Tonight (03:00)</button></div>`, { hidden: { version: latest } })
|
|
73
73
|
: "";
|
|
74
74
|
const rollback = view.rollbackTo !== null ? html `<p class="meta">To go back to ${view.rollbackTo}: <code>toolroll update --rollback</code></p>` : "";
|
|
75
75
|
return html `<section class="updates">${note}${whatsNew}${j ? outcomeHtml(j) : ""}<div class="card" data-update-state="${newer ? "available" : "current"}">${state}${form}${rollback}</div></section>`;
|
package/dist/toolroll-update.js
CHANGED
|
@@ -32,10 +32,11 @@ import { createRequire } from "node:module";
|
|
|
32
32
|
import { clearWords, durableJson, LINGERING_LIMIT_MS, lingeringRun, lingeringWords, settleLeftoverRecords, sqliteLock, desktopUpdateWaitingOf, stoppedWaitingWords, updateWaitingWords, verifiedDatabaseBackup } from "./desktop-update.js";
|
|
33
33
|
import { activeUpdateWork, freezeUpdateGate, installUpdateGate, removeUpdateGate, updateAdmissionPaused, updateGateOwned } from "./desktop-update-gate.js";
|
|
34
34
|
import { processMayBeAlive } from "./process-liveness.js";
|
|
35
|
+
import { removeTerminalUpdateGate } from "./terminal-catalog.js";
|
|
35
36
|
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
|
|
36
37
|
import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
|
|
37
38
|
import { NAME } from "./names.js";
|
|
38
|
-
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS } from "./store.js";
|
|
39
|
+
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS } from "./store.js";
|
|
39
40
|
import { isNewer, REGISTRY } from "./releases.js";
|
|
40
41
|
import { markNeverIndex } from "./never-index.js";
|
|
41
42
|
import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
|
|
@@ -373,6 +374,8 @@ function addedColumns(...versions) {
|
|
|
373
374
|
}
|
|
374
375
|
return rules;
|
|
375
376
|
}
|
|
377
|
+
/** Rules on tables that later gained declared columns (addedColumns merges into these instead of replacing them). */
|
|
378
|
+
const EARLIER_RULES = { flow_trigger: { fromRoutines: "trigger" } };
|
|
376
379
|
const HISTORY_RULES = {
|
|
377
380
|
...Object.fromEntries(Object.entries(CHAT_MOVES).map(([table, moved]) => [table, { moved }])),
|
|
378
381
|
action_ledger: { appendOnly: true }, ledger_seal: { appendOnly: true }, ledger_checkpoint: { appendOnly: true },
|
|
@@ -401,7 +404,11 @@ const HISTORY_RULES = {
|
|
|
401
404
|
// v121: live steering's columns and a run's attempt key read none, and no sends, the same way.
|
|
402
405
|
// v122: a project's recorded remote reads none the same way.
|
|
403
406
|
// v123: a plan authorization's basis reads the operating mode, and a trigger firing reads not on time and no script ran.
|
|
404
|
-
|
|
407
|
+
// v126: a receipt's state, acceptance time and held item, a trigger's credential generation and turned-away count,
|
|
408
|
+
// and who made a card read none, 0, or none. A table with an earlier rule (flow_trigger's v115 move) keeps it too.
|
|
409
|
+
// v128: a lead message's pending flag reads waiting exactly for a team queue's unadmitted operator message.
|
|
410
|
+
...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS))
|
|
411
|
+
.map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
|
|
405
412
|
};
|
|
406
413
|
/** The tables a migration may remove whole (v115's removed features). */
|
|
407
414
|
export const RETIRED_TABLES = Object.freeze(Object.keys(HISTORY_RULES).filter(name => HISTORY_RULES[name].retired));
|
|
@@ -908,8 +915,10 @@ function ungate(j) {
|
|
|
908
915
|
db.exec("PRAGMA busy_timeout=5000");
|
|
909
916
|
if (updateGateOwned(db, j.id))
|
|
910
917
|
removeUpdateGate(db, j.id);
|
|
911
|
-
else
|
|
918
|
+
else {
|
|
912
919
|
removeCodingUpdateGate(db, j.id);
|
|
920
|
+
removeTerminalUpdateGate(db, j.id);
|
|
921
|
+
}
|
|
913
922
|
}
|
|
914
923
|
finally {
|
|
915
924
|
db.close();
|
|
@@ -920,6 +929,7 @@ function ungateCoding(j) {
|
|
|
920
929
|
try {
|
|
921
930
|
db.exec("PRAGMA busy_timeout=5000");
|
|
922
931
|
removeCodingUpdateGate(db, j.id);
|
|
932
|
+
removeTerminalUpdateGate(db, j.id);
|
|
923
933
|
}
|
|
924
934
|
finally {
|
|
925
935
|
db.close();
|
package/dist/tools-ui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Settings → Tools: one project's MCP servers, in plain words. Secrets are
|
|
2
2
|
* entered here and nowhere else, and never shown again. */
|
|
3
|
-
import { html, postForm } from "./html.js";
|
|
3
|
+
import { html, postForm, reauth } from "./html.js";
|
|
4
4
|
import { brandMarkHtml, TILE_PHONE } from "./brand-mark.js";
|
|
5
5
|
import { signedIn } from "./oauth-run-token.js";
|
|
6
6
|
import { credentialModeOf, toolCommandLine, toolStanding } from "./project-tools.js";
|
|
@@ -15,7 +15,7 @@ export function toolsProjectPicker(projects, chosen, keep = {}) {
|
|
|
15
15
|
}
|
|
16
16
|
/** Choosing another project marks every form on the page as stale before it reloads, so a tap racing the reload is refused. */
|
|
17
17
|
export const TOOLS_PROJECT_SCRIPT = `(function(){var s=document.querySelector('select[data-tools-project]');if(!s)return;s.addEventListener('change',function(){document.querySelectorAll('input[name=shown]').forEach(function(i){i.value=s.value;});});})();`;
|
|
18
|
-
const password = html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label
|
|
18
|
+
const password = html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}`;
|
|
19
19
|
function toolCard(one, base, manage, project, research = null) {
|
|
20
20
|
const { tool, secretsSet } = one;
|
|
21
21
|
const standing = toolStanding(tool, secretsSet);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests that run real commands on a machine with no OS fence (CI's Ubuntu runners, where unprivileged namespaces are
|
|
3
|
+
* off) allow unconfined agents on their own store, as an instance operator there would; anywhere a fence exists,
|
|
4
|
+
* nothing changes and the fence is what the test runs under.
|
|
5
|
+
*/
|
|
6
|
+
import { linuxFenceAvailable, macosFenceAvailable } from "./agent-fence.js";
|
|
7
|
+
export function allowWhereUnfenced(store) {
|
|
8
|
+
if (!macosFenceAvailable() && !linuxFenceAvailable())
|
|
9
|
+
store.setUnfencedAgentsAllowed(true, "test", new Date(0));
|
|
10
|
+
return store;
|
|
11
|
+
}
|