@jpcasabiancai/cayenne 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1122 -171
- package/package.json +3 -1
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
|
|
|
@@ -487,34 +527,104 @@ var init_events = __esm({
|
|
|
487
527
|
}
|
|
488
528
|
});
|
|
489
529
|
|
|
490
|
-
// ../../packages/shared/src/protocol/
|
|
530
|
+
// ../../packages/shared/src/protocol/local.ts
|
|
491
531
|
import { z as z7 } from "zod";
|
|
532
|
+
function parseLocalTopic(topic) {
|
|
533
|
+
const m = LOCAL_TOPIC_RE.exec(topic);
|
|
534
|
+
if (!m?.[1] || !m[2]) return null;
|
|
535
|
+
return { kind: m[1], id: m[2] };
|
|
536
|
+
}
|
|
537
|
+
function localProofMessage(nonce, runnerId, origin) {
|
|
538
|
+
return utf82.encode(`cayenne-local-v1
|
|
539
|
+
${nonce}
|
|
540
|
+
${runnerId}
|
|
541
|
+
${origin}`);
|
|
542
|
+
}
|
|
543
|
+
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;
|
|
544
|
+
var init_local = __esm({
|
|
545
|
+
"../../packages/shared/src/protocol/local.ts"() {
|
|
546
|
+
"use strict";
|
|
547
|
+
init_command();
|
|
548
|
+
init_events();
|
|
549
|
+
init_terminal();
|
|
550
|
+
LOCAL_HEALTH_PATH = "/health";
|
|
551
|
+
LOCAL_WS_PATH = "/ws";
|
|
552
|
+
LOCAL_CLOSE_NOT_READY = 1013;
|
|
553
|
+
LOCAL_CLOSE_BAD_HANDSHAKE = 4400;
|
|
554
|
+
LOCAL_CLOSE_UNAUTHORIZED = 4401;
|
|
555
|
+
LOCAL_CLOSE_TOO_MANY = 4429;
|
|
556
|
+
Base64Url = z7.string().regex(/^[A-Za-z0-9_-]+$/);
|
|
557
|
+
UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
558
|
+
LOCAL_TOPIC_RE = new RegExp(`^(session|terminal):(${UUID})$`);
|
|
559
|
+
LOCAL_SESSION_TOPIC_RE = new RegExp(`^session:${UUID}$`);
|
|
560
|
+
LocalTopic = z7.string().regex(LOCAL_TOPIC_RE);
|
|
561
|
+
LocalSessionTopic = z7.string().regex(LOCAL_SESSION_TOPIC_RE);
|
|
562
|
+
LocalHealth = z7.object({
|
|
563
|
+
runnerId: z7.uuid(),
|
|
564
|
+
protocolVersion: z7.number().int(),
|
|
565
|
+
version: z7.string()
|
|
566
|
+
});
|
|
567
|
+
utf82 = new TextEncoder();
|
|
568
|
+
LocalClientFrame = z7.discriminatedUnion("t", [
|
|
569
|
+
z7.object({ t: z7.literal("hello"), nonce: Base64Url.length(43) }),
|
|
570
|
+
z7.object({ t: z7.literal("auth"), token: z7.string().min(1).max(8192) }),
|
|
571
|
+
z7.object({ t: z7.literal("cmd"), id: z7.uuid() }),
|
|
572
|
+
z7.object({ t: z7.literal("term"), terminalId: z7.uuid(), m: TerminalClientMessage }),
|
|
573
|
+
z7.object({ t: z7.literal("sub"), topic: LocalTopic }),
|
|
574
|
+
z7.object({ t: z7.literal("unsub"), topic: LocalTopic })
|
|
575
|
+
]);
|
|
576
|
+
LocalRunnerFrame = z7.discriminatedUnion("t", [
|
|
577
|
+
z7.object({
|
|
578
|
+
t: z7.literal("hello"),
|
|
579
|
+
runnerId: z7.uuid(),
|
|
580
|
+
protocolVersion: z7.number().int(),
|
|
581
|
+
sig: Base64Url.length(86)
|
|
582
|
+
}),
|
|
583
|
+
z7.object({ t: z7.literal("ready"), userId: z7.uuid() }),
|
|
584
|
+
z7.object({ t: z7.literal("ack"), id: z7.uuid() }),
|
|
585
|
+
z7.object({
|
|
586
|
+
t: z7.literal("res"),
|
|
587
|
+
id: z7.uuid(),
|
|
588
|
+
status: CommandStatus.extract(["done", "failed", "cancelled"]),
|
|
589
|
+
result: z7.unknown().nullable(),
|
|
590
|
+
error: z7.string().nullable()
|
|
591
|
+
}),
|
|
592
|
+
z7.object({ t: z7.literal("cmd.err"), id: z7.uuid(), error: z7.string() }),
|
|
593
|
+
z7.object({ t: z7.literal("evt"), topic: LocalSessionTopic, e: SessionEvent }),
|
|
594
|
+
z7.object({ t: z7.literal("term"), terminalId: z7.uuid(), m: TerminalRunnerMessage }),
|
|
595
|
+
z7.object({ t: z7.literal("sub.err"), topic: LocalTopic, error: z7.string() })
|
|
596
|
+
]);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// ../../packages/shared/src/protocol/runner.ts
|
|
601
|
+
import { z as z8 } from "zod";
|
|
492
602
|
var ProviderModel, ProviderCapability, UNAVAILABLE, RunnerCapabilities, CODEX_TOOL_NAMES;
|
|
493
603
|
var init_runner = __esm({
|
|
494
604
|
"../../packages/shared/src/protocol/runner.ts"() {
|
|
495
605
|
"use strict";
|
|
496
606
|
init_command();
|
|
497
|
-
ProviderModel =
|
|
498
|
-
id:
|
|
499
|
-
label:
|
|
500
|
-
default:
|
|
607
|
+
ProviderModel = z8.object({
|
|
608
|
+
id: z8.string().min(1).max(200),
|
|
609
|
+
label: z8.string().min(1).max(200),
|
|
610
|
+
default: z8.boolean().optional(),
|
|
501
611
|
/** From the app-server's `supportedReasoningEfforts`; empty means the control is hidden. */
|
|
502
|
-
efforts:
|
|
612
|
+
efforts: z8.array(Effort).default([]),
|
|
503
613
|
/** The effort a new chat on this model opens at, sent back on `session.start`. */
|
|
504
614
|
defaultEffort: Effort.optional()
|
|
505
615
|
});
|
|
506
|
-
ProviderCapability =
|
|
507
|
-
available:
|
|
508
|
-
reason:
|
|
509
|
-
version:
|
|
510
|
-
models:
|
|
616
|
+
ProviderCapability = z8.object({
|
|
617
|
+
available: z8.boolean(),
|
|
618
|
+
reason: z8.enum(["not_installed", "not_logged_in", "error"]).optional(),
|
|
619
|
+
version: z8.string().max(50).optional(),
|
|
620
|
+
models: z8.array(ProviderModel).max(50).default([])
|
|
511
621
|
});
|
|
512
622
|
UNAVAILABLE = { available: false, reason: "not_installed", models: [] };
|
|
513
|
-
RunnerCapabilities =
|
|
514
|
-
kinds:
|
|
515
|
-
local_port:
|
|
516
|
-
claude:
|
|
517
|
-
providers:
|
|
623
|
+
RunnerCapabilities = z8.object({
|
|
624
|
+
kinds: z8.array(z8.string()),
|
|
625
|
+
local_port: z8.number().int(),
|
|
626
|
+
claude: z8.boolean(),
|
|
627
|
+
providers: z8.object({
|
|
518
628
|
claude: ProviderCapability.default({ available: true, models: [] }),
|
|
519
629
|
codex: ProviderCapability.default(UNAVAILABLE)
|
|
520
630
|
}).default({ claude: { available: true, models: [] }, codex: UNAVAILABLE }),
|
|
@@ -523,7 +633,13 @@ var init_runner = __esm({
|
|
|
523
633
|
* config `allowMemberTerminals` names (protocol §9). Absent reads as none; a workspace admin
|
|
524
634
|
* cannot grant a terminal. Optional so an older row still parses.
|
|
525
635
|
*/
|
|
526
|
-
memberTerminalIds:
|
|
636
|
+
memberTerminalIds: z8.array(z8.uuid()).max(64).optional(),
|
|
637
|
+
/**
|
|
638
|
+
* The raw Ed25519 public key of the running process (32 bytes, unpadded base64url), which signs
|
|
639
|
+
* the local server's `hello` (protocol §6.2). New on every start, republished by every capabilities refresh that changes the row;
|
|
640
|
+
* a client that finds it absent never opens the fast path. Optional so an older row still parses.
|
|
641
|
+
*/
|
|
642
|
+
local_pubkey: z8.string().regex(/^[A-Za-z0-9_-]{43}$/).optional()
|
|
527
643
|
});
|
|
528
644
|
CODEX_TOOL_NAMES = {
|
|
529
645
|
command: "codex.command_execution",
|
|
@@ -550,6 +666,7 @@ var init_protocol = __esm({
|
|
|
550
666
|
init_events();
|
|
551
667
|
init_git();
|
|
552
668
|
init_github();
|
|
669
|
+
init_local();
|
|
553
670
|
init_runner();
|
|
554
671
|
init_terminal();
|
|
555
672
|
init_transport();
|
|
@@ -652,31 +769,6 @@ var init_claude_dir = __esm({
|
|
|
652
769
|
}
|
|
653
770
|
});
|
|
654
771
|
|
|
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
772
|
// src/profile.ts
|
|
681
773
|
import { join as join2 } from "path";
|
|
682
774
|
function assertProfileName(name) {
|
|
@@ -725,7 +817,7 @@ import {
|
|
|
725
817
|
} from "fs";
|
|
726
818
|
import { homedir as homedir2 } from "os";
|
|
727
819
|
import { dirname, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
728
|
-
import { z as
|
|
820
|
+
import { z as z9 } from "zod";
|
|
729
821
|
function setProfileOverride(name) {
|
|
730
822
|
profileOverride = name;
|
|
731
823
|
}
|
|
@@ -800,22 +892,24 @@ var init_config = __esm({
|
|
|
800
892
|
init_cloud();
|
|
801
893
|
init_profile();
|
|
802
894
|
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
|
-
|
|
895
|
+
RunnerConfig = z9.object({
|
|
896
|
+
supabaseUrl: z9.url(),
|
|
897
|
+
supabaseAnonKey: z9.string().min(1),
|
|
898
|
+
workspaceId: z9.uuid().optional(),
|
|
899
|
+
runnerId: z9.uuid().optional(),
|
|
900
|
+
name: z9.string().optional(),
|
|
901
|
+
roots: z9.array(z9.string()).default([]),
|
|
902
|
+
port: z9.number().int().default(LOCAL_PORT),
|
|
903
|
+
/** Extra page origins the local server admits, beside the built-in list (`buildOrigins`). */
|
|
904
|
+
localOrigins: z9.array(z9.string()).default([]),
|
|
905
|
+
allowBypassPermissions: z9.boolean().default(false),
|
|
812
906
|
/** Absolute path to the `codex` binary, when it is not on the daemon's PATH. */
|
|
813
|
-
codexPath:
|
|
907
|
+
codexPath: z9.string().optional(),
|
|
814
908
|
/**
|
|
815
909
|
* The Claude Code config directory this profile's sessions run on (D33), handed to them as
|
|
816
910
|
* `CLAUDE_CONFIG_DIR`. Unset means the CLI's own default, `~/.claude`.
|
|
817
911
|
*/
|
|
818
|
-
claudeConfigDir:
|
|
912
|
+
claudeConfigDir: z9.string().optional(),
|
|
819
913
|
/**
|
|
820
914
|
* Who besides the runner owner may hold a terminal here (D34, plan ruling 14): `false`, or the
|
|
821
915
|
* user ids this machine's owner names. Machine-local like `allowBypassPermissions`: a workspace
|
|
@@ -823,15 +917,15 @@ var init_config = __esm({
|
|
|
823
917
|
* `true` is refused — there is no "every member" switch. A listed member's shell runs as this
|
|
824
918
|
* machine's OS user; the env allowlist is not a boundary.
|
|
825
919
|
*/
|
|
826
|
-
allowMemberTerminals:
|
|
920
|
+
allowMemberTerminals: z9.union(
|
|
827
921
|
[
|
|
828
|
-
|
|
829
|
-
|
|
922
|
+
z9.literal(false),
|
|
923
|
+
z9.array(z9.uuid({ error: MEMBER_TERMINALS_ERROR }), { error: MEMBER_TERMINALS_ERROR }).max(64, { error: MEMBER_TERMINALS_ERROR })
|
|
830
924
|
],
|
|
831
925
|
{ error: MEMBER_TERMINALS_ERROR }
|
|
832
926
|
).default(false),
|
|
833
927
|
/** Minutes a terminal nobody attends stays alive before the runner kills it (features §2). */
|
|
834
|
-
terminalIdleMinutes:
|
|
928
|
+
terminalIdleMinutes: z9.number().int().min(1).max(1440).default(10)
|
|
835
929
|
});
|
|
836
930
|
memberTerminalIdsOf = (cfg) => cfg.allowMemberTerminals === false ? [] : [...cfg.allowMemberTerminals];
|
|
837
931
|
configPath = (dir) => join3(dir, "config.json");
|
|
@@ -911,24 +1005,12 @@ function grantedColumns(deps) {
|
|
|
911
1005
|
protocol_version: PROTOCOL_VERSION,
|
|
912
1006
|
status: "online",
|
|
913
1007
|
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
1008
|
// Parsed, not merely typed: this row is the writer and every client's `RunnerCapabilities`
|
|
920
1009
|
// parse is the reader. A value the reader rejects fails that parse *whole*, so one bad model
|
|
921
1010
|
// 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
|
-
}),
|
|
1011
|
+
// loud, on one machine, at the moment the value is produced. The parse also returns a copy, so
|
|
1012
|
+
// the composer's arrays are never handed to a serialiser that could mutate them.
|
|
1013
|
+
capabilities: RunnerCapabilities.parse(deps.capabilities),
|
|
932
1014
|
// The sandbox boundary from the config, resolved and never widened here.
|
|
933
1015
|
allowed_roots: publishedRoots(cfg.roots)
|
|
934
1016
|
};
|
|
@@ -1088,7 +1170,7 @@ import {
|
|
|
1088
1170
|
} from "fs";
|
|
1089
1171
|
import { uptime } from "os";
|
|
1090
1172
|
import { join as join5 } from "path";
|
|
1091
|
-
import { z as
|
|
1173
|
+
import { z as z10 } from "zod";
|
|
1092
1174
|
function bootedAt(now = Date.now()) {
|
|
1093
1175
|
return new Date(Math.round((now - uptime() * 1e3) / 1e3) * 1e3).toISOString();
|
|
1094
1176
|
}
|
|
@@ -1231,13 +1313,13 @@ var LockRecord, BOOT_DRIFT_MS, ATTEMPTS, BACKOFF_MS, CLAIM_TTL_MS, errno, claimO
|
|
|
1231
1313
|
var init_lock = __esm({
|
|
1232
1314
|
"src/lock.ts"() {
|
|
1233
1315
|
"use strict";
|
|
1234
|
-
LockRecord =
|
|
1235
|
-
pid:
|
|
1236
|
-
profile:
|
|
1237
|
-
startedAt:
|
|
1316
|
+
LockRecord = z10.object({
|
|
1317
|
+
pid: z10.number().int().positive(),
|
|
1318
|
+
profile: z10.string(),
|
|
1319
|
+
startedAt: z10.string(),
|
|
1238
1320
|
/** `bootedAt()` when the lock was taken; a lock from another boot is stale whatever its pid. */
|
|
1239
|
-
bootedAt:
|
|
1240
|
-
version:
|
|
1321
|
+
bootedAt: z10.string(),
|
|
1322
|
+
version: z10.string()
|
|
1241
1323
|
});
|
|
1242
1324
|
BOOT_DRIFT_MS = 1e4;
|
|
1243
1325
|
ATTEMPTS = 8;
|
|
@@ -1339,6 +1421,44 @@ var init_version = __esm({
|
|
|
1339
1421
|
}
|
|
1340
1422
|
});
|
|
1341
1423
|
|
|
1424
|
+
// src/exec-path.ts
|
|
1425
|
+
import { accessSync, constants, statSync as statSync3 } from "fs";
|
|
1426
|
+
import { delimiter, isAbsolute as isAbsolute4, join as join8 } from "path";
|
|
1427
|
+
function findOnPath(name, path) {
|
|
1428
|
+
for (const dir of (path ?? "").split(delimiter)) {
|
|
1429
|
+
if (!isAbsolute4(dir)) continue;
|
|
1430
|
+
const candidate = join8(dir, name);
|
|
1431
|
+
try {
|
|
1432
|
+
if (!statSync3(candidate).isFile()) continue;
|
|
1433
|
+
accessSync(candidate, constants.X_OK);
|
|
1434
|
+
return candidate;
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
function makeLocator(name, locate = findOnPath) {
|
|
1441
|
+
const located = /* @__PURE__ */ new Map();
|
|
1442
|
+
return {
|
|
1443
|
+
find(path) {
|
|
1444
|
+
const key = path ?? "";
|
|
1445
|
+
const cached = located.get(key);
|
|
1446
|
+
if (cached !== void 0) return cached;
|
|
1447
|
+
const found = locate(name, path);
|
|
1448
|
+
if (found !== null) located.set(key, found);
|
|
1449
|
+
return found;
|
|
1450
|
+
},
|
|
1451
|
+
forget(path) {
|
|
1452
|
+
located.delete(path ?? "");
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
var init_exec_path = __esm({
|
|
1457
|
+
"src/exec-path.ts"() {
|
|
1458
|
+
"use strict";
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1342
1462
|
// src/codex/rpc.ts
|
|
1343
1463
|
var RpcError, JsonRpc;
|
|
1344
1464
|
var init_rpc = __esm({
|
|
@@ -1446,20 +1566,9 @@ var init_rpc = __esm({
|
|
|
1446
1566
|
|
|
1447
1567
|
// src/codex/detect.ts
|
|
1448
1568
|
import { execFile as execFile2, spawn } from "child_process";
|
|
1449
|
-
import { accessSync, constants } from "fs";
|
|
1450
|
-
import { delimiter, join as join8 } from "path";
|
|
1451
1569
|
import { promisify } from "util";
|
|
1452
1570
|
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;
|
|
1571
|
+
return findOnPath("codex", env.PATH);
|
|
1463
1572
|
}
|
|
1464
1573
|
function resolveCodexPath(cfg, env, onPath) {
|
|
1465
1574
|
return env.CAYENNE_CODEX_PATH ?? cfg.codexPath ?? onPath();
|
|
@@ -1536,6 +1645,7 @@ var init_detect = __esm({
|
|
|
1536
1645
|
"src/codex/detect.ts"() {
|
|
1537
1646
|
"use strict";
|
|
1538
1647
|
init_src();
|
|
1648
|
+
init_exec_path();
|
|
1539
1649
|
init_rpc();
|
|
1540
1650
|
exec = promisify(execFile2);
|
|
1541
1651
|
PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -1564,11 +1674,12 @@ var init_dispatcher = __esm({
|
|
|
1564
1674
|
|
|
1565
1675
|
// src/commands/source.ts
|
|
1566
1676
|
import { LRUCache } from "lru-cache";
|
|
1567
|
-
var byCreatedAt, GIT_READ_KINDS, GIT_WORKTREE_KINDS, laneOf, CommandSource;
|
|
1677
|
+
var STOPPING, byCreatedAt, GIT_READ_KINDS, GIT_WORKTREE_KINDS, laneOf, CommandSource;
|
|
1568
1678
|
var init_source = __esm({
|
|
1569
1679
|
"src/commands/source.ts"() {
|
|
1570
1680
|
"use strict";
|
|
1571
1681
|
init_src();
|
|
1682
|
+
STOPPING = "runner stopping";
|
|
1572
1683
|
byCreatedAt = (a, b) => Date.parse(a.created_at) - Date.parse(b.created_at) || a.id.localeCompare(b.id);
|
|
1573
1684
|
GIT_READ_KINDS = /* @__PURE__ */ new Set(["git.status", "git.diff", "git.branches"]);
|
|
1574
1685
|
GIT_WORKTREE_KINDS = /* @__PURE__ */ new Set([
|
|
@@ -1598,6 +1709,10 @@ var init_source = __esm({
|
|
|
1598
1709
|
dispatch;
|
|
1599
1710
|
log;
|
|
1600
1711
|
seen = new LRUCache({ max: 1e3 });
|
|
1712
|
+
/** Local-socket hooks per row id; dropped once fired, on `stop()`, or by the function `offer` returns. */
|
|
1713
|
+
hooks = /* @__PURE__ */ new Map();
|
|
1714
|
+
/** Rows between `enqueue` and their settlement: an `offer` for one of these registers hooks only. */
|
|
1715
|
+
inflight = /* @__PURE__ */ new Set();
|
|
1601
1716
|
queues = /* @__PURE__ */ new Map();
|
|
1602
1717
|
channel = null;
|
|
1603
1718
|
staleMs;
|
|
@@ -1664,6 +1779,38 @@ var init_source = __esm({
|
|
|
1664
1779
|
this.pending = [];
|
|
1665
1780
|
await this.draining;
|
|
1666
1781
|
while (this.queues.size > 0) await Promise.allSettled([...this.queues.values()]);
|
|
1782
|
+
for (const id of [...this.hooks.keys()]) this.dropHooks(id, STOPPING);
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* A pointer that arrived over the local socket (§6.2; SEC-P12, SP16). The same admission a
|
|
1786
|
+
* broadcast gets — dedupe by id, conditional ack, the same lane — so a row the relay already
|
|
1787
|
+
* delivered runs once, and a `terminal.open` pointer serialises behind the relay's copy in the
|
|
1788
|
+
* `terminal:<id>` lane. Hooks are registered either way so the socket hears the outcome. A row
|
|
1789
|
+
* this source already settled (or never claimed) is answered from the row itself, never from a
|
|
1790
|
+
* guess (PR15). Returns the function that drops these hooks; the connection calls it on close.
|
|
1791
|
+
*/
|
|
1792
|
+
offer(row, hooks) {
|
|
1793
|
+
if (this.stopped) {
|
|
1794
|
+
hooks.onDropped?.(STOPPING);
|
|
1795
|
+
return () => {
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
const list = this.hooks.get(row.id) ?? [];
|
|
1799
|
+
list.push(hooks);
|
|
1800
|
+
this.hooks.set(row.id, list);
|
|
1801
|
+
if (!this.seen.has(row.id)) {
|
|
1802
|
+
if (this.draining) this.pending.push(row);
|
|
1803
|
+
else this.enqueue(row);
|
|
1804
|
+
} else if (!this.inflight.has(row.id)) {
|
|
1805
|
+
void this.settleFromRow(row.id);
|
|
1806
|
+
}
|
|
1807
|
+
return () => {
|
|
1808
|
+
const current = this.hooks.get(row.id);
|
|
1809
|
+
if (!current) return;
|
|
1810
|
+
const at = current.indexOf(hooks);
|
|
1811
|
+
if (at >= 0) current.splice(at, 1);
|
|
1812
|
+
if (current.length === 0) this.hooks.delete(row.id);
|
|
1813
|
+
};
|
|
1667
1814
|
}
|
|
1668
1815
|
/**
|
|
1669
1816
|
* Claim the row now; queue only its execution.
|
|
@@ -1681,6 +1828,7 @@ var init_source = __esm({
|
|
|
1681
1828
|
if (this.stopped) return;
|
|
1682
1829
|
if (this.seen.has(row.id)) return;
|
|
1683
1830
|
this.seen.set(row.id, true);
|
|
1831
|
+
this.inflight.add(row.id);
|
|
1684
1832
|
const ageMs = this.now() - Date.parse(row.created_at);
|
|
1685
1833
|
const admitted = this.admit(row, ageMs);
|
|
1686
1834
|
const key = laneOf(row);
|
|
@@ -1707,6 +1855,7 @@ var init_source = __esm({
|
|
|
1707
1855
|
if (row.flow_run_step_id === null && ageMs > this.staleMs) {
|
|
1708
1856
|
const { error: error2 } = await this.finish(row.id, "cancelled", null, "STALE");
|
|
1709
1857
|
if (error2) this.release(row.id, error2.message, "stale cancel failed");
|
|
1858
|
+
else this.settle(row.id, { status: "cancelled", result: null, error: "STALE" });
|
|
1710
1859
|
return "skip";
|
|
1711
1860
|
}
|
|
1712
1861
|
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 +1865,10 @@ var init_source = __esm({
|
|
|
1716
1865
|
}
|
|
1717
1866
|
if (!data || data.length === 0) {
|
|
1718
1867
|
this.log.info({ id: row.id }, "command no longer queued; not dispatching");
|
|
1868
|
+
await this.settleFromRow(row.id);
|
|
1719
1869
|
return "skip";
|
|
1720
1870
|
}
|
|
1871
|
+
for (const h of this.hooks.get(row.id) ?? []) h.onAck?.();
|
|
1721
1872
|
return "dispatch";
|
|
1722
1873
|
} catch (err) {
|
|
1723
1874
|
this.release(row.id, err instanceof Error ? err.message : String(err), "ack failed");
|
|
@@ -1734,17 +1885,29 @@ var init_source = __esm({
|
|
|
1734
1885
|
*/
|
|
1735
1886
|
release(id, err, msg) {
|
|
1736
1887
|
this.seen.delete(id);
|
|
1888
|
+
this.inflight.delete(id);
|
|
1737
1889
|
this.log.warn({ id, err }, msg);
|
|
1738
1890
|
}
|
|
1739
1891
|
async run(row, admitted) {
|
|
1740
1892
|
if (await admitted !== "dispatch") return;
|
|
1893
|
+
let answered = false;
|
|
1894
|
+
try {
|
|
1895
|
+
await this.runAndFinish(row);
|
|
1896
|
+
answered = true;
|
|
1897
|
+
} finally {
|
|
1898
|
+
if (!answered) await this.settleFromRow(row.id);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
async runAndFinish(row) {
|
|
1741
1902
|
let status = "done";
|
|
1742
1903
|
let finished;
|
|
1904
|
+
let result = null;
|
|
1905
|
+
let message = null;
|
|
1743
1906
|
try {
|
|
1744
|
-
|
|
1907
|
+
result = await this.dispatch(row);
|
|
1745
1908
|
finished = await this.finish(row.id, "done", result ?? null, null);
|
|
1746
1909
|
} catch (err) {
|
|
1747
|
-
|
|
1910
|
+
message = err instanceof Error ? err.message : String(err);
|
|
1748
1911
|
this.log.warn({ id: row.id, kind: row.kind, err: message }, "command failed");
|
|
1749
1912
|
status = "failed";
|
|
1750
1913
|
finished = await this.finish(row.id, status, null, message);
|
|
@@ -1755,6 +1918,52 @@ var init_source = __esm({
|
|
|
1755
1918
|
"command finish write refused"
|
|
1756
1919
|
);
|
|
1757
1920
|
}
|
|
1921
|
+
if (finished.error) await this.settleFromRow(row.id);
|
|
1922
|
+
else {
|
|
1923
|
+
this.settle(row.id, {
|
|
1924
|
+
status,
|
|
1925
|
+
result: status === "done" ? result ?? null : null,
|
|
1926
|
+
error: status === "done" ? null : message
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
settle(id, r) {
|
|
1931
|
+
this.inflight.delete(id);
|
|
1932
|
+
const list = this.hooks.get(id);
|
|
1933
|
+
if (!list) return;
|
|
1934
|
+
this.hooks.delete(id);
|
|
1935
|
+
for (const h of list) h.onFinished?.(r);
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* The outcome of a row this source did not finish itself — cancelled by its creator between the
|
|
1939
|
+
* socket's read and our ack, claimed by another process, or settled before the pointer arrived.
|
|
1940
|
+
* Read once; answered only when the row is terminal, otherwise the hooks are dropped and the
|
|
1941
|
+
* client's relay wait sees the row (PR15: never invent a status).
|
|
1942
|
+
*/
|
|
1943
|
+
async settleFromRow(id) {
|
|
1944
|
+
this.inflight.delete(id);
|
|
1945
|
+
if (!this.hooks.has(id)) return;
|
|
1946
|
+
let reason = "row is not terminal";
|
|
1947
|
+
try {
|
|
1948
|
+
const { data } = await this.db.from("runner_commands").select("status, result, error").eq("id", id).maybeSingle();
|
|
1949
|
+
const status = data?.status;
|
|
1950
|
+
if (status === "done" || status === "failed" || status === "cancelled") {
|
|
1951
|
+
this.settle(id, { status, result: data?.result ?? null, error: data?.error ?? null });
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
if (!data) reason = "row not found";
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
this.log.warn({ id, err }, "could not re-read a command for its local pointer");
|
|
1957
|
+
reason = "row re-read failed";
|
|
1958
|
+
}
|
|
1959
|
+
this.dropHooks(id, reason);
|
|
1960
|
+
}
|
|
1961
|
+
/** Hooks this source will never fire `onFinished` for: told so, so a socket frees its pending slot. */
|
|
1962
|
+
dropHooks(id, reason) {
|
|
1963
|
+
const list = this.hooks.get(id);
|
|
1964
|
+
if (!list) return;
|
|
1965
|
+
this.hooks.delete(id);
|
|
1966
|
+
for (const h of list) h.onDropped?.(reason);
|
|
1758
1967
|
}
|
|
1759
1968
|
async finish(id, status, result, error) {
|
|
1760
1969
|
const res = await this.db.from("runner_commands").update({ status, result, error, finished_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
|
|
@@ -1766,7 +1975,7 @@ var init_source = __esm({
|
|
|
1766
1975
|
|
|
1767
1976
|
// src/paths.ts
|
|
1768
1977
|
import { lstatSync, realpathSync as realpathSync3 } from "fs";
|
|
1769
|
-
import { dirname as dirname3, isAbsolute as
|
|
1978
|
+
import { dirname as dirname3, isAbsolute as isAbsolute5, join as join9, relative, sep } from "path";
|
|
1770
1979
|
function assertInsideRoots(target, roots) {
|
|
1771
1980
|
return resolveInsideRoots(target, roots).real;
|
|
1772
1981
|
}
|
|
@@ -1784,7 +1993,7 @@ function resolveInsideRoots(target, roots) {
|
|
|
1784
1993
|
}
|
|
1785
1994
|
function assertInsideRootsOrAncestor(target, roots) {
|
|
1786
1995
|
assertRoots(roots);
|
|
1787
|
-
const absolute =
|
|
1996
|
+
const absolute = isAbsolute5(target) ? target : `${process.cwd()}${sep}${target}`;
|
|
1788
1997
|
let walked = walk(target, absolute);
|
|
1789
1998
|
if (walked.tail.includes("..")) walked = walk(target, join9(walked.real, ...walked.tail));
|
|
1790
1999
|
const checked = walked.tail.length === 0 ? walked.real : join9(walked.real, ...walked.tail);
|
|
@@ -1833,7 +2042,7 @@ function matchRoot(candidate, roots) {
|
|
|
1833
2042
|
continue;
|
|
1834
2043
|
}
|
|
1835
2044
|
const rel = relative(rootReal, candidate);
|
|
1836
|
-
const inside2 = rel === "" || !rel.startsWith("..") && !
|
|
2045
|
+
const inside2 = rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
|
|
1837
2046
|
if (inside2 && (match === null || rootReal.length < match.length)) match = rootReal;
|
|
1838
2047
|
}
|
|
1839
2048
|
return match;
|
|
@@ -1847,7 +2056,7 @@ var init_paths = __esm({
|
|
|
1847
2056
|
// src/git/introspect.ts
|
|
1848
2057
|
import { execFile as execFile3 } from "child_process";
|
|
1849
2058
|
import { readFileSync as readFileSync4, realpathSync as realpathSync4 } from "fs";
|
|
1850
|
-
import { delimiter as delimiter2, dirname as dirname4, isAbsolute as
|
|
2059
|
+
import { delimiter as delimiter2, dirname as dirname4, isAbsolute as isAbsolute6, join as join10, resolve } from "path";
|
|
1851
2060
|
import { promisify as promisify2 } from "util";
|
|
1852
2061
|
function parseGitHubRemote(url) {
|
|
1853
2062
|
const trimmed = url.trim();
|
|
@@ -1888,17 +2097,24 @@ function gitEnv(ceiling, env = process.env) {
|
|
|
1888
2097
|
};
|
|
1889
2098
|
}
|
|
1890
2099
|
function makeGitRun(opts) {
|
|
2100
|
+
const git = makeLocator("git", opts.locate);
|
|
1891
2101
|
return async (args, cwd, ceiling) => {
|
|
2102
|
+
const env = gitEnv(ceiling);
|
|
2103
|
+
const file = git.find(env.PATH);
|
|
2104
|
+
if (file === null) {
|
|
2105
|
+
return { code: 127, stdout: "", stderr: "git not found on an absolute PATH entry" };
|
|
2106
|
+
}
|
|
1892
2107
|
try {
|
|
1893
|
-
const { stdout, stderr } = await exec2(
|
|
2108
|
+
const { stdout, stderr } = await exec2(file, [...HARDENING, ...args], {
|
|
1894
2109
|
cwd,
|
|
1895
|
-
env
|
|
2110
|
+
env,
|
|
1896
2111
|
timeout: opts.timeoutMs,
|
|
1897
2112
|
maxBuffer: GIT_MAX_BUFFER
|
|
1898
2113
|
});
|
|
1899
2114
|
return { code: 0, stdout, stderr };
|
|
1900
2115
|
} catch (err) {
|
|
1901
2116
|
const e = err;
|
|
2117
|
+
if (e.code === "ENOENT") git.forget(env.PATH);
|
|
1902
2118
|
return {
|
|
1903
2119
|
code: typeof e.code === "number" ? e.code : 1,
|
|
1904
2120
|
stdout: e.stdout ?? "",
|
|
@@ -1932,7 +2148,7 @@ function isLinkedWorktree(gitDir, commonDir, repoRoot) {
|
|
|
1932
2148
|
return false;
|
|
1933
2149
|
}
|
|
1934
2150
|
}
|
|
1935
|
-
async function introspectRepo(cwd, run, root) {
|
|
2151
|
+
async function introspectRepo(cwd, run, root, { names = true } = {}) {
|
|
1936
2152
|
try {
|
|
1937
2153
|
const ceiling = dirname4(root);
|
|
1938
2154
|
if (ceiling.includes(delimiter2)) return { ...NONE };
|
|
@@ -1950,16 +2166,16 @@ async function introspectRepo(cwd, run, root) {
|
|
|
1950
2166
|
} catch {
|
|
1951
2167
|
return { ...NONE };
|
|
1952
2168
|
}
|
|
1953
|
-
const head = await line(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2169
|
+
const head = names ? await line(["rev-parse", "--abbrev-ref", "HEAD"]) : null;
|
|
1954
2170
|
const branch = head === "HEAD" ? null : head;
|
|
1955
2171
|
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) =>
|
|
2172
|
+
const [gitDir, commonDir] = dirs?.length === 2 && dirs.every((d) => isAbsolute6(d)) ? dirs.map((d) => canonical(d, cwd)) : [null, null];
|
|
1957
2173
|
if (commonDir == null || gitDir == null) return { ...NONE };
|
|
1958
2174
|
if (!insideRoot(commonDir, root) && !isLinkedWorktree(gitDir, commonDir, repoRoot)) {
|
|
1959
2175
|
return { ...NONE };
|
|
1960
2176
|
}
|
|
1961
2177
|
const isWorktree = gitDir !== commonDir;
|
|
1962
|
-
const remote = await line(["remote", "get-url", "origin"]);
|
|
2178
|
+
const remote = names ? await line(["remote", "get-url", "origin"]) : null;
|
|
1963
2179
|
const gh = remote === null ? null : parseGitHubRemote(remote);
|
|
1964
2180
|
return {
|
|
1965
2181
|
repo_root: repoRoot,
|
|
@@ -1977,6 +2193,7 @@ var init_introspect = __esm({
|
|
|
1977
2193
|
"src/git/introspect.ts"() {
|
|
1978
2194
|
"use strict";
|
|
1979
2195
|
init_src();
|
|
2196
|
+
init_exec_path();
|
|
1980
2197
|
init_paths();
|
|
1981
2198
|
exec2 = promisify2(execFile3);
|
|
1982
2199
|
GIT_TIMEOUT_MS = 5e3;
|
|
@@ -2016,10 +2233,10 @@ var init_introspect = __esm({
|
|
|
2016
2233
|
});
|
|
2017
2234
|
|
|
2018
2235
|
// src/git/commands.ts
|
|
2019
|
-
import { lstatSync as lstatSync2, realpathSync as realpathSync5, statSync as
|
|
2020
|
-
import { dirname as dirname5, isAbsolute as
|
|
2236
|
+
import { lstatSync as lstatSync2, realpathSync as realpathSync5, statSync as statSync4 } from "fs";
|
|
2237
|
+
import { dirname as dirname5, isAbsolute as isAbsolute7, join as join11, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
2021
2238
|
async function scopeRepo(cwd, run, root) {
|
|
2022
|
-
const identity = await introspectRepo(cwd, run, root);
|
|
2239
|
+
const identity = await introspectRepo(cwd, run, root, { names: false });
|
|
2023
2240
|
if (identity.repo_root === null) {
|
|
2024
2241
|
throw new Error(`VALIDATION: ${cwd} is not inside a git repository under an allowed root`);
|
|
2025
2242
|
}
|
|
@@ -2175,7 +2392,7 @@ async function gitDirs(scope, run) {
|
|
|
2175
2392
|
);
|
|
2176
2393
|
if (r.code !== 0) return null;
|
|
2177
2394
|
const lines = r.stdout.split("\n").filter((l) => l !== "");
|
|
2178
|
-
if (lines.length === 0 || !lines.every((l) =>
|
|
2395
|
+
if (lines.length === 0 || !lines.every((l) => isAbsolute7(l))) return null;
|
|
2179
2396
|
try {
|
|
2180
2397
|
return lines.map((l) => realpathSync5.native(l));
|
|
2181
2398
|
} catch {
|
|
@@ -2189,7 +2406,7 @@ function isGitDirShape(dir) {
|
|
|
2189
2406
|
if (commondir?.isFile()) return true;
|
|
2190
2407
|
const isDir = (name) => {
|
|
2191
2408
|
try {
|
|
2192
|
-
return
|
|
2409
|
+
return statSync4(join11(dir, name), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
2193
2410
|
} catch {
|
|
2194
2411
|
return false;
|
|
2195
2412
|
}
|
|
@@ -2206,13 +2423,13 @@ function enclosesHome(repoRoot, home) {
|
|
|
2206
2423
|
if (within(real, repoRoot)) return true;
|
|
2207
2424
|
let root;
|
|
2208
2425
|
try {
|
|
2209
|
-
root =
|
|
2426
|
+
root = statSync4(repoRoot, { bigint: true });
|
|
2210
2427
|
} catch {
|
|
2211
2428
|
return false;
|
|
2212
2429
|
}
|
|
2213
2430
|
for (let cur = real; ; cur = dirname5(cur)) {
|
|
2214
2431
|
try {
|
|
2215
|
-
const st =
|
|
2432
|
+
const st = statSync4(cur, { bigint: true, throwIfNoEntry: false });
|
|
2216
2433
|
if (st && st.dev === root.dev && st.ino === root.ino) return true;
|
|
2217
2434
|
} catch {
|
|
2218
2435
|
}
|
|
@@ -2224,7 +2441,7 @@ function protectedTest(paths) {
|
|
|
2224
2441
|
for (const path of paths) {
|
|
2225
2442
|
try {
|
|
2226
2443
|
const real = realpathSync5.native(path);
|
|
2227
|
-
const st =
|
|
2444
|
+
const st = statSync4(real, { bigint: true });
|
|
2228
2445
|
entries.push({ real, dev: st.dev, ino: st.ino });
|
|
2229
2446
|
} catch {
|
|
2230
2447
|
}
|
|
@@ -2236,7 +2453,7 @@ function protectedTest(paths) {
|
|
|
2236
2453
|
if (cached !== void 0) return cached;
|
|
2237
2454
|
let hit = false;
|
|
2238
2455
|
try {
|
|
2239
|
-
const st =
|
|
2456
|
+
const st = statSync4(path, { bigint: true, throwIfNoEntry: false });
|
|
2240
2457
|
hit = st !== void 0 && entries.some((e) => e.dev === st.dev && e.ino === st.ino);
|
|
2241
2458
|
} catch {
|
|
2242
2459
|
}
|
|
@@ -2406,11 +2623,11 @@ import {
|
|
|
2406
2623
|
openSync,
|
|
2407
2624
|
readFileSync as readFileSync5,
|
|
2408
2625
|
realpathSync as realpathSync6,
|
|
2409
|
-
statSync as
|
|
2626
|
+
statSync as statSync5,
|
|
2410
2627
|
writeSync
|
|
2411
2628
|
} from "fs";
|
|
2412
2629
|
import { readdir } from "fs/promises";
|
|
2413
|
-
import { dirname as dirname6, isAbsolute as
|
|
2630
|
+
import { dirname as dirname6, isAbsolute as isAbsolute8, join as join12, relative as relative3, sep as sep3 } from "path";
|
|
2414
2631
|
import { setImmediate } from "timers/promises";
|
|
2415
2632
|
function worktreePathFor(repoRoot, name) {
|
|
2416
2633
|
return join12(repoRoot, WORKTREES_DIR, name);
|
|
@@ -2435,7 +2652,7 @@ async function excludeCayenneDir(scope, run) {
|
|
|
2435
2652
|
scope.ceiling
|
|
2436
2653
|
);
|
|
2437
2654
|
const common = r.stdout.trim();
|
|
2438
|
-
if (r.code !== 0 || !
|
|
2655
|
+
if (r.code !== 0 || !isAbsolute8(common)) return;
|
|
2439
2656
|
const dotGit = join12(scope.repoRoot, ".git");
|
|
2440
2657
|
let dotGitIsRealDir;
|
|
2441
2658
|
try {
|
|
@@ -2587,9 +2804,9 @@ async function worktreeIdentity(scope, run) {
|
|
|
2587
2804
|
scope.ceiling
|
|
2588
2805
|
);
|
|
2589
2806
|
const gitDir = r.stdout.trim();
|
|
2590
|
-
if (r.code !== 0 || !
|
|
2807
|
+
if (r.code !== 0 || !isAbsolute8(gitDir)) throw failed2("git rev-parse --git-dir", r.stderr);
|
|
2591
2808
|
try {
|
|
2592
|
-
const st =
|
|
2809
|
+
const st = statSync5(realpathSync6.native(gitDir), { bigint: true });
|
|
2593
2810
|
return {
|
|
2594
2811
|
gitDirDev: st.dev.toString(),
|
|
2595
2812
|
gitDirIno: st.ino.toString(),
|
|
@@ -2624,7 +2841,7 @@ async function removeWorktree(scope, run, worktreePath, force) {
|
|
|
2624
2841
|
throw new Error(`ROOT_NOT_ALLOWED: ${real}`);
|
|
2625
2842
|
}
|
|
2626
2843
|
const rel = relative3(base, real);
|
|
2627
|
-
if (rel === "" || rel.startsWith("..") ||
|
|
2844
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute8(rel) || rel.includes("/")) {
|
|
2628
2845
|
throw new Error(`VALIDATION: ${worktreePath} is not under ${base}`);
|
|
2629
2846
|
}
|
|
2630
2847
|
const inside2 = `${real}${sep3}`;
|
|
@@ -2748,7 +2965,7 @@ var init_worktree_owners = __esm({
|
|
|
2748
2965
|
import { rmSync as rmSync4 } from "fs";
|
|
2749
2966
|
import { join as join14 } from "path";
|
|
2750
2967
|
function createSessionWorktrees(dir) {
|
|
2751
|
-
const fileFor = (sessionId) =>
|
|
2968
|
+
const fileFor = (sessionId) => UUID2.test(sessionId) ? join14(dir, `${sessionId}.json`) : null;
|
|
2752
2969
|
return {
|
|
2753
2970
|
record(rec) {
|
|
2754
2971
|
const file = fileFor(rec.sessionId);
|
|
@@ -2784,12 +3001,620 @@ function createSessionWorktrees(dir) {
|
|
|
2784
3001
|
}
|
|
2785
3002
|
};
|
|
2786
3003
|
}
|
|
2787
|
-
var
|
|
3004
|
+
var UUID2;
|
|
2788
3005
|
var init_session_worktrees = __esm({
|
|
2789
3006
|
"src/git/session-worktrees.ts"() {
|
|
2790
3007
|
"use strict";
|
|
2791
3008
|
init_worktree_owners();
|
|
2792
|
-
|
|
3009
|
+
UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
3012
|
+
|
|
3013
|
+
// src/local/auth.ts
|
|
3014
|
+
import { createHash as createHash2 } from "crypto";
|
|
3015
|
+
import { LRUCache as LRUCache2 } from "lru-cache";
|
|
3016
|
+
function claimsOf(token) {
|
|
3017
|
+
const payload = token.split(".")[1];
|
|
3018
|
+
if (!payload) return null;
|
|
3019
|
+
try {
|
|
3020
|
+
const c = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
3021
|
+
return typeof c.sub === "string" && typeof c.exp === "number" ? { sub: c.sub, exp: c.exp } : null;
|
|
3022
|
+
} catch {
|
|
3023
|
+
return null;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
function retryable(error) {
|
|
3027
|
+
const { name, status } = error;
|
|
3028
|
+
if (name === "AuthRetryableFetchError") return true;
|
|
3029
|
+
if (typeof status !== "number" || status === 0) return true;
|
|
3030
|
+
return status >= 500 || status === 429;
|
|
3031
|
+
}
|
|
3032
|
+
function createTokenVerifier({
|
|
3033
|
+
db,
|
|
3034
|
+
ttlMs = LOCAL_TOKEN_CACHE_MS,
|
|
3035
|
+
negativeTtlMs = LOCAL_TOKEN_NEGATIVE_CACHE_MS,
|
|
3036
|
+
now = Date.now,
|
|
3037
|
+
admit = () => true,
|
|
3038
|
+
log
|
|
3039
|
+
}) {
|
|
3040
|
+
const cache = new LRUCache2({ max: 1024 });
|
|
3041
|
+
const key = (token) => createHash2("sha256").update(token).digest("hex");
|
|
3042
|
+
let windowStart = now();
|
|
3043
|
+
let calls = 0;
|
|
3044
|
+
let warned2 = false;
|
|
3045
|
+
const budgeted = () => {
|
|
3046
|
+
const t = now();
|
|
3047
|
+
if (t - windowStart >= 6e4) {
|
|
3048
|
+
windowStart = t;
|
|
3049
|
+
calls = 0;
|
|
3050
|
+
warned2 = false;
|
|
3051
|
+
}
|
|
3052
|
+
if (calls < LOCAL_GETUSER_PER_MINUTE) {
|
|
3053
|
+
calls += 1;
|
|
3054
|
+
return true;
|
|
3055
|
+
}
|
|
3056
|
+
if (!warned2) {
|
|
3057
|
+
warned2 = true;
|
|
3058
|
+
log?.warn(
|
|
3059
|
+
{ perMinute: LOCAL_GETUSER_PER_MINUTE },
|
|
3060
|
+
"local token verification over budget; refusing until the minute turns"
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
return false;
|
|
3064
|
+
};
|
|
3065
|
+
const refuse = (k) => {
|
|
3066
|
+
cache.set(k, { userId: null, exp: 0, until: now() + negativeTtlMs });
|
|
3067
|
+
return { status: "refused" };
|
|
3068
|
+
};
|
|
3069
|
+
return {
|
|
3070
|
+
cacheKeys: () => [...cache.keys()],
|
|
3071
|
+
async verify(token) {
|
|
3072
|
+
const k = key(token);
|
|
3073
|
+
const hit = cache.get(k);
|
|
3074
|
+
if (hit && hit.until > now()) {
|
|
3075
|
+
if (hit.userId === null) return { status: "refused" };
|
|
3076
|
+
if (hit.exp * 1e3 <= now()) return refuse(k);
|
|
3077
|
+
return { status: "valid", userId: hit.userId, exp: hit.exp };
|
|
3078
|
+
}
|
|
3079
|
+
cache.delete(k);
|
|
3080
|
+
const claims = claimsOf(token);
|
|
3081
|
+
if (!claims || claims.exp * 1e3 <= now() || !admit(claims.sub)) return refuse(k);
|
|
3082
|
+
if (!budgeted()) return { status: "unavailable" };
|
|
3083
|
+
const user = await db.auth.getUser(token).then(
|
|
3084
|
+
(r) => r.error ? retryable(r.error) ? void 0 : null : r.data.user,
|
|
3085
|
+
() => void 0
|
|
3086
|
+
);
|
|
3087
|
+
if (user === void 0) return { status: "unavailable" };
|
|
3088
|
+
if (!user || user.id !== claims.sub) return refuse(k);
|
|
3089
|
+
cache.set(k, { userId: user.id, exp: claims.exp, until: now() + ttlMs });
|
|
3090
|
+
return { status: "valid", userId: user.id, exp: claims.exp };
|
|
3091
|
+
}
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
var init_auth = __esm({
|
|
3095
|
+
"src/local/auth.ts"() {
|
|
3096
|
+
"use strict";
|
|
3097
|
+
init_src();
|
|
3098
|
+
}
|
|
3099
|
+
});
|
|
3100
|
+
|
|
3101
|
+
// src/local/identity.ts
|
|
3102
|
+
import { generateKeyPairSync, sign } from "crypto";
|
|
3103
|
+
function createLocalIdentity() {
|
|
3104
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
3105
|
+
const x = publicKey.export({ format: "jwk" }).x;
|
|
3106
|
+
if (typeof x !== "string" || x.length !== 43) throw new Error("ed25519 public key export failed");
|
|
3107
|
+
return {
|
|
3108
|
+
publicKey: x,
|
|
3109
|
+
sign: (message) => sign(null, message, privateKey).toString("base64url")
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
var init_identity = __esm({
|
|
3113
|
+
"src/local/identity.ts"() {
|
|
3114
|
+
"use strict";
|
|
3115
|
+
}
|
|
3116
|
+
});
|
|
3117
|
+
|
|
3118
|
+
// src/local/origins.ts
|
|
3119
|
+
function normalise(origin) {
|
|
3120
|
+
try {
|
|
3121
|
+
const u = new URL(origin);
|
|
3122
|
+
if (u.username || u.password) return null;
|
|
3123
|
+
return u.origin !== "null" ? u.origin.toLowerCase() : `${u.protocol}//${u.host}`.toLowerCase();
|
|
3124
|
+
} catch {
|
|
3125
|
+
return null;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
function buildOrigins(extra, cloudOrigin = CLOUD_APP_ORIGIN) {
|
|
3129
|
+
const builtIn = cloudOrigin ? [cloudOrigin, ...SHELLS] : [...DEV, ...SHELLS];
|
|
3130
|
+
const out = /* @__PURE__ */ new Set();
|
|
3131
|
+
for (const o of [...builtIn, ...extra]) {
|
|
3132
|
+
const n = normalise(o);
|
|
3133
|
+
if (n) out.add(n);
|
|
3134
|
+
}
|
|
3135
|
+
return [...out];
|
|
3136
|
+
}
|
|
3137
|
+
function originAllowed(origin, allowed) {
|
|
3138
|
+
if (!origin) return false;
|
|
3139
|
+
const n = normalise(origin);
|
|
3140
|
+
return n !== null && allowed.includes(n);
|
|
3141
|
+
}
|
|
3142
|
+
var SHELLS, DEV;
|
|
3143
|
+
var init_origins = __esm({
|
|
3144
|
+
"src/local/origins.ts"() {
|
|
3145
|
+
"use strict";
|
|
3146
|
+
init_cloud();
|
|
3147
|
+
SHELLS = ["capacitor://localhost", "app://cayenne"];
|
|
3148
|
+
DEV = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
|
3149
|
+
}
|
|
3150
|
+
});
|
|
3151
|
+
|
|
3152
|
+
// src/local/connection.ts
|
|
3153
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3154
|
+
function closeSoon(ws, code, reason) {
|
|
3155
|
+
if (ws.readyState === ws.CLOSED) return;
|
|
3156
|
+
ws.close(code, reason);
|
|
3157
|
+
const timer = setTimeout(() => ws.terminate(), CLOSE_GRACE_MS);
|
|
3158
|
+
timer.unref();
|
|
3159
|
+
ws.once("close", () => clearTimeout(timer));
|
|
3160
|
+
}
|
|
3161
|
+
function parseFrame(data) {
|
|
3162
|
+
try {
|
|
3163
|
+
const parsed = LocalClientFrame.safeParse(JSON.parse(String(data)));
|
|
3164
|
+
return parsed.success ? parsed.data : null;
|
|
3165
|
+
} catch {
|
|
3166
|
+
return null;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
function handleConnection(ws, who, ctx, log, now) {
|
|
3170
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
3171
|
+
const terminals = /* @__PURE__ */ new Set();
|
|
3172
|
+
const pending = /* @__PURE__ */ new Map();
|
|
3173
|
+
let closed = false;
|
|
3174
|
+
const pendingSubs = /* @__PURE__ */ new Map();
|
|
3175
|
+
const conn = {
|
|
3176
|
+
id: randomUUID2(),
|
|
3177
|
+
userId: who.userId,
|
|
3178
|
+
tokenExp: who.tokenExp,
|
|
3179
|
+
sessions,
|
|
3180
|
+
terminals,
|
|
3181
|
+
send(frame) {
|
|
3182
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
3183
|
+
if (ws.bufferedAmount > MAX_BUFFERED_BYTES) {
|
|
3184
|
+
log.warn({ conn: conn.id, userId: conn.userId }, "local client is not reading; closing");
|
|
3185
|
+
closeSoon(ws, LOCAL_CLOSE_NOT_READY, "too far behind");
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
ws.send(JSON.stringify(frame));
|
|
3189
|
+
},
|
|
3190
|
+
close(code, reason) {
|
|
3191
|
+
closeSoon(ws, code, reason);
|
|
3192
|
+
},
|
|
3193
|
+
recheckSessions: () => recheckSessions()
|
|
3194
|
+
};
|
|
3195
|
+
const cmdErr = (id, error) => conn.send({ t: "cmd.err", id, error });
|
|
3196
|
+
const onCmd = async (id) => {
|
|
3197
|
+
if (pending.has(id)) return;
|
|
3198
|
+
if (pending.size >= LOCAL_MAX_PENDING_CMDS) {
|
|
3199
|
+
return cmdErr(id, "VALIDATION: too many pending commands on this socket");
|
|
3200
|
+
}
|
|
3201
|
+
pending.set(id, () => {
|
|
3202
|
+
});
|
|
3203
|
+
let row;
|
|
3204
|
+
try {
|
|
3205
|
+
row = await ctx.readRow(id);
|
|
3206
|
+
} catch (err) {
|
|
3207
|
+
pending.delete(id);
|
|
3208
|
+
log.warn({ conn: conn.id, id, err }, "local cmd: row read failed");
|
|
3209
|
+
return cmdErr(id, "PROVIDER_ERROR: could not read the command");
|
|
3210
|
+
}
|
|
3211
|
+
if (closed) return;
|
|
3212
|
+
if (!row || row.runner_id !== ctx.runnerId || row.created_by !== conn.userId) {
|
|
3213
|
+
const reason = !row ? "no row" : row.runner_id !== ctx.runnerId ? "another runner" : "another creator";
|
|
3214
|
+
pending.delete(id);
|
|
3215
|
+
log.info({ conn: conn.id, userId: conn.userId, id, reason }, "local cmd refused");
|
|
3216
|
+
return cmdErr(id, "VALIDATION: no such command on this runner");
|
|
3217
|
+
}
|
|
3218
|
+
if (row.status === "done" || row.status === "failed" || row.status === "cancelled") {
|
|
3219
|
+
pending.delete(id);
|
|
3220
|
+
return conn.send({ t: "res", id, status: row.status, result: row.result, error: row.error });
|
|
3221
|
+
}
|
|
3222
|
+
const drop = ctx.source.offer(row, {
|
|
3223
|
+
onAck: () => conn.send({ t: "ack", id }),
|
|
3224
|
+
onFinished: (r) => {
|
|
3225
|
+
pending.delete(id);
|
|
3226
|
+
conn.send({ t: "res", id, ...r });
|
|
3227
|
+
},
|
|
3228
|
+
// The source will never settle this pointer (the row is claimed elsewhere and still running,
|
|
3229
|
+
// or could not be re-read): free the slot and say so without settling (ruling B10, R-B6).
|
|
3230
|
+
onDropped: (reason) => {
|
|
3231
|
+
pending.delete(id);
|
|
3232
|
+
log.info({ conn: conn.id, userId: conn.userId, id, reason }, "local cmd pointer dropped");
|
|
3233
|
+
cmdErr(id, "VALIDATION: no such command on this runner");
|
|
3234
|
+
}
|
|
3235
|
+
});
|
|
3236
|
+
if (pending.has(id)) pending.set(id, drop);
|
|
3237
|
+
};
|
|
3238
|
+
const onAuth = async (token) => {
|
|
3239
|
+
const next = await ctx.verifier.verify(token);
|
|
3240
|
+
if (next.status === "unavailable") {
|
|
3241
|
+
log.info({ conn: conn.id, userId: conn.userId }, "local auth refresh unverified; kept");
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
if (next.status !== "valid" || next.userId !== conn.userId || expiresAt(next.exp) <= now() || !ctx.serves(next.userId)) {
|
|
3245
|
+
conn.close(LOCAL_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
conn.tokenExp = expiresAt(next.exp);
|
|
3249
|
+
conn.send({ t: "ready", userId: conn.userId });
|
|
3250
|
+
await recheckSessions();
|
|
3251
|
+
};
|
|
3252
|
+
const SESSION_REFUSED = "SESSION_NOT_FOUND: not hosted on this runner";
|
|
3253
|
+
const sessionRefusal = (s) => !s ? "no row" : s.runner_id !== ctx.runnerId ? "another runner" : s.owner_id !== conn.userId && s.visibility !== "workspace" ? "private" : null;
|
|
3254
|
+
const recheckSessions = async () => {
|
|
3255
|
+
await Promise.all(
|
|
3256
|
+
[...sessions].map(async (id) => {
|
|
3257
|
+
let s;
|
|
3258
|
+
try {
|
|
3259
|
+
s = await ctx.readSession(id);
|
|
3260
|
+
} catch (err) {
|
|
3261
|
+
log.warn({ conn: conn.id, sessionId: id, err }, "local sub recheck: session read failed");
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
const reason = sessionRefusal(s);
|
|
3265
|
+
if (!reason || !sessions.delete(id)) return;
|
|
3266
|
+
log.info(
|
|
3267
|
+
{ conn: conn.id, userId: conn.userId, sessionId: id, reason },
|
|
3268
|
+
"local sub revoked"
|
|
3269
|
+
);
|
|
3270
|
+
conn.send({ t: "sub.err", topic: `session:${id}`, error: SESSION_REFUSED });
|
|
3271
|
+
})
|
|
3272
|
+
);
|
|
3273
|
+
};
|
|
3274
|
+
const onSub = async (topic) => {
|
|
3275
|
+
const parsed = parseLocalTopic(topic);
|
|
3276
|
+
if (!parsed) return;
|
|
3277
|
+
if (parsed.kind === "session") {
|
|
3278
|
+
const mine = {};
|
|
3279
|
+
pendingSubs.set(parsed.id, mine);
|
|
3280
|
+
const s = await ctx.readSession(parsed.id).catch(() => null);
|
|
3281
|
+
if (closed || pendingSubs.get(parsed.id) !== mine) return;
|
|
3282
|
+
pendingSubs.delete(parsed.id);
|
|
3283
|
+
const reason = sessionRefusal(s);
|
|
3284
|
+
if (reason) {
|
|
3285
|
+
log.info(
|
|
3286
|
+
{ conn: conn.id, userId: conn.userId, sessionId: parsed.id, reason },
|
|
3287
|
+
"local sub refused"
|
|
3288
|
+
);
|
|
3289
|
+
return conn.send({ t: "sub.err", topic, error: SESSION_REFUSED });
|
|
3290
|
+
}
|
|
3291
|
+
sessions.add(parsed.id);
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
const t = ctx.terminals;
|
|
3295
|
+
if (!t?.has(parsed.id) || t.ownerOf(parsed.id) !== conn.userId || !ctx.mayHoldTerminal(conn.userId)) {
|
|
3296
|
+
return conn.send({
|
|
3297
|
+
t: "sub.err",
|
|
3298
|
+
topic,
|
|
3299
|
+
error: "TERMINAL_NOT_FOUND: not your terminal here"
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
terminals.add(parsed.id);
|
|
3303
|
+
t.attachLocal(parsed.id, conn.id);
|
|
3304
|
+
};
|
|
3305
|
+
const onUnsub = (topic) => {
|
|
3306
|
+
const parsed = parseLocalTopic(topic);
|
|
3307
|
+
if (!parsed) return;
|
|
3308
|
+
if (parsed.kind === "session") {
|
|
3309
|
+
sessions.delete(parsed.id);
|
|
3310
|
+
pendingSubs.delete(parsed.id);
|
|
3311
|
+
} else if (terminals.delete(parsed.id)) ctx.terminals?.detachLocal(parsed.id, conn.id);
|
|
3312
|
+
};
|
|
3313
|
+
const onTerm = (terminalId, m) => {
|
|
3314
|
+
const t = ctx.terminals;
|
|
3315
|
+
if (!t || !terminals.has(terminalId)) {
|
|
3316
|
+
log.warn(
|
|
3317
|
+
{ conn: conn.id },
|
|
3318
|
+
"local term frame for a terminal this socket is not subscribed to; dropped"
|
|
3319
|
+
);
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
try {
|
|
3323
|
+
t.receive(terminalId, m.m, "local");
|
|
3324
|
+
} catch (err) {
|
|
3325
|
+
log.warn({ conn: conn.id, terminalId, err }, "local terminal message refused");
|
|
3326
|
+
}
|
|
3327
|
+
};
|
|
3328
|
+
ws.on("message", (data) => {
|
|
3329
|
+
const f = parseFrame(data);
|
|
3330
|
+
if (!f) {
|
|
3331
|
+
log.warn({ conn: conn.id }, "local frame did not parse");
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
if (f.t === "hello") conn.close(LOCAL_CLOSE_BAD_HANDSHAKE, "handshake already done");
|
|
3335
|
+
else if (f.t === "auth") void onAuth(f.token);
|
|
3336
|
+
else if (f.t === "cmd") void onCmd(f.id);
|
|
3337
|
+
else if (f.t === "sub") void onSub(f.topic);
|
|
3338
|
+
else if (f.t === "unsub") onUnsub(f.topic);
|
|
3339
|
+
else onTerm(f.terminalId, f);
|
|
3340
|
+
});
|
|
3341
|
+
ws.on("close", () => {
|
|
3342
|
+
closed = true;
|
|
3343
|
+
for (const id of terminals) ctx.terminals?.detachLocal(id, conn.id);
|
|
3344
|
+
for (const drop of pending.values()) drop();
|
|
3345
|
+
pending.clear();
|
|
3346
|
+
terminals.clear();
|
|
3347
|
+
sessions.clear();
|
|
3348
|
+
pendingSubs.clear();
|
|
3349
|
+
});
|
|
3350
|
+
return conn;
|
|
3351
|
+
}
|
|
3352
|
+
function sessionFrame(sessionId, e) {
|
|
3353
|
+
return { t: "evt", topic: `session:${sessionId}`, e };
|
|
3354
|
+
}
|
|
3355
|
+
var MAX_BUFFERED_BYTES, CLOSE_GRACE_MS, expiresAt;
|
|
3356
|
+
var init_connection = __esm({
|
|
3357
|
+
"src/local/connection.ts"() {
|
|
3358
|
+
"use strict";
|
|
3359
|
+
init_src();
|
|
3360
|
+
MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
3361
|
+
CLOSE_GRACE_MS = 250;
|
|
3362
|
+
expiresAt = (exp) => exp * 1e3;
|
|
3363
|
+
}
|
|
3364
|
+
});
|
|
3365
|
+
|
|
3366
|
+
// src/local/server.ts
|
|
3367
|
+
import { createServer } from "http";
|
|
3368
|
+
import { WebSocketServer } from "ws";
|
|
3369
|
+
function createLocalServer({
|
|
3370
|
+
port,
|
|
3371
|
+
origins,
|
|
3372
|
+
log,
|
|
3373
|
+
handshakeTimeoutMs = LOCAL_HANDSHAKE_TIMEOUT_MS,
|
|
3374
|
+
recheckMs = LOCAL_RECHECK_MS,
|
|
3375
|
+
now = Date.now,
|
|
3376
|
+
maxSockets = LOCAL_MAX_SOCKETS
|
|
3377
|
+
}) {
|
|
3378
|
+
let ctx = null;
|
|
3379
|
+
let bound = null;
|
|
3380
|
+
const conns = /* @__PURE__ */ new Set();
|
|
3381
|
+
let pendingUpgrades = 0;
|
|
3382
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: LOCAL_MAX_FRAME_BYTES });
|
|
3383
|
+
let untapTerminals = null;
|
|
3384
|
+
let unwatchRoster = null;
|
|
3385
|
+
let recheckTimer = null;
|
|
3386
|
+
const hostAllowed = (host) => bound !== null && (host === `${HOST}:${bound}` || host === `localhost:${bound}`);
|
|
3387
|
+
const socketsOf = (userId) => {
|
|
3388
|
+
let n = 0;
|
|
3389
|
+
for (const c of conns) if (c.userId === userId) n += 1;
|
|
3390
|
+
return n;
|
|
3391
|
+
};
|
|
3392
|
+
const recheck = () => {
|
|
3393
|
+
const current = ctx;
|
|
3394
|
+
if (!current) return;
|
|
3395
|
+
const t = now();
|
|
3396
|
+
for (const c of [...conns]) {
|
|
3397
|
+
if (!current.serves(c.userId) || t >= c.tokenExp) {
|
|
3398
|
+
c.close(LOCAL_CLOSE_UNAUTHORIZED, "no longer authorised");
|
|
3399
|
+
} else {
|
|
3400
|
+
void c.recheckSessions();
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
3404
|
+
const cors = (req, res) => {
|
|
3405
|
+
const origin = req.headers.origin;
|
|
3406
|
+
if (!originAllowed(origin, origins)) return false;
|
|
3407
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
3408
|
+
res.setHeader("Vary", "Origin");
|
|
3409
|
+
return true;
|
|
3410
|
+
};
|
|
3411
|
+
const server = createServer((req, res) => {
|
|
3412
|
+
if (!hostAllowed(req.headers.host)) {
|
|
3413
|
+
res.writeHead(403).end();
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
if (req.url !== LOCAL_HEALTH_PATH) {
|
|
3417
|
+
res.writeHead(404).end();
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
const allowed = cors(req, res);
|
|
3421
|
+
if (req.method === "OPTIONS") {
|
|
3422
|
+
if (!allowed) {
|
|
3423
|
+
res.writeHead(403).end();
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
res.setHeader("Access-Control-Allow-Methods", "GET");
|
|
3427
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
3428
|
+
res.setHeader("Access-Control-Max-Age", "600");
|
|
3429
|
+
res.writeHead(204).end();
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
if (req.method !== "GET") {
|
|
3433
|
+
res.writeHead(405).end();
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
res.setHeader("Content-Type", "application/json");
|
|
3437
|
+
res.setHeader("Cache-Control", "no-store");
|
|
3438
|
+
if (!ctx) {
|
|
3439
|
+
res.writeHead(503).end(JSON.stringify({ error: "starting" }));
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
res.writeHead(200).end(
|
|
3443
|
+
JSON.stringify({
|
|
3444
|
+
runnerId: ctx.runnerId,
|
|
3445
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
3446
|
+
version: ctx.version
|
|
3447
|
+
})
|
|
3448
|
+
);
|
|
3449
|
+
});
|
|
3450
|
+
server.maxConnections = LOCAL_MAX_CONNECTIONS;
|
|
3451
|
+
server.headersTimeout = LOCAL_HANDSHAKE_TIMEOUT_MS;
|
|
3452
|
+
server.requestTimeout = LOCAL_HANDSHAKE_TIMEOUT_MS;
|
|
3453
|
+
server.keepAliveTimeout = 1e3;
|
|
3454
|
+
const refuse = (socket, status) => {
|
|
3455
|
+
const text = status === 403 ? "Forbidden" : "Service Unavailable";
|
|
3456
|
+
socket.write(`HTTP/1.1 ${status} ${text}\r
|
|
3457
|
+
Connection: close\r
|
|
3458
|
+
Content-Length: 0\r
|
|
3459
|
+
\r
|
|
3460
|
+
`);
|
|
3461
|
+
socket.destroy();
|
|
3462
|
+
};
|
|
3463
|
+
server.on("upgrade", (req, socket, head) => {
|
|
3464
|
+
const origin = req.headers.origin ?? "";
|
|
3465
|
+
if (req.url !== LOCAL_WS_PATH || !hostAllowed(req.headers.host) || !originAllowed(origin, origins)) {
|
|
3466
|
+
refuse(socket, 403);
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
if (pendingUpgrades >= LOCAL_MAX_PENDING_UPGRADES || conns.size >= maxSockets) {
|
|
3470
|
+
refuse(socket, 503);
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
pendingUpgrades += 1;
|
|
3474
|
+
let released = false;
|
|
3475
|
+
const release = () => {
|
|
3476
|
+
if (released) return;
|
|
3477
|
+
released = true;
|
|
3478
|
+
pendingUpgrades -= 1;
|
|
3479
|
+
};
|
|
3480
|
+
let ws = null;
|
|
3481
|
+
const timer = setTimeout(() => {
|
|
3482
|
+
if (ws) closeSoon(ws, LOCAL_CLOSE_NOT_READY, "handshake timeout");
|
|
3483
|
+
else socket.destroy();
|
|
3484
|
+
}, handshakeTimeoutMs);
|
|
3485
|
+
socket.once("close", release);
|
|
3486
|
+
wss.handleUpgrade(req, socket, head, (admitted) => {
|
|
3487
|
+
ws = admitted;
|
|
3488
|
+
admitted.on("close", release);
|
|
3489
|
+
admitted.on("error", (err) => {
|
|
3490
|
+
log.warn({ code: err.code }, "local socket error");
|
|
3491
|
+
});
|
|
3492
|
+
const current = ctx;
|
|
3493
|
+
if (!current) {
|
|
3494
|
+
clearTimeout(timer);
|
|
3495
|
+
closeSoon(admitted, LOCAL_CLOSE_NOT_READY, "starting");
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
let state = "awaiting-hello";
|
|
3499
|
+
const onHandshake = async (data) => {
|
|
3500
|
+
const f = parseFrame(data);
|
|
3501
|
+
if (state === "awaiting-hello") {
|
|
3502
|
+
if (f?.t !== "hello") {
|
|
3503
|
+
closeSoon(admitted, LOCAL_CLOSE_BAD_HANDSHAKE, "expected hello");
|
|
3504
|
+
return;
|
|
3505
|
+
}
|
|
3506
|
+
state = "awaiting-auth";
|
|
3507
|
+
const sig = current.identity.sign(localProofMessage(f.nonce, current.runnerId, origin));
|
|
3508
|
+
admitted.send(
|
|
3509
|
+
JSON.stringify({
|
|
3510
|
+
t: "hello",
|
|
3511
|
+
runnerId: current.runnerId,
|
|
3512
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
3513
|
+
sig
|
|
3514
|
+
})
|
|
3515
|
+
);
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
if (state !== "awaiting-auth" || !f || f.t !== "auth") {
|
|
3519
|
+
closeSoon(admitted, LOCAL_CLOSE_BAD_HANDSHAKE, "expected auth");
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
state = "verifying";
|
|
3523
|
+
const who = await current.verifier.verify(f.token);
|
|
3524
|
+
if (admitted.readyState !== admitted.OPEN) return;
|
|
3525
|
+
if (who.status !== "valid" || expiresAt(who.exp) <= now() || !current.serves(who.userId)) {
|
|
3526
|
+
closeSoon(admitted, LOCAL_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
const tokenExp = expiresAt(who.exp);
|
|
3530
|
+
if (socketsOf(who.userId) >= LOCAL_MAX_SOCKETS_PER_USER) {
|
|
3531
|
+
closeSoon(admitted, LOCAL_CLOSE_TOO_MANY, "too many sockets for this user");
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
if (conns.size >= maxSockets) {
|
|
3535
|
+
closeSoon(admitted, LOCAL_CLOSE_TOO_MANY, "too many sockets");
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
state = "ready";
|
|
3539
|
+
clearTimeout(timer);
|
|
3540
|
+
release();
|
|
3541
|
+
admitted.off("message", listener);
|
|
3542
|
+
const conn = handleConnection(
|
|
3543
|
+
admitted,
|
|
3544
|
+
{ userId: who.userId, tokenExp },
|
|
3545
|
+
current,
|
|
3546
|
+
log,
|
|
3547
|
+
now
|
|
3548
|
+
);
|
|
3549
|
+
conns.add(conn);
|
|
3550
|
+
admitted.on("close", () => conns.delete(conn));
|
|
3551
|
+
conn.send({ t: "ready", userId: who.userId });
|
|
3552
|
+
log.info({ conn: conn.id, userId: who.userId }, "local client attached");
|
|
3553
|
+
};
|
|
3554
|
+
const listener = (data) => {
|
|
3555
|
+
void onHandshake(data);
|
|
3556
|
+
};
|
|
3557
|
+
admitted.on("message", listener);
|
|
3558
|
+
});
|
|
3559
|
+
});
|
|
3560
|
+
return {
|
|
3561
|
+
get port() {
|
|
3562
|
+
return bound;
|
|
3563
|
+
},
|
|
3564
|
+
listen() {
|
|
3565
|
+
return new Promise((resolve3, reject) => {
|
|
3566
|
+
server.once("error", reject);
|
|
3567
|
+
server.listen(port, HOST, () => {
|
|
3568
|
+
server.off("error", reject);
|
|
3569
|
+
const address = server.address();
|
|
3570
|
+
bound = typeof address === "object" && address ? address.port : port;
|
|
3571
|
+
log.info({ host: HOST, port: bound }, "local server listening");
|
|
3572
|
+
resolve3(bound);
|
|
3573
|
+
});
|
|
3574
|
+
});
|
|
3575
|
+
},
|
|
3576
|
+
attach(next) {
|
|
3577
|
+
ctx = next;
|
|
3578
|
+
untapTerminals?.();
|
|
3579
|
+
untapTerminals = next.terminals?.subscribe((terminalId, m, origin) => {
|
|
3580
|
+
if (m.type === "replay" && origin === "relay") return;
|
|
3581
|
+
for (const c of conns)
|
|
3582
|
+
if (c.terminals.has(terminalId)) c.send({ t: "term", terminalId, m });
|
|
3583
|
+
}) ?? null;
|
|
3584
|
+
unwatchRoster?.();
|
|
3585
|
+
unwatchRoster = next.onRosterRefresh(recheck);
|
|
3586
|
+
if (recheckTimer) clearInterval(recheckTimer);
|
|
3587
|
+
recheckTimer = setInterval(recheck, recheckMs);
|
|
3588
|
+
recheckTimer.unref();
|
|
3589
|
+
},
|
|
3590
|
+
emit(sessionId, e) {
|
|
3591
|
+
const frame = sessionFrame(sessionId, e);
|
|
3592
|
+
for (const c of conns) if (c.sessions.has(sessionId)) c.send(frame);
|
|
3593
|
+
},
|
|
3594
|
+
close() {
|
|
3595
|
+
if (recheckTimer) clearInterval(recheckTimer);
|
|
3596
|
+
recheckTimer = null;
|
|
3597
|
+
unwatchRoster?.();
|
|
3598
|
+
unwatchRoster = null;
|
|
3599
|
+
untapTerminals?.();
|
|
3600
|
+
untapTerminals = null;
|
|
3601
|
+
for (const client of wss.clients) client.close(1001, "runner stopping");
|
|
3602
|
+
return new Promise((resolve3) => {
|
|
3603
|
+
wss.close();
|
|
3604
|
+
server.close(() => resolve3());
|
|
3605
|
+
server.closeAllConnections();
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
var HOST;
|
|
3611
|
+
var init_server = __esm({
|
|
3612
|
+
"src/local/server.ts"() {
|
|
3613
|
+
"use strict";
|
|
3614
|
+
init_src();
|
|
3615
|
+
init_connection();
|
|
3616
|
+
init_origins();
|
|
3617
|
+
HOST = "127.0.0.1";
|
|
2793
3618
|
}
|
|
2794
3619
|
});
|
|
2795
3620
|
|
|
@@ -3030,21 +3855,31 @@ function checkRunsArgs(req, sha) {
|
|
|
3030
3855
|
}
|
|
3031
3856
|
return ["api", `repos/${req.owner}/${req.name}/commits/${sha}/check-runs`];
|
|
3032
3857
|
}
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3858
|
+
function makeGhRun(opts = {}) {
|
|
3859
|
+
const gh = makeLocator("gh", opts.locate);
|
|
3860
|
+
const currentPath = opts.path ?? (() => process.env.PATH);
|
|
3861
|
+
return async (args) => {
|
|
3862
|
+
const path = currentPath();
|
|
3863
|
+
const file = gh.find(path);
|
|
3864
|
+
if (file === null) {
|
|
3865
|
+
return { code: 127, stdout: "", stderr: "gh not found on an absolute PATH entry" };
|
|
3866
|
+
}
|
|
3867
|
+
try {
|
|
3868
|
+
const { stdout, stderr } = await exec3(file, args, {
|
|
3869
|
+
timeout: GH_TIMEOUT_MS,
|
|
3870
|
+
maxBuffer: MAX_OUTPUT * 2
|
|
3871
|
+
});
|
|
3872
|
+
return { code: 0, stdout, stderr };
|
|
3873
|
+
} catch (err) {
|
|
3874
|
+
const e = err;
|
|
3875
|
+
if (e.code === "ENOENT") gh.forget(path);
|
|
3876
|
+
return {
|
|
3877
|
+
code: typeof e.code === "number" ? e.code : 1,
|
|
3878
|
+
stdout: e.stdout ?? "",
|
|
3879
|
+
stderr: e.stderr ?? ""
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
};
|
|
3048
3883
|
}
|
|
3049
3884
|
function statusFrom(stderr) {
|
|
3050
3885
|
const m = /\(HTTP (\d{3})\)/.exec(stderr);
|
|
@@ -3082,16 +3917,18 @@ function createGhGateway(run) {
|
|
|
3082
3917
|
}
|
|
3083
3918
|
};
|
|
3084
3919
|
}
|
|
3085
|
-
var exec3, GH_TIMEOUT_MS, MAX_OUTPUT, ALL_DOTS3, COMMIT_SHA;
|
|
3920
|
+
var exec3, GH_TIMEOUT_MS, MAX_OUTPUT, ALL_DOTS3, COMMIT_SHA, runGh;
|
|
3086
3921
|
var init_gh = __esm({
|
|
3087
3922
|
"src/github/gh.ts"() {
|
|
3088
3923
|
"use strict";
|
|
3089
3924
|
init_src();
|
|
3925
|
+
init_exec_path();
|
|
3090
3926
|
exec3 = promisify3(execFile4);
|
|
3091
3927
|
GH_TIMEOUT_MS = 2e4;
|
|
3092
3928
|
MAX_OUTPUT = 512e3;
|
|
3093
3929
|
ALL_DOTS3 = /^\.+$/;
|
|
3094
3930
|
COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
3931
|
+
runGh = makeGhRun();
|
|
3095
3932
|
}
|
|
3096
3933
|
});
|
|
3097
3934
|
|
|
@@ -3307,11 +4144,11 @@ var init_handlers = __esm({
|
|
|
3307
4144
|
|
|
3308
4145
|
// src/git/watch.ts
|
|
3309
4146
|
import { watch } from "fs";
|
|
3310
|
-
import { isAbsolute as
|
|
4147
|
+
import { isAbsolute as isAbsolute9, normalize, sep as sep4 } from "path";
|
|
3311
4148
|
function ignored(name) {
|
|
3312
4149
|
if (name === null || name === "") return false;
|
|
3313
4150
|
const rel = normalize(name);
|
|
3314
|
-
if (
|
|
4151
|
+
if (isAbsolute9(rel) || rel === ".." || rel.startsWith(`..${sep4}`)) return true;
|
|
3315
4152
|
return rel.split(/[\\/]/).some((segment) => IGNORED_NAMES.has(segment));
|
|
3316
4153
|
}
|
|
3317
4154
|
function watchChanges(cwd, onChange, debounceMs = CHANGES_DEBOUNCE_MS, onError = () => void 0, fsWatch = recursiveWatch) {
|
|
@@ -3513,7 +4350,7 @@ var init_pushable = __esm({
|
|
|
3513
4350
|
});
|
|
3514
4351
|
|
|
3515
4352
|
// src/claude/session.ts
|
|
3516
|
-
import { randomUUID as
|
|
4353
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3517
4354
|
import { EventEmitter } from "events";
|
|
3518
4355
|
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
3519
4356
|
var ClaudeSession;
|
|
@@ -3621,7 +4458,7 @@ var init_session = __esm({
|
|
|
3621
4458
|
}
|
|
3622
4459
|
requestPermission(toolName2, input, suggestions, toolUseId, signal) {
|
|
3623
4460
|
return new Promise((resolve3) => {
|
|
3624
|
-
const id =
|
|
4461
|
+
const id = randomUUID3();
|
|
3625
4462
|
const settle = (d) => {
|
|
3626
4463
|
const entry = this.pending.get(id);
|
|
3627
4464
|
if (!entry) return;
|
|
@@ -4160,9 +4997,9 @@ var init_create = __esm({
|
|
|
4160
4997
|
});
|
|
4161
4998
|
|
|
4162
4999
|
// src/sessions/manager.ts
|
|
4163
|
-
import { randomUUID as
|
|
5000
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
4164
5001
|
import { realpathSync as realpathSync7 } from "fs";
|
|
4165
|
-
import { isAbsolute as
|
|
5002
|
+
import { isAbsolute as isAbsolute10, sep as sep5 } from "path";
|
|
4166
5003
|
import { isDeepStrictEqual } from "util";
|
|
4167
5004
|
function narrowToSession(suggestions) {
|
|
4168
5005
|
if (!suggestions) return suggestions;
|
|
@@ -4320,7 +5157,7 @@ var init_manager = __esm({
|
|
|
4320
5157
|
if (p.initialPrompt) {
|
|
4321
5158
|
await this.prompt(
|
|
4322
5159
|
sessionId,
|
|
4323
|
-
{ text: p.initialPrompt, clientUuid:
|
|
5160
|
+
{ text: p.initialPrompt, clientUuid: randomUUID4(), attachments: [] },
|
|
4324
5161
|
createdBy
|
|
4325
5162
|
);
|
|
4326
5163
|
}
|
|
@@ -4797,7 +5634,7 @@ var init_manager = __esm({
|
|
|
4797
5634
|
}
|
|
4798
5635
|
/** A path as the SDK reads it: relative means relative to the session's cwd. */
|
|
4799
5636
|
fromCwd(cwd, value) {
|
|
4800
|
-
return
|
|
5637
|
+
return isAbsolute10(value) ? value : `${cwd}${sep5}${value}`;
|
|
4801
5638
|
}
|
|
4802
5639
|
/**
|
|
4803
5640
|
* §2.9: a path in `updatedInput` passes the same boundary the session's `cwd` did, and the
|
|
@@ -5362,6 +6199,11 @@ var init_manager = __esm({
|
|
|
5362
6199
|
}
|
|
5363
6200
|
message = envelope(shrunk);
|
|
5364
6201
|
}
|
|
6202
|
+
try {
|
|
6203
|
+
this.deps.tap?.(sessionId, message.payload);
|
|
6204
|
+
} catch (err) {
|
|
6205
|
+
this.deps.log.warn({ err, sessionId, type: event.type }, "session event tap threw");
|
|
6206
|
+
}
|
|
5365
6207
|
const result = await live.channel.send(message);
|
|
5366
6208
|
if (result !== "ok") {
|
|
5367
6209
|
this.deps.log.warn(
|
|
@@ -5476,7 +6318,7 @@ var init_channels = __esm({
|
|
|
5476
6318
|
// src/terminal/env.ts
|
|
5477
6319
|
import { readFileSync as readFileSync7, realpathSync as realpathSync8 } from "fs";
|
|
5478
6320
|
import { lstat } from "fs/promises";
|
|
5479
|
-
import { dirname as dirname7, isAbsolute as
|
|
6321
|
+
import { dirname as dirname7, isAbsolute as isAbsolute11, relative as relative4 } from "path";
|
|
5480
6322
|
function terminalEnv(base, { runnerOwner, claudeConfigDir }) {
|
|
5481
6323
|
const env = {};
|
|
5482
6324
|
for (const [key, value] of Object.entries(base)) {
|
|
@@ -5546,7 +6388,7 @@ function tryRealpath(fs, p) {
|
|
|
5546
6388
|
}
|
|
5547
6389
|
function inside(candidate, root) {
|
|
5548
6390
|
const rel = relative4(root, candidate);
|
|
5549
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
6391
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute11(rel);
|
|
5550
6392
|
}
|
|
5551
6393
|
var SECRET_ENV, MEMBER_ENV_ALLOW, MEMBER_ENV_KEYS, LOCALE_ENV, nodeShellFs;
|
|
5552
6394
|
var init_env = __esm({
|
|
@@ -5753,6 +6595,7 @@ var init_manager2 = __esm({
|
|
|
5753
6595
|
constructor(deps) {
|
|
5754
6596
|
this.deps = deps;
|
|
5755
6597
|
this.flushMs = deps.flushMs ?? TERMINAL_FLUSH_MS;
|
|
6598
|
+
this.localFlushMs = deps.localFlushMs ?? TERMINAL_LOCAL_FLUSH_MS;
|
|
5756
6599
|
this.flushBytes = deps.flushBytes ?? TERMINAL_FLUSH_BYTES;
|
|
5757
6600
|
this.ringBytes = deps.ringBytes ?? TERMINAL_RING_BYTES;
|
|
5758
6601
|
this.reaper = setInterval(() => this.reap(), deps.reapIntervalMs ?? TERMINAL_REAP_INTERVAL_MS);
|
|
@@ -5769,6 +6612,7 @@ var init_manager2 = __esm({
|
|
|
5769
6612
|
opens = /* @__PURE__ */ new Set();
|
|
5770
6613
|
reaper;
|
|
5771
6614
|
flushMs;
|
|
6615
|
+
localFlushMs;
|
|
5772
6616
|
flushBytes;
|
|
5773
6617
|
ringBytes;
|
|
5774
6618
|
/** Set once by `shutdown()` (fix round 1 L7): `open()` checks it after every await. */
|
|
@@ -6092,9 +6936,14 @@ var init_manager2 = __esm({
|
|
|
6092
6936
|
return;
|
|
6093
6937
|
}
|
|
6094
6938
|
if (term.flushTimer) return;
|
|
6095
|
-
let
|
|
6096
|
-
|
|
6097
|
-
|
|
6939
|
+
let delay2 = this.localFlushMs;
|
|
6940
|
+
if (!this.relaySuppressed(terminalId)) {
|
|
6941
|
+
let others = 0;
|
|
6942
|
+
for (const [id, t] of this.terms) {
|
|
6943
|
+
if (t.flushTimer !== null && !this.relaySuppressed(id)) others += 1;
|
|
6944
|
+
}
|
|
6945
|
+
delay2 = others > 0 ? this.flushMs * 2 : this.flushMs;
|
|
6946
|
+
}
|
|
6098
6947
|
term.flushTimer = setTimeout(() => {
|
|
6099
6948
|
term.flushTimer = null;
|
|
6100
6949
|
this.flush(terminalId, term);
|
|
@@ -6635,9 +7484,36 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6635
7484
|
const pty = await loadPty();
|
|
6636
7485
|
const memberTerminalIds = memberTerminalIdsOf(cfg);
|
|
6637
7486
|
const kinds2 = handledKinds({ terminal: pty !== null });
|
|
7487
|
+
const localIdentity = createLocalIdentity();
|
|
7488
|
+
const origins = buildOrigins(cfg.localOrigins);
|
|
7489
|
+
if (CLOUD_SUPABASE_URL !== "" && CLOUD_APP_ORIGIN === "") {
|
|
7490
|
+
logger.warn(
|
|
7491
|
+
{ origins },
|
|
7492
|
+
"local origins: dev origins enabled because CAYENNE_CLOUD_APP_ORIGIN is empty"
|
|
7493
|
+
);
|
|
7494
|
+
}
|
|
7495
|
+
logger.info({ origins }, "local origins");
|
|
7496
|
+
const local = createLocalServer({ port: cfg.port, origins, log: logger });
|
|
7497
|
+
let localPort = 0;
|
|
7498
|
+
if (cfg.port < LOCAL_PORT_MIN || cfg.port > LOCAL_PORT_MAX) {
|
|
7499
|
+
logger.warn(
|
|
7500
|
+
{ port: cfg.port },
|
|
7501
|
+
`local port ${cfg.port} is outside ${LOCAL_PORT_MIN}\u2013${LOCAL_PORT_MAX}; fast path disabled for this run`
|
|
7502
|
+
);
|
|
7503
|
+
} else {
|
|
7504
|
+
try {
|
|
7505
|
+
localPort = await local.listen();
|
|
7506
|
+
} catch (err) {
|
|
7507
|
+
logger.warn(
|
|
7508
|
+
{ port: cfg.port, err: err instanceof Error ? err.message : String(err) },
|
|
7509
|
+
"local server could not bind; fast path disabled for this run"
|
|
7510
|
+
);
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
6638
7513
|
const capabilities = () => ({
|
|
6639
7514
|
kinds: [...kinds2],
|
|
6640
|
-
local_port:
|
|
7515
|
+
local_port: localPort,
|
|
7516
|
+
local_pubkey: localIdentity.publicKey,
|
|
6641
7517
|
claude: true,
|
|
6642
7518
|
providers,
|
|
6643
7519
|
// Who the runner's own config names (D34); every heartbeat refresh republishes it.
|
|
@@ -6654,17 +7530,27 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6654
7530
|
}
|
|
6655
7531
|
return capabilities();
|
|
6656
7532
|
};
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
7533
|
+
let identity;
|
|
7534
|
+
try {
|
|
7535
|
+
identity = await registerRunner({
|
|
7536
|
+
db,
|
|
7537
|
+
cfg: registerAs === cfg.runnerId ? cfg : { ...cfg, runnerId: void 0 },
|
|
7538
|
+
hostname: hostname(),
|
|
7539
|
+
profile,
|
|
7540
|
+
platform: `${process.platform}-${process.arch}`,
|
|
7541
|
+
version: VERSION,
|
|
7542
|
+
capabilities: registered
|
|
7543
|
+
});
|
|
7544
|
+
} catch (err) {
|
|
7545
|
+
await local.close();
|
|
7546
|
+
throw err;
|
|
7547
|
+
}
|
|
7548
|
+
try {
|
|
7549
|
+
lock.take(identity.runnerId);
|
|
7550
|
+
} catch (err) {
|
|
7551
|
+
await local.close();
|
|
7552
|
+
throw err;
|
|
7553
|
+
}
|
|
6668
7554
|
let stopHeartbeat = null;
|
|
6669
7555
|
let roster = null;
|
|
6670
7556
|
try {
|
|
@@ -6699,11 +7585,14 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6699
7585
|
codexAvailable: () => providers.codex.available,
|
|
6700
7586
|
claudeEnv: claudeEnv(claudeConfigDir),
|
|
6701
7587
|
git,
|
|
6702
|
-
log: logger
|
|
7588
|
+
log: logger,
|
|
7589
|
+
// Every session event also reaches the loopback sockets subscribed to it (Task 10a).
|
|
7590
|
+
tap: (sessionId, event) => local.emit(sessionId, event)
|
|
6703
7591
|
});
|
|
6704
7592
|
let terminals;
|
|
7593
|
+
let terminalManager = null;
|
|
6705
7594
|
if (pty) {
|
|
6706
|
-
const
|
|
7595
|
+
const tm = new TerminalManager({
|
|
6707
7596
|
pty,
|
|
6708
7597
|
roots,
|
|
6709
7598
|
env: process.env,
|
|
@@ -6726,16 +7615,17 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6726
7615
|
memberTerminalIds,
|
|
6727
7616
|
serves: (userId) => liveRoster.serves(userId),
|
|
6728
7617
|
rosterHealthy: () => liveRoster.healthy(),
|
|
6729
|
-
manager:
|
|
6730
|
-
channels: createTerminalChannels({ db, manager:
|
|
7618
|
+
manager: tm,
|
|
7619
|
+
channels: createTerminalChannels({ db, manager: tm, log: logger }),
|
|
6731
7620
|
log: logger
|
|
6732
7621
|
});
|
|
6733
7622
|
liveRoster.onRefresh(() => {
|
|
6734
|
-
|
|
7623
|
+
tm.revalidate();
|
|
6735
7624
|
void service2.reconcileRows();
|
|
6736
7625
|
});
|
|
6737
7626
|
await service2.closeOpenRows();
|
|
6738
7627
|
terminals = service2;
|
|
7628
|
+
terminalManager = tm;
|
|
6739
7629
|
}
|
|
6740
7630
|
const dispatch = createDispatcher(
|
|
6741
7631
|
buildHandlers(manager, {
|
|
@@ -6758,6 +7648,43 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6758
7648
|
);
|
|
6759
7649
|
stopHeartbeat = heartbeat;
|
|
6760
7650
|
await source.start();
|
|
7651
|
+
if (localPort !== 0) {
|
|
7652
|
+
local.attach({
|
|
7653
|
+
runnerId: identity.runnerId,
|
|
7654
|
+
version: VERSION,
|
|
7655
|
+
identity: localIdentity,
|
|
7656
|
+
// R-B4: a token whose subject the roster does not serve (or whose payload is malformed or
|
|
7657
|
+
// expired) is refused inside the verifier, before any GoTrue call — junk on the loopback
|
|
7658
|
+
// port cannot rate-limit the real member's `/user` check.
|
|
7659
|
+
verifier: createTokenVerifier({
|
|
7660
|
+
db,
|
|
7661
|
+
admit: (sub) => liveRoster.serves(sub),
|
|
7662
|
+
log: logger
|
|
7663
|
+
}),
|
|
7664
|
+
// The roster's own predicate, read live: the owner while a member, listed members while
|
|
7665
|
+
// still members (PR16); every refresh re-checks every socket (SEC-P2).
|
|
7666
|
+
serves: (userId) => liveRoster.serves(userId),
|
|
7667
|
+
onRosterRefresh: (cb) => liveRoster.onRefresh(cb),
|
|
7668
|
+
// Exactly the manager's rule (ruling 14, R2-H1), built from the same deps it was given.
|
|
7669
|
+
mayHoldTerminal: (userId) => mayHoldTerminal(userId, {
|
|
7670
|
+
runnerUserId: identity.userId,
|
|
7671
|
+
memberTerminalIds,
|
|
7672
|
+
serves: (u) => liveRoster.serves(u),
|
|
7673
|
+
rosterHealthy: () => liveRoster.healthy()
|
|
7674
|
+
}),
|
|
7675
|
+
source,
|
|
7676
|
+
readRow: async (id) => {
|
|
7677
|
+
const { data } = await db.from("runner_commands").select("*").eq("id", id).maybeSingle();
|
|
7678
|
+
const parsed = CommandRow.safeParse(data);
|
|
7679
|
+
return parsed.success ? parsed.data : null;
|
|
7680
|
+
},
|
|
7681
|
+
readSession: async (id) => {
|
|
7682
|
+
const { data } = await db.from("chat_sessions").select("runner_id, owner_id, visibility").eq("id", id).maybeSingle();
|
|
7683
|
+
return data;
|
|
7684
|
+
},
|
|
7685
|
+
terminals: terminalManager
|
|
7686
|
+
});
|
|
7687
|
+
}
|
|
6761
7688
|
logger.info(
|
|
6762
7689
|
{
|
|
6763
7690
|
runnerId: identity.runnerId,
|
|
@@ -6792,6 +7719,7 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6792
7719
|
await stage("terminals.shutdown", async () => {
|
|
6793
7720
|
await terminals?.shutdown();
|
|
6794
7721
|
});
|
|
7722
|
+
await stage("local.close", () => local.close());
|
|
6795
7723
|
await stage("roster.stop", () => liveRoster.stop());
|
|
6796
7724
|
if (failures.length > 0) throw failures[0];
|
|
6797
7725
|
} finally {
|
|
@@ -6858,6 +7786,7 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
6858
7786
|
"runner roster stop failed during a failed start"
|
|
6859
7787
|
);
|
|
6860
7788
|
}
|
|
7789
|
+
await local.close().catch(() => void 0);
|
|
6861
7790
|
await markOffline(db, identity.runnerId);
|
|
6862
7791
|
throw err;
|
|
6863
7792
|
}
|
|
@@ -6892,6 +7821,7 @@ var init_start = __esm({
|
|
|
6892
7821
|
"use strict";
|
|
6893
7822
|
init_src();
|
|
6894
7823
|
init_claude_dir();
|
|
7824
|
+
init_cloud();
|
|
6895
7825
|
init_detect();
|
|
6896
7826
|
init_dispatcher();
|
|
6897
7827
|
init_source();
|
|
@@ -6899,6 +7829,10 @@ var init_start = __esm({
|
|
|
6899
7829
|
init_ops();
|
|
6900
7830
|
init_session_worktrees();
|
|
6901
7831
|
init_worktree_owners();
|
|
7832
|
+
init_auth();
|
|
7833
|
+
init_identity();
|
|
7834
|
+
init_origins();
|
|
7835
|
+
init_server();
|
|
6902
7836
|
init_lock();
|
|
6903
7837
|
init_log();
|
|
6904
7838
|
init_profile();
|
|
@@ -6944,6 +7878,9 @@ async function logout(db) {
|
|
|
6944
7878
|
await db.auth.signOut();
|
|
6945
7879
|
}
|
|
6946
7880
|
|
|
7881
|
+
// src/cli.ts
|
|
7882
|
+
init_cloud();
|
|
7883
|
+
|
|
6947
7884
|
// src/commands/doctor.ts
|
|
6948
7885
|
async function probeBackend(url, anonKey, fetchFn = fetch) {
|
|
6949
7886
|
try {
|
|
@@ -7022,6 +7959,19 @@ async function doctor(deps) {
|
|
|
7022
7959
|
fix: TERMINAL_FIX
|
|
7023
7960
|
}
|
|
7024
7961
|
);
|
|
7962
|
+
const devOriginsInPublishedBuild = deps.cloud.supabaseUrl !== "" && deps.cloud.appOrigin === "";
|
|
7963
|
+
checks.push(
|
|
7964
|
+
devOriginsInPublishedBuild ? {
|
|
7965
|
+
name: "local origins",
|
|
7966
|
+
ok: false,
|
|
7967
|
+
detail: "local origins: dev origins enabled because CAYENNE_CLOUD_APP_ORIGIN is empty",
|
|
7968
|
+
fix: "Rebuild the runner with CAYENNE_CLOUD_APP_ORIGIN set to the app origin (tsup.config.ts `define`)."
|
|
7969
|
+
} : {
|
|
7970
|
+
name: "local origins",
|
|
7971
|
+
ok: true,
|
|
7972
|
+
detail: deps.cloud.appOrigin || "source checkout: dev origins"
|
|
7973
|
+
}
|
|
7974
|
+
);
|
|
7025
7975
|
return checks;
|
|
7026
7976
|
}
|
|
7027
7977
|
|
|
@@ -7495,7 +8445,8 @@ program.command("doctor").description("Check the install and say how to fix what
|
|
|
7495
8445
|
probe: (url) => probeBackend(url, backend?.anonKey ?? ""),
|
|
7496
8446
|
runnerId,
|
|
7497
8447
|
holder: runnerId ? holderOf({ root: cayenneHome(), runnerId }) : null,
|
|
7498
|
-
pty: await loadPty() !== null
|
|
8448
|
+
pty: await loadPty() !== null,
|
|
8449
|
+
cloud: { appOrigin: CLOUD_APP_ORIGIN, supabaseUrl: CLOUD_SUPABASE_URL }
|
|
7499
8450
|
});
|
|
7500
8451
|
for (const c of checks) {
|
|
7501
8452
|
console.log(`${c.ok ? "ok " : "FAIL"} ${c.name}: ${c.detail}`);
|