taskchef 7.22.4 → 7.23.0

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/src/dashboard.js CHANGED
@@ -25,9 +25,18 @@ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
25
25
  import { taskGitHubProjection } from "./dashboard/github-links.js";
26
26
  import { CODEX_CHAT_ARCHIVE_ENABLED } from "./dashboard/state.js";
27
27
  import { createUsageTracker } from "./usage-tracker.js";
28
+ import {
29
+ MAX_DASHBOARD_SESSION_PIDS,
30
+ MAX_TRANSFERRED_DASHBOARD_SESSION_PIDS,
31
+ validSessionPid,
32
+ } from "./dashboard-session.js";
28
33
  import {
29
34
  DASHBOARD_CONTROL_CHALLENGE_PATH,
35
+ DASHBOARD_CONTROL_HANDOFF_COMMIT_PATH,
36
+ DASHBOARD_CONTROL_SESSION_PATH,
37
+ DASHBOARD_CONTROL_HANDOFF_PATH,
30
38
  DASHBOARD_CONTROL_SHUTDOWN_PATH,
39
+ createDashboardControlNonce,
31
40
  dashboardControlProof,
32
41
  validDashboardControlNonce,
33
42
  validDashboardControlSecret,
@@ -41,6 +50,11 @@ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1"]);
41
50
  const DEFAULT_MAX_FILE_BYTES = 16 * 1024 * 1024;
42
51
  const DEFAULT_MAX_TASKS = 2_000;
43
52
  const DEFAULT_MAX_EVENT_CLIENTS = 16;
53
+ const DASHBOARD_HANDOFF_PREPARE_TTL_MS = 5_000;
54
+ const DASHBOARD_HANDOFF_COMMIT_GRACE_MS = 250;
55
+ const DASHBOARD_HANDOFF_COMMIT_RETRY_MS = 1_000;
56
+ const DASHBOARD_CONTROL_NONCE_LIMIT = 4_096;
57
+ const DASHBOARD_CONTROL_NONCE_TTL_MS = 5 * 60_000;
44
58
  const MAX_MANUAL_TRANSITION_BODY_BYTES = 4 * 1024;
45
59
  export const DASHBOARD_HEALTH_PATH = "/api/health";
46
60
  export const DASHBOARD_HEALTH_MAX_BYTES = 8 * 1024;
@@ -537,6 +551,33 @@ function publicMonitorError() {
537
551
  };
538
552
  }
539
553
 
554
+ export function createDashboardControlReplayCache({
555
+ maximum = DASHBOARD_CONTROL_NONCE_LIMIT,
556
+ ttlMs = DASHBOARD_CONTROL_NONCE_TTL_MS,
557
+ now = Date.now,
558
+ } = {}) {
559
+ if (!Number.isSafeInteger(maximum) || maximum <= 0) {
560
+ throw new Error("dashboard control replay limit must be a positive integer");
561
+ }
562
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0 || typeof now !== "function") {
563
+ throw new Error("dashboard control replay window is invalid");
564
+ }
565
+ const entries = new Map();
566
+ return {
567
+ accept(nonce) {
568
+ const timestamp = now();
569
+ for (const [candidate, expiresAt] of entries) {
570
+ if (expiresAt > timestamp) break;
571
+ entries.delete(candidate);
572
+ }
573
+ if (entries.has(nonce) || entries.size >= maximum) return false;
574
+ entries.set(nonce, timestamp + ttlMs);
575
+ return true;
576
+ },
577
+ get size() { return entries.size; },
578
+ };
579
+ }
580
+
540
581
  export async function createDashboardServer({
541
582
  archiveEnabled = CODEX_CHAT_ARCHIVE_ENABLED,
542
583
  archiveThread = archiveThreadInCodex,
@@ -553,6 +594,7 @@ export async function createDashboardServer({
553
594
  serverVersion = DASHBOARD_SERVER_VERSION,
554
595
  usageTracker = null,
555
596
  control = null,
597
+ controlReplayCache = createDashboardControlReplayCache(),
556
598
  } = {}) {
557
599
  if (!LOOPBACK_HOSTS.has(host)) {
558
600
  throw new Error("dashboard host must be a loopback address");
@@ -563,13 +605,15 @@ export async function createDashboardServer({
563
605
  if (!Number.isInteger(maxEventClients) || maxEventClients < 0) {
564
606
  throw new Error("dashboard event-client limit must be a non-negative integer");
565
607
  }
566
- if (!new Set(["mcp", "standalone"]).has(launcher)) {
567
- throw new Error("dashboard launcher must be mcp or standalone");
608
+ if (!new Set(["mcp", "session", "standalone"]).has(launcher)) {
609
+ throw new Error("dashboard launcher must be mcp, session, or standalone");
568
610
  }
569
- if (control !== null && (launcher !== "mcp"
611
+ if (control !== null && (!new Set(["mcp", "session"]).has(launcher)
570
612
  || !validDashboardControlSecret(control?.secret)
571
- || typeof control?.onShutdown !== "function")) {
572
- throw new Error("dashboard control requires a valid MCP ownership controller");
613
+ || typeof control?.onShutdown !== "function"
614
+ || (launcher === "session" && (typeof control?.onSession !== "function"
615
+ || typeof control?.onHandoff !== "function")))) {
616
+ throw new Error("dashboard control requires a valid TaskChef ownership controller");
573
617
  }
574
618
  const monitor = new DashboardMonitor(workspace, monitorOptions);
575
619
  await monitor.start();
@@ -588,7 +632,8 @@ export async function createDashboardServer({
588
632
  }
589
633
  const clients = new Set();
590
634
  const archiveRequests = new Set();
591
- const controlNonces = new Set();
635
+ let controlHandoff = null;
636
+ let controlHandoffTimer = null;
592
637
  let allowedAuthority;
593
638
  let allowedOrigin;
594
639
 
@@ -655,16 +700,251 @@ export async function createDashboardServer({
655
700
  sendJson(response, 403, { message: "Dashboard control authentication failed." });
656
701
  return;
657
702
  }
658
- if (controlNonces.has(nonce)) {
703
+ if (!controlReplayCache.accept(nonce)) {
659
704
  sendJson(response, 409, { message: "Dashboard control credential was already used." });
660
705
  return;
661
706
  }
662
- controlNonces.add(nonce);
663
707
  sendJson(response, 202, { schemaVersion: 1, accepted: true });
664
708
  setImmediate(() => { void control.onShutdown().catch(() => {}); });
665
709
  return;
666
710
  }
667
711
 
712
+ if (url.pathname === DASHBOARD_CONTROL_SESSION_PATH && method === "POST") {
713
+ if (!control || typeof control.onSession !== "function") {
714
+ sendJson(response, 404, { message: "Not found." });
715
+ return;
716
+ }
717
+ const body = await readBoundedJsonBody(request);
718
+ const nonce = body?.nonce;
719
+ const pid = body?.pid;
720
+ if (!Number.isSafeInteger(pid) || pid <= 1
721
+ || !verifyDashboardControlProof(
722
+ control.secret, `session:${pid}`, nonce, body?.proof,
723
+ )) {
724
+ sendJson(response, 403, { message: "Dashboard control authentication failed." });
725
+ return;
726
+ }
727
+ if (controlHandoff) {
728
+ sendJson(response, 409, {
729
+ message: "Dashboard handoff is already in progress.",
730
+ reason: "retiring",
731
+ });
732
+ return;
733
+ }
734
+ if (!controlReplayCache.accept(nonce)) {
735
+ sendJson(response, 409, { message: "Dashboard control credential was already used." });
736
+ return;
737
+ }
738
+ try {
739
+ await control.onSession(pid);
740
+ sendJson(response, 200, {
741
+ schemaVersion: 1,
742
+ accepted: true,
743
+ nonce,
744
+ proof: dashboardControlProof(control.secret, `session-accepted:${pid}`, nonce),
745
+ });
746
+ } catch (error) {
747
+ sendJson(response, 409, {
748
+ message: "Dashboard session registration was refused.",
749
+ reason: error?.code === "TASKCHEF_DASHBOARD_SESSION_RETIRING"
750
+ ? "retiring"
751
+ : "refused",
752
+ });
753
+ }
754
+ return;
755
+ }
756
+
757
+ if (url.pathname === DASHBOARD_CONTROL_HANDOFF_PATH && method === "POST") {
758
+ if (!control || typeof control.onHandoff !== "function") {
759
+ sendJson(response, 404, { message: "Not found." });
760
+ return;
761
+ }
762
+ const body = await readBoundedJsonBody(request);
763
+ const nonce = body?.nonce;
764
+ const pid = body?.pid;
765
+ if (!Number.isSafeInteger(pid) || pid <= 1
766
+ || !verifyDashboardControlProof(
767
+ control.secret, `handoff:${pid}`, nonce, body?.proof,
768
+ )) {
769
+ sendJson(response, 403, { message: "Dashboard control authentication failed." });
770
+ return;
771
+ }
772
+ if (!controlReplayCache.accept(nonce)) {
773
+ sendJson(response, 409, { message: "Dashboard control credential was already used." });
774
+ return;
775
+ }
776
+ if (controlHandoff) {
777
+ if (controlHandoff.pending || controlHandoff.finalizing || controlHandoff.finalized) {
778
+ sendJson(response, 409, {
779
+ message: "Dashboard handoff is already in progress.",
780
+ reason: "retiring",
781
+ });
782
+ return;
783
+ }
784
+ try {
785
+ const pids = controlHandoff.participants.has(pid)
786
+ ? controlHandoff.pids
787
+ : await control.onHandoff(pid);
788
+ if (!Array.isArray(pids) || pids.length > MAX_DASHBOARD_SESSION_PIDS
789
+ || pids.some((value) => !validSessionPid(value))
790
+ || new Set(pids).size !== pids.length
791
+ || !pids.includes(pid)) {
792
+ throw new Error("dashboard handoff returned invalid session leases");
793
+ }
794
+ controlHandoff.pids = pids;
795
+ controlHandoff.participants.add(pid);
796
+ const { id } = controlHandoff;
797
+ sendJson(response, 200, {
798
+ schemaVersion: 1,
799
+ accepted: true,
800
+ id,
801
+ pids,
802
+ nonce,
803
+ proof: dashboardControlProof(
804
+ control.secret,
805
+ `handoff-prepared:${pid}:${id}:${JSON.stringify(pids)}`,
806
+ nonce,
807
+ ),
808
+ });
809
+ } catch {
810
+ sendJson(response, 409, {
811
+ message: "Dashboard handoff was refused.",
812
+ reason: "refused",
813
+ });
814
+ }
815
+ return;
816
+ }
817
+ controlHandoff = { pid, pending: true, committed: false };
818
+ try {
819
+ const pids = await control.onHandoff(pid);
820
+ if (!Array.isArray(pids) || pids.length > MAX_DASHBOARD_SESSION_PIDS
821
+ || pids.some((value) => !validSessionPid(value))
822
+ || new Set(pids).size !== pids.length
823
+ || !pids.includes(pid)) {
824
+ throw new Error("dashboard handoff returned invalid session leases");
825
+ }
826
+ const id = createDashboardControlNonce();
827
+ controlHandoff = {
828
+ id,
829
+ pid,
830
+ pids,
831
+ participants: new Set([pid]),
832
+ pending: false,
833
+ committed: false,
834
+ };
835
+ controlHandoffTimer = setTimeout(() => {
836
+ if (controlHandoff?.id === id && !controlHandoff.committed) controlHandoff = null;
837
+ controlHandoffTimer = null;
838
+ }, DASHBOARD_HANDOFF_PREPARE_TTL_MS);
839
+ controlHandoffTimer.unref?.();
840
+ sendJson(response, 200, {
841
+ schemaVersion: 1,
842
+ accepted: true,
843
+ id,
844
+ pids,
845
+ nonce,
846
+ proof: dashboardControlProof(
847
+ control.secret,
848
+ `handoff-prepared:${pid}:${id}:${JSON.stringify(pids)}`,
849
+ nonce,
850
+ ),
851
+ });
852
+ } catch {
853
+ if (controlHandoff?.pid === pid && controlHandoff.pending) controlHandoff = null;
854
+ sendJson(response, 409, {
855
+ message: "Dashboard handoff was refused.",
856
+ reason: "refused",
857
+ });
858
+ }
859
+ return;
860
+ }
861
+
862
+ if (url.pathname === DASHBOARD_CONTROL_HANDOFF_COMMIT_PATH && method === "POST") {
863
+ if (!control || typeof control.onHandoff !== "function") {
864
+ sendJson(response, 404, { message: "Not found." });
865
+ return;
866
+ }
867
+ const body = await readBoundedJsonBody(request);
868
+ const nonce = body?.nonce;
869
+ const id = body?.id;
870
+ if (!validDashboardControlNonce(id)
871
+ || !verifyDashboardControlProof(
872
+ control.secret, `handoff-commit:${id}`, nonce, body?.proof,
873
+ )) {
874
+ sendJson(response, 403, { message: "Dashboard control authentication failed." });
875
+ return;
876
+ }
877
+ if (!controlReplayCache.accept(nonce)) {
878
+ sendJson(response, 409, { message: "Dashboard control credential was already used." });
879
+ return;
880
+ }
881
+ if (!controlHandoff || controlHandoff.id !== id) {
882
+ sendJson(response, 409, {
883
+ message: "Dashboard handoff preparation expired or did not match.",
884
+ reason: "refused",
885
+ });
886
+ return;
887
+ }
888
+ if (!controlHandoff.commitPromise) {
889
+ controlHandoff.committed = true;
890
+ if (controlHandoffTimer) clearTimeout(controlHandoffTimer);
891
+ controlHandoffTimer = null;
892
+ controlHandoff.commitPromise = new Promise((resolve, reject) => {
893
+ const finalizationTimer = setTimeout(async () => {
894
+ try {
895
+ controlHandoff.finalizing = true;
896
+ const pids = await control.onHandoff(controlHandoff.pid);
897
+ if (!Array.isArray(pids)
898
+ || pids.length > MAX_TRANSFERRED_DASHBOARD_SESSION_PIDS
899
+ || pids.some((value) => !validSessionPid(value))
900
+ || new Set(pids).size !== pids.length
901
+ || !pids.includes(controlHandoff.pid)) {
902
+ throw new Error("dashboard handoff returned invalid final session leases");
903
+ }
904
+ controlHandoff.pids = pids;
905
+ await control.onHandoffFinalized?.({ id, pids });
906
+ controlHandoff.finalized = true;
907
+ resolve(pids);
908
+ } catch (error) {
909
+ reject(error);
910
+ }
911
+ }, DASHBOARD_HANDOFF_COMMIT_GRACE_MS);
912
+ finalizationTimer.unref?.();
913
+ });
914
+ }
915
+ let pids;
916
+ try {
917
+ pids = await controlHandoff.commitPromise;
918
+ } catch {
919
+ if (controlHandoff?.id === id) controlHandoff = null;
920
+ sendJson(response, 409, {
921
+ message: "Dashboard handoff finalization was refused.",
922
+ reason: "refused",
923
+ });
924
+ return;
925
+ }
926
+ sendJson(response, 202, {
927
+ schemaVersion: 1,
928
+ accepted: true,
929
+ id,
930
+ pids,
931
+ nonce,
932
+ proof: dashboardControlProof(
933
+ control.secret,
934
+ `handoff-committed:${id}:${JSON.stringify(pids)}`,
935
+ nonce,
936
+ ),
937
+ });
938
+ if (!controlHandoff.shutdownScheduled) {
939
+ controlHandoff.shutdownScheduled = true;
940
+ const shutdownTimer = setTimeout(() => {
941
+ void control.onShutdown().catch(() => {});
942
+ }, DASHBOARD_HANDOFF_COMMIT_RETRY_MS);
943
+ shutdownTimer.unref?.();
944
+ }
945
+ return;
946
+ }
947
+
668
948
  if (url.pathname === "/api/snapshot" && (method === "GET" || method === "HEAD")) {
669
949
  if (method === "HEAD") {
670
950
  response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
package/src/mcp.js CHANGED
@@ -107,7 +107,7 @@ const preparationSchema = z.object({
107
107
 
108
108
  const dashboardSchema = z.object({
109
109
  action: z.enum(["started", "reused"]),
110
- launcher: z.literal("mcp"),
110
+ launcher: z.literal("session"),
111
111
  url: z.string().url(),
112
112
  workspace: z.string(),
113
113
  taskchefVersion: z.string(),
@@ -201,7 +201,7 @@ export function createTaskChefMcpServer({
201
201
  {
202
202
  title: "Ensure TaskChef dashboard",
203
203
  description:
204
- "Best-effort ensure the canonical TaskChef dashboard is available on 127.0.0.1:3210. Starts one dashboard inside this MCP process or reuses only an exact compatible MCP-launched TaskChef dashboard for the same canonical workspace; standalone and unknown listeners are never terminated or replaced.",
204
+ "Best-effort ensure the canonical TaskChef dashboard is available on 127.0.0.1:3210. Starts or reuses the authenticated TaskChef dashboard for this Codex session and canonical workspace; verified older TaskChef session listeners are handed off to the installed version, while standalone and unknown listeners are never terminated or replaced.",
205
205
  inputSchema: {},
206
206
  outputSchema: { dashboard: dashboardSchema },
207
207
  annotations: {
package/src/version.js CHANGED
@@ -4,4 +4,4 @@ const require = createRequire(import.meta.url);
4
4
  const packageMetadata = require("../package.json");
5
5
 
6
6
  export const TASKCHEF_VERSION = packageMetadata.version;
7
- export const DASHBOARD_SERVER_VERSION = "3";
7
+ export const DASHBOARD_SERVER_VERSION = "4";