opensoma 0.5.0 → 0.6.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/package.json +5 -1
- package/dist/src/agent-browser-launcher.d.ts +43 -0
- package/dist/src/agent-browser-launcher.d.ts.map +1 -0
- package/dist/src/agent-browser-launcher.js +97 -0
- package/dist/src/agent-browser-launcher.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +36 -7
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +231 -63
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/agent-browser.d.ts +3 -0
- package/dist/src/commands/agent-browser.d.ts.map +1 -0
- package/dist/src/commands/agent-browser.js +27 -0
- package/dist/src/commands/agent-browser.js.map +1 -0
- package/dist/src/commands/auth.d.ts +1 -1
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +4 -2
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/dashboard.d.ts +13 -0
- package/dist/src/commands/dashboard.d.ts.map +1 -1
- package/dist/src/commands/dashboard.js +10 -18
- package/dist/src/commands/dashboard.js.map +1 -1
- package/dist/src/commands/helpers.d.ts +1 -1
- package/dist/src/commands/helpers.d.ts.map +1 -1
- package/dist/src/commands/helpers.js +2 -2
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/index.d.ts +2 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/mentoring.d.ts.map +1 -1
- package/dist/src/commands/mentoring.js +54 -29
- package/dist/src/commands/mentoring.js.map +1 -1
- package/dist/src/commands/notice.d.ts.map +1 -1
- package/dist/src/commands/notice.js +2 -1
- package/dist/src/commands/notice.js.map +1 -1
- package/dist/src/commands/report.d.ts.map +1 -1
- package/dist/src/commands/report.js +4 -2
- package/dist/src/commands/report.js.map +1 -1
- package/dist/src/commands/room.d.ts.map +1 -1
- package/dist/src/commands/room.js +125 -2
- package/dist/src/commands/room.js.map +1 -1
- package/dist/src/commands/schedule.d.ts +3 -0
- package/dist/src/commands/schedule.d.ts.map +1 -0
- package/dist/src/commands/schedule.js +27 -0
- package/dist/src/commands/schedule.js.map +1 -0
- package/dist/src/commands/team.d.ts.map +1 -1
- package/dist/src/commands/team.js +55 -4
- package/dist/src/commands/team.js.map +1 -1
- package/dist/src/constants.d.ts +5 -5
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +20 -8
- package/dist/src/constants.js.map +1 -1
- package/dist/src/credential-manager.d.ts +9 -0
- package/dist/src/credential-manager.d.ts.map +1 -1
- package/dist/src/credential-manager.js +24 -0
- package/dist/src/credential-manager.js.map +1 -1
- package/dist/src/formatters.d.ts +11 -3
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +281 -52
- package/dist/src/formatters.js.map +1 -1
- package/dist/src/http.d.ts +8 -0
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +29 -1
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/shared/utils/swmaestro.d.ts +34 -1
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js +102 -43
- package/dist/src/shared/utils/swmaestro.js.map +1 -1
- package/dist/src/shared/utils/team-action-params.d.ts +3 -0
- package/dist/src/shared/utils/team-action-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-action-params.js +10 -0
- package/dist/src/shared/utils/team-action-params.js.map +1 -0
- package/dist/src/shared/utils/team-params.d.ts +12 -0
- package/dist/src/shared/utils/team-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-params.js +38 -0
- package/dist/src/shared/utils/team-params.js.map +1 -0
- package/dist/src/types.d.ts +147 -10
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +74 -6
- package/dist/src/types.js.map +1 -1
- package/package.json +5 -1
- package/src/agent-browser-launcher.test.ts +263 -0
- package/src/agent-browser-launcher.ts +159 -0
- package/src/cli.ts +4 -2
- package/src/client.test.ts +801 -140
- package/src/client.ts +293 -79
- package/src/commands/agent-browser.ts +33 -0
- package/src/commands/auth.test.ts +83 -32
- package/src/commands/auth.ts +5 -3
- package/src/commands/dashboard.test.ts +57 -0
- package/src/commands/dashboard.ts +22 -19
- package/src/commands/helpers.test.ts +79 -32
- package/src/commands/helpers.ts +3 -3
- package/src/commands/index.ts +2 -1
- package/src/commands/mentoring.ts +60 -29
- package/src/commands/notice.ts +2 -1
- package/src/commands/report.test.ts +7 -7
- package/src/commands/report.ts +4 -2
- package/src/commands/room.ts +160 -1
- package/src/commands/schedule.ts +32 -0
- package/src/commands/team.ts +73 -5
- package/src/constants.ts +20 -8
- package/src/credential-manager.test.ts +49 -5
- package/src/credential-manager.ts +27 -0
- package/src/formatters.test.ts +548 -53
- package/src/formatters.ts +309 -55
- package/src/http.test.ts +108 -39
- package/src/http.ts +41 -2
- package/src/index.ts +10 -1
- package/src/shared/utils/mentoring-params.test.ts +16 -16
- package/src/shared/utils/swmaestro.test.ts +326 -11
- package/src/shared/utils/swmaestro.ts +150 -52
- package/src/shared/utils/team-action-params.test.ts +32 -0
- package/src/shared/utils/team-action-params.ts +10 -0
- package/src/shared/utils/team-params.test.ts +141 -0
- package/src/shared/utils/team-params.ts +53 -0
- package/src/shared/utils/toz.test.ts +12 -7
- package/src/token-extractor.test.ts +12 -12
- package/src/toz-http.test.ts +11 -11
- package/src/types.test.ts +235 -206
- package/src/types.ts +87 -7
- package/dist/src/commands/event.d.ts +0 -3
- package/dist/src/commands/event.d.ts.map +0 -1
- package/dist/src/commands/event.js +0 -58
- package/dist/src/commands/event.js.map +0 -1
- package/src/commands/event.ts +0 -73
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { AgentBrowserLauncher, assertSwmaestroUrl, buildStorageState } from './agent-browser-launcher'
|
|
8
|
+
import type { Spawner } from './agent-browser-launcher'
|
|
9
|
+
|
|
10
|
+
let createdDirs: string[] = []
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
for (const dir of createdDirs) {
|
|
14
|
+
await rm(dir, { recursive: true, force: true })
|
|
15
|
+
}
|
|
16
|
+
createdDirs = []
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('buildStorageState', () => {
|
|
20
|
+
it('returns a Playwright-compatible storage state with JSESSIONID host-only on www.swmaestro.ai', () => {
|
|
21
|
+
const state = buildStorageState('test-session-value')
|
|
22
|
+
|
|
23
|
+
expect(state).toEqual({
|
|
24
|
+
cookies: [
|
|
25
|
+
{
|
|
26
|
+
name: 'JSESSIONID',
|
|
27
|
+
value: 'test-session-value',
|
|
28
|
+
domain: 'www.swmaestro.ai',
|
|
29
|
+
path: '/',
|
|
30
|
+
expires: -1,
|
|
31
|
+
httpOnly: true,
|
|
32
|
+
secure: true,
|
|
33
|
+
sameSite: 'Lax',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
origins: [],
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('does not use a leading-dot domain (host-only, not domain cookie)', () => {
|
|
41
|
+
const state = buildStorageState('x')
|
|
42
|
+
expect(state.cookies[0]?.domain).toBe('www.swmaestro.ai')
|
|
43
|
+
expect(state.cookies[0]?.domain.startsWith('.')).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('assertSwmaestroUrl', () => {
|
|
48
|
+
it('accepts https://www.swmaestro.ai URLs', () => {
|
|
49
|
+
expect(() => assertSwmaestroUrl('https://www.swmaestro.ai/sw/main/main.do')).not.toThrow()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('accepts the bare swmaestro.ai apex', () => {
|
|
53
|
+
expect(() => assertSwmaestroUrl('https://swmaestro.ai/')).not.toThrow()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('rejects non-swmaestro hosts', () => {
|
|
57
|
+
expect(() => assertSwmaestroUrl('https://evil.example.com/path')).toThrow(/swmaestro\.ai/)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('rejects http:// (forces https)', () => {
|
|
61
|
+
expect(() => assertSwmaestroUrl('http://www.swmaestro.ai/')).toThrow(/https/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('rejects malformed URLs', () => {
|
|
65
|
+
expect(() => assertSwmaestroUrl('not-a-url')).toThrow(/Invalid URL/)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('AgentBrowserLauncher.launch', () => {
|
|
70
|
+
it('writes a state file with the JSESSIONID and invokes agent-browser with --state and open', async () => {
|
|
71
|
+
const tmpRoot = await makeTempRoot()
|
|
72
|
+
const allCommands: string[][] = []
|
|
73
|
+
let stateContent = ''
|
|
74
|
+
let stateMode = 0
|
|
75
|
+
let dirMode = 0
|
|
76
|
+
|
|
77
|
+
const spawn: Spawner = (command) => ({
|
|
78
|
+
exited: (async () => {
|
|
79
|
+
allCommands.push([...command])
|
|
80
|
+
if (command[1] === '--state') {
|
|
81
|
+
const statePath = command[2]
|
|
82
|
+
if (!statePath) throw new Error('missing state path argument')
|
|
83
|
+
stateContent = await readFile(statePath, 'utf8')
|
|
84
|
+
stateMode = (await stat(statePath)).mode & 0o777
|
|
85
|
+
const dir = statePath.replace(/\/state\.json$/, '')
|
|
86
|
+
dirMode = (await stat(dir)).mode & 0o777
|
|
87
|
+
}
|
|
88
|
+
return 0
|
|
89
|
+
})(),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const launcher = new AgentBrowserLauncher({ spawn, binary: 'agent-browser', tmpDir: tmpRoot })
|
|
93
|
+
const result = await launcher.launch({
|
|
94
|
+
url: 'https://www.swmaestro.ai/sw/mypage/myMain/dashboard.do',
|
|
95
|
+
sessionCookie: 'abc-123-jsessionid',
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expect(result.exitCode).toBe(0)
|
|
99
|
+
expect(allCommands[0]).toEqual(['agent-browser', 'close', '--all'])
|
|
100
|
+
|
|
101
|
+
const launchCmd = allCommands[1]
|
|
102
|
+
expect(launchCmd?.[0]).toBe('agent-browser')
|
|
103
|
+
expect(launchCmd?.[1]).toBe('--state')
|
|
104
|
+
expect(launchCmd?.[3]).toBe('open')
|
|
105
|
+
expect(launchCmd?.[4]).toBe('https://www.swmaestro.ai/sw/mypage/myMain/dashboard.do')
|
|
106
|
+
|
|
107
|
+
const parsed = JSON.parse(stateContent) as { cookies: Array<{ name: string; value: string }> }
|
|
108
|
+
expect(parsed.cookies[0]?.name).toBe('JSESSIONID')
|
|
109
|
+
expect(parsed.cookies[0]?.value).toBe('abc-123-jsessionid')
|
|
110
|
+
|
|
111
|
+
expect(stateMode).toBe(0o600)
|
|
112
|
+
expect(dirMode).toBe(0o700)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('closes any running daemon before launching so --state is not silently ignored', async () => {
|
|
116
|
+
const tmpRoot = await makeTempRoot()
|
|
117
|
+
const calls: string[] = []
|
|
118
|
+
|
|
119
|
+
const spawn: Spawner = (command) => ({
|
|
120
|
+
exited: (async () => {
|
|
121
|
+
if (command[1] === 'close') calls.push('close')
|
|
122
|
+
if (command[1] === '--state') calls.push('launch')
|
|
123
|
+
return 0
|
|
124
|
+
})(),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
await new AgentBrowserLauncher({ spawn, tmpDir: tmpRoot }).launch({
|
|
128
|
+
url: 'https://www.swmaestro.ai/sw/main/main.do',
|
|
129
|
+
sessionCookie: 'x',
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(calls).toEqual(['close', 'launch'])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('never includes the JSESSIONID value in argv', async () => {
|
|
136
|
+
const tmpRoot = await makeTempRoot()
|
|
137
|
+
const allArgs: string[] = []
|
|
138
|
+
|
|
139
|
+
const spawn: Spawner = (command) => ({
|
|
140
|
+
exited: (async () => {
|
|
141
|
+
allArgs.push(...command)
|
|
142
|
+
return 0
|
|
143
|
+
})(),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const launcher = new AgentBrowserLauncher({ spawn, tmpDir: tmpRoot })
|
|
147
|
+
await launcher.launch({
|
|
148
|
+
url: 'https://www.swmaestro.ai/sw/main/main.do',
|
|
149
|
+
sessionCookie: 'super-secret-jsessionid-value',
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
for (const arg of allArgs) {
|
|
153
|
+
expect(arg).not.toContain('super-secret-jsessionid-value')
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('cleans up the state directory after agent-browser exits successfully', async () => {
|
|
158
|
+
const tmpRoot = await makeTempRoot()
|
|
159
|
+
let capturedStatePath = ''
|
|
160
|
+
|
|
161
|
+
const spawn: Spawner = (command) => ({
|
|
162
|
+
exited: (async () => {
|
|
163
|
+
if (command[1] === '--state') {
|
|
164
|
+
capturedStatePath = command[2] ?? ''
|
|
165
|
+
}
|
|
166
|
+
return 0
|
|
167
|
+
})(),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
await new AgentBrowserLauncher({ spawn, tmpDir: tmpRoot }).launch({
|
|
171
|
+
url: 'https://www.swmaestro.ai/sw/main/main.do',
|
|
172
|
+
sessionCookie: 'x',
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(existsSync(capturedStatePath)).toBe(false)
|
|
176
|
+
const remaining = await readdir(tmpRoot)
|
|
177
|
+
expect(remaining).toEqual([])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('cleans up the state directory even if agent-browser throws', async () => {
|
|
181
|
+
const tmpRoot = await makeTempRoot()
|
|
182
|
+
let capturedStatePath = ''
|
|
183
|
+
|
|
184
|
+
const spawn: Spawner = (command) => ({
|
|
185
|
+
exited: (async () => {
|
|
186
|
+
if (command[1] === '--state') {
|
|
187
|
+
capturedStatePath = command[2] ?? ''
|
|
188
|
+
throw new Error('boom')
|
|
189
|
+
}
|
|
190
|
+
return 0
|
|
191
|
+
})(),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
await expect(
|
|
195
|
+
new AgentBrowserLauncher({ spawn, tmpDir: tmpRoot }).launch({
|
|
196
|
+
url: 'https://www.swmaestro.ai/sw/main/main.do',
|
|
197
|
+
sessionCookie: 'x',
|
|
198
|
+
}),
|
|
199
|
+
).rejects.toThrow('boom')
|
|
200
|
+
|
|
201
|
+
expect(existsSync(capturedStatePath)).toBe(false)
|
|
202
|
+
const remaining = await readdir(tmpRoot)
|
|
203
|
+
expect(remaining).toEqual([])
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('propagates the agent-browser non-zero exit code', async () => {
|
|
207
|
+
const tmpRoot = await makeTempRoot()
|
|
208
|
+
const spawn: Spawner = (command) => ({
|
|
209
|
+
exited: Promise.resolve(command[1] === '--state' ? 42 : 0),
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const result = await new AgentBrowserLauncher({ spawn, tmpDir: tmpRoot }).launch({
|
|
213
|
+
url: 'https://www.swmaestro.ai/sw/main/main.do',
|
|
214
|
+
sessionCookie: 'x',
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
expect(result.exitCode).toBe(42)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('uses a custom binary path when provided', async () => {
|
|
221
|
+
const tmpRoot = await makeTempRoot()
|
|
222
|
+
const observedBinaries: string[] = []
|
|
223
|
+
const spawn: Spawner = (command) => ({
|
|
224
|
+
exited: (async () => {
|
|
225
|
+
observedBinaries.push(command[0] ?? '')
|
|
226
|
+
return 0
|
|
227
|
+
})(),
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
await new AgentBrowserLauncher({ spawn, tmpDir: tmpRoot, binary: '/custom/path/agent-browser' }).launch({
|
|
231
|
+
url: 'https://www.swmaestro.ai/sw/main/main.do',
|
|
232
|
+
sessionCookie: 'x',
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
expect(new Set(observedBinaries)).toEqual(new Set(['/custom/path/agent-browser']))
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('rejects non-swmaestro URLs before spawning anything', async () => {
|
|
239
|
+
const tmpRoot = await makeTempRoot()
|
|
240
|
+
let spawnCalled = false
|
|
241
|
+
const spawn: Spawner = () => {
|
|
242
|
+
spawnCalled = true
|
|
243
|
+
return { exited: Promise.resolve(0) }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await expect(
|
|
247
|
+
new AgentBrowserLauncher({ spawn, tmpDir: tmpRoot }).launch({
|
|
248
|
+
url: 'https://evil.example.com/path',
|
|
249
|
+
sessionCookie: 'x',
|
|
250
|
+
}),
|
|
251
|
+
).rejects.toThrow(/swmaestro\.ai/)
|
|
252
|
+
|
|
253
|
+
expect(spawnCalled).toBe(false)
|
|
254
|
+
const remaining = await readdir(tmpRoot)
|
|
255
|
+
expect(remaining).toEqual([])
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
async function makeTempRoot(): Promise<string> {
|
|
260
|
+
const dir = await mkdtemp(join(tmpdir(), 'opensoma-launcher-test-'))
|
|
261
|
+
createdDirs.push(dir)
|
|
262
|
+
return dir
|
|
263
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
// Minimal declaration so the web workspace tsconfig (which lacks @types/bun)
|
|
7
|
+
// can type-check this file without error. The real implementation always runs
|
|
8
|
+
// under Bun, which provides the full global at runtime.
|
|
9
|
+
declare const Bun: {
|
|
10
|
+
spawn(
|
|
11
|
+
command: string[],
|
|
12
|
+
options: { stdout: 'inherit'; stderr: 'inherit'; stdin: 'inherit' },
|
|
13
|
+
): { exited: Promise<number> }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AgentBrowserLaunchInput {
|
|
17
|
+
url: string
|
|
18
|
+
sessionCookie: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SpawnedProcess {
|
|
22
|
+
exited: Promise<number>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type Spawner = (command: string[]) => SpawnedProcess
|
|
26
|
+
|
|
27
|
+
export interface AgentBrowserLauncherOptions {
|
|
28
|
+
spawn?: Spawner
|
|
29
|
+
binary?: string
|
|
30
|
+
tmpDir?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LaunchResult {
|
|
34
|
+
exitCode: number
|
|
35
|
+
statePath: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SWMAESTRO_HOST = 'www.swmaestro.ai'
|
|
39
|
+
const ALLOWED_HOSTS = new Set([SWMAESTRO_HOST, 'swmaestro.ai'])
|
|
40
|
+
|
|
41
|
+
interface StorageStateCookie {
|
|
42
|
+
name: string
|
|
43
|
+
value: string
|
|
44
|
+
domain: string
|
|
45
|
+
path: string
|
|
46
|
+
expires: number
|
|
47
|
+
httpOnly: boolean
|
|
48
|
+
secure: boolean
|
|
49
|
+
sameSite: 'Strict' | 'Lax' | 'None'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface StorageState {
|
|
53
|
+
cookies: StorageStateCookie[]
|
|
54
|
+
origins: never[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildStorageState(sessionCookie: string): StorageState {
|
|
58
|
+
// Java EE JSESSIONID is host-only on the exact host that issued it. Use
|
|
59
|
+
// www.swmaestro.ai (no leading dot) to match how the native server sets it.
|
|
60
|
+
// Using `.swmaestro.ai` here would either fail to import or produce a
|
|
61
|
+
// domain-cookie that the server treats as foreign.
|
|
62
|
+
return {
|
|
63
|
+
cookies: [
|
|
64
|
+
{
|
|
65
|
+
name: 'JSESSIONID',
|
|
66
|
+
value: sessionCookie,
|
|
67
|
+
domain: SWMAESTRO_HOST,
|
|
68
|
+
path: '/',
|
|
69
|
+
// -1 is the Playwright/agent-browser convention for session cookies
|
|
70
|
+
// (no Expires/Max-Age attribute). JSESSIONID is a session cookie.
|
|
71
|
+
expires: -1,
|
|
72
|
+
httpOnly: true,
|
|
73
|
+
secure: true,
|
|
74
|
+
sameSite: 'Lax',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
origins: [],
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function assertSwmaestroUrl(rawUrl: string): URL {
|
|
82
|
+
let parsed: URL
|
|
83
|
+
try {
|
|
84
|
+
parsed = new URL(rawUrl)
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error(`Invalid URL: ${rawUrl}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (parsed.protocol !== 'https:') {
|
|
90
|
+
throw new Error(`URL must use https:// (got ${parsed.protocol}//): ${rawUrl}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`URL must point to swmaestro.ai (got ${parsed.hostname}). Cookie injection only works for swmaestro.ai targets.`,
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return parsed
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function defaultSpawn(command: string[]): SpawnedProcess {
|
|
103
|
+
// Bun.spawn with array args avoids shell interpretation entirely. The
|
|
104
|
+
// randomized state-file path appears in argv but the JSESSIONID value
|
|
105
|
+
// never does — that's the security guarantee.
|
|
106
|
+
const proc = Bun.spawn(command, {
|
|
107
|
+
stdout: 'inherit',
|
|
108
|
+
stderr: 'inherit',
|
|
109
|
+
stdin: 'inherit',
|
|
110
|
+
})
|
|
111
|
+
return { exited: proc.exited }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class AgentBrowserLauncher {
|
|
115
|
+
private readonly spawn: Spawner
|
|
116
|
+
private readonly binary: string
|
|
117
|
+
private readonly tmpRoot: string
|
|
118
|
+
|
|
119
|
+
constructor(options: AgentBrowserLauncherOptions = {}) {
|
|
120
|
+
this.spawn = options.spawn ?? defaultSpawn
|
|
121
|
+
this.binary = options.binary ?? 'agent-browser'
|
|
122
|
+
this.tmpRoot = options.tmpDir ?? tmpdir()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async launch(input: AgentBrowserLaunchInput): Promise<LaunchResult> {
|
|
126
|
+
const target = assertSwmaestroUrl(input.url)
|
|
127
|
+
const state = buildStorageState(input.sessionCookie)
|
|
128
|
+
|
|
129
|
+
// agent-browser ignores --state if a daemon is already running. Close
|
|
130
|
+
// first so our cookie injection actually takes effect. We don't care
|
|
131
|
+
// about the close exit code (no-op if no daemon was running).
|
|
132
|
+
await this.spawn([this.binary, 'close', '--all']).exited
|
|
133
|
+
|
|
134
|
+
const stateDir = await this.createStateDir()
|
|
135
|
+
const statePath = join(stateDir, 'state.json')
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await writeFile(statePath, JSON.stringify(state), { mode: 0o600 })
|
|
139
|
+
await chmod(statePath, 0o600)
|
|
140
|
+
|
|
141
|
+
const proc = this.spawn([this.binary, '--state', statePath, 'open', target.toString()])
|
|
142
|
+
const exitCode = await proc.exited
|
|
143
|
+
return { exitCode, statePath }
|
|
144
|
+
} finally {
|
|
145
|
+
await rm(stateDir, { recursive: true, force: true })
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async createStateDir(): Promise<string> {
|
|
150
|
+
// Random suffix on the directory name so concurrent launches don't
|
|
151
|
+
// collide and so the path is hard to predict for same-UID local
|
|
152
|
+
// attackers. mkdtemp creates with 0700 by default on macOS/Linux but we
|
|
153
|
+
// chmod to be explicit across platforms.
|
|
154
|
+
const prefix = `opensoma-agent-browser-${randomBytes(8).toString('hex')}-`
|
|
155
|
+
const dir = await mkdtemp(join(this.tmpRoot, prefix))
|
|
156
|
+
await chmod(dir, 0o700)
|
|
157
|
+
return dir
|
|
158
|
+
}
|
|
159
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -4,14 +4,15 @@ import { Command } from 'commander'
|
|
|
4
4
|
|
|
5
5
|
import pkg from '../package.json' with { type: 'json' }
|
|
6
6
|
import {
|
|
7
|
+
agentBrowserCommand,
|
|
7
8
|
authCommand,
|
|
8
9
|
dashboardCommand,
|
|
9
|
-
eventCommand,
|
|
10
10
|
memberCommand,
|
|
11
11
|
mentoringCommand,
|
|
12
12
|
noticeCommand,
|
|
13
13
|
reportCommand,
|
|
14
14
|
roomCommand,
|
|
15
|
+
scheduleCommand,
|
|
15
16
|
teamCommand,
|
|
16
17
|
} from './commands/index'
|
|
17
18
|
|
|
@@ -45,13 +46,14 @@ program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
|
45
46
|
})
|
|
46
47
|
|
|
47
48
|
program.addCommand(authCommand)
|
|
49
|
+
program.addCommand(agentBrowserCommand)
|
|
48
50
|
program.addCommand(mentoringCommand)
|
|
49
51
|
program.addCommand(roomCommand)
|
|
50
52
|
program.addCommand(dashboardCommand)
|
|
51
53
|
program.addCommand(noticeCommand)
|
|
52
54
|
program.addCommand(teamCommand)
|
|
53
55
|
program.addCommand(memberCommand)
|
|
54
|
-
program.addCommand(
|
|
56
|
+
program.addCommand(scheduleCommand)
|
|
55
57
|
program.addCommand(reportCommand)
|
|
56
58
|
|
|
57
59
|
program.parse(process.argv)
|