@flrande/browserctl 0.4.0-dev.15.1 → 0.5.0-dev.19.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/apps/browserctl/src/commands/act.test.ts +71 -0
- package/apps/browserctl/src/commands/act.ts +45 -1
- package/apps/browserctl/src/commands/command-wrappers.test.ts +302 -0
- package/apps/browserctl/src/commands/console-list.test.ts +102 -0
- package/apps/browserctl/src/commands/console-list.ts +89 -1
- package/apps/browserctl/src/commands/har-export.test.ts +112 -0
- package/apps/browserctl/src/commands/har-export.ts +120 -0
- package/apps/browserctl/src/commands/memory-delete.ts +20 -0
- package/apps/browserctl/src/commands/memory-inspect.ts +20 -0
- package/apps/browserctl/src/commands/memory-list.ts +90 -0
- package/apps/browserctl/src/commands/memory-mode-set.ts +29 -0
- package/apps/browserctl/src/commands/memory-purge.ts +16 -0
- package/apps/browserctl/src/commands/memory-resolve.ts +56 -0
- package/apps/browserctl/src/commands/memory-status.ts +16 -0
- package/apps/browserctl/src/commands/memory-ttl-set.ts +28 -0
- package/apps/browserctl/src/commands/memory-upsert.ts +142 -0
- package/apps/browserctl/src/commands/network-list.test.ts +110 -0
- package/apps/browserctl/src/commands/network-list.ts +112 -0
- package/apps/browserctl/src/commands/session-drop.test.ts +36 -0
- package/apps/browserctl/src/commands/session-drop.ts +16 -0
- package/apps/browserctl/src/commands/session-list.test.ts +81 -0
- package/apps/browserctl/src/commands/session-list.ts +70 -0
- package/apps/browserctl/src/commands/trace-get.test.ts +61 -0
- package/apps/browserctl/src/commands/trace-get.ts +62 -0
- package/apps/browserctl/src/commands/wait-element.test.ts +80 -0
- package/apps/browserctl/src/commands/wait-element.ts +76 -0
- package/apps/browserctl/src/commands/wait-text.test.ts +110 -0
- package/apps/browserctl/src/commands/wait-text.ts +93 -0
- package/apps/browserctl/src/commands/wait-url.test.ts +80 -0
- package/apps/browserctl/src/commands/wait-url.ts +76 -0
- package/apps/browserctl/src/main.dispatch.test.ts +206 -1
- package/apps/browserctl/src/main.test.ts +30 -0
- package/apps/browserctl/src/main.ts +246 -4
- package/apps/browserd/src/container.ts +1603 -48
- package/apps/browserd/src/main.test.ts +538 -1
- package/apps/browserd/src/tool-matrix.test.ts +492 -3
- package/package.json +5 -1
- package/packages/core/src/driver.ts +1 -1
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/navigation-memory.test.ts +259 -0
- package/packages/core/src/navigation-memory.ts +360 -0
- package/packages/core/src/session-store.test.ts +33 -0
- package/packages/core/src/session-store.ts +111 -6
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +112 -2
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +233 -10
- package/packages/driver-managed/src/managed-driver.test.ts +124 -0
- package/packages/driver-managed/src/managed-driver.ts +233 -17
- package/packages/driver-managed/src/managed-local-driver.test.ts +104 -2
- package/packages/driver-managed/src/managed-local-driver.ts +232 -10
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +112 -2
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +232 -10
- package/packages/transport-mcp-stdio/src/tool-map.ts +18 -1
|
@@ -111,6 +111,14 @@ describe("browserd container", () => {
|
|
|
111
111
|
expect(config.downloadRoot).toBeUndefined();
|
|
112
112
|
expect(config.authToken).toBeUndefined();
|
|
113
113
|
expect(config.authScopes).toEqual(["read", "act", "upload", "download"]);
|
|
114
|
+
expect(config.sessionTtlMs).toBe(30 * 60 * 1_000);
|
|
115
|
+
expect(config.sessionCleanupIntervalMs).toBe(60 * 1_000);
|
|
116
|
+
expect(config.domainAllowlistMode).toBe("off");
|
|
117
|
+
expect(config.domainAllowlist).toEqual([]);
|
|
118
|
+
expect(config.sessionMaxTotal).toBe(200);
|
|
119
|
+
expect(config.sessionMaxPerTenant).toBe(50);
|
|
120
|
+
expect(config.sessionRequireTenantPrefix).toBe(false);
|
|
121
|
+
expect(config.tenantAllowlist).toEqual([]);
|
|
114
122
|
});
|
|
115
123
|
|
|
116
124
|
it("registers managed-local, managed, chrome-relay, and remote-cdp drivers by default", () => {
|
|
@@ -129,7 +137,15 @@ describe("browserd container", () => {
|
|
|
129
137
|
BROWSERD_UPLOAD_ROOT: "C:\\safe\\uploads",
|
|
130
138
|
BROWSERD_DOWNLOAD_ROOT: "C:\\safe\\downloads",
|
|
131
139
|
BROWSERD_AUTH_TOKEN: "test-token",
|
|
132
|
-
BROWSERD_AUTH_SCOPES: " read, download "
|
|
140
|
+
BROWSERD_AUTH_SCOPES: " read, download ",
|
|
141
|
+
BROWSERD_SESSION_TTL_MS: "120000",
|
|
142
|
+
BROWSERD_SESSION_CLEANUP_INTERVAL_MS: "5000",
|
|
143
|
+
BROWSERD_DOMAIN_ALLOWLIST_MODE: "enforce",
|
|
144
|
+
BROWSERD_DOMAIN_ALLOWLIST: "example.com,*.corp.local",
|
|
145
|
+
BROWSERD_SESSION_MAX_TOTAL: "2",
|
|
146
|
+
BROWSERD_SESSION_MAX_PER_TENANT: "1",
|
|
147
|
+
BROWSERD_SESSION_REQUIRE_TENANT_PREFIX: "true",
|
|
148
|
+
BROWSERD_TENANT_ALLOWLIST: "finance,ops"
|
|
133
149
|
});
|
|
134
150
|
|
|
135
151
|
expect(config.remoteCdpUrl).toBe("http://127.0.0.1:9333/devtools/browser/override");
|
|
@@ -140,6 +156,37 @@ describe("browserd container", () => {
|
|
|
140
156
|
expect(config.downloadRoot).toBe("C:\\safe\\downloads");
|
|
141
157
|
expect(config.authToken).toBe("test-token");
|
|
142
158
|
expect(config.authScopes).toEqual(["read", "download"]);
|
|
159
|
+
expect(config.sessionTtlMs).toBe(120000);
|
|
160
|
+
expect(config.sessionCleanupIntervalMs).toBe(5000);
|
|
161
|
+
expect(config.domainAllowlistMode).toBe("enforce");
|
|
162
|
+
expect(config.domainAllowlist).toEqual(["example.com", "*.corp.local"]);
|
|
163
|
+
expect(config.sessionMaxTotal).toBe(2);
|
|
164
|
+
expect(config.sessionMaxPerTenant).toBe(1);
|
|
165
|
+
expect(config.sessionRequireTenantPrefix).toBe(true);
|
|
166
|
+
expect(config.tenantAllowlist).toEqual(["finance", "ops"]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("uses memory defaults in loadBrowserdConfig", () => {
|
|
170
|
+
const config = loadBrowserdConfig({});
|
|
171
|
+
|
|
172
|
+
expect(config.memoryEnabled).toBe(true);
|
|
173
|
+
expect(config.memoryMode).toBe("ask");
|
|
174
|
+
expect(config.memoryTtlDays).toBe(30);
|
|
175
|
+
expect(config.memoryPath).toBe(".browserctl-runtime/navigation-memory.json");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("uses memory env overrides in loadBrowserdConfig", () => {
|
|
179
|
+
const config = loadBrowserdConfig({
|
|
180
|
+
BROWSERD_MEMORY_ENABLED: "false",
|
|
181
|
+
BROWSERD_MEMORY_MODE: "auto",
|
|
182
|
+
BROWSERD_MEMORY_TTL_DAYS: "7",
|
|
183
|
+
BROWSERD_MEMORY_PATH: ".browserctl-runtime/memory-custom.json"
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(config.memoryEnabled).toBe(false);
|
|
187
|
+
expect(config.memoryMode).toBe("auto");
|
|
188
|
+
expect(config.memoryTtlDays).toBe(7);
|
|
189
|
+
expect(config.memoryPath).toBe(".browserctl-runtime/memory-custom.json");
|
|
143
190
|
});
|
|
144
191
|
|
|
145
192
|
it("parses chrome relay extension mode env values", () => {
|
|
@@ -677,6 +724,87 @@ describe("browserd bootstrap", () => {
|
|
|
677
724
|
output.end();
|
|
678
725
|
});
|
|
679
726
|
|
|
727
|
+
it("supports POSIX-style upload/download allowlist roots", async () => {
|
|
728
|
+
const input = new PassThrough();
|
|
729
|
+
const output = new PassThrough();
|
|
730
|
+
const runtime = bootstrapBrowserd({
|
|
731
|
+
env: createTestEnv({
|
|
732
|
+
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
733
|
+
BROWSERD_UPLOAD_ROOT: "/allowed/uploads",
|
|
734
|
+
BROWSERD_DOWNLOAD_ROOT: "/allowed/downloads"
|
|
735
|
+
}),
|
|
736
|
+
input,
|
|
737
|
+
output,
|
|
738
|
+
stdioProtocol: "legacy"
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const openResponse = await sendToolRequest(input, output, {
|
|
742
|
+
id: "request-posix-open",
|
|
743
|
+
name: "browser.tab.open",
|
|
744
|
+
traceId: "trace:posix:open",
|
|
745
|
+
arguments: {
|
|
746
|
+
sessionId: "session:posix-root",
|
|
747
|
+
url: "https://example.com/posix"
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
752
|
+
|
|
753
|
+
const uploadResponse = await sendToolRequest(input, output, {
|
|
754
|
+
id: "request-posix-upload",
|
|
755
|
+
name: "browser.upload.arm",
|
|
756
|
+
traceId: "trace:posix:upload",
|
|
757
|
+
arguments: {
|
|
758
|
+
sessionId: "session:posix-root",
|
|
759
|
+
targetId,
|
|
760
|
+
files: ["reports/q1.csv"]
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
expect(uploadResponse.ok).toBe(true);
|
|
765
|
+
expect(uploadResponse.data).toMatchObject({
|
|
766
|
+
files: ["/allowed/uploads/reports/q1.csv"]
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const downloadResponse = await sendToolRequest(input, output, {
|
|
770
|
+
id: "request-posix-download",
|
|
771
|
+
name: "browser.download.wait",
|
|
772
|
+
traceId: "trace:posix:download",
|
|
773
|
+
arguments: {
|
|
774
|
+
sessionId: "session:posix-root",
|
|
775
|
+
targetId,
|
|
776
|
+
path: "reports/out.bin"
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
expect(downloadResponse.ok).toBe(true);
|
|
781
|
+
expect(downloadResponse.data).toMatchObject({
|
|
782
|
+
download: {
|
|
783
|
+
path: "/allowed/downloads/reports/out.bin"
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
const traversalResponse = await sendToolRequest(input, output, {
|
|
788
|
+
id: "request-posix-download-traversal",
|
|
789
|
+
name: "browser.download.wait",
|
|
790
|
+
traceId: "trace:posix:download:traversal",
|
|
791
|
+
arguments: {
|
|
792
|
+
sessionId: "session:posix-root",
|
|
793
|
+
targetId,
|
|
794
|
+
path: "../escape.bin"
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
expect(traversalResponse.ok).toBe(false);
|
|
799
|
+
expect(traversalResponse.error).toMatchObject({
|
|
800
|
+
code: "E_PERMISSION"
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
runtime.close();
|
|
804
|
+
input.end();
|
|
805
|
+
output.end();
|
|
806
|
+
});
|
|
807
|
+
|
|
680
808
|
it("rejects download-wait when download root is not configured", async () => {
|
|
681
809
|
const input = new PassThrough();
|
|
682
810
|
const output = new PassThrough();
|
|
@@ -811,6 +939,66 @@ describe("browserd bootstrap", () => {
|
|
|
811
939
|
entries: []
|
|
812
940
|
});
|
|
813
941
|
|
|
942
|
+
const consoleFilteredResponse = await sendToolRequest(input, output, {
|
|
943
|
+
id: "request-console-filtered",
|
|
944
|
+
name: "browser.console.list",
|
|
945
|
+
traceId: "trace:tools:console:filtered",
|
|
946
|
+
arguments: {
|
|
947
|
+
sessionId: "session:tools",
|
|
948
|
+
targetId,
|
|
949
|
+
type: "error",
|
|
950
|
+
contains: "timeout",
|
|
951
|
+
since: "2026-01-01T00:00:00.000Z",
|
|
952
|
+
limit: 10
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
expect(consoleFilteredResponse.ok).toBe(true);
|
|
957
|
+
expect(consoleFilteredResponse.data).toEqual({
|
|
958
|
+
driver: "managed",
|
|
959
|
+
targetId,
|
|
960
|
+
entries: []
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
const networkListResponse = await sendToolRequest(input, output, {
|
|
964
|
+
id: "request-network-list",
|
|
965
|
+
name: "browser.network.list",
|
|
966
|
+
traceId: "trace:tools:network:list",
|
|
967
|
+
arguments: {
|
|
968
|
+
sessionId: "session:tools",
|
|
969
|
+
targetId,
|
|
970
|
+
limit: 20
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
expect(networkListResponse.ok).toBe(true);
|
|
975
|
+
expect(networkListResponse.data).toEqual({
|
|
976
|
+
driver: "managed",
|
|
977
|
+
targetId,
|
|
978
|
+
requests: []
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
const harExportResponse = await sendToolRequest(input, output, {
|
|
982
|
+
id: "request-network-har-export",
|
|
983
|
+
name: "browser.network.harExport",
|
|
984
|
+
traceId: "trace:tools:network:har",
|
|
985
|
+
arguments: {
|
|
986
|
+
sessionId: "session:tools",
|
|
987
|
+
targetId
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
expect(harExportResponse.ok).toBe(true);
|
|
992
|
+
expect(harExportResponse.data).toMatchObject({
|
|
993
|
+
driver: "managed",
|
|
994
|
+
targetId,
|
|
995
|
+
har: {
|
|
996
|
+
log: {
|
|
997
|
+
entries: []
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
|
|
814
1002
|
const networkResponse = await sendToolRequest(input, output, {
|
|
815
1003
|
id: "request-network",
|
|
816
1004
|
name: "browser.network.responseBody",
|
|
@@ -831,6 +1019,29 @@ describe("browserd bootstrap", () => {
|
|
|
831
1019
|
encoding: "utf8"
|
|
832
1020
|
});
|
|
833
1021
|
|
|
1022
|
+
const traceResponse = await sendToolRequest(input, output, {
|
|
1023
|
+
id: "request-trace-get",
|
|
1024
|
+
name: "browser.trace.get",
|
|
1025
|
+
traceId: "trace:tools:trace",
|
|
1026
|
+
arguments: {
|
|
1027
|
+
sessionId: "session:tools",
|
|
1028
|
+
limit: 50
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
expect(traceResponse.ok).toBe(true);
|
|
1033
|
+
expect(traceResponse.data).toMatchObject({
|
|
1034
|
+
sessionId: "session:tools"
|
|
1035
|
+
});
|
|
1036
|
+
const tracePayload = traceResponse.data as {
|
|
1037
|
+
steps?: Array<{ tool?: string }>;
|
|
1038
|
+
keyResponses?: Array<{ kind?: string }>;
|
|
1039
|
+
};
|
|
1040
|
+
expect(Array.isArray(tracePayload.steps)).toBe(true);
|
|
1041
|
+
expect(tracePayload.steps?.some((step) => step.tool === "browser.console.list")).toBe(true);
|
|
1042
|
+
expect(Array.isArray(tracePayload.keyResponses)).toBe(true);
|
|
1043
|
+
expect(tracePayload.keyResponses?.some((event) => event.kind === "network.list")).toBe(true);
|
|
1044
|
+
|
|
834
1045
|
const screenshotResponse = await sendToolRequest(input, output, {
|
|
835
1046
|
id: "request-screenshot",
|
|
836
1047
|
name: "browser.screenshot",
|
|
@@ -851,6 +1062,332 @@ describe("browserd bootstrap", () => {
|
|
|
851
1062
|
output.end();
|
|
852
1063
|
});
|
|
853
1064
|
|
|
1065
|
+
it("enforces domain allowlist for tab.open", async () => {
|
|
1066
|
+
const input = new PassThrough();
|
|
1067
|
+
const output = new PassThrough();
|
|
1068
|
+
const runtime = bootstrapBrowserd({
|
|
1069
|
+
env: createTestEnv({
|
|
1070
|
+
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
1071
|
+
BROWSERD_DOMAIN_ALLOWLIST_MODE: "enforce",
|
|
1072
|
+
BROWSERD_DOMAIN_ALLOWLIST: "example.com,*.corp.local"
|
|
1073
|
+
}),
|
|
1074
|
+
input,
|
|
1075
|
+
output,
|
|
1076
|
+
stdioProtocol: "legacy"
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
const allowed = await sendToolRequest(input, output, {
|
|
1080
|
+
id: "request-domain-allowed",
|
|
1081
|
+
name: "browser.tab.open",
|
|
1082
|
+
traceId: "trace:domain:allowed",
|
|
1083
|
+
arguments: {
|
|
1084
|
+
sessionId: "finance:domain-allowed",
|
|
1085
|
+
url: "https://example.com/home"
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
expect(allowed.ok).toBe(true);
|
|
1090
|
+
|
|
1091
|
+
const blocked = await sendToolRequest(input, output, {
|
|
1092
|
+
id: "request-domain-blocked",
|
|
1093
|
+
name: "browser.tab.open",
|
|
1094
|
+
traceId: "trace:domain:blocked",
|
|
1095
|
+
arguments: {
|
|
1096
|
+
sessionId: "finance:domain-blocked",
|
|
1097
|
+
url: "https://evil.test/home"
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
expect(blocked.ok).toBe(false);
|
|
1102
|
+
expect(blocked.error).toMatchObject({
|
|
1103
|
+
code: "E_PERMISSION"
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
runtime.close();
|
|
1107
|
+
input.end();
|
|
1108
|
+
output.end();
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it("enforces domain allowlist for act navigate/goto", async () => {
|
|
1112
|
+
const input = new PassThrough();
|
|
1113
|
+
const output = new PassThrough();
|
|
1114
|
+
const runtime = bootstrapBrowserd({
|
|
1115
|
+
env: createTestEnv({
|
|
1116
|
+
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
1117
|
+
BROWSERD_DOMAIN_ALLOWLIST_MODE: "enforce",
|
|
1118
|
+
BROWSERD_DOMAIN_ALLOWLIST: "example.com"
|
|
1119
|
+
}),
|
|
1120
|
+
input,
|
|
1121
|
+
output,
|
|
1122
|
+
stdioProtocol: "legacy"
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
const openResponse = await sendToolRequest(input, output, {
|
|
1126
|
+
id: "request-domain-act-open",
|
|
1127
|
+
name: "browser.tab.open",
|
|
1128
|
+
traceId: "trace:domain:act:open",
|
|
1129
|
+
arguments: {
|
|
1130
|
+
sessionId: "finance:domain-act",
|
|
1131
|
+
url: "https://example.com"
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
1135
|
+
|
|
1136
|
+
const actResponse = await sendToolRequest(input, output, {
|
|
1137
|
+
id: "request-domain-act-blocked",
|
|
1138
|
+
name: "browser.act",
|
|
1139
|
+
traceId: "trace:domain:act:blocked",
|
|
1140
|
+
arguments: {
|
|
1141
|
+
sessionId: "finance:domain-act",
|
|
1142
|
+
targetId,
|
|
1143
|
+
action: {
|
|
1144
|
+
type: "navigate",
|
|
1145
|
+
payload: {
|
|
1146
|
+
url: "https://unauthorized.example.net"
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
expect(actResponse.ok).toBe(false);
|
|
1153
|
+
expect(actResponse.error).toMatchObject({
|
|
1154
|
+
code: "E_PERMISSION"
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
runtime.close();
|
|
1158
|
+
input.end();
|
|
1159
|
+
output.end();
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it("enforces max total session limit", async () => {
|
|
1163
|
+
const input = new PassThrough();
|
|
1164
|
+
const output = new PassThrough();
|
|
1165
|
+
const runtime = bootstrapBrowserd({
|
|
1166
|
+
env: createTestEnv({
|
|
1167
|
+
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
1168
|
+
BROWSERD_SESSION_MAX_TOTAL: "1"
|
|
1169
|
+
}),
|
|
1170
|
+
input,
|
|
1171
|
+
output,
|
|
1172
|
+
stdioProtocol: "legacy"
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
const first = await sendToolRequest(input, output, {
|
|
1176
|
+
id: "request-session-limit-first",
|
|
1177
|
+
name: "browser.status",
|
|
1178
|
+
traceId: "trace:session-limit:first",
|
|
1179
|
+
arguments: {
|
|
1180
|
+
sessionId: "team-a:first"
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
expect(first.ok).toBe(true);
|
|
1184
|
+
|
|
1185
|
+
const second = await sendToolRequest(input, output, {
|
|
1186
|
+
id: "request-session-limit-second",
|
|
1187
|
+
name: "browser.status",
|
|
1188
|
+
traceId: "trace:session-limit:second",
|
|
1189
|
+
arguments: {
|
|
1190
|
+
sessionId: "team-b:second"
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
expect(second.ok).toBe(false);
|
|
1194
|
+
expect(second.error).toMatchObject({
|
|
1195
|
+
code: "E_CONFLICT"
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
runtime.close();
|
|
1199
|
+
input.end();
|
|
1200
|
+
output.end();
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
it("enforces tenant prefix, allowlist, and per-tenant session limit", async () => {
|
|
1204
|
+
const input = new PassThrough();
|
|
1205
|
+
const output = new PassThrough();
|
|
1206
|
+
const runtime = bootstrapBrowserd({
|
|
1207
|
+
env: createTestEnv({
|
|
1208
|
+
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
1209
|
+
BROWSERD_SESSION_REQUIRE_TENANT_PREFIX: "true",
|
|
1210
|
+
BROWSERD_TENANT_ALLOWLIST: "finance,ops",
|
|
1211
|
+
BROWSERD_SESSION_MAX_TOTAL: "10",
|
|
1212
|
+
BROWSERD_SESSION_MAX_PER_TENANT: "1"
|
|
1213
|
+
}),
|
|
1214
|
+
input,
|
|
1215
|
+
output,
|
|
1216
|
+
stdioProtocol: "legacy"
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
const missingTenant = await sendToolRequest(input, output, {
|
|
1220
|
+
id: "request-tenant-missing",
|
|
1221
|
+
name: "browser.status",
|
|
1222
|
+
traceId: "trace:tenant:missing",
|
|
1223
|
+
arguments: {
|
|
1224
|
+
sessionId: "missingTenant"
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
expect(missingTenant.ok).toBe(false);
|
|
1228
|
+
expect(missingTenant.error).toMatchObject({
|
|
1229
|
+
code: "E_PERMISSION"
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
const disallowedTenant = await sendToolRequest(input, output, {
|
|
1233
|
+
id: "request-tenant-disallowed",
|
|
1234
|
+
name: "browser.status",
|
|
1235
|
+
traceId: "trace:tenant:disallowed",
|
|
1236
|
+
arguments: {
|
|
1237
|
+
sessionId: "personal:task-1"
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
expect(disallowedTenant.ok).toBe(false);
|
|
1241
|
+
expect(disallowedTenant.error).toMatchObject({
|
|
1242
|
+
code: "E_PERMISSION"
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
const financeFirst = await sendToolRequest(input, output, {
|
|
1246
|
+
id: "request-tenant-finance-first",
|
|
1247
|
+
name: "browser.status",
|
|
1248
|
+
traceId: "trace:tenant:finance:first",
|
|
1249
|
+
arguments: {
|
|
1250
|
+
sessionId: "finance:task-1"
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
expect(financeFirst.ok).toBe(true);
|
|
1254
|
+
|
|
1255
|
+
const financeSecond = await sendToolRequest(input, output, {
|
|
1256
|
+
id: "request-tenant-finance-second",
|
|
1257
|
+
name: "browser.status",
|
|
1258
|
+
traceId: "trace:tenant:finance:second",
|
|
1259
|
+
arguments: {
|
|
1260
|
+
sessionId: "finance:task-2"
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
expect(financeSecond.ok).toBe(false);
|
|
1264
|
+
expect(financeSecond.error).toMatchObject({
|
|
1265
|
+
code: "E_CONFLICT"
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
const opsFirst = await sendToolRequest(input, output, {
|
|
1269
|
+
id: "request-tenant-ops-first",
|
|
1270
|
+
name: "browser.status",
|
|
1271
|
+
traceId: "trace:tenant:ops:first",
|
|
1272
|
+
arguments: {
|
|
1273
|
+
sessionId: "ops:task-1"
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
expect(opsFirst.ok).toBe(true);
|
|
1277
|
+
|
|
1278
|
+
runtime.close();
|
|
1279
|
+
input.end();
|
|
1280
|
+
output.end();
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
it("supports session-list and session-drop governance tools", async () => {
|
|
1284
|
+
const input = new PassThrough();
|
|
1285
|
+
const output = new PassThrough();
|
|
1286
|
+
const runtime = bootstrapBrowserd({
|
|
1287
|
+
env: createTestEnv({
|
|
1288
|
+
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
1289
|
+
}),
|
|
1290
|
+
input,
|
|
1291
|
+
output,
|
|
1292
|
+
stdioProtocol: "legacy"
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
const financeStatus = await sendToolRequest(input, output, {
|
|
1296
|
+
id: "request-session-governance-finance",
|
|
1297
|
+
name: "browser.status",
|
|
1298
|
+
traceId: "trace:session-governance:finance",
|
|
1299
|
+
arguments: {
|
|
1300
|
+
sessionId: "finance:task-1"
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
expect(financeStatus.ok).toBe(true);
|
|
1304
|
+
|
|
1305
|
+
const opsStatus = await sendToolRequest(input, output, {
|
|
1306
|
+
id: "request-session-governance-ops",
|
|
1307
|
+
name: "browser.status",
|
|
1308
|
+
traceId: "trace:session-governance:ops",
|
|
1309
|
+
arguments: {
|
|
1310
|
+
sessionId: "ops:task-1"
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
expect(opsStatus.ok).toBe(true);
|
|
1314
|
+
|
|
1315
|
+
const listBeforeDrop = await sendToolRequest(input, output, {
|
|
1316
|
+
id: "request-session-list-before",
|
|
1317
|
+
name: "browser.session.list",
|
|
1318
|
+
traceId: "trace:session-governance:list:before",
|
|
1319
|
+
arguments: {
|
|
1320
|
+
sessionId: "finance:task-1",
|
|
1321
|
+
limit: 20
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
expect(listBeforeDrop.ok).toBe(true);
|
|
1325
|
+
const listPayloadBeforeDrop = listBeforeDrop.data as { sessions: Array<{ sessionId: string }> };
|
|
1326
|
+
expect(Array.isArray(listPayloadBeforeDrop.sessions)).toBe(true);
|
|
1327
|
+
expect(listPayloadBeforeDrop.sessions.some((entry) => entry.sessionId === "ops:task-1")).toBe(true);
|
|
1328
|
+
|
|
1329
|
+
const dropResponse = await sendToolRequest(input, output, {
|
|
1330
|
+
id: "request-session-drop",
|
|
1331
|
+
name: "browser.session.drop",
|
|
1332
|
+
traceId: "trace:session-governance:drop",
|
|
1333
|
+
arguments: {
|
|
1334
|
+
sessionId: "finance:task-1",
|
|
1335
|
+
sessionIdToDelete: "ops:task-1"
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
expect(dropResponse.ok).toBe(false);
|
|
1339
|
+
expect(dropResponse.error).toMatchObject({
|
|
1340
|
+
code: "E_PERMISSION"
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
const listAfterDrop = await sendToolRequest(input, output, {
|
|
1344
|
+
id: "request-session-list-after",
|
|
1345
|
+
name: "browser.session.list",
|
|
1346
|
+
traceId: "trace:session-governance:list:after",
|
|
1347
|
+
arguments: {
|
|
1348
|
+
sessionId: "finance:task-1",
|
|
1349
|
+
limit: 20
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
expect(listAfterDrop.ok).toBe(true);
|
|
1353
|
+
const listPayloadAfterDrop = listAfterDrop.data as { sessions: Array<{ sessionId: string }> };
|
|
1354
|
+
expect(listPayloadAfterDrop.sessions.some((entry) => entry.sessionId === "ops:task-1")).toBe(true);
|
|
1355
|
+
|
|
1356
|
+
const selfDropResponse = await sendToolRequest(input, output, {
|
|
1357
|
+
id: "request-session-drop-self",
|
|
1358
|
+
name: "browser.session.drop",
|
|
1359
|
+
traceId: "trace:session-governance:drop:self",
|
|
1360
|
+
arguments: {
|
|
1361
|
+
sessionId: "finance:task-1",
|
|
1362
|
+
sessionIdToDelete: "finance:task-1"
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
expect(selfDropResponse.ok).toBe(true);
|
|
1366
|
+
expect(selfDropResponse.data).toMatchObject({
|
|
1367
|
+
sessionIdToDelete: "finance:task-1",
|
|
1368
|
+
dropped: true
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
const listAfterSelfDrop = await sendToolRequest(input, output, {
|
|
1372
|
+
id: "request-session-list-after-self-drop",
|
|
1373
|
+
name: "browser.session.list",
|
|
1374
|
+
traceId: "trace:session-governance:list:after:self",
|
|
1375
|
+
arguments: {
|
|
1376
|
+
sessionId: "ops:task-1",
|
|
1377
|
+
limit: 20
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
expect(listAfterSelfDrop.ok).toBe(true);
|
|
1381
|
+
const listPayloadAfterSelfDrop = listAfterSelfDrop.data as {
|
|
1382
|
+
sessions: Array<{ sessionId: string }>;
|
|
1383
|
+
};
|
|
1384
|
+
expect(listPayloadAfterSelfDrop.sessions.some((entry) => entry.sessionId === "finance:task-1")).toBe(false);
|
|
1385
|
+
|
|
1386
|
+
runtime.close();
|
|
1387
|
+
input.end();
|
|
1388
|
+
output.end();
|
|
1389
|
+
});
|
|
1390
|
+
|
|
854
1391
|
it("preserves id and trace/session metadata when queue-level error handling runs", async () => {
|
|
855
1392
|
const input = new PassThrough();
|
|
856
1393
|
const output = new PassThrough();
|