@jpcasabiancai/cayenne 0.2.0 → 0.3.1
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/dist/cli.js +1224 -180
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -9,8 +9,34 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/cloud.ts
|
|
13
|
+
function assertPublishable(key) {
|
|
14
|
+
if (!key) return key;
|
|
15
|
+
if (key.startsWith("sb_secret_"))
|
|
16
|
+
throw new Error("CLOUD key is a secret key; bake the publishable key");
|
|
17
|
+
if (key.startsWith("sb_publishable_")) return key;
|
|
18
|
+
const payload = key.split(".")[1];
|
|
19
|
+
if (!payload) throw new Error("CLOUD key is not a recognised Supabase key");
|
|
20
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
21
|
+
if (claims.role !== "anon") {
|
|
22
|
+
throw new Error(`CLOUD key has role "${claims.role}"; only the anon key may be baked in`);
|
|
23
|
+
}
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
26
|
+
var CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, CLOUD_APP_ORIGIN;
|
|
27
|
+
var init_cloud = __esm({
|
|
28
|
+
"src/cloud.ts"() {
|
|
29
|
+
"use strict";
|
|
30
|
+
CLOUD_SUPABASE_URL = "https://ndqrkqxwkzxgnhkvcunr.supabase.co";
|
|
31
|
+
CLOUD_SUPABASE_ANON_KEY = assertPublishable(
|
|
32
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5kcXJrcXh3a3p4Z25oa3ZjdW5yIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODg3MTMyNzYsImV4cCI6MjEwNDI4OTI3Nn0.Dlsxw91E1QYZ4eoRDjP80Rf4feY77Z-T-fiIwUDY2k0"
|
|
33
|
+
);
|
|
34
|
+
CLOUD_APP_ORIGIN = "https://cayenne-pink.vercel.app";
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
12
38
|
// ../../packages/shared/src/constants.ts
|
|
13
|
-
var PROTOCOL_VERSION, LOCAL_PORT, BROADCAST_MAX_BYTES, COMMAND_STALE_MS, PERMISSION_TIMEOUT_MS, HEARTBEAT_INTERVAL_MS, RUNNER_ONLINE_WINDOW_MS, TERMINAL_FLUSH_MS, TERMINAL_FLUSH_BYTES, TERMINAL_RING_BYTES, TERMINAL_INPUT_MAX_BYTES, TERMINAL_REPLAY_PART_BYTES, TERMINAL_REPLAY_MAX_PER_MINUTE, TERMINAL_MAX_PER_OWNER, TERMINAL_MAX_PER_RUNNER, TERMINAL_KILL_GRACE_MS;
|
|
39
|
+
var PROTOCOL_VERSION, LOCAL_PORT, BROADCAST_MAX_BYTES, COMMAND_STALE_MS, PERMISSION_TIMEOUT_MS, HEARTBEAT_INTERVAL_MS, RUNNER_ONLINE_WINDOW_MS, TERMINAL_FLUSH_MS, TERMINAL_FLUSH_BYTES, TERMINAL_LOCAL_FLUSH_MS, TERMINAL_RING_BYTES, TERMINAL_INPUT_MAX_BYTES, TERMINAL_REPLAY_PART_BYTES, TERMINAL_REPLAY_MAX_PER_MINUTE, TERMINAL_MAX_PER_OWNER, TERMINAL_MAX_PER_RUNNER, TERMINAL_KILL_GRACE_MS, LOCAL_PORT_MIN, LOCAL_PORT_MAX, LOCAL_TOKEN_CACHE_MS, LOCAL_TOKEN_NEGATIVE_CACHE_MS, LOCAL_RECHECK_MS, LOCAL_HANDSHAKE_TIMEOUT_MS, LOCAL_MAX_FRAME_BYTES, LOCAL_MAX_PENDING_UPGRADES, LOCAL_MAX_SOCKETS_PER_USER, LOCAL_MAX_SOCKETS, LOCAL_MAX_CONNECTIONS, LOCAL_GETUSER_PER_MINUTE, LOCAL_MAX_PENDING_CMDS;
|
|
14
40
|
var init_constants = __esm({
|
|
15
41
|
"../../packages/shared/src/constants.ts"() {
|
|
16
42
|
"use strict";
|
|
@@ -23,6 +49,7 @@ var init_constants = __esm({
|
|
|
23
49
|
RUNNER_ONLINE_WINDOW_MS = 45e3;
|
|
24
50
|
TERMINAL_FLUSH_MS = 33;
|
|
25
51
|
TERMINAL_FLUSH_BYTES = 16384;
|
|
52
|
+
TERMINAL_LOCAL_FLUSH_MS = 4;
|
|
26
53
|
TERMINAL_RING_BYTES = 262144;
|
|
27
54
|
TERMINAL_INPUT_MAX_BYTES = 65536;
|
|
28
55
|
TERMINAL_REPLAY_PART_BYTES = 65536;
|
|
@@ -30,6 +57,19 @@ var init_constants = __esm({
|
|
|
30
57
|
TERMINAL_MAX_PER_OWNER = 8;
|
|
31
58
|
TERMINAL_MAX_PER_RUNNER = 32;
|
|
32
59
|
TERMINAL_KILL_GRACE_MS = 2e3;
|
|
60
|
+
LOCAL_PORT_MIN = 47831;
|
|
61
|
+
LOCAL_PORT_MAX = 47850;
|
|
62
|
+
LOCAL_TOKEN_CACHE_MS = 3e5;
|
|
63
|
+
LOCAL_TOKEN_NEGATIVE_CACHE_MS = 1e4;
|
|
64
|
+
LOCAL_RECHECK_MS = LOCAL_TOKEN_CACHE_MS;
|
|
65
|
+
LOCAL_HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
66
|
+
LOCAL_MAX_FRAME_BYTES = 262144;
|
|
67
|
+
LOCAL_MAX_PENDING_UPGRADES = 8;
|
|
68
|
+
LOCAL_MAX_SOCKETS_PER_USER = 4;
|
|
69
|
+
LOCAL_MAX_SOCKETS = 16;
|
|
70
|
+
LOCAL_MAX_CONNECTIONS = 32;
|
|
71
|
+
LOCAL_GETUSER_PER_MINUTE = 60;
|
|
72
|
+
LOCAL_MAX_PENDING_CMDS = 256;
|
|
33
73
|
}
|
|
34
74
|
});
|
|
35
75
|
|
|
@@ -284,8 +324,35 @@ var init_command = __esm({
|
|
|
284
324
|
effort: Effort.optional(),
|
|
285
325
|
model: z4.string().optional(),
|
|
286
326
|
resumeProviderSessionId: z4.string().optional(),
|
|
327
|
+
// Duplicate a chat (D36): start from a fork of `resumeProviderSessionId` instead of appending
|
|
328
|
+
// to its transcript. Claude-only, no `worktree`. The runner additionally refuses a source that
|
|
329
|
+
// is not the caller's own Claude session on this runner, or a cwd other than the source's own directory.
|
|
330
|
+
forkSession: z4.boolean().optional(),
|
|
287
331
|
initialPrompt: z4.string().optional(),
|
|
288
332
|
title: z4.string().max(200).optional()
|
|
333
|
+
}).superRefine((p, ctx) => {
|
|
334
|
+
if (p.forkSession !== true) return;
|
|
335
|
+
if (p.resumeProviderSessionId === void 0) {
|
|
336
|
+
ctx.addIssue({
|
|
337
|
+
code: "custom",
|
|
338
|
+
message: "forkSession needs resumeProviderSessionId",
|
|
339
|
+
path: ["forkSession"]
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (p.worktree !== void 0) {
|
|
343
|
+
ctx.addIssue({
|
|
344
|
+
code: "custom",
|
|
345
|
+
message: "a forked session keeps its working directory",
|
|
346
|
+
path: ["worktree"]
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (p.provider === "codex") {
|
|
350
|
+
ctx.addIssue({
|
|
351
|
+
code: "custom",
|
|
352
|
+
message: "codex sessions cannot be forked",
|
|
353
|
+
path: ["provider"]
|
|
354
|
+
});
|
|
355
|
+
}
|
|
289
356
|
}),
|
|
290
357
|
"session.prompt": z4.object({
|
|
291
358
|
text: z4.string().min(1).max(2e5),
|
|
@@ -487,34 +554,104 @@ var init_events = __esm({
|
|
|
487
554
|
}
|
|
488
555
|
});
|
|
489
556
|
|
|
490
|
-
// ../../packages/shared/src/protocol/
|
|
557
|
+
// ../../packages/shared/src/protocol/local.ts
|
|
491
558
|
import { z as z7 } from "zod";
|
|
559
|
+
function parseLocalTopic(topic) {
|
|
560
|
+
const m = LOCAL_TOPIC_RE.exec(topic);
|
|
561
|
+
if (!m?.[1] || !m[2]) return null;
|
|
562
|
+
return { kind: m[1], id: m[2] };
|
|
563
|
+
}
|
|
564
|
+
function localProofMessage(nonce, runnerId, origin) {
|
|
565
|
+
return utf82.encode(`cayenne-local-v1
|
|
566
|
+
${nonce}
|
|
567
|
+
${runnerId}
|
|
568
|
+
${origin}`);
|
|
569
|
+
}
|
|
570
|
+
var LOCAL_HEALTH_PATH, LOCAL_WS_PATH, LOCAL_CLOSE_NOT_READY, LOCAL_CLOSE_BAD_HANDSHAKE, LOCAL_CLOSE_UNAUTHORIZED, LOCAL_CLOSE_TOO_MANY, Base64Url, UUID, LOCAL_TOPIC_RE, LOCAL_SESSION_TOPIC_RE, LocalTopic, LocalSessionTopic, LocalHealth, utf82, LocalClientFrame, LocalRunnerFrame;
|
|
571
|
+
var init_local = __esm({
|
|
572
|
+
"../../packages/shared/src/protocol/local.ts"() {
|
|
573
|
+
"use strict";
|
|
574
|
+
init_command();
|
|
575
|
+
init_events();
|
|
576
|
+
init_terminal();
|
|
577
|
+
LOCAL_HEALTH_PATH = "/health";
|
|
578
|
+
LOCAL_WS_PATH = "/ws";
|
|
579
|
+
LOCAL_CLOSE_NOT_READY = 1013;
|
|
580
|
+
LOCAL_CLOSE_BAD_HANDSHAKE = 4400;
|
|
581
|
+
LOCAL_CLOSE_UNAUTHORIZED = 4401;
|
|
582
|
+
LOCAL_CLOSE_TOO_MANY = 4429;
|
|
583
|
+
Base64Url = z7.string().regex(/^[A-Za-z0-9_-]+$/);
|
|
584
|
+
UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
585
|
+
LOCAL_TOPIC_RE = new RegExp(`^(session|terminal):(${UUID})$`);
|
|
586
|
+
LOCAL_SESSION_TOPIC_RE = new RegExp(`^session:${UUID}$`);
|
|
587
|
+
LocalTopic = z7.string().regex(LOCAL_TOPIC_RE);
|
|
588
|
+
LocalSessionTopic = z7.string().regex(LOCAL_SESSION_TOPIC_RE);
|
|
589
|
+
LocalHealth = z7.object({
|
|
590
|
+
runnerId: z7.uuid(),
|
|
591
|
+
protocolVersion: z7.number().int(),
|
|
592
|
+
version: z7.string()
|
|
593
|
+
});
|
|
594
|
+
utf82 = new TextEncoder();
|
|
595
|
+
LocalClientFrame = z7.discriminatedUnion("t", [
|
|
596
|
+
z7.object({ t: z7.literal("hello"), nonce: Base64Url.length(43) }),
|
|
597
|
+
z7.object({ t: z7.literal("auth"), token: z7.string().min(1).max(8192) }),
|
|
598
|
+
z7.object({ t: z7.literal("cmd"), id: z7.uuid() }),
|
|
599
|
+
z7.object({ t: z7.literal("term"), terminalId: z7.uuid(), m: TerminalClientMessage }),
|
|
600
|
+
z7.object({ t: z7.literal("sub"), topic: LocalTopic }),
|
|
601
|
+
z7.object({ t: z7.literal("unsub"), topic: LocalTopic })
|
|
602
|
+
]);
|
|
603
|
+
LocalRunnerFrame = z7.discriminatedUnion("t", [
|
|
604
|
+
z7.object({
|
|
605
|
+
t: z7.literal("hello"),
|
|
606
|
+
runnerId: z7.uuid(),
|
|
607
|
+
protocolVersion: z7.number().int(),
|
|
608
|
+
sig: Base64Url.length(86)
|
|
609
|
+
}),
|
|
610
|
+
z7.object({ t: z7.literal("ready"), userId: z7.uuid() }),
|
|
611
|
+
z7.object({ t: z7.literal("ack"), id: z7.uuid() }),
|
|
612
|
+
z7.object({
|
|
613
|
+
t: z7.literal("res"),
|
|
614
|
+
id: z7.uuid(),
|
|
615
|
+
status: CommandStatus.extract(["done", "failed", "cancelled"]),
|
|
616
|
+
result: z7.unknown().nullable(),
|
|
617
|
+
error: z7.string().nullable()
|
|
618
|
+
}),
|
|
619
|
+
z7.object({ t: z7.literal("cmd.err"), id: z7.uuid(), error: z7.string() }),
|
|
620
|
+
z7.object({ t: z7.literal("evt"), topic: LocalSessionTopic, e: SessionEvent }),
|
|
621
|
+
z7.object({ t: z7.literal("term"), terminalId: z7.uuid(), m: TerminalRunnerMessage }),
|
|
622
|
+
z7.object({ t: z7.literal("sub.err"), topic: LocalTopic, error: z7.string() })
|
|
623
|
+
]);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// ../../packages/shared/src/protocol/runner.ts
|
|
628
|
+
import { z as z8 } from "zod";
|
|
492
629
|
var ProviderModel, ProviderCapability, UNAVAILABLE, RunnerCapabilities, CODEX_TOOL_NAMES;
|
|
493
630
|
var init_runner = __esm({
|
|
494
631
|
"../../packages/shared/src/protocol/runner.ts"() {
|
|
495
632
|
"use strict";
|
|
496
633
|
init_command();
|
|
497
|
-
ProviderModel =
|
|
498
|
-
id:
|
|
499
|
-
label:
|
|
500
|
-
default:
|
|
634
|
+
ProviderModel = z8.object({
|
|
635
|
+
id: z8.string().min(1).max(200),
|
|
636
|
+
label: z8.string().min(1).max(200),
|
|
637
|
+
default: z8.boolean().optional(),
|
|
501
638
|
/** From the app-server's `supportedReasoningEfforts`; empty means the control is hidden. */
|
|
502
|
-
efforts:
|
|
639
|
+
efforts: z8.array(Effort).default([]),
|
|
503
640
|
/** The effort a new chat on this model opens at, sent back on `session.start`. */
|
|
504
641
|
defaultEffort: Effort.optional()
|
|
505
642
|
});
|
|
506
|
-
ProviderCapability =
|
|
507
|
-
available:
|
|
508
|
-
reason:
|
|
509
|
-
version:
|
|
510
|
-
models:
|
|
643
|
+
ProviderCapability = z8.object({
|
|
644
|
+
available: z8.boolean(),
|
|
645
|
+
reason: z8.enum(["not_installed", "not_logged_in", "error"]).optional(),
|
|
646
|
+
version: z8.string().max(50).optional(),
|
|
647
|
+
models: z8.array(ProviderModel).max(50).default([])
|
|
511
648
|
});
|
|
512
649
|
UNAVAILABLE = { available: false, reason: "not_installed", models: [] };
|
|
513
|
-
RunnerCapabilities =
|
|
514
|
-
kinds:
|
|
515
|
-
local_port:
|
|
516
|
-
claude:
|
|
517
|
-
providers:
|
|
650
|
+
RunnerCapabilities = z8.object({
|
|
651
|
+
kinds: z8.array(z8.string()),
|
|
652
|
+
local_port: z8.number().int(),
|
|
653
|
+
claude: z8.boolean(),
|
|
654
|
+
providers: z8.object({
|
|
518
655
|
claude: ProviderCapability.default({ available: true, models: [] }),
|
|
519
656
|
codex: ProviderCapability.default(UNAVAILABLE)
|
|
520
657
|
}).default({ claude: { available: true, models: [] }, codex: UNAVAILABLE }),
|
|
@@ -523,7 +660,19 @@ var init_runner = __esm({
|
|
|
523
660
|
* config `allowMemberTerminals` names (protocol §9). Absent reads as none; a workspace admin
|
|
524
661
|
* cannot grant a terminal. Optional so an older row still parses.
|
|
525
662
|
*/
|
|
526
|
-
memberTerminalIds:
|
|
663
|
+
memberTerminalIds: z8.array(z8.uuid()).max(64).optional(),
|
|
664
|
+
/**
|
|
665
|
+
* Whether this runner honours `session.start { forkSession: true }` (D36, protocol §9). An older
|
|
666
|
+
* runner strips the field and then refuses the resume under R30, so a client sends
|
|
667
|
+
* `forkSession` only when this is true. Absent reads as false; optional so an older row parses.
|
|
668
|
+
*/
|
|
669
|
+
sessionFork: z8.boolean().optional(),
|
|
670
|
+
/**
|
|
671
|
+
* The raw Ed25519 public key of the running process (32 bytes, unpadded base64url), which signs
|
|
672
|
+
* the local server's `hello` (protocol §6.2). New on every start, republished by every capabilities refresh that changes the row;
|
|
673
|
+
* a client that finds it absent never opens the fast path. Optional so an older row still parses.
|
|
674
|
+
*/
|
|
675
|
+
local_pubkey: z8.string().regex(/^[A-Za-z0-9_-]{43}$/).optional()
|
|
527
676
|
});
|
|
528
677
|
CODEX_TOOL_NAMES = {
|
|
529
678
|
command: "codex.command_execution",
|
|
@@ -534,6 +683,21 @@ var init_runner = __esm({
|
|
|
534
683
|
}
|
|
535
684
|
});
|
|
536
685
|
|
|
686
|
+
// ../../packages/shared/src/protocol/session.ts
|
|
687
|
+
import { z as z9 } from "zod";
|
|
688
|
+
var SlashCommand, SessionCommandsListed;
|
|
689
|
+
var init_session = __esm({
|
|
690
|
+
"../../packages/shared/src/protocol/session.ts"() {
|
|
691
|
+
"use strict";
|
|
692
|
+
SlashCommand = z9.object({
|
|
693
|
+
name: z9.string(),
|
|
694
|
+
description: z9.string(),
|
|
695
|
+
argumentHint: z9.string()
|
|
696
|
+
});
|
|
697
|
+
SessionCommandsListed = z9.object({ slashCommands: z9.array(SlashCommand) });
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
537
701
|
// ../../packages/shared/src/protocol/transport.ts
|
|
538
702
|
var init_transport = __esm({
|
|
539
703
|
"../../packages/shared/src/protocol/transport.ts"() {
|
|
@@ -550,7 +714,9 @@ var init_protocol = __esm({
|
|
|
550
714
|
init_events();
|
|
551
715
|
init_git();
|
|
552
716
|
init_github();
|
|
717
|
+
init_local();
|
|
553
718
|
init_runner();
|
|
719
|
+
init_session();
|
|
554
720
|
init_terminal();
|
|
555
721
|
init_transport();
|
|
556
722
|
}
|
|
@@ -652,31 +818,6 @@ var init_claude_dir = __esm({
|
|
|
652
818
|
}
|
|
653
819
|
});
|
|
654
820
|
|
|
655
|
-
// src/cloud.ts
|
|
656
|
-
function assertPublishable(key) {
|
|
657
|
-
if (!key) return key;
|
|
658
|
-
if (key.startsWith("sb_secret_"))
|
|
659
|
-
throw new Error("CLOUD key is a secret key; bake the publishable key");
|
|
660
|
-
if (key.startsWith("sb_publishable_")) return key;
|
|
661
|
-
const payload = key.split(".")[1];
|
|
662
|
-
if (!payload) throw new Error("CLOUD key is not a recognised Supabase key");
|
|
663
|
-
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
664
|
-
if (claims.role !== "anon") {
|
|
665
|
-
throw new Error(`CLOUD key has role "${claims.role}"; only the anon key may be baked in`);
|
|
666
|
-
}
|
|
667
|
-
return key;
|
|
668
|
-
}
|
|
669
|
-
var CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY;
|
|
670
|
-
var init_cloud = __esm({
|
|
671
|
-
"src/cloud.ts"() {
|
|
672
|
-
"use strict";
|
|
673
|
-
CLOUD_SUPABASE_URL = "https://ndqrkqxwkzxgnhkvcunr.supabase.co";
|
|
674
|
-
CLOUD_SUPABASE_ANON_KEY = assertPublishable(
|
|
675
|
-
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5kcXJrcXh3a3p4Z25oa3ZjdW5yIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODg3MTMyNzYsImV4cCI6MjEwNDI4OTI3Nn0.Dlsxw91E1QYZ4eoRDjP80Rf4feY77Z-T-fiIwUDY2k0"
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
|
|
680
821
|
// src/profile.ts
|
|
681
822
|
import { join as join2 } from "path";
|
|
682
823
|
function assertProfileName(name) {
|
|
@@ -725,7 +866,7 @@ import {
|
|
|
725
866
|
} from "fs";
|
|
726
867
|
import { homedir as homedir2 } from "os";
|
|
727
868
|
import { dirname, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
728
|
-
import { z as
|
|
869
|
+
import { z as z10 } from "zod";
|
|
729
870
|
function setProfileOverride(name) {
|
|
730
871
|
profileOverride = name;
|
|
731
872
|
}
|
|
@@ -800,22 +941,24 @@ var init_config = __esm({
|
|
|
800
941
|
init_cloud();
|
|
801
942
|
init_profile();
|
|
802
943
|
MEMBER_TERMINALS_ERROR = "allowMemberTerminals must be false or a list of user ids";
|
|
803
|
-
RunnerConfig =
|
|
804
|
-
supabaseUrl:
|
|
805
|
-
supabaseAnonKey:
|
|
806
|
-
workspaceId:
|
|
807
|
-
runnerId:
|
|
808
|
-
name:
|
|
809
|
-
roots:
|
|
810
|
-
port:
|
|
811
|
-
|
|
944
|
+
RunnerConfig = z10.object({
|
|
945
|
+
supabaseUrl: z10.url(),
|
|
946
|
+
supabaseAnonKey: z10.string().min(1),
|
|
947
|
+
workspaceId: z10.uuid().optional(),
|
|
948
|
+
runnerId: z10.uuid().optional(),
|
|
949
|
+
name: z10.string().optional(),
|
|
950
|
+
roots: z10.array(z10.string()).default([]),
|
|
951
|
+
port: z10.number().int().default(LOCAL_PORT),
|
|
952
|
+
/** Extra page origins the local server admits, beside the built-in list (`buildOrigins`). */
|
|
953
|
+
localOrigins: z10.array(z10.string()).default([]),
|
|
954
|
+
allowBypassPermissions: z10.boolean().default(false),
|
|
812
955
|
/** Absolute path to the `codex` binary, when it is not on the daemon's PATH. */
|
|
813
|
-
codexPath:
|
|
956
|
+
codexPath: z10.string().optional(),
|
|
814
957
|
/**
|
|
815
958
|
* The Claude Code config directory this profile's sessions run on (D33), handed to them as
|
|
816
959
|
* `CLAUDE_CONFIG_DIR`. Unset means the CLI's own default, `~/.claude`.
|
|
817
960
|
*/
|
|
818
|
-
claudeConfigDir:
|
|
961
|
+
claudeConfigDir: z10.string().optional(),
|
|
819
962
|
/**
|
|
820
963
|
* Who besides the runner owner may hold a terminal here (D34, plan ruling 14): `false`, or the
|
|
821
964
|
* user ids this machine's owner names. Machine-local like `allowBypassPermissions`: a workspace
|
|
@@ -823,15 +966,15 @@ var init_config = __esm({
|
|
|
823
966
|
* `true` is refused — there is no "every member" switch. A listed member's shell runs as this
|
|
824
967
|
* machine's OS user; the env allowlist is not a boundary.
|
|
825
968
|
*/
|
|
826
|
-
allowMemberTerminals:
|
|
969
|
+
allowMemberTerminals: z10.union(
|
|
827
970
|
[
|
|
828
|
-
|
|
829
|
-
|
|
971
|
+
z10.literal(false),
|
|
972
|
+
z10.array(z10.uuid({ error: MEMBER_TERMINALS_ERROR }), { error: MEMBER_TERMINALS_ERROR }).max(64, { error: MEMBER_TERMINALS_ERROR })
|
|
830
973
|
],
|
|
831
974
|
{ error: MEMBER_TERMINALS_ERROR }
|
|
832
975
|
).default(false),
|
|
833
976
|
/** Minutes a terminal nobody attends stays alive before the runner kills it (features §2). */
|
|
834
|
-
terminalIdleMinutes:
|
|
977
|
+
terminalIdleMinutes: z10.number().int().min(1).max(1440).default(10)
|
|
835
978
|
});
|
|
836
979
|
memberTerminalIdsOf = (cfg) => cfg.allowMemberTerminals === false ? [] : [...cfg.allowMemberTerminals];
|
|
837
980
|
configPath = (dir) => join3(dir, "config.json");
|
|
@@ -911,24 +1054,12 @@ function grantedColumns(deps) {
|
|
|
911
1054
|
protocol_version: PROTOCOL_VERSION,
|
|
912
1055
|
status: "online",
|
|
913
1056
|
last_seen_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
914
|
-
// Spread: `deps.kinds` is the composer's list, not a value to hand to a serialiser that could
|
|
915
|
-
// mutate it. `claude: true` stays beside `providers`: clients written
|
|
916
|
-
// before this change look for it, and dropping it would read as "this runner cannot run
|
|
917
|
-
// Claude".
|
|
918
|
-
//
|
|
919
1057
|
// Parsed, not merely typed: this row is the writer and every client's `RunnerCapabilities`
|
|
920
1058
|
// parse is the reader. A value the reader rejects fails that parse *whole*, so one bad model
|
|
921
1059
|
// reads to a member as "my Mac cannot run Codex", with nothing said anywhere. Failing here is
|
|
922
|
-
// loud, on one machine, at the moment the value is produced.
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
local_port: cfg.port,
|
|
926
|
-
claude: true,
|
|
927
|
-
providers: deps.providers,
|
|
928
|
-
// Who the runner's own config names (D34): the web shows `not-allowed` to anyone else
|
|
929
|
-
// instead of an "Open terminal" button the runner would refuse.
|
|
930
|
-
memberTerminalIds: memberTerminalIdsOf(cfg)
|
|
931
|
-
}),
|
|
1060
|
+
// loud, on one machine, at the moment the value is produced. The parse also returns a copy, so
|
|
1061
|
+
// the composer's arrays are never handed to a serialiser that could mutate them.
|
|
1062
|
+
capabilities: RunnerCapabilities.parse(deps.capabilities),
|
|
932
1063
|
// The sandbox boundary from the config, resolved and never widened here.
|
|
933
1064
|
allowed_roots: publishedRoots(cfg.roots)
|
|
934
1065
|
};
|
|
@@ -1088,7 +1219,7 @@ import {
|
|
|
1088
1219
|
} from "fs";
|
|
1089
1220
|
import { uptime } from "os";
|
|
1090
1221
|
import { join as join5 } from "path";
|
|
1091
|
-
import { z as
|
|
1222
|
+
import { z as z11 } from "zod";
|
|
1092
1223
|
function bootedAt(now = Date.now()) {
|
|
1093
1224
|
return new Date(Math.round((now - uptime() * 1e3) / 1e3) * 1e3).toISOString();
|
|
1094
1225
|
}
|
|
@@ -1231,13 +1362,13 @@ var LockRecord, BOOT_DRIFT_MS, ATTEMPTS, BACKOFF_MS, CLAIM_TTL_MS, errno, claimO
|
|
|
1231
1362
|
var init_lock = __esm({
|
|
1232
1363
|
"src/lock.ts"() {
|
|
1233
1364
|
"use strict";
|
|
1234
|
-
LockRecord =
|
|
1235
|
-
pid:
|
|
1236
|
-
profile:
|
|
1237
|
-
startedAt:
|
|
1365
|
+
LockRecord = z11.object({
|
|
1366
|
+
pid: z11.number().int().positive(),
|
|
1367
|
+
profile: z11.string(),
|
|
1368
|
+
startedAt: z11.string(),
|
|
1238
1369
|
/** `bootedAt()` when the lock was taken; a lock from another boot is stale whatever its pid. */
|
|
1239
|
-
bootedAt:
|
|
1240
|
-
version:
|
|
1370
|
+
bootedAt: z11.string(),
|
|
1371
|
+
version: z11.string()
|
|
1241
1372
|
});
|
|
1242
1373
|
BOOT_DRIFT_MS = 1e4;
|
|
1243
1374
|
ATTEMPTS = 8;
|
|
@@ -1339,6 +1470,44 @@ var init_version = __esm({
|
|
|
1339
1470
|
}
|
|
1340
1471
|
});
|
|
1341
1472
|
|
|
1473
|
+
// src/exec-path.ts
|
|
1474
|
+
import { accessSync, constants, statSync as statSync3 } from "fs";
|
|
1475
|
+
import { delimiter, isAbsolute as isAbsolute4, join as join8 } from "path";
|
|
1476
|
+
function findOnPath(name, path) {
|
|
1477
|
+
for (const dir of (path ?? "").split(delimiter)) {
|
|
1478
|
+
if (!isAbsolute4(dir)) continue;
|
|
1479
|
+
const candidate = join8(dir, name);
|
|
1480
|
+
try {
|
|
1481
|
+
if (!statSync3(candidate).isFile()) continue;
|
|
1482
|
+
accessSync(candidate, constants.X_OK);
|
|
1483
|
+
return candidate;
|
|
1484
|
+
} catch {
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
function makeLocator(name, locate = findOnPath) {
|
|
1490
|
+
const located = /* @__PURE__ */ new Map();
|
|
1491
|
+
return {
|
|
1492
|
+
find(path) {
|
|
1493
|
+
const key = path ?? "";
|
|
1494
|
+
const cached = located.get(key);
|
|
1495
|
+
if (cached !== void 0) return cached;
|
|
1496
|
+
const found = locate(name, path);
|
|
1497
|
+
if (found !== null) located.set(key, found);
|
|
1498
|
+
return found;
|
|
1499
|
+
},
|
|
1500
|
+
forget(path) {
|
|
1501
|
+
located.delete(path ?? "");
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
var init_exec_path = __esm({
|
|
1506
|
+
"src/exec-path.ts"() {
|
|
1507
|
+
"use strict";
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1342
1511
|
// src/codex/rpc.ts
|
|
1343
1512
|
var RpcError, JsonRpc;
|
|
1344
1513
|
var init_rpc = __esm({
|
|
@@ -1446,20 +1615,9 @@ var init_rpc = __esm({
|
|
|
1446
1615
|
|
|
1447
1616
|
// src/codex/detect.ts
|
|
1448
1617
|
import { execFile as execFile2, spawn } from "child_process";
|
|
1449
|
-
import { accessSync, constants } from "fs";
|
|
1450
|
-
import { delimiter, join as join8 } from "path";
|
|
1451
1618
|
import { promisify } from "util";
|
|
1452
1619
|
function findCodexOnPath(env) {
|
|
1453
|
-
|
|
1454
|
-
if (!dir) continue;
|
|
1455
|
-
const candidate = join8(dir, "codex");
|
|
1456
|
-
try {
|
|
1457
|
-
accessSync(candidate, constants.X_OK);
|
|
1458
|
-
return candidate;
|
|
1459
|
-
} catch {
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
return null;
|
|
1620
|
+
return findOnPath("codex", env.PATH);
|
|
1463
1621
|
}
|
|
1464
1622
|
function resolveCodexPath(cfg, env, onPath) {
|
|
1465
1623
|
return env.CAYENNE_CODEX_PATH ?? cfg.codexPath ?? onPath();
|
|
@@ -1536,6 +1694,7 @@ var init_detect = __esm({
|
|
|
1536
1694
|
"src/codex/detect.ts"() {
|
|
1537
1695
|
"use strict";
|
|
1538
1696
|
init_src();
|
|
1697
|
+
init_exec_path();
|
|
1539
1698
|
init_rpc();
|
|
1540
1699
|
exec = promisify(execFile2);
|
|
1541
1700
|
PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -1564,11 +1723,12 @@ var init_dispatcher = __esm({
|
|
|
1564
1723
|
|
|
1565
1724
|
// src/commands/source.ts
|
|
1566
1725
|
import { LRUCache } from "lru-cache";
|
|
1567
|
-
var byCreatedAt, GIT_READ_KINDS, GIT_WORKTREE_KINDS, laneOf, CommandSource;
|
|
1726
|
+
var STOPPING, byCreatedAt, GIT_READ_KINDS, GIT_WORKTREE_KINDS, laneOf, CommandSource;
|
|
1568
1727
|
var init_source = __esm({
|
|
1569
1728
|
"src/commands/source.ts"() {
|
|
1570
1729
|
"use strict";
|
|
1571
1730
|
init_src();
|
|
1731
|
+
STOPPING = "runner stopping";
|
|
1572
1732
|
byCreatedAt = (a, b) => Date.parse(a.created_at) - Date.parse(b.created_at) || a.id.localeCompare(b.id);
|
|
1573
1733
|
GIT_READ_KINDS = /* @__PURE__ */ new Set(["git.status", "git.diff", "git.branches"]);
|
|
1574
1734
|
GIT_WORKTREE_KINDS = /* @__PURE__ */ new Set([
|
|
@@ -1598,6 +1758,10 @@ var init_source = __esm({
|
|
|
1598
1758
|
dispatch;
|
|
1599
1759
|
log;
|
|
1600
1760
|
seen = new LRUCache({ max: 1e3 });
|
|
1761
|
+
/** Local-socket hooks per row id; dropped once fired, on `stop()`, or by the function `offer` returns. */
|
|
1762
|
+
hooks = /* @__PURE__ */ new Map();
|
|
1763
|
+
/** Rows between `enqueue` and their settlement: an `offer` for one of these registers hooks only. */
|
|
1764
|
+
inflight = /* @__PURE__ */ new Set();
|
|
1601
1765
|
queues = /* @__PURE__ */ new Map();
|
|
1602
1766
|
channel = null;
|
|
1603
1767
|
staleMs;
|
|
@@ -1664,6 +1828,38 @@ var init_source = __esm({
|
|
|
1664
1828
|
this.pending = [];
|
|
1665
1829
|
await this.draining;
|
|
1666
1830
|
while (this.queues.size > 0) await Promise.allSettled([...this.queues.values()]);
|
|
1831
|
+
for (const id of [...this.hooks.keys()]) this.dropHooks(id, STOPPING);
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* A pointer that arrived over the local socket (§6.2; SEC-P12, SP16). The same admission a
|
|
1835
|
+
* broadcast gets — dedupe by id, conditional ack, the same lane — so a row the relay already
|
|
1836
|
+
* delivered runs once, and a `terminal.open` pointer serialises behind the relay's copy in the
|
|
1837
|
+
* `terminal:<id>` lane. Hooks are registered either way so the socket hears the outcome. A row
|
|
1838
|
+
* this source already settled (or never claimed) is answered from the row itself, never from a
|
|
1839
|
+
* guess (PR15). Returns the function that drops these hooks; the connection calls it on close.
|
|
1840
|
+
*/
|
|
1841
|
+
offer(row, hooks) {
|
|
1842
|
+
if (this.stopped) {
|
|
1843
|
+
hooks.onDropped?.(STOPPING);
|
|
1844
|
+
return () => {
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const list = this.hooks.get(row.id) ?? [];
|
|
1848
|
+
list.push(hooks);
|
|
1849
|
+
this.hooks.set(row.id, list);
|
|
1850
|
+
if (!this.seen.has(row.id)) {
|
|
1851
|
+
if (this.draining) this.pending.push(row);
|
|
1852
|
+
else this.enqueue(row);
|
|
1853
|
+
} else if (!this.inflight.has(row.id)) {
|
|
1854
|
+
void this.settleFromRow(row.id);
|
|
1855
|
+
}
|
|
1856
|
+
return () => {
|
|
1857
|
+
const current = this.hooks.get(row.id);
|
|
1858
|
+
if (!current) return;
|
|
1859
|
+
const at = current.indexOf(hooks);
|
|
1860
|
+
if (at >= 0) current.splice(at, 1);
|
|
1861
|
+
if (current.length === 0) this.hooks.delete(row.id);
|
|
1862
|
+
};
|
|
1667
1863
|
}
|
|
1668
1864
|
/**
|
|
1669
1865
|
* Claim the row now; queue only its execution.
|
|
@@ -1681,6 +1877,7 @@ var init_source = __esm({
|
|
|
1681
1877
|
if (this.stopped) return;
|
|
1682
1878
|
if (this.seen.has(row.id)) return;
|
|
1683
1879
|
this.seen.set(row.id, true);
|
|
1880
|
+
this.inflight.add(row.id);
|
|
1684
1881
|
const ageMs = this.now() - Date.parse(row.created_at);
|
|
1685
1882
|
const admitted = this.admit(row, ageMs);
|
|
1686
1883
|
const key = laneOf(row);
|
|
@@ -1707,6 +1904,7 @@ var init_source = __esm({
|
|
|
1707
1904
|
if (row.flow_run_step_id === null && ageMs > this.staleMs) {
|
|
1708
1905
|
const { error: error2 } = await this.finish(row.id, "cancelled", null, "STALE");
|
|
1709
1906
|
if (error2) this.release(row.id, error2.message, "stale cancel failed");
|
|
1907
|
+
else this.settle(row.id, { status: "cancelled", result: null, error: "STALE" });
|
|
1710
1908
|
return "skip";
|
|
1711
1909
|
}
|
|
1712
1910
|
const { data, error } = await this.db.from("runner_commands").update({ status: "acked", acked_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", row.id).eq("status", "queued").select("id");
|
|
@@ -1716,8 +1914,10 @@ var init_source = __esm({
|
|
|
1716
1914
|
}
|
|
1717
1915
|
if (!data || data.length === 0) {
|
|
1718
1916
|
this.log.info({ id: row.id }, "command no longer queued; not dispatching");
|
|
1917
|
+
await this.settleFromRow(row.id);
|
|
1719
1918
|
return "skip";
|
|
1720
1919
|
}
|
|
1920
|
+
for (const h of this.hooks.get(row.id) ?? []) h.onAck?.();
|
|
1721
1921
|
return "dispatch";
|
|
1722
1922
|
} catch (err) {
|
|
1723
1923
|
this.release(row.id, err instanceof Error ? err.message : String(err), "ack failed");
|
|
@@ -1734,17 +1934,29 @@ var init_source = __esm({
|
|
|
1734
1934
|
*/
|
|
1735
1935
|
release(id, err, msg) {
|
|
1736
1936
|
this.seen.delete(id);
|
|
1937
|
+
this.inflight.delete(id);
|
|
1737
1938
|
this.log.warn({ id, err }, msg);
|
|
1738
1939
|
}
|
|
1739
1940
|
async run(row, admitted) {
|
|
1740
1941
|
if (await admitted !== "dispatch") return;
|
|
1942
|
+
let answered = false;
|
|
1943
|
+
try {
|
|
1944
|
+
await this.runAndFinish(row);
|
|
1945
|
+
answered = true;
|
|
1946
|
+
} finally {
|
|
1947
|
+
if (!answered) await this.settleFromRow(row.id);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
async runAndFinish(row) {
|
|
1741
1951
|
let status = "done";
|
|
1742
1952
|
let finished;
|
|
1953
|
+
let result = null;
|
|
1954
|
+
let message = null;
|
|
1743
1955
|
try {
|
|
1744
|
-
|
|
1956
|
+
result = await this.dispatch(row);
|
|
1745
1957
|
finished = await this.finish(row.id, "done", result ?? null, null);
|
|
1746
1958
|
} catch (err) {
|
|
1747
|
-
|
|
1959
|
+
message = err instanceof Error ? err.message : String(err);
|
|
1748
1960
|
this.log.warn({ id: row.id, kind: row.kind, err: message }, "command failed");
|
|
1749
1961
|
status = "failed";
|
|
1750
1962
|
finished = await this.finish(row.id, status, null, message);
|
|
@@ -1755,6 +1967,52 @@ var init_source = __esm({
|
|
|
1755
1967
|
"command finish write refused"
|
|
1756
1968
|
);
|
|
1757
1969
|
}
|
|
1970
|
+
if (finished.error) await this.settleFromRow(row.id);
|
|
1971
|
+
else {
|
|
1972
|
+
this.settle(row.id, {
|
|
1973
|
+
status,
|
|
1974
|
+
result: status === "done" ? result ?? null : null,
|
|
1975
|
+
error: status === "done" ? null : message
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
settle(id, r) {
|
|
1980
|
+
this.inflight.delete(id);
|
|
1981
|
+
const list = this.hooks.get(id);
|
|
1982
|
+
if (!list) return;
|
|
1983
|
+
this.hooks.delete(id);
|
|
1984
|
+
for (const h of list) h.onFinished?.(r);
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* The outcome of a row this source did not finish itself — cancelled by its creator between the
|
|
1988
|
+
* socket's read and our ack, claimed by another process, or settled before the pointer arrived.
|
|
1989
|
+
* Read once; answered only when the row is terminal, otherwise the hooks are dropped and the
|
|
1990
|
+
* client's relay wait sees the row (PR15: never invent a status).
|
|
1991
|
+
*/
|
|
1992
|
+
async settleFromRow(id) {
|
|
1993
|
+
this.inflight.delete(id);
|
|
1994
|
+
if (!this.hooks.has(id)) return;
|
|
1995
|
+
let reason = "row is not terminal";
|
|
1996
|
+
try {
|
|
1997
|
+
const { data } = await this.db.from("runner_commands").select("status, result, error").eq("id", id).maybeSingle();
|
|
1998
|
+
const status = data?.status;
|
|
1999
|
+
if (status === "done" || status === "failed" || status === "cancelled") {
|
|
2000
|
+
this.settle(id, { status, result: data?.result ?? null, error: data?.error ?? null });
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (!data) reason = "row not found";
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
this.log.warn({ id, err }, "could not re-read a command for its local pointer");
|
|
2006
|
+
reason = "row re-read failed";
|
|
2007
|
+
}
|
|
2008
|
+
this.dropHooks(id, reason);
|
|
2009
|
+
}
|
|
2010
|
+
/** Hooks this source will never fire `onFinished` for: told so, so a socket frees its pending slot. */
|
|
2011
|
+
dropHooks(id, reason) {
|
|
2012
|
+
const list = this.hooks.get(id);
|
|
2013
|
+
if (!list) return;
|
|
2014
|
+
this.hooks.delete(id);
|
|
2015
|
+
for (const h of list) h.onDropped?.(reason);
|
|
1758
2016
|
}
|
|
1759
2017
|
async finish(id, status, result, error) {
|
|
1760
2018
|
const res = await this.db.from("runner_commands").update({ status, result, error, finished_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
|
|
@@ -1766,7 +2024,7 @@ var init_source = __esm({
|
|
|
1766
2024
|
|
|
1767
2025
|
// src/paths.ts
|
|
1768
2026
|
import { lstatSync, realpathSync as realpathSync3 } from "fs";
|
|
1769
|
-
import { dirname as dirname3, isAbsolute as
|
|
2027
|
+
import { dirname as dirname3, isAbsolute as isAbsolute5, join as join9, relative, sep } from "path";
|
|
1770
2028
|
function assertInsideRoots(target, roots) {
|
|
1771
2029
|
return resolveInsideRoots(target, roots).real;
|
|
1772
2030
|
}
|
|
@@ -1784,7 +2042,7 @@ function resolveInsideRoots(target, roots) {
|
|
|
1784
2042
|
}
|
|
1785
2043
|
function assertInsideRootsOrAncestor(target, roots) {
|
|
1786
2044
|
assertRoots(roots);
|
|
1787
|
-
const absolute =
|
|
2045
|
+
const absolute = isAbsolute5(target) ? target : `${process.cwd()}${sep}${target}`;
|
|
1788
2046
|
let walked = walk(target, absolute);
|
|
1789
2047
|
if (walked.tail.includes("..")) walked = walk(target, join9(walked.real, ...walked.tail));
|
|
1790
2048
|
const checked = walked.tail.length === 0 ? walked.real : join9(walked.real, ...walked.tail);
|
|
@@ -1833,7 +2091,7 @@ function matchRoot(candidate, roots) {
|
|
|
1833
2091
|
continue;
|
|
1834
2092
|
}
|
|
1835
2093
|
const rel = relative(rootReal, candidate);
|
|
1836
|
-
const inside2 = rel === "" || !rel.startsWith("..") && !
|
|
2094
|
+
const inside2 = rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
|
|
1837
2095
|
if (inside2 && (match === null || rootReal.length < match.length)) match = rootReal;
|
|
1838
2096
|
}
|
|
1839
2097
|
return match;
|
|
@@ -1847,7 +2105,7 @@ var init_paths = __esm({
|
|
|
1847
2105
|
// src/git/introspect.ts
|
|
1848
2106
|
import { execFile as execFile3 } from "child_process";
|
|
1849
2107
|
import { readFileSync as readFileSync4, realpathSync as realpathSync4 } from "fs";
|
|
1850
|
-
import { delimiter as delimiter2, dirname as dirname4, isAbsolute as
|
|
2108
|
+
import { delimiter as delimiter2, dirname as dirname4, isAbsolute as isAbsolute6, join as join10, resolve } from "path";
|
|
1851
2109
|
import { promisify as promisify2 } from "util";
|
|
1852
2110
|
function parseGitHubRemote(url) {
|
|
1853
2111
|
const trimmed = url.trim();
|
|
@@ -1888,17 +2146,24 @@ function gitEnv(ceiling, env = process.env) {
|
|
|
1888
2146
|
};
|
|
1889
2147
|
}
|
|
1890
2148
|
function makeGitRun(opts) {
|
|
2149
|
+
const git = makeLocator("git", opts.locate);
|
|
1891
2150
|
return async (args, cwd, ceiling) => {
|
|
2151
|
+
const env = gitEnv(ceiling);
|
|
2152
|
+
const file = git.find(env.PATH);
|
|
2153
|
+
if (file === null) {
|
|
2154
|
+
return { code: 127, stdout: "", stderr: "git not found on an absolute PATH entry" };
|
|
2155
|
+
}
|
|
1892
2156
|
try {
|
|
1893
|
-
const { stdout, stderr } = await exec2(
|
|
2157
|
+
const { stdout, stderr } = await exec2(file, [...HARDENING, ...args], {
|
|
1894
2158
|
cwd,
|
|
1895
|
-
env
|
|
2159
|
+
env,
|
|
1896
2160
|
timeout: opts.timeoutMs,
|
|
1897
2161
|
maxBuffer: GIT_MAX_BUFFER
|
|
1898
2162
|
});
|
|
1899
2163
|
return { code: 0, stdout, stderr };
|
|
1900
2164
|
} catch (err) {
|
|
1901
2165
|
const e = err;
|
|
2166
|
+
if (e.code === "ENOENT") git.forget(env.PATH);
|
|
1902
2167
|
return {
|
|
1903
2168
|
code: typeof e.code === "number" ? e.code : 1,
|
|
1904
2169
|
stdout: e.stdout ?? "",
|
|
@@ -1932,7 +2197,7 @@ function isLinkedWorktree(gitDir, commonDir, repoRoot) {
|
|
|
1932
2197
|
return false;
|
|
1933
2198
|
}
|
|
1934
2199
|
}
|
|
1935
|
-
async function introspectRepo(cwd, run, root) {
|
|
2200
|
+
async function introspectRepo(cwd, run, root, { names = true } = {}) {
|
|
1936
2201
|
try {
|
|
1937
2202
|
const ceiling = dirname4(root);
|
|
1938
2203
|
if (ceiling.includes(delimiter2)) return { ...NONE };
|
|
@@ -1950,16 +2215,16 @@ async function introspectRepo(cwd, run, root) {
|
|
|
1950
2215
|
} catch {
|
|
1951
2216
|
return { ...NONE };
|
|
1952
2217
|
}
|
|
1953
|
-
const head = await line(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2218
|
+
const head = names ? await line(["rev-parse", "--abbrev-ref", "HEAD"]) : null;
|
|
1954
2219
|
const branch = head === "HEAD" ? null : head;
|
|
1955
2220
|
const dirs = (await line(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]))?.split("\n").map((d) => d.trim());
|
|
1956
|
-
const [gitDir, commonDir] = dirs?.length === 2 && dirs.every((d) =>
|
|
2221
|
+
const [gitDir, commonDir] = dirs?.length === 2 && dirs.every((d) => isAbsolute6(d)) ? dirs.map((d) => canonical(d, cwd)) : [null, null];
|
|
1957
2222
|
if (commonDir == null || gitDir == null) return { ...NONE };
|
|
1958
2223
|
if (!insideRoot(commonDir, root) && !isLinkedWorktree(gitDir, commonDir, repoRoot)) {
|
|
1959
2224
|
return { ...NONE };
|
|
1960
2225
|
}
|
|
1961
2226
|
const isWorktree = gitDir !== commonDir;
|
|
1962
|
-
const remote = await line(["remote", "get-url", "origin"]);
|
|
2227
|
+
const remote = names ? await line(["remote", "get-url", "origin"]) : null;
|
|
1963
2228
|
const gh = remote === null ? null : parseGitHubRemote(remote);
|
|
1964
2229
|
return {
|
|
1965
2230
|
repo_root: repoRoot,
|
|
@@ -1977,6 +2242,7 @@ var init_introspect = __esm({
|
|
|
1977
2242
|
"src/git/introspect.ts"() {
|
|
1978
2243
|
"use strict";
|
|
1979
2244
|
init_src();
|
|
2245
|
+
init_exec_path();
|
|
1980
2246
|
init_paths();
|
|
1981
2247
|
exec2 = promisify2(execFile3);
|
|
1982
2248
|
GIT_TIMEOUT_MS = 5e3;
|
|
@@ -2016,10 +2282,10 @@ var init_introspect = __esm({
|
|
|
2016
2282
|
});
|
|
2017
2283
|
|
|
2018
2284
|
// src/git/commands.ts
|
|
2019
|
-
import { lstatSync as lstatSync2, realpathSync as realpathSync5, statSync as
|
|
2020
|
-
import { dirname as dirname5, isAbsolute as
|
|
2285
|
+
import { lstatSync as lstatSync2, realpathSync as realpathSync5, statSync as statSync4 } from "fs";
|
|
2286
|
+
import { dirname as dirname5, isAbsolute as isAbsolute7, join as join11, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
2021
2287
|
async function scopeRepo(cwd, run, root) {
|
|
2022
|
-
const identity = await introspectRepo(cwd, run, root);
|
|
2288
|
+
const identity = await introspectRepo(cwd, run, root, { names: false });
|
|
2023
2289
|
if (identity.repo_root === null) {
|
|
2024
2290
|
throw new Error(`VALIDATION: ${cwd} is not inside a git repository under an allowed root`);
|
|
2025
2291
|
}
|
|
@@ -2175,7 +2441,7 @@ async function gitDirs(scope, run) {
|
|
|
2175
2441
|
);
|
|
2176
2442
|
if (r.code !== 0) return null;
|
|
2177
2443
|
const lines = r.stdout.split("\n").filter((l) => l !== "");
|
|
2178
|
-
if (lines.length === 0 || !lines.every((l) =>
|
|
2444
|
+
if (lines.length === 0 || !lines.every((l) => isAbsolute7(l))) return null;
|
|
2179
2445
|
try {
|
|
2180
2446
|
return lines.map((l) => realpathSync5.native(l));
|
|
2181
2447
|
} catch {
|
|
@@ -2189,7 +2455,7 @@ function isGitDirShape(dir) {
|
|
|
2189
2455
|
if (commondir?.isFile()) return true;
|
|
2190
2456
|
const isDir = (name) => {
|
|
2191
2457
|
try {
|
|
2192
|
-
return
|
|
2458
|
+
return statSync4(join11(dir, name), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
2193
2459
|
} catch {
|
|
2194
2460
|
return false;
|
|
2195
2461
|
}
|
|
@@ -2206,13 +2472,13 @@ function enclosesHome(repoRoot, home) {
|
|
|
2206
2472
|
if (within(real, repoRoot)) return true;
|
|
2207
2473
|
let root;
|
|
2208
2474
|
try {
|
|
2209
|
-
root =
|
|
2475
|
+
root = statSync4(repoRoot, { bigint: true });
|
|
2210
2476
|
} catch {
|
|
2211
2477
|
return false;
|
|
2212
2478
|
}
|
|
2213
2479
|
for (let cur = real; ; cur = dirname5(cur)) {
|
|
2214
2480
|
try {
|
|
2215
|
-
const st =
|
|
2481
|
+
const st = statSync4(cur, { bigint: true, throwIfNoEntry: false });
|
|
2216
2482
|
if (st && st.dev === root.dev && st.ino === root.ino) return true;
|
|
2217
2483
|
} catch {
|
|
2218
2484
|
}
|
|
@@ -2224,7 +2490,7 @@ function protectedTest(paths) {
|
|
|
2224
2490
|
for (const path of paths) {
|
|
2225
2491
|
try {
|
|
2226
2492
|
const real = realpathSync5.native(path);
|
|
2227
|
-
const st =
|
|
2493
|
+
const st = statSync4(real, { bigint: true });
|
|
2228
2494
|
entries.push({ real, dev: st.dev, ino: st.ino });
|
|
2229
2495
|
} catch {
|
|
2230
2496
|
}
|
|
@@ -2236,7 +2502,7 @@ function protectedTest(paths) {
|
|
|
2236
2502
|
if (cached !== void 0) return cached;
|
|
2237
2503
|
let hit = false;
|
|
2238
2504
|
try {
|
|
2239
|
-
const st =
|
|
2505
|
+
const st = statSync4(path, { bigint: true, throwIfNoEntry: false });
|
|
2240
2506
|
hit = st !== void 0 && entries.some((e) => e.dev === st.dev && e.ino === st.ino);
|
|
2241
2507
|
} catch {
|
|
2242
2508
|
}
|
|
@@ -2406,11 +2672,11 @@ import {
|
|
|
2406
2672
|
openSync,
|
|
2407
2673
|
readFileSync as readFileSync5,
|
|
2408
2674
|
realpathSync as realpathSync6,
|
|
2409
|
-
statSync as
|
|
2675
|
+
statSync as statSync5,
|
|
2410
2676
|
writeSync
|
|
2411
2677
|
} from "fs";
|
|
2412
2678
|
import { readdir } from "fs/promises";
|
|
2413
|
-
import { dirname as dirname6, isAbsolute as
|
|
2679
|
+
import { dirname as dirname6, isAbsolute as isAbsolute8, join as join12, relative as relative3, sep as sep3 } from "path";
|
|
2414
2680
|
import { setImmediate } from "timers/promises";
|
|
2415
2681
|
function worktreePathFor(repoRoot, name) {
|
|
2416
2682
|
return join12(repoRoot, WORKTREES_DIR, name);
|
|
@@ -2435,7 +2701,7 @@ async function excludeCayenneDir(scope, run) {
|
|
|
2435
2701
|
scope.ceiling
|
|
2436
2702
|
);
|
|
2437
2703
|
const common = r.stdout.trim();
|
|
2438
|
-
if (r.code !== 0 || !
|
|
2704
|
+
if (r.code !== 0 || !isAbsolute8(common)) return;
|
|
2439
2705
|
const dotGit = join12(scope.repoRoot, ".git");
|
|
2440
2706
|
let dotGitIsRealDir;
|
|
2441
2707
|
try {
|
|
@@ -2587,9 +2853,9 @@ async function worktreeIdentity(scope, run) {
|
|
|
2587
2853
|
scope.ceiling
|
|
2588
2854
|
);
|
|
2589
2855
|
const gitDir = r.stdout.trim();
|
|
2590
|
-
if (r.code !== 0 || !
|
|
2856
|
+
if (r.code !== 0 || !isAbsolute8(gitDir)) throw failed2("git rev-parse --git-dir", r.stderr);
|
|
2591
2857
|
try {
|
|
2592
|
-
const st =
|
|
2858
|
+
const st = statSync5(realpathSync6.native(gitDir), { bigint: true });
|
|
2593
2859
|
return {
|
|
2594
2860
|
gitDirDev: st.dev.toString(),
|
|
2595
2861
|
gitDirIno: st.ino.toString(),
|
|
@@ -2624,7 +2890,7 @@ async function removeWorktree(scope, run, worktreePath, force) {
|
|
|
2624
2890
|
throw new Error(`ROOT_NOT_ALLOWED: ${real}`);
|
|
2625
2891
|
}
|
|
2626
2892
|
const rel = relative3(base, real);
|
|
2627
|
-
if (rel === "" || rel.startsWith("..") ||
|
|
2893
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute8(rel) || rel.includes("/")) {
|
|
2628
2894
|
throw new Error(`VALIDATION: ${worktreePath} is not under ${base}`);
|
|
2629
2895
|
}
|
|
2630
2896
|
const inside2 = `${real}${sep3}`;
|
|
@@ -2748,7 +3014,7 @@ var init_worktree_owners = __esm({
|
|
|
2748
3014
|
import { rmSync as rmSync4 } from "fs";
|
|
2749
3015
|
import { join as join14 } from "path";
|
|
2750
3016
|
function createSessionWorktrees(dir) {
|
|
2751
|
-
const fileFor = (sessionId) =>
|
|
3017
|
+
const fileFor = (sessionId) => UUID2.test(sessionId) ? join14(dir, `${sessionId}.json`) : null;
|
|
2752
3018
|
return {
|
|
2753
3019
|
record(rec) {
|
|
2754
3020
|
const file = fileFor(rec.sessionId);
|
|
@@ -2784,12 +3050,620 @@ function createSessionWorktrees(dir) {
|
|
|
2784
3050
|
}
|
|
2785
3051
|
};
|
|
2786
3052
|
}
|
|
2787
|
-
var
|
|
3053
|
+
var UUID2;
|
|
2788
3054
|
var init_session_worktrees = __esm({
|
|
2789
3055
|
"src/git/session-worktrees.ts"() {
|
|
2790
3056
|
"use strict";
|
|
2791
3057
|
init_worktree_owners();
|
|
2792
|
-
|
|
3058
|
+
UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3059
|
+
}
|
|
3060
|
+
});
|
|
3061
|
+
|
|
3062
|
+
// src/local/auth.ts
|
|
3063
|
+
import { createHash as createHash2 } from "crypto";
|
|
3064
|
+
import { LRUCache as LRUCache2 } from "lru-cache";
|
|
3065
|
+
function claimsOf(token) {
|
|
3066
|
+
const payload = token.split(".")[1];
|
|
3067
|
+
if (!payload) return null;
|
|
3068
|
+
try {
|
|
3069
|
+
const c = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
3070
|
+
return typeof c.sub === "string" && typeof c.exp === "number" ? { sub: c.sub, exp: c.exp } : null;
|
|
3071
|
+
} catch {
|
|
3072
|
+
return null;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
function retryable(error) {
|
|
3076
|
+
const { name, status } = error;
|
|
3077
|
+
if (name === "AuthRetryableFetchError") return true;
|
|
3078
|
+
if (typeof status !== "number" || status === 0) return true;
|
|
3079
|
+
return status >= 500 || status === 429;
|
|
3080
|
+
}
|
|
3081
|
+
function createTokenVerifier({
|
|
3082
|
+
db,
|
|
3083
|
+
ttlMs = LOCAL_TOKEN_CACHE_MS,
|
|
3084
|
+
negativeTtlMs = LOCAL_TOKEN_NEGATIVE_CACHE_MS,
|
|
3085
|
+
now = Date.now,
|
|
3086
|
+
admit = () => true,
|
|
3087
|
+
log
|
|
3088
|
+
}) {
|
|
3089
|
+
const cache = new LRUCache2({ max: 1024 });
|
|
3090
|
+
const key = (token) => createHash2("sha256").update(token).digest("hex");
|
|
3091
|
+
let windowStart = now();
|
|
3092
|
+
let calls = 0;
|
|
3093
|
+
let warned2 = false;
|
|
3094
|
+
const budgeted = () => {
|
|
3095
|
+
const t = now();
|
|
3096
|
+
if (t - windowStart >= 6e4) {
|
|
3097
|
+
windowStart = t;
|
|
3098
|
+
calls = 0;
|
|
3099
|
+
warned2 = false;
|
|
3100
|
+
}
|
|
3101
|
+
if (calls < LOCAL_GETUSER_PER_MINUTE) {
|
|
3102
|
+
calls += 1;
|
|
3103
|
+
return true;
|
|
3104
|
+
}
|
|
3105
|
+
if (!warned2) {
|
|
3106
|
+
warned2 = true;
|
|
3107
|
+
log?.warn(
|
|
3108
|
+
{ perMinute: LOCAL_GETUSER_PER_MINUTE },
|
|
3109
|
+
"local token verification over budget; refusing until the minute turns"
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3112
|
+
return false;
|
|
3113
|
+
};
|
|
3114
|
+
const refuse = (k) => {
|
|
3115
|
+
cache.set(k, { userId: null, exp: 0, until: now() + negativeTtlMs });
|
|
3116
|
+
return { status: "refused" };
|
|
3117
|
+
};
|
|
3118
|
+
return {
|
|
3119
|
+
cacheKeys: () => [...cache.keys()],
|
|
3120
|
+
async verify(token) {
|
|
3121
|
+
const k = key(token);
|
|
3122
|
+
const hit = cache.get(k);
|
|
3123
|
+
if (hit && hit.until > now()) {
|
|
3124
|
+
if (hit.userId === null) return { status: "refused" };
|
|
3125
|
+
if (hit.exp * 1e3 <= now()) return refuse(k);
|
|
3126
|
+
return { status: "valid", userId: hit.userId, exp: hit.exp };
|
|
3127
|
+
}
|
|
3128
|
+
cache.delete(k);
|
|
3129
|
+
const claims = claimsOf(token);
|
|
3130
|
+
if (!claims || claims.exp * 1e3 <= now() || !admit(claims.sub)) return refuse(k);
|
|
3131
|
+
if (!budgeted()) return { status: "unavailable" };
|
|
3132
|
+
const user = await db.auth.getUser(token).then(
|
|
3133
|
+
(r) => r.error ? retryable(r.error) ? void 0 : null : r.data.user,
|
|
3134
|
+
() => void 0
|
|
3135
|
+
);
|
|
3136
|
+
if (user === void 0) return { status: "unavailable" };
|
|
3137
|
+
if (!user || user.id !== claims.sub) return refuse(k);
|
|
3138
|
+
cache.set(k, { userId: user.id, exp: claims.exp, until: now() + ttlMs });
|
|
3139
|
+
return { status: "valid", userId: user.id, exp: claims.exp };
|
|
3140
|
+
}
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
var init_auth = __esm({
|
|
3144
|
+
"src/local/auth.ts"() {
|
|
3145
|
+
"use strict";
|
|
3146
|
+
init_src();
|
|
3147
|
+
}
|
|
3148
|
+
});
|
|
3149
|
+
|
|
3150
|
+
// src/local/identity.ts
|
|
3151
|
+
import { generateKeyPairSync, sign } from "crypto";
|
|
3152
|
+
function createLocalIdentity() {
|
|
3153
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
3154
|
+
const x = publicKey.export({ format: "jwk" }).x;
|
|
3155
|
+
if (typeof x !== "string" || x.length !== 43) throw new Error("ed25519 public key export failed");
|
|
3156
|
+
return {
|
|
3157
|
+
publicKey: x,
|
|
3158
|
+
sign: (message) => sign(null, message, privateKey).toString("base64url")
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
var init_identity = __esm({
|
|
3162
|
+
"src/local/identity.ts"() {
|
|
3163
|
+
"use strict";
|
|
3164
|
+
}
|
|
3165
|
+
});
|
|
3166
|
+
|
|
3167
|
+
// src/local/origins.ts
|
|
3168
|
+
function normalise(origin) {
|
|
3169
|
+
try {
|
|
3170
|
+
const u = new URL(origin);
|
|
3171
|
+
if (u.username || u.password) return null;
|
|
3172
|
+
return u.origin !== "null" ? u.origin.toLowerCase() : `${u.protocol}//${u.host}`.toLowerCase();
|
|
3173
|
+
} catch {
|
|
3174
|
+
return null;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
function buildOrigins(extra, cloudOrigin = CLOUD_APP_ORIGIN) {
|
|
3178
|
+
const builtIn = cloudOrigin ? [cloudOrigin, ...SHELLS] : [...DEV, ...SHELLS];
|
|
3179
|
+
const out = /* @__PURE__ */ new Set();
|
|
3180
|
+
for (const o of [...builtIn, ...extra]) {
|
|
3181
|
+
const n = normalise(o);
|
|
3182
|
+
if (n) out.add(n);
|
|
3183
|
+
}
|
|
3184
|
+
return [...out];
|
|
3185
|
+
}
|
|
3186
|
+
function originAllowed(origin, allowed) {
|
|
3187
|
+
if (!origin) return false;
|
|
3188
|
+
const n = normalise(origin);
|
|
3189
|
+
return n !== null && allowed.includes(n);
|
|
3190
|
+
}
|
|
3191
|
+
var SHELLS, DEV;
|
|
3192
|
+
var init_origins = __esm({
|
|
3193
|
+
"src/local/origins.ts"() {
|
|
3194
|
+
"use strict";
|
|
3195
|
+
init_cloud();
|
|
3196
|
+
SHELLS = ["capacitor://localhost", "app://cayenne"];
|
|
3197
|
+
DEV = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3200
|
+
|
|
3201
|
+
// src/local/connection.ts
|
|
3202
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3203
|
+
function closeSoon(ws, code, reason) {
|
|
3204
|
+
if (ws.readyState === ws.CLOSED) return;
|
|
3205
|
+
ws.close(code, reason);
|
|
3206
|
+
const timer = setTimeout(() => ws.terminate(), CLOSE_GRACE_MS);
|
|
3207
|
+
timer.unref();
|
|
3208
|
+
ws.once("close", () => clearTimeout(timer));
|
|
3209
|
+
}
|
|
3210
|
+
function parseFrame(data) {
|
|
3211
|
+
try {
|
|
3212
|
+
const parsed = LocalClientFrame.safeParse(JSON.parse(String(data)));
|
|
3213
|
+
return parsed.success ? parsed.data : null;
|
|
3214
|
+
} catch {
|
|
3215
|
+
return null;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
function handleConnection(ws, who, ctx, log, now) {
|
|
3219
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
3220
|
+
const terminals = /* @__PURE__ */ new Set();
|
|
3221
|
+
const pending = /* @__PURE__ */ new Map();
|
|
3222
|
+
let closed = false;
|
|
3223
|
+
const pendingSubs = /* @__PURE__ */ new Map();
|
|
3224
|
+
const conn = {
|
|
3225
|
+
id: randomUUID2(),
|
|
3226
|
+
userId: who.userId,
|
|
3227
|
+
tokenExp: who.tokenExp,
|
|
3228
|
+
sessions,
|
|
3229
|
+
terminals,
|
|
3230
|
+
send(frame) {
|
|
3231
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
3232
|
+
if (ws.bufferedAmount > MAX_BUFFERED_BYTES) {
|
|
3233
|
+
log.warn({ conn: conn.id, userId: conn.userId }, "local client is not reading; closing");
|
|
3234
|
+
closeSoon(ws, LOCAL_CLOSE_NOT_READY, "too far behind");
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
ws.send(JSON.stringify(frame));
|
|
3238
|
+
},
|
|
3239
|
+
close(code, reason) {
|
|
3240
|
+
closeSoon(ws, code, reason);
|
|
3241
|
+
},
|
|
3242
|
+
recheckSessions: () => recheckSessions()
|
|
3243
|
+
};
|
|
3244
|
+
const cmdErr = (id, error) => conn.send({ t: "cmd.err", id, error });
|
|
3245
|
+
const onCmd = async (id) => {
|
|
3246
|
+
if (pending.has(id)) return;
|
|
3247
|
+
if (pending.size >= LOCAL_MAX_PENDING_CMDS) {
|
|
3248
|
+
return cmdErr(id, "VALIDATION: too many pending commands on this socket");
|
|
3249
|
+
}
|
|
3250
|
+
pending.set(id, () => {
|
|
3251
|
+
});
|
|
3252
|
+
let row;
|
|
3253
|
+
try {
|
|
3254
|
+
row = await ctx.readRow(id);
|
|
3255
|
+
} catch (err) {
|
|
3256
|
+
pending.delete(id);
|
|
3257
|
+
log.warn({ conn: conn.id, id, err }, "local cmd: row read failed");
|
|
3258
|
+
return cmdErr(id, "PROVIDER_ERROR: could not read the command");
|
|
3259
|
+
}
|
|
3260
|
+
if (closed) return;
|
|
3261
|
+
if (!row || row.runner_id !== ctx.runnerId || row.created_by !== conn.userId) {
|
|
3262
|
+
const reason = !row ? "no row" : row.runner_id !== ctx.runnerId ? "another runner" : "another creator";
|
|
3263
|
+
pending.delete(id);
|
|
3264
|
+
log.info({ conn: conn.id, userId: conn.userId, id, reason }, "local cmd refused");
|
|
3265
|
+
return cmdErr(id, "VALIDATION: no such command on this runner");
|
|
3266
|
+
}
|
|
3267
|
+
if (row.status === "done" || row.status === "failed" || row.status === "cancelled") {
|
|
3268
|
+
pending.delete(id);
|
|
3269
|
+
return conn.send({ t: "res", id, status: row.status, result: row.result, error: row.error });
|
|
3270
|
+
}
|
|
3271
|
+
const drop = ctx.source.offer(row, {
|
|
3272
|
+
onAck: () => conn.send({ t: "ack", id }),
|
|
3273
|
+
onFinished: (r) => {
|
|
3274
|
+
pending.delete(id);
|
|
3275
|
+
conn.send({ t: "res", id, ...r });
|
|
3276
|
+
},
|
|
3277
|
+
// The source will never settle this pointer (the row is claimed elsewhere and still running,
|
|
3278
|
+
// or could not be re-read): free the slot and say so without settling (ruling B10, R-B6).
|
|
3279
|
+
onDropped: (reason) => {
|
|
3280
|
+
pending.delete(id);
|
|
3281
|
+
log.info({ conn: conn.id, userId: conn.userId, id, reason }, "local cmd pointer dropped");
|
|
3282
|
+
cmdErr(id, "VALIDATION: no such command on this runner");
|
|
3283
|
+
}
|
|
3284
|
+
});
|
|
3285
|
+
if (pending.has(id)) pending.set(id, drop);
|
|
3286
|
+
};
|
|
3287
|
+
const onAuth = async (token) => {
|
|
3288
|
+
const next = await ctx.verifier.verify(token);
|
|
3289
|
+
if (next.status === "unavailable") {
|
|
3290
|
+
log.info({ conn: conn.id, userId: conn.userId }, "local auth refresh unverified; kept");
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
if (next.status !== "valid" || next.userId !== conn.userId || expiresAt(next.exp) <= now() || !ctx.serves(next.userId)) {
|
|
3294
|
+
conn.close(LOCAL_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
conn.tokenExp = expiresAt(next.exp);
|
|
3298
|
+
conn.send({ t: "ready", userId: conn.userId });
|
|
3299
|
+
await recheckSessions();
|
|
3300
|
+
};
|
|
3301
|
+
const SESSION_REFUSED = "SESSION_NOT_FOUND: not hosted on this runner";
|
|
3302
|
+
const sessionRefusal = (s) => !s ? "no row" : s.runner_id !== ctx.runnerId ? "another runner" : s.owner_id !== conn.userId && s.visibility !== "workspace" ? "private" : null;
|
|
3303
|
+
const recheckSessions = async () => {
|
|
3304
|
+
await Promise.all(
|
|
3305
|
+
[...sessions].map(async (id) => {
|
|
3306
|
+
let s;
|
|
3307
|
+
try {
|
|
3308
|
+
s = await ctx.readSession(id);
|
|
3309
|
+
} catch (err) {
|
|
3310
|
+
log.warn({ conn: conn.id, sessionId: id, err }, "local sub recheck: session read failed");
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
const reason = sessionRefusal(s);
|
|
3314
|
+
if (!reason || !sessions.delete(id)) return;
|
|
3315
|
+
log.info(
|
|
3316
|
+
{ conn: conn.id, userId: conn.userId, sessionId: id, reason },
|
|
3317
|
+
"local sub revoked"
|
|
3318
|
+
);
|
|
3319
|
+
conn.send({ t: "sub.err", topic: `session:${id}`, error: SESSION_REFUSED });
|
|
3320
|
+
})
|
|
3321
|
+
);
|
|
3322
|
+
};
|
|
3323
|
+
const onSub = async (topic) => {
|
|
3324
|
+
const parsed = parseLocalTopic(topic);
|
|
3325
|
+
if (!parsed) return;
|
|
3326
|
+
if (parsed.kind === "session") {
|
|
3327
|
+
const mine = {};
|
|
3328
|
+
pendingSubs.set(parsed.id, mine);
|
|
3329
|
+
const s = await ctx.readSession(parsed.id).catch(() => null);
|
|
3330
|
+
if (closed || pendingSubs.get(parsed.id) !== mine) return;
|
|
3331
|
+
pendingSubs.delete(parsed.id);
|
|
3332
|
+
const reason = sessionRefusal(s);
|
|
3333
|
+
if (reason) {
|
|
3334
|
+
log.info(
|
|
3335
|
+
{ conn: conn.id, userId: conn.userId, sessionId: parsed.id, reason },
|
|
3336
|
+
"local sub refused"
|
|
3337
|
+
);
|
|
3338
|
+
return conn.send({ t: "sub.err", topic, error: SESSION_REFUSED });
|
|
3339
|
+
}
|
|
3340
|
+
sessions.add(parsed.id);
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
const t = ctx.terminals;
|
|
3344
|
+
if (!t?.has(parsed.id) || t.ownerOf(parsed.id) !== conn.userId || !ctx.mayHoldTerminal(conn.userId)) {
|
|
3345
|
+
return conn.send({
|
|
3346
|
+
t: "sub.err",
|
|
3347
|
+
topic,
|
|
3348
|
+
error: "TERMINAL_NOT_FOUND: not your terminal here"
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
terminals.add(parsed.id);
|
|
3352
|
+
t.attachLocal(parsed.id, conn.id);
|
|
3353
|
+
};
|
|
3354
|
+
const onUnsub = (topic) => {
|
|
3355
|
+
const parsed = parseLocalTopic(topic);
|
|
3356
|
+
if (!parsed) return;
|
|
3357
|
+
if (parsed.kind === "session") {
|
|
3358
|
+
sessions.delete(parsed.id);
|
|
3359
|
+
pendingSubs.delete(parsed.id);
|
|
3360
|
+
} else if (terminals.delete(parsed.id)) ctx.terminals?.detachLocal(parsed.id, conn.id);
|
|
3361
|
+
};
|
|
3362
|
+
const onTerm = (terminalId, m) => {
|
|
3363
|
+
const t = ctx.terminals;
|
|
3364
|
+
if (!t || !terminals.has(terminalId)) {
|
|
3365
|
+
log.warn(
|
|
3366
|
+
{ conn: conn.id },
|
|
3367
|
+
"local term frame for a terminal this socket is not subscribed to; dropped"
|
|
3368
|
+
);
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
try {
|
|
3372
|
+
t.receive(terminalId, m.m, "local");
|
|
3373
|
+
} catch (err) {
|
|
3374
|
+
log.warn({ conn: conn.id, terminalId, err }, "local terminal message refused");
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3377
|
+
ws.on("message", (data) => {
|
|
3378
|
+
const f = parseFrame(data);
|
|
3379
|
+
if (!f) {
|
|
3380
|
+
log.warn({ conn: conn.id }, "local frame did not parse");
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
if (f.t === "hello") conn.close(LOCAL_CLOSE_BAD_HANDSHAKE, "handshake already done");
|
|
3384
|
+
else if (f.t === "auth") void onAuth(f.token);
|
|
3385
|
+
else if (f.t === "cmd") void onCmd(f.id);
|
|
3386
|
+
else if (f.t === "sub") void onSub(f.topic);
|
|
3387
|
+
else if (f.t === "unsub") onUnsub(f.topic);
|
|
3388
|
+
else onTerm(f.terminalId, f);
|
|
3389
|
+
});
|
|
3390
|
+
ws.on("close", () => {
|
|
3391
|
+
closed = true;
|
|
3392
|
+
for (const id of terminals) ctx.terminals?.detachLocal(id, conn.id);
|
|
3393
|
+
for (const drop of pending.values()) drop();
|
|
3394
|
+
pending.clear();
|
|
3395
|
+
terminals.clear();
|
|
3396
|
+
sessions.clear();
|
|
3397
|
+
pendingSubs.clear();
|
|
3398
|
+
});
|
|
3399
|
+
return conn;
|
|
3400
|
+
}
|
|
3401
|
+
function sessionFrame(sessionId, e) {
|
|
3402
|
+
return { t: "evt", topic: `session:${sessionId}`, e };
|
|
3403
|
+
}
|
|
3404
|
+
var MAX_BUFFERED_BYTES, CLOSE_GRACE_MS, expiresAt;
|
|
3405
|
+
var init_connection = __esm({
|
|
3406
|
+
"src/local/connection.ts"() {
|
|
3407
|
+
"use strict";
|
|
3408
|
+
init_src();
|
|
3409
|
+
MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
3410
|
+
CLOSE_GRACE_MS = 250;
|
|
3411
|
+
expiresAt = (exp) => exp * 1e3;
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
|
|
3415
|
+
// src/local/server.ts
|
|
3416
|
+
import { createServer } from "http";
|
|
3417
|
+
import { WebSocketServer } from "ws";
|
|
3418
|
+
function createLocalServer({
|
|
3419
|
+
port,
|
|
3420
|
+
origins,
|
|
3421
|
+
log,
|
|
3422
|
+
handshakeTimeoutMs = LOCAL_HANDSHAKE_TIMEOUT_MS,
|
|
3423
|
+
recheckMs = LOCAL_RECHECK_MS,
|
|
3424
|
+
now = Date.now,
|
|
3425
|
+
maxSockets = LOCAL_MAX_SOCKETS
|
|
3426
|
+
}) {
|
|
3427
|
+
let ctx = null;
|
|
3428
|
+
let bound = null;
|
|
3429
|
+
const conns = /* @__PURE__ */ new Set();
|
|
3430
|
+
let pendingUpgrades = 0;
|
|
3431
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: LOCAL_MAX_FRAME_BYTES });
|
|
3432
|
+
let untapTerminals = null;
|
|
3433
|
+
let unwatchRoster = null;
|
|
3434
|
+
let recheckTimer = null;
|
|
3435
|
+
const hostAllowed = (host) => bound !== null && (host === `${HOST}:${bound}` || host === `localhost:${bound}`);
|
|
3436
|
+
const socketsOf = (userId) => {
|
|
3437
|
+
let n = 0;
|
|
3438
|
+
for (const c of conns) if (c.userId === userId) n += 1;
|
|
3439
|
+
return n;
|
|
3440
|
+
};
|
|
3441
|
+
const recheck = () => {
|
|
3442
|
+
const current = ctx;
|
|
3443
|
+
if (!current) return;
|
|
3444
|
+
const t = now();
|
|
3445
|
+
for (const c of [...conns]) {
|
|
3446
|
+
if (!current.serves(c.userId) || t >= c.tokenExp) {
|
|
3447
|
+
c.close(LOCAL_CLOSE_UNAUTHORIZED, "no longer authorised");
|
|
3448
|
+
} else {
|
|
3449
|
+
void c.recheckSessions();
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
};
|
|
3453
|
+
const cors = (req, res) => {
|
|
3454
|
+
const origin = req.headers.origin;
|
|
3455
|
+
if (!originAllowed(origin, origins)) return false;
|
|
3456
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
3457
|
+
res.setHeader("Vary", "Origin");
|
|
3458
|
+
return true;
|
|
3459
|
+
};
|
|
3460
|
+
const server = createServer((req, res) => {
|
|
3461
|
+
if (!hostAllowed(req.headers.host)) {
|
|
3462
|
+
res.writeHead(403).end();
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
if (req.url !== LOCAL_HEALTH_PATH) {
|
|
3466
|
+
res.writeHead(404).end();
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
const allowed = cors(req, res);
|
|
3470
|
+
if (req.method === "OPTIONS") {
|
|
3471
|
+
if (!allowed) {
|
|
3472
|
+
res.writeHead(403).end();
|
|
3473
|
+
return;
|
|
3474
|
+
}
|
|
3475
|
+
res.setHeader("Access-Control-Allow-Methods", "GET");
|
|
3476
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
3477
|
+
res.setHeader("Access-Control-Max-Age", "600");
|
|
3478
|
+
res.writeHead(204).end();
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
if (req.method !== "GET") {
|
|
3482
|
+
res.writeHead(405).end();
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
res.setHeader("Content-Type", "application/json");
|
|
3486
|
+
res.setHeader("Cache-Control", "no-store");
|
|
3487
|
+
if (!ctx) {
|
|
3488
|
+
res.writeHead(503).end(JSON.stringify({ error: "starting" }));
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
res.writeHead(200).end(
|
|
3492
|
+
JSON.stringify({
|
|
3493
|
+
runnerId: ctx.runnerId,
|
|
3494
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
3495
|
+
version: ctx.version
|
|
3496
|
+
})
|
|
3497
|
+
);
|
|
3498
|
+
});
|
|
3499
|
+
server.maxConnections = LOCAL_MAX_CONNECTIONS;
|
|
3500
|
+
server.headersTimeout = LOCAL_HANDSHAKE_TIMEOUT_MS;
|
|
3501
|
+
server.requestTimeout = LOCAL_HANDSHAKE_TIMEOUT_MS;
|
|
3502
|
+
server.keepAliveTimeout = 1e3;
|
|
3503
|
+
const refuse = (socket, status) => {
|
|
3504
|
+
const text = status === 403 ? "Forbidden" : "Service Unavailable";
|
|
3505
|
+
socket.write(`HTTP/1.1 ${status} ${text}\r
|
|
3506
|
+
Connection: close\r
|
|
3507
|
+
Content-Length: 0\r
|
|
3508
|
+
\r
|
|
3509
|
+
`);
|
|
3510
|
+
socket.destroy();
|
|
3511
|
+
};
|
|
3512
|
+
server.on("upgrade", (req, socket, head) => {
|
|
3513
|
+
const origin = req.headers.origin ?? "";
|
|
3514
|
+
if (req.url !== LOCAL_WS_PATH || !hostAllowed(req.headers.host) || !originAllowed(origin, origins)) {
|
|
3515
|
+
refuse(socket, 403);
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
if (pendingUpgrades >= LOCAL_MAX_PENDING_UPGRADES || conns.size >= maxSockets) {
|
|
3519
|
+
refuse(socket, 503);
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
pendingUpgrades += 1;
|
|
3523
|
+
let released = false;
|
|
3524
|
+
const release = () => {
|
|
3525
|
+
if (released) return;
|
|
3526
|
+
released = true;
|
|
3527
|
+
pendingUpgrades -= 1;
|
|
3528
|
+
};
|
|
3529
|
+
let ws = null;
|
|
3530
|
+
const timer = setTimeout(() => {
|
|
3531
|
+
if (ws) closeSoon(ws, LOCAL_CLOSE_NOT_READY, "handshake timeout");
|
|
3532
|
+
else socket.destroy();
|
|
3533
|
+
}, handshakeTimeoutMs);
|
|
3534
|
+
socket.once("close", release);
|
|
3535
|
+
wss.handleUpgrade(req, socket, head, (admitted) => {
|
|
3536
|
+
ws = admitted;
|
|
3537
|
+
admitted.on("close", release);
|
|
3538
|
+
admitted.on("error", (err) => {
|
|
3539
|
+
log.warn({ code: err.code }, "local socket error");
|
|
3540
|
+
});
|
|
3541
|
+
const current = ctx;
|
|
3542
|
+
if (!current) {
|
|
3543
|
+
clearTimeout(timer);
|
|
3544
|
+
closeSoon(admitted, LOCAL_CLOSE_NOT_READY, "starting");
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
let state = "awaiting-hello";
|
|
3548
|
+
const onHandshake = async (data) => {
|
|
3549
|
+
const f = parseFrame(data);
|
|
3550
|
+
if (state === "awaiting-hello") {
|
|
3551
|
+
if (f?.t !== "hello") {
|
|
3552
|
+
closeSoon(admitted, LOCAL_CLOSE_BAD_HANDSHAKE, "expected hello");
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
state = "awaiting-auth";
|
|
3556
|
+
const sig = current.identity.sign(localProofMessage(f.nonce, current.runnerId, origin));
|
|
3557
|
+
admitted.send(
|
|
3558
|
+
JSON.stringify({
|
|
3559
|
+
t: "hello",
|
|
3560
|
+
runnerId: current.runnerId,
|
|
3561
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
3562
|
+
sig
|
|
3563
|
+
})
|
|
3564
|
+
);
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
if (state !== "awaiting-auth" || !f || f.t !== "auth") {
|
|
3568
|
+
closeSoon(admitted, LOCAL_CLOSE_BAD_HANDSHAKE, "expected auth");
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
state = "verifying";
|
|
3572
|
+
const who = await current.verifier.verify(f.token);
|
|
3573
|
+
if (admitted.readyState !== admitted.OPEN) return;
|
|
3574
|
+
if (who.status !== "valid" || expiresAt(who.exp) <= now() || !current.serves(who.userId)) {
|
|
3575
|
+
closeSoon(admitted, LOCAL_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
const tokenExp = expiresAt(who.exp);
|
|
3579
|
+
if (socketsOf(who.userId) >= LOCAL_MAX_SOCKETS_PER_USER) {
|
|
3580
|
+
closeSoon(admitted, LOCAL_CLOSE_TOO_MANY, "too many sockets for this user");
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
if (conns.size >= maxSockets) {
|
|
3584
|
+
closeSoon(admitted, LOCAL_CLOSE_TOO_MANY, "too many sockets");
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
state = "ready";
|
|
3588
|
+
clearTimeout(timer);
|
|
3589
|
+
release();
|
|
3590
|
+
admitted.off("message", listener);
|
|
3591
|
+
const conn = handleConnection(
|
|
3592
|
+
admitted,
|
|
3593
|
+
{ userId: who.userId, tokenExp },
|
|
3594
|
+
current,
|
|
3595
|
+
log,
|
|
3596
|
+
now
|
|
3597
|
+
);
|
|
3598
|
+
conns.add(conn);
|
|
3599
|
+
admitted.on("close", () => conns.delete(conn));
|
|
3600
|
+
conn.send({ t: "ready", userId: who.userId });
|
|
3601
|
+
log.info({ conn: conn.id, userId: who.userId }, "local client attached");
|
|
3602
|
+
};
|
|
3603
|
+
const listener = (data) => {
|
|
3604
|
+
void onHandshake(data);
|
|
3605
|
+
};
|
|
3606
|
+
admitted.on("message", listener);
|
|
3607
|
+
});
|
|
3608
|
+
});
|
|
3609
|
+
return {
|
|
3610
|
+
get port() {
|
|
3611
|
+
return bound;
|
|
3612
|
+
},
|
|
3613
|
+
listen() {
|
|
3614
|
+
return new Promise((resolve3, reject) => {
|
|
3615
|
+
server.once("error", reject);
|
|
3616
|
+
server.listen(port, HOST, () => {
|
|
3617
|
+
server.off("error", reject);
|
|
3618
|
+
const address = server.address();
|
|
3619
|
+
bound = typeof address === "object" && address ? address.port : port;
|
|
3620
|
+
log.info({ host: HOST, port: bound }, "local server listening");
|
|
3621
|
+
resolve3(bound);
|
|
3622
|
+
});
|
|
3623
|
+
});
|
|
3624
|
+
},
|
|
3625
|
+
attach(next) {
|
|
3626
|
+
ctx = next;
|
|
3627
|
+
untapTerminals?.();
|
|
3628
|
+
untapTerminals = next.terminals?.subscribe((terminalId, m, origin) => {
|
|
3629
|
+
if (m.type === "replay" && origin === "relay") return;
|
|
3630
|
+
for (const c of conns)
|
|
3631
|
+
if (c.terminals.has(terminalId)) c.send({ t: "term", terminalId, m });
|
|
3632
|
+
}) ?? null;
|
|
3633
|
+
unwatchRoster?.();
|
|
3634
|
+
unwatchRoster = next.onRosterRefresh(recheck);
|
|
3635
|
+
if (recheckTimer) clearInterval(recheckTimer);
|
|
3636
|
+
recheckTimer = setInterval(recheck, recheckMs);
|
|
3637
|
+
recheckTimer.unref();
|
|
3638
|
+
},
|
|
3639
|
+
emit(sessionId, e) {
|
|
3640
|
+
const frame = sessionFrame(sessionId, e);
|
|
3641
|
+
for (const c of conns) if (c.sessions.has(sessionId)) c.send(frame);
|
|
3642
|
+
},
|
|
3643
|
+
close() {
|
|
3644
|
+
if (recheckTimer) clearInterval(recheckTimer);
|
|
3645
|
+
recheckTimer = null;
|
|
3646
|
+
unwatchRoster?.();
|
|
3647
|
+
unwatchRoster = null;
|
|
3648
|
+
untapTerminals?.();
|
|
3649
|
+
untapTerminals = null;
|
|
3650
|
+
for (const client of wss.clients) client.close(1001, "runner stopping");
|
|
3651
|
+
return new Promise((resolve3) => {
|
|
3652
|
+
wss.close();
|
|
3653
|
+
server.close(() => resolve3());
|
|
3654
|
+
server.closeAllConnections();
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
var HOST;
|
|
3660
|
+
var init_server = __esm({
|
|
3661
|
+
"src/local/server.ts"() {
|
|
3662
|
+
"use strict";
|
|
3663
|
+
init_src();
|
|
3664
|
+
init_connection();
|
|
3665
|
+
init_origins();
|
|
3666
|
+
HOST = "127.0.0.1";
|
|
2793
3667
|
}
|
|
2794
3668
|
});
|
|
2795
3669
|
|
|
@@ -3030,21 +3904,31 @@ function checkRunsArgs(req, sha) {
|
|
|
3030
3904
|
}
|
|
3031
3905
|
return ["api", `repos/${req.owner}/${req.name}/commits/${sha}/check-runs`];
|
|
3032
3906
|
}
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3907
|
+
function makeGhRun(opts = {}) {
|
|
3908
|
+
const gh = makeLocator("gh", opts.locate);
|
|
3909
|
+
const currentPath = opts.path ?? (() => process.env.PATH);
|
|
3910
|
+
return async (args) => {
|
|
3911
|
+
const path = currentPath();
|
|
3912
|
+
const file = gh.find(path);
|
|
3913
|
+
if (file === null) {
|
|
3914
|
+
return { code: 127, stdout: "", stderr: "gh not found on an absolute PATH entry" };
|
|
3915
|
+
}
|
|
3916
|
+
try {
|
|
3917
|
+
const { stdout, stderr } = await exec3(file, args, {
|
|
3918
|
+
timeout: GH_TIMEOUT_MS,
|
|
3919
|
+
maxBuffer: MAX_OUTPUT * 2
|
|
3920
|
+
});
|
|
3921
|
+
return { code: 0, stdout, stderr };
|
|
3922
|
+
} catch (err) {
|
|
3923
|
+
const e = err;
|
|
3924
|
+
if (e.code === "ENOENT") gh.forget(path);
|
|
3925
|
+
return {
|
|
3926
|
+
code: typeof e.code === "number" ? e.code : 1,
|
|
3927
|
+
stdout: e.stdout ?? "",
|
|
3928
|
+
stderr: e.stderr ?? ""
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
};
|
|
3048
3932
|
}
|
|
3049
3933
|
function statusFrom(stderr) {
|
|
3050
3934
|
const m = /\(HTTP (\d{3})\)/.exec(stderr);
|
|
@@ -3082,16 +3966,18 @@ function createGhGateway(run) {
|
|
|
3082
3966
|
}
|
|
3083
3967
|
};
|
|
3084
3968
|
}
|
|
3085
|
-
var exec3, GH_TIMEOUT_MS, MAX_OUTPUT, ALL_DOTS3, COMMIT_SHA;
|
|
3969
|
+
var exec3, GH_TIMEOUT_MS, MAX_OUTPUT, ALL_DOTS3, COMMIT_SHA, runGh;
|
|
3086
3970
|
var init_gh = __esm({
|
|
3087
3971
|
"src/github/gh.ts"() {
|
|
3088
3972
|
"use strict";
|
|
3089
3973
|
init_src();
|
|
3974
|
+
init_exec_path();
|
|
3090
3975
|
exec3 = promisify3(execFile4);
|
|
3091
3976
|
GH_TIMEOUT_MS = 2e4;
|
|
3092
3977
|
MAX_OUTPUT = 512e3;
|
|
3093
3978
|
ALL_DOTS3 = /^\.+$/;
|
|
3094
3979
|
COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
3980
|
+
runGh = makeGhRun();
|
|
3095
3981
|
}
|
|
3096
3982
|
});
|
|
3097
3983
|
|
|
@@ -3125,11 +4011,12 @@ var init_refusals = __esm({
|
|
|
3125
4011
|
});
|
|
3126
4012
|
|
|
3127
4013
|
// src/sessions/handlers.ts
|
|
4014
|
+
import { ZodError } from "zod";
|
|
3128
4015
|
function parsePayload(row, kind) {
|
|
3129
4016
|
try {
|
|
3130
4017
|
return parseCommand(row, kind);
|
|
3131
4018
|
} catch (err) {
|
|
3132
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4019
|
+
const message = err instanceof ZodError ? err.issues.map((i) => i.message).join("; ") : err instanceof Error ? err.message : String(err);
|
|
3133
4020
|
throw new Error(message.startsWith("VALIDATION:") ? message : `VALIDATION: ${message}`);
|
|
3134
4021
|
}
|
|
3135
4022
|
}
|
|
@@ -3307,11 +4194,11 @@ var init_handlers = __esm({
|
|
|
3307
4194
|
|
|
3308
4195
|
// src/git/watch.ts
|
|
3309
4196
|
import { watch } from "fs";
|
|
3310
|
-
import { isAbsolute as
|
|
4197
|
+
import { isAbsolute as isAbsolute9, normalize, sep as sep4 } from "path";
|
|
3311
4198
|
function ignored(name) {
|
|
3312
4199
|
if (name === null || name === "") return false;
|
|
3313
4200
|
const rel = normalize(name);
|
|
3314
|
-
if (
|
|
4201
|
+
if (isAbsolute9(rel) || rel === ".." || rel.startsWith(`..${sep4}`)) return true;
|
|
3315
4202
|
return rel.split(/[\\/]/).some((segment) => IGNORED_NAMES.has(segment));
|
|
3316
4203
|
}
|
|
3317
4204
|
function watchChanges(cwd, onChange, debounceMs = CHANGES_DEBOUNCE_MS, onError = () => void 0, fsWatch = recursiveWatch) {
|
|
@@ -3513,11 +4400,11 @@ var init_pushable = __esm({
|
|
|
3513
4400
|
});
|
|
3514
4401
|
|
|
3515
4402
|
// src/claude/session.ts
|
|
3516
|
-
import { randomUUID as
|
|
4403
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3517
4404
|
import { EventEmitter } from "events";
|
|
3518
4405
|
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
3519
4406
|
var ClaudeSession;
|
|
3520
|
-
var
|
|
4407
|
+
var init_session2 = __esm({
|
|
3521
4408
|
"src/claude/session.ts"() {
|
|
3522
4409
|
"use strict";
|
|
3523
4410
|
init_src();
|
|
@@ -3527,7 +4414,7 @@ var init_session = __esm({
|
|
|
3527
4414
|
constructor(opts) {
|
|
3528
4415
|
super();
|
|
3529
4416
|
this.opts = opts;
|
|
3530
|
-
this.providerSessionId = opts.resume ?? null;
|
|
4417
|
+
this.providerSessionId = opts.forkSession ? null : opts.resume ?? null;
|
|
3531
4418
|
}
|
|
3532
4419
|
opts;
|
|
3533
4420
|
provider = "claude";
|
|
@@ -3560,6 +4447,7 @@ var init_session = __esm({
|
|
|
3560
4447
|
// Present only when asked for, so the default call stays the CLI's own behaviour.
|
|
3561
4448
|
...this.opts.strictMcpConfig === void 0 ? {} : { strictMcpConfig: this.opts.strictMcpConfig },
|
|
3562
4449
|
...this.opts.mcpServers === void 0 ? {} : { mcpServers: this.opts.mcpServers },
|
|
4450
|
+
...this.opts.forkSession ? { forkSession: true } : {},
|
|
3563
4451
|
canUseTool,
|
|
3564
4452
|
abortController: this.abort,
|
|
3565
4453
|
stderr: (line) => {
|
|
@@ -3621,7 +4509,7 @@ var init_session = __esm({
|
|
|
3621
4509
|
}
|
|
3622
4510
|
requestPermission(toolName2, input, suggestions, toolUseId, signal) {
|
|
3623
4511
|
return new Promise((resolve3) => {
|
|
3624
|
-
const id =
|
|
4512
|
+
const id = randomUUID3();
|
|
3625
4513
|
const settle = (d) => {
|
|
3626
4514
|
const entry = this.pending.get(id);
|
|
3627
4515
|
if (!entry) return;
|
|
@@ -3808,7 +4696,7 @@ function scrubSecrets(text) {
|
|
|
3808
4696
|
return SECRET_PATTERNS.reduce((acc, [pattern, to]) => acc.replace(pattern, to), text);
|
|
3809
4697
|
}
|
|
3810
4698
|
var STDERR_TAIL_MAX, SIGKILL_AFTER_MS, SECRET_PATTERNS, APPROVAL_METHODS, CodexSession;
|
|
3811
|
-
var
|
|
4699
|
+
var init_session3 = __esm({
|
|
3812
4700
|
"src/codex/session.ts"() {
|
|
3813
4701
|
"use strict";
|
|
3814
4702
|
init_src();
|
|
@@ -4154,15 +5042,15 @@ function createProviderSession(provider, opts) {
|
|
|
4154
5042
|
var init_create = __esm({
|
|
4155
5043
|
"src/providers/create.ts"() {
|
|
4156
5044
|
"use strict";
|
|
4157
|
-
init_session();
|
|
4158
5045
|
init_session2();
|
|
5046
|
+
init_session3();
|
|
4159
5047
|
}
|
|
4160
5048
|
});
|
|
4161
5049
|
|
|
4162
5050
|
// src/sessions/manager.ts
|
|
4163
|
-
import { randomUUID as
|
|
5051
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
4164
5052
|
import { realpathSync as realpathSync7 } from "fs";
|
|
4165
|
-
import { isAbsolute as
|
|
5053
|
+
import { isAbsolute as isAbsolute10, sep as sep5 } from "path";
|
|
4166
5054
|
import { isDeepStrictEqual } from "util";
|
|
4167
5055
|
function narrowToSession(suggestions) {
|
|
4168
5056
|
if (!suggestions) return suggestions;
|
|
@@ -4258,10 +5146,16 @@ var init_manager = __esm({
|
|
|
4258
5146
|
}
|
|
4259
5147
|
this.assertModeAllowed(p.permissionMode);
|
|
4260
5148
|
const provider = p.provider ?? "claude";
|
|
5149
|
+
if (p.forkSession && provider !== "claude") {
|
|
5150
|
+
throw new Error("VALIDATION: codex sessions cannot be forked");
|
|
5151
|
+
}
|
|
4261
5152
|
this.assertProviderAllowed(provider, createdBy);
|
|
4262
5153
|
const typed = resolveInsideRoots(p.cwd, this.deps.roots);
|
|
4263
5154
|
this.assertNotRemoving(typed.real);
|
|
4264
|
-
|
|
5155
|
+
const forking = p.forkSession === true && p.resumeProviderSessionId !== void 0;
|
|
5156
|
+
if (forking) {
|
|
5157
|
+
await this.assertForkable(p.resumeProviderSessionId, createdBy, typed.real);
|
|
5158
|
+
} else if (p.resumeProviderSessionId !== void 0) {
|
|
4265
5159
|
await this.assertResumable(sessionId, p.resumeProviderSessionId);
|
|
4266
5160
|
}
|
|
4267
5161
|
let { real: cwd, root } = typed;
|
|
@@ -4269,7 +5163,7 @@ var init_manager = __esm({
|
|
|
4269
5163
|
if (p.worktree) {
|
|
4270
5164
|
cwd = await this.startWorktree(typed.real, typed.root, p.worktree, createdBy);
|
|
4271
5165
|
inWorktree = true;
|
|
4272
|
-
} else if (p.resumeProviderSessionId !== void 0) {
|
|
5166
|
+
} else if (p.resumeProviderSessionId !== void 0 && !forking) {
|
|
4273
5167
|
const recorded = await this.recordedWorktree(sessionId);
|
|
4274
5168
|
if (recorded) ({ real: cwd, root } = recorded);
|
|
4275
5169
|
}
|
|
@@ -4290,6 +5184,7 @@ var init_manager = __esm({
|
|
|
4290
5184
|
permissionMode: p.permissionMode,
|
|
4291
5185
|
model: p.model,
|
|
4292
5186
|
resume: p.resumeProviderSessionId,
|
|
5187
|
+
...forking ? { forkSession: true } : {},
|
|
4293
5188
|
...provider === "codex" ? {
|
|
4294
5189
|
codexPath: this.deps.codexPath,
|
|
4295
5190
|
checkInput: this.codexGate(sessionId, cwd),
|
|
@@ -4300,7 +5195,9 @@ var init_manager = __esm({
|
|
|
4300
5195
|
started();
|
|
4301
5196
|
}
|
|
4302
5197
|
if (inWorktree) await this.recordSessionWorktree(sessionId, cwd, root);
|
|
4303
|
-
else if (p.resumeProviderSessionId === void 0)
|
|
5198
|
+
else if (p.resumeProviderSessionId === void 0 || forking) {
|
|
5199
|
+
this.forgetSessionWorktree(sessionId);
|
|
5200
|
+
}
|
|
4304
5201
|
live.status = "idle";
|
|
4305
5202
|
await this.updateSession(sessionId, { status: "idle", model: p.model ?? null });
|
|
4306
5203
|
await this.broadcast(sessionId, {
|
|
@@ -4320,7 +5217,7 @@ var init_manager = __esm({
|
|
|
4320
5217
|
if (p.initialPrompt) {
|
|
4321
5218
|
await this.prompt(
|
|
4322
5219
|
sessionId,
|
|
4323
|
-
{ text: p.initialPrompt, clientUuid:
|
|
5220
|
+
{ text: p.initialPrompt, clientUuid: randomUUID4(), attachments: [] },
|
|
4324
5221
|
createdBy
|
|
4325
5222
|
);
|
|
4326
5223
|
}
|
|
@@ -4747,6 +5644,37 @@ var init_manager = __esm({
|
|
|
4747
5644
|
throw new Error("VALIDATION: resumeProviderSessionId does not belong to this session");
|
|
4748
5645
|
}
|
|
4749
5646
|
}
|
|
5647
|
+
/**
|
|
5648
|
+
* D36, amending R30 for `forkSession: true`: the resume id may be held by another session, but
|
|
5649
|
+
* only one on this runner that `createdBy` owns — a member must not fork the runner owner's
|
|
5650
|
+
* conversation, or anyone else's. A command with no creator owns nothing. Only a Claude row with
|
|
5651
|
+
* a non-empty id can be a source. The new session must run where the source did, decided the way
|
|
5652
|
+
* a resume of the source would be: the worktree this runner recorded for it, else its canonical
|
|
5653
|
+
* `cwd`. Never `worktree_path`: the runner owner can rewrite that column on a member's chat, and
|
|
5654
|
+
* it must not steer where the member's fork runs. A source path that no longer resolves inside
|
|
5655
|
+
* the roots cannot be proved equal, so it is refused, not thrown through.
|
|
5656
|
+
*/
|
|
5657
|
+
async assertForkable(providerSessionId, createdBy, cwd) {
|
|
5658
|
+
if (providerSessionId === "") {
|
|
5659
|
+
throw new Error("VALIDATION: forkSession source is not a session you own on this runner");
|
|
5660
|
+
}
|
|
5661
|
+
const { data, error } = await this.deps.db.from("chat_sessions").select("id, owner_id, cwd").eq("provider_session_id", providerSessionId).eq("runner_id", this.deps.runnerId).eq("provider", "claude").maybeSingle();
|
|
5662
|
+
if (error) throw new Error(`PROVIDER_ERROR: could not read the session: ${error.message}`);
|
|
5663
|
+
if (!data || createdBy === null || data.owner_id !== createdBy) {
|
|
5664
|
+
throw new Error("VALIDATION: forkSession source is not a session you own on this runner");
|
|
5665
|
+
}
|
|
5666
|
+
let sourceCwd = (await this.recordedWorktree(data.id))?.real ?? null;
|
|
5667
|
+
if (sourceCwd === null) {
|
|
5668
|
+
try {
|
|
5669
|
+
sourceCwd = resolveInsideRoots(data.cwd, this.deps.roots).real;
|
|
5670
|
+
} catch {
|
|
5671
|
+
sourceCwd = null;
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
if (sourceCwd !== cwd) {
|
|
5675
|
+
throw new Error("VALIDATION: a forked session keeps its working directory");
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
4750
5678
|
/**
|
|
4751
5679
|
* Whether this runner may open a session on `provider` for `createdBy`.
|
|
4752
5680
|
*
|
|
@@ -4797,7 +5725,7 @@ var init_manager = __esm({
|
|
|
4797
5725
|
}
|
|
4798
5726
|
/** A path as the SDK reads it: relative means relative to the session's cwd. */
|
|
4799
5727
|
fromCwd(cwd, value) {
|
|
4800
|
-
return
|
|
5728
|
+
return isAbsolute10(value) ? value : `${cwd}${sep5}${value}`;
|
|
4801
5729
|
}
|
|
4802
5730
|
/**
|
|
4803
5731
|
* §2.9: a path in `updatedInput` passes the same boundary the session's `cwd` did, and the
|
|
@@ -5362,6 +6290,11 @@ var init_manager = __esm({
|
|
|
5362
6290
|
}
|
|
5363
6291
|
message = envelope(shrunk);
|
|
5364
6292
|
}
|
|
6293
|
+
try {
|
|
6294
|
+
this.deps.tap?.(sessionId, message.payload);
|
|
6295
|
+
} catch (err) {
|
|
6296
|
+
this.deps.log.warn({ err, sessionId, type: event.type }, "session event tap threw");
|
|
6297
|
+
}
|
|
5365
6298
|
const result = await live.channel.send(message);
|
|
5366
6299
|
if (result !== "ok") {
|
|
5367
6300
|
this.deps.log.warn(
|
|
@@ -5476,7 +6409,7 @@ var init_channels = __esm({
|
|
|
5476
6409
|
// src/terminal/env.ts
|
|
5477
6410
|
import { readFileSync as readFileSync7, realpathSync as realpathSync8 } from "fs";
|
|
5478
6411
|
import { lstat } from "fs/promises";
|
|
5479
|
-
import { dirname as dirname7, isAbsolute as
|
|
6412
|
+
import { dirname as dirname7, isAbsolute as isAbsolute11, relative as relative4 } from "path";
|
|
5480
6413
|
function terminalEnv(base, { runnerOwner, claudeConfigDir }) {
|
|
5481
6414
|
const env = {};
|
|
5482
6415
|
for (const [key, value] of Object.entries(base)) {
|
|
@@ -5546,7 +6479,7 @@ function tryRealpath(fs, p) {
|
|
|
5546
6479
|
}
|
|
5547
6480
|
function inside(candidate, root) {
|
|
5548
6481
|
const rel = relative4(root, candidate);
|
|
5549
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
6482
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute11(rel);
|
|
5550
6483
|
}
|
|
5551
6484
|
var SECRET_ENV, MEMBER_ENV_ALLOW, MEMBER_ENV_KEYS, LOCALE_ENV, nodeShellFs;
|
|
5552
6485
|
var init_env = __esm({
|
|
@@ -5753,6 +6686,7 @@ var init_manager2 = __esm({
|
|
|
5753
6686
|
constructor(deps) {
|
|
5754
6687
|
this.deps = deps;
|
|
5755
6688
|
this.flushMs = deps.flushMs ?? TERMINAL_FLUSH_MS;
|
|
6689
|
+
this.localFlushMs = deps.localFlushMs ?? TERMINAL_LOCAL_FLUSH_MS;
|
|
5756
6690
|
this.flushBytes = deps.flushBytes ?? TERMINAL_FLUSH_BYTES;
|
|
5757
6691
|
this.ringBytes = deps.ringBytes ?? TERMINAL_RING_BYTES;
|
|
5758
6692
|
this.reaper = setInterval(() => this.reap(), deps.reapIntervalMs ?? TERMINAL_REAP_INTERVAL_MS);
|
|
@@ -5769,6 +6703,7 @@ var init_manager2 = __esm({
|
|
|
5769
6703
|
opens = /* @__PURE__ */ new Set();
|
|
5770
6704
|
reaper;
|
|
5771
6705
|
flushMs;
|
|
6706
|
+
localFlushMs;
|
|
5772
6707
|
flushBytes;
|
|
5773
6708
|
ringBytes;
|
|
5774
6709
|
/** Set once by `shutdown()` (fix round 1 L7): `open()` checks it after every await. */
|
|
@@ -6092,9 +7027,14 @@ var init_manager2 = __esm({
|
|
|
6092
7027
|
return;
|
|
6093
7028
|
}
|
|
6094
7029
|
if (term.flushTimer) return;
|
|
6095
|
-
let
|
|
6096
|
-
|
|
6097
|
-
|
|
7030
|
+
let delay2 = this.localFlushMs;
|
|
7031
|
+
if (!this.relaySuppressed(terminalId)) {
|
|
7032
|
+
let others = 0;
|
|
7033
|
+
for (const [id, t] of this.terms) {
|
|
7034
|
+
if (t.flushTimer !== null && !this.relaySuppressed(id)) others += 1;
|
|
7035
|
+
}
|
|
7036
|
+
delay2 = others > 0 ? this.flushMs * 2 : this.flushMs;
|
|
7037
|
+
}
|
|
6098
7038
|
term.flushTimer = setTimeout(() => {
|
|
6099
7039
|
term.flushTimer = null;
|
|
6100
7040
|
this.flush(terminalId, term);
|
|
@@ -6635,13 +7575,42 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6635
7575
|
const pty = await loadPty();
|
|
6636
7576
|
const memberTerminalIds = memberTerminalIdsOf(cfg);
|
|
6637
7577
|
const kinds2 = handledKinds({ terminal: pty !== null });
|
|
7578
|
+
const localIdentity = createLocalIdentity();
|
|
7579
|
+
const origins = buildOrigins(cfg.localOrigins);
|
|
7580
|
+
if (CLOUD_SUPABASE_URL !== "" && CLOUD_APP_ORIGIN === "") {
|
|
7581
|
+
logger.warn(
|
|
7582
|
+
{ origins },
|
|
7583
|
+
"local origins: dev origins enabled because CAYENNE_CLOUD_APP_ORIGIN is empty"
|
|
7584
|
+
);
|
|
7585
|
+
}
|
|
7586
|
+
logger.info({ origins }, "local origins");
|
|
7587
|
+
const local = createLocalServer({ port: cfg.port, origins, log: logger });
|
|
7588
|
+
let localPort = 0;
|
|
7589
|
+
if (cfg.port < LOCAL_PORT_MIN || cfg.port > LOCAL_PORT_MAX) {
|
|
7590
|
+
logger.warn(
|
|
7591
|
+
{ port: cfg.port },
|
|
7592
|
+
`local port ${cfg.port} is outside ${LOCAL_PORT_MIN}\u2013${LOCAL_PORT_MAX}; fast path disabled for this run`
|
|
7593
|
+
);
|
|
7594
|
+
} else {
|
|
7595
|
+
try {
|
|
7596
|
+
localPort = await local.listen();
|
|
7597
|
+
} catch (err) {
|
|
7598
|
+
logger.warn(
|
|
7599
|
+
{ port: cfg.port, err: err instanceof Error ? err.message : String(err) },
|
|
7600
|
+
"local server could not bind; fast path disabled for this run"
|
|
7601
|
+
);
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
6638
7604
|
const capabilities = () => ({
|
|
6639
7605
|
kinds: [...kinds2],
|
|
6640
|
-
local_port:
|
|
7606
|
+
local_port: localPort,
|
|
7607
|
+
local_pubkey: localIdentity.publicKey,
|
|
6641
7608
|
claude: true,
|
|
6642
7609
|
providers,
|
|
6643
7610
|
// Who the runner's own config names (D34); every heartbeat refresh republishes it.
|
|
6644
|
-
memberTerminalIds: [...memberTerminalIds]
|
|
7611
|
+
memberTerminalIds: [...memberTerminalIds],
|
|
7612
|
+
// D36: this build forks a Claude session on `session.start { forkSession: true }`.
|
|
7613
|
+
sessionFork: true
|
|
6645
7614
|
});
|
|
6646
7615
|
const registered = capabilities();
|
|
6647
7616
|
let loggedCodex = null;
|
|
@@ -6654,17 +7623,27 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6654
7623
|
}
|
|
6655
7624
|
return capabilities();
|
|
6656
7625
|
};
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
7626
|
+
let identity;
|
|
7627
|
+
try {
|
|
7628
|
+
identity = await registerRunner({
|
|
7629
|
+
db,
|
|
7630
|
+
cfg: registerAs === cfg.runnerId ? cfg : { ...cfg, runnerId: void 0 },
|
|
7631
|
+
hostname: hostname(),
|
|
7632
|
+
profile,
|
|
7633
|
+
platform: `${process.platform}-${process.arch}`,
|
|
7634
|
+
version: VERSION,
|
|
7635
|
+
capabilities: registered
|
|
7636
|
+
});
|
|
7637
|
+
} catch (err) {
|
|
7638
|
+
await local.close();
|
|
7639
|
+
throw err;
|
|
7640
|
+
}
|
|
7641
|
+
try {
|
|
7642
|
+
lock.take(identity.runnerId);
|
|
7643
|
+
} catch (err) {
|
|
7644
|
+
await local.close();
|
|
7645
|
+
throw err;
|
|
7646
|
+
}
|
|
6668
7647
|
let stopHeartbeat = null;
|
|
6669
7648
|
let roster = null;
|
|
6670
7649
|
try {
|
|
@@ -6699,11 +7678,14 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6699
7678
|
codexAvailable: () => providers.codex.available,
|
|
6700
7679
|
claudeEnv: claudeEnv(claudeConfigDir),
|
|
6701
7680
|
git,
|
|
6702
|
-
log: logger
|
|
7681
|
+
log: logger,
|
|
7682
|
+
// Every session event also reaches the loopback sockets subscribed to it (Task 10a).
|
|
7683
|
+
tap: (sessionId, event) => local.emit(sessionId, event)
|
|
6703
7684
|
});
|
|
6704
7685
|
let terminals;
|
|
7686
|
+
let terminalManager = null;
|
|
6705
7687
|
if (pty) {
|
|
6706
|
-
const
|
|
7688
|
+
const tm = new TerminalManager({
|
|
6707
7689
|
pty,
|
|
6708
7690
|
roots,
|
|
6709
7691
|
env: process.env,
|
|
@@ -6726,16 +7708,17 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6726
7708
|
memberTerminalIds,
|
|
6727
7709
|
serves: (userId) => liveRoster.serves(userId),
|
|
6728
7710
|
rosterHealthy: () => liveRoster.healthy(),
|
|
6729
|
-
manager:
|
|
6730
|
-
channels: createTerminalChannels({ db, manager:
|
|
7711
|
+
manager: tm,
|
|
7712
|
+
channels: createTerminalChannels({ db, manager: tm, log: logger }),
|
|
6731
7713
|
log: logger
|
|
6732
7714
|
});
|
|
6733
7715
|
liveRoster.onRefresh(() => {
|
|
6734
|
-
|
|
7716
|
+
tm.revalidate();
|
|
6735
7717
|
void service2.reconcileRows();
|
|
6736
7718
|
});
|
|
6737
7719
|
await service2.closeOpenRows();
|
|
6738
7720
|
terminals = service2;
|
|
7721
|
+
terminalManager = tm;
|
|
6739
7722
|
}
|
|
6740
7723
|
const dispatch = createDispatcher(
|
|
6741
7724
|
buildHandlers(manager, {
|
|
@@ -6758,6 +7741,43 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6758
7741
|
);
|
|
6759
7742
|
stopHeartbeat = heartbeat;
|
|
6760
7743
|
await source.start();
|
|
7744
|
+
if (localPort !== 0) {
|
|
7745
|
+
local.attach({
|
|
7746
|
+
runnerId: identity.runnerId,
|
|
7747
|
+
version: VERSION,
|
|
7748
|
+
identity: localIdentity,
|
|
7749
|
+
// R-B4: a token whose subject the roster does not serve (or whose payload is malformed or
|
|
7750
|
+
// expired) is refused inside the verifier, before any GoTrue call — junk on the loopback
|
|
7751
|
+
// port cannot rate-limit the real member's `/user` check.
|
|
7752
|
+
verifier: createTokenVerifier({
|
|
7753
|
+
db,
|
|
7754
|
+
admit: (sub) => liveRoster.serves(sub),
|
|
7755
|
+
log: logger
|
|
7756
|
+
}),
|
|
7757
|
+
// The roster's own predicate, read live: the owner while a member, listed members while
|
|
7758
|
+
// still members (PR16); every refresh re-checks every socket (SEC-P2).
|
|
7759
|
+
serves: (userId) => liveRoster.serves(userId),
|
|
7760
|
+
onRosterRefresh: (cb) => liveRoster.onRefresh(cb),
|
|
7761
|
+
// Exactly the manager's rule (ruling 14, R2-H1), built from the same deps it was given.
|
|
7762
|
+
mayHoldTerminal: (userId) => mayHoldTerminal(userId, {
|
|
7763
|
+
runnerUserId: identity.userId,
|
|
7764
|
+
memberTerminalIds,
|
|
7765
|
+
serves: (u) => liveRoster.serves(u),
|
|
7766
|
+
rosterHealthy: () => liveRoster.healthy()
|
|
7767
|
+
}),
|
|
7768
|
+
source,
|
|
7769
|
+
readRow: async (id) => {
|
|
7770
|
+
const { data } = await db.from("runner_commands").select("*").eq("id", id).maybeSingle();
|
|
7771
|
+
const parsed = CommandRow.safeParse(data);
|
|
7772
|
+
return parsed.success ? parsed.data : null;
|
|
7773
|
+
},
|
|
7774
|
+
readSession: async (id) => {
|
|
7775
|
+
const { data } = await db.from("chat_sessions").select("runner_id, owner_id, visibility").eq("id", id).maybeSingle();
|
|
7776
|
+
return data;
|
|
7777
|
+
},
|
|
7778
|
+
terminals: terminalManager
|
|
7779
|
+
});
|
|
7780
|
+
}
|
|
6761
7781
|
logger.info(
|
|
6762
7782
|
{
|
|
6763
7783
|
runnerId: identity.runnerId,
|
|
@@ -6792,6 +7812,7 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6792
7812
|
await stage("terminals.shutdown", async () => {
|
|
6793
7813
|
await terminals?.shutdown();
|
|
6794
7814
|
});
|
|
7815
|
+
await stage("local.close", () => local.close());
|
|
6795
7816
|
await stage("roster.stop", () => liveRoster.stop());
|
|
6796
7817
|
if (failures.length > 0) throw failures[0];
|
|
6797
7818
|
} finally {
|
|
@@ -6858,6 +7879,7 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6858
7879
|
"runner roster stop failed during a failed start"
|
|
6859
7880
|
);
|
|
6860
7881
|
}
|
|
7882
|
+
await local.close().catch(() => void 0);
|
|
6861
7883
|
await markOffline(db, identity.runnerId);
|
|
6862
7884
|
throw err;
|
|
6863
7885
|
}
|
|
@@ -6892,6 +7914,7 @@ var init_start = __esm({
|
|
|
6892
7914
|
"use strict";
|
|
6893
7915
|
init_src();
|
|
6894
7916
|
init_claude_dir();
|
|
7917
|
+
init_cloud();
|
|
6895
7918
|
init_detect();
|
|
6896
7919
|
init_dispatcher();
|
|
6897
7920
|
init_source();
|
|
@@ -6899,6 +7922,10 @@ var init_start = __esm({
|
|
|
6899
7922
|
init_ops();
|
|
6900
7923
|
init_session_worktrees();
|
|
6901
7924
|
init_worktree_owners();
|
|
7925
|
+
init_auth();
|
|
7926
|
+
init_identity();
|
|
7927
|
+
init_origins();
|
|
7928
|
+
init_server();
|
|
6902
7929
|
init_lock();
|
|
6903
7930
|
init_log();
|
|
6904
7931
|
init_profile();
|
|
@@ -6944,6 +7971,9 @@ async function logout(db) {
|
|
|
6944
7971
|
await db.auth.signOut();
|
|
6945
7972
|
}
|
|
6946
7973
|
|
|
7974
|
+
// src/cli.ts
|
|
7975
|
+
init_cloud();
|
|
7976
|
+
|
|
6947
7977
|
// src/commands/doctor.ts
|
|
6948
7978
|
async function probeBackend(url, anonKey, fetchFn = fetch) {
|
|
6949
7979
|
try {
|
|
@@ -7022,6 +8052,19 @@ async function doctor(deps) {
|
|
|
7022
8052
|
fix: TERMINAL_FIX
|
|
7023
8053
|
}
|
|
7024
8054
|
);
|
|
8055
|
+
const devOriginsInPublishedBuild = deps.cloud.supabaseUrl !== "" && deps.cloud.appOrigin === "";
|
|
8056
|
+
checks.push(
|
|
8057
|
+
devOriginsInPublishedBuild ? {
|
|
8058
|
+
name: "local origins",
|
|
8059
|
+
ok: false,
|
|
8060
|
+
detail: "local origins: dev origins enabled because CAYENNE_CLOUD_APP_ORIGIN is empty",
|
|
8061
|
+
fix: "Rebuild the runner with CAYENNE_CLOUD_APP_ORIGIN set to the app origin (tsup.config.ts `define`)."
|
|
8062
|
+
} : {
|
|
8063
|
+
name: "local origins",
|
|
8064
|
+
ok: true,
|
|
8065
|
+
detail: deps.cloud.appOrigin || "source checkout: dev origins"
|
|
8066
|
+
}
|
|
8067
|
+
);
|
|
7025
8068
|
return checks;
|
|
7026
8069
|
}
|
|
7027
8070
|
|
|
@@ -7495,7 +8538,8 @@ program.command("doctor").description("Check the install and say how to fix what
|
|
|
7495
8538
|
probe: (url) => probeBackend(url, backend?.anonKey ?? ""),
|
|
7496
8539
|
runnerId,
|
|
7497
8540
|
holder: runnerId ? holderOf({ root: cayenneHome(), runnerId }) : null,
|
|
7498
|
-
pty: await loadPty() !== null
|
|
8541
|
+
pty: await loadPty() !== null,
|
|
8542
|
+
cloud: { appOrigin: CLOUD_APP_ORIGIN, supabaseUrl: CLOUD_SUPABASE_URL }
|
|
7499
8543
|
});
|
|
7500
8544
|
for (const c of checks) {
|
|
7501
8545
|
console.log(`${c.ok ? "ok " : "FAIL"} ${c.name}: ${c.detail}`);
|