|
@@ -8,6 +8,7 @@ import request from 'supertest'
|
|
|
import { createApplication, type ApplicationRuntime } from '../../src/server/app'
|
|
import { createApplication, type ApplicationRuntime } from '../../src/server/app'
|
|
|
import { loadServerConfig } from '../../src/server/config'
|
|
import { loadServerConfig } from '../../src/server/config'
|
|
|
import { LocalAuthStore } from '../../src/server/localAuth'
|
|
import { LocalAuthStore } from '../../src/server/localAuth'
|
|
|
|
|
+import type { Work } from '../../src/server/workspaceScheduler'
|
|
|
|
|
|
|
|
let runtime: ApplicationRuntime, home: string, admin: ReturnType<typeof request.agent>, child: ReturnType<typeof request.agent>
|
|
let runtime: ApplicationRuntime, home: string, admin: ReturnType<typeof request.agent>, child: ReturnType<typeof request.agent>
|
|
|
let adminCSRF: string, childCSRF: string, childID: string, upstreamPaths: string[]
|
|
let adminCSRF: string, childCSRF: string, childID: string, upstreamPaths: string[]
|
|
@@ -44,6 +45,50 @@ beforeEach(async () => {
|
|
|
afterEach(() => { runtime.close(); rmSync(home, { recursive: true, force: true }); vi.restoreAllMocks() })
|
|
afterEach(() => { runtime.close(); rmSync(home, { recursive: true, force: true }); vi.restoreAllMocks() })
|
|
|
|
|
|
|
|
describe('subaccount Bot-only security boundary with real login sessions', () => {
|
|
describe('subaccount Bot-only security boundary with real login sessions', () => {
|
|
|
|
|
+ async function startBotTurn() {
|
|
|
|
|
+ const agent = (await call(child, 'post', '/api/app/agents', childCSRF).send({ name: '子账号负责人', profile: 'allowed' }).expect(201)).body.agent
|
|
|
|
|
+ const conversation = (await call(child, 'get', '/api/app/conversations').expect(200)).body.conversations[0]
|
|
|
|
|
+ await call(child, 'post', `/api/app/conversations/${conversation.id}/messages`, childCSRF)
|
|
|
|
|
+ .send({ requestId: randomUUID(), content: '帮我创建一个长期研究助手' }).expect(202)
|
|
|
|
|
+ const work = runtime.workspace.list<Work>(childID, 'turn')[0]!
|
|
|
|
|
+ // Simulate the grant normally issued after a healthy Hermes bridge handshake.
|
|
|
|
|
+ runtime.workspace.put(childID, 'turn', work.id, { ...work, status: 'running', teamManagementRevision: agent.revision })
|
|
|
|
|
+ return { agent, work }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ it('lets a subaccount Bot create persistent Bots from its assigned source with owner isolation and idempotency', async () => {
|
|
|
|
|
+ const { agent, work } = await startBotTurn()
|
|
|
|
|
+ const tools = runtime.workspaceRuntime.teamTools
|
|
|
|
|
+ expect(tools.catalog(childID, work.id).map(tool => tool.name)).toContain('workspace_create_agent')
|
|
|
|
|
+ const sources = await tools.call(childID, work.id, 'workspace_list_sources', {})
|
|
|
|
|
+ expect(sources).toMatchObject({ sources: [{ nodeId: 'local', profile: 'allowed' }] })
|
|
|
|
|
+ const input = { requestId: randomUUID(), name: '研究助手', profile: 'allowed', instructions: '核对资料并提供来源' }
|
|
|
|
|
+ const result = await tools.call(childID, work.id, 'workspace_create_agent', input)
|
|
|
|
|
+ expect(await tools.call(childID, work.id, 'workspace_create_agent', input)).toEqual(result)
|
|
|
|
|
+ const agents = (await call(child, 'get', '/api/app/agents').expect(200)).body.agents
|
|
|
|
|
+ expect(agents).toHaveLength(2)
|
|
|
|
|
+ expect(agents.find((item: any) => item.name === input.name)).toMatchObject({
|
|
|
|
|
+ profile: 'allowed', createdByAgentId: agent.id, createdFromRunId: work.runId, canManageTeam: true,
|
|
|
|
|
+ })
|
|
|
|
|
+ expect((await call(child, 'get', '/api/app/conversations').expect(200)).body.conversations).toHaveLength(2)
|
|
|
|
|
+ expect((await call(admin, 'get', '/api/app/agents').expect(200)).body.agents).toEqual([])
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('rejects unassigned sources and revokes a subaccount Bot creation grant after reassignment', async () => {
|
|
|
|
|
+ const { work } = await startBotTurn()
|
|
|
|
|
+ const tools = runtime.workspaceRuntime.teamTools
|
|
|
|
|
+ for (const source of [{ profile: 'private' }, { profile: 'allowed', nodeId: randomUUID() }]) {
|
|
|
|
|
+ await expect(tools.call(childID, work.id, 'workspace_create_agent', {
|
|
|
|
|
+ requestId: randomUUID(), name: '越权助手', ...source,
|
|
|
|
|
+ })).rejects.toMatchObject({ status: 403, code: 'agent_source_forbidden' })
|
|
|
|
|
+ }
|
|
|
|
|
+ await call(admin, 'patch', `/api/app/admin/users/${childID}`, adminCSRF).send({ assignedProfiles: [] }).expect(200)
|
|
|
|
|
+ await expect(tools.call(childID, work.id, 'workspace_create_agent', {
|
|
|
|
|
+ requestId: randomUUID(), name: '撤权后助手', profile: 'allowed',
|
|
|
|
|
+ })).rejects.toMatchObject({ status: 403 })
|
|
|
|
|
+ expect(runtime.workspace.list(childID, 'agent')).toHaveLength(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
it('persists assignment, filters sources, hides native profiles and rejects unassigned/local-node spoofing', async () => {
|
|
it('persists assignment, filters sources, hides native profiles and rejects unassigned/local-node spoofing', async () => {
|
|
|
expect((await call(child, 'get', '/api/app/bootstrap')).body.profiles).toEqual([])
|
|
expect((await call(child, 'get', '/api/app/bootstrap')).body.profiles).toEqual([])
|
|
|
expect((await call(child, 'get', '/api/profiles')).body.profiles).toEqual([])
|
|
expect((await call(child, 'get', '/api/profiles')).body.profiles).toEqual([])
|