Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/blueclaw-posix-helper/directory_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ func TestCreateUserCommandsUseAShellThatExistsOnMacOS(testInstance *testing.T) {
}

func TestCreateGroupCommandsCarryTheAllocatedIdentity(testInstance *testing.T) {
commands := createGroupCommands("bc_circle_staff", 100004)
commands := createGroupCommands("bc_circle_member", 100004)

if commands[0][2] != "/Groups/bc_circle_staff" {
if commands[0][2] != "/Groups/bc_circle_member" {
testInstance.Fatalf("expected the record to be created first, got %v", commands[0])
}
requireAttribute(testInstance, commands, "PrimaryGroupID", "100004")
Expand Down
6 changes: 3 additions & 3 deletions cmd/blueclaw-posix-helper/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ func TestLoadPOSIXStatePrefersStateDocument(t *testing.T) {
statePath := filepath.Join(rootPath, "state.json")
stateDocument, errorValue := json.Marshal(security.POSIXState{
Directories: []security.POSIXDirectory{{
Path: "/workspace/circles/staff/sites",
Path: "/workspace/circles/member/sites",
Owner: "blueclaw",
Group: "bc_circle_staff",
Group: "bc_circle_member",
ModeText: "2770",
}},
})
Expand All @@ -65,7 +65,7 @@ func TestLoadPOSIXStatePrefersStateDocument(t *testing.T) {
if errorValue != nil {
t.Fatal(errorValue)
}
if len(state.Directories) != 1 || state.Directories[0].Path != "/workspace/circles/staff/sites" {
if len(state.Directories) != 1 || state.Directories[0].Path != "/workspace/circles/member/sites" {
t.Fatalf("expected state document to be loaded, got %+v", state.Directories)
}
}
Expand Down
62 changes: 31 additions & 31 deletions internal/access/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import (
"github.com/yeomyeonggeori/blueclaw/internal/policy"
)

func TestStaffCanAccessStaffCircleFile(t *testing.T) {
personAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff"}}
resource := "file:circle:staff"
func TestMemberCanAccessMemberCircleFile(t *testing.T) {
personAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"member"}}
resource := "file:circle:member"

if !CanAccess(Request{PersonAccess: personAccess, Action: ActionWrite, Resource: resource}) {
t.Fatal("staff should write staff circle files")
t.Fatal("a member should write member circle files")
}
}

func TestCircleMemberCanAccessCircleFile(t *testing.T) {
personAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff", "finance"}}
personAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"member", "finance"}}
resource := "file:circle:finance"

if !CanAccess(Request{PersonAccess: personAccess, Action: ActionRead, Resource: resource}) {
Expand All @@ -25,7 +25,7 @@ func TestCircleMemberCanAccessCircleFile(t *testing.T) {
}

func TestCircleNonMemberCannotAccessCircleFile(t *testing.T) {
personAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff"}}
personAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"member"}}
resource := "file:circle:finance"

if CanAccess(Request{PersonAccess: personAccess, Action: ActionRead, Resource: resource}) {
Expand All @@ -34,8 +34,8 @@ func TestCircleNonMemberCannotAccessCircleFile(t *testing.T) {
}

func TestPrivateFileOnlyAllowsOwner(t *testing.T) {
ownerAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff"}}
otherAccess := policy.PersonAccess{PersonID: "person-2", Circles: []string{"staff"}}
ownerAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"member"}}
otherAccess := policy.PersonAccess{PersonID: "person-2", Circles: []string{"member"}}
resource := "file:private:person-1"

if !CanAccess(Request{PersonAccess: ownerAccess, Action: ActionRead, Resource: resource}) {
Expand All @@ -52,50 +52,50 @@ func TestRepresentativeToolPolicy(t *testing.T) {
Actions: []string{ActionExecute},
Circles: []string{"representative"},
}}
representativeAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff", "representative"}, ResourceAccessRules: resourceAccessRules}
staffAccess := policy.PersonAccess{PersonID: "person-2", Circles: []string{"staff"}, ResourceAccessRules: resourceAccessRules}
representativeAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"member", "representative"}, ResourceAccessRules: resourceAccessRules}
memberAccess := policy.PersonAccess{PersonID: "person-2", Circles: []string{"member"}, ResourceAccessRules: resourceAccessRules}

if !CanAccess(Request{PersonAccess: representativeAccess, Action: ActionExecute, Resource: "tool:company_broadcast_send"}) {
t.Fatal("representative should execute representative tool")
}
if CanAccess(Request{PersonAccess: staffAccess, Action: ActionExecute, Resource: "tool:company_broadcast_send"}) {
t.Fatal("staff should not execute representative tool")
if CanAccess(Request{PersonAccess: memberAccess, Action: ActionExecute, Resource: "tool:company_broadcast_send"}) {
t.Fatal("a member should not execute representative tool")
}
}

func TestFlowResourcePolicies(t *testing.T) {
resourceAccessRules := []policy.ResourceAccessPolicy{
{Resource: "api:flow.summary", Actions: []string{ActionRead}, Circles: []string{"staff"}},
{Resource: "api:flow.task", Actions: []string{"create", "update"}, Circles: []string{"staff"}},
{Resource: "api:flow.summary", Actions: []string{ActionRead}, Circles: []string{"member"}},
{Resource: "api:flow.task", Actions: []string{"create", "update"}, Circles: []string{"member"}},
{Resource: "api:flow.definition", Actions: []string{ActionManage}, Circles: []string{"admin"}},
{Resource: "tool:task_add", Actions: []string{ActionExecute}, Circles: []string{"staff"}},
{Resource: "tool:task_list", Actions: []string{ActionExecute}, Circles: []string{"staff"}},
{Resource: "tool:task_update", Actions: []string{ActionExecute}, Circles: []string{"staff"}},
{Resource: "tool:task_add", Actions: []string{ActionExecute}, Circles: []string{"member"}},
{Resource: "tool:task_list", Actions: []string{ActionExecute}, Circles: []string{"member"}},
{Resource: "tool:task_update", Actions: []string{ActionExecute}, Circles: []string{"member"}},
}
staffAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff"}, ResourceAccessRules: resourceAccessRules}
adminAccess := policy.PersonAccess{PersonID: "person-2", Circles: []string{"staff", "admin"}, ResourceAccessRules: resourceAccessRules}
memberAccess := policy.PersonAccess{PersonID: "person-1", Circles: []string{"member"}, ResourceAccessRules: resourceAccessRules}
adminAccess := policy.PersonAccess{PersonID: "person-2", Circles: []string{"member", "admin"}, ResourceAccessRules: resourceAccessRules}
guestAccess := policy.PersonAccess{PersonID: "person-3", ResourceAccessRules: resourceAccessRules}

if !CanAccess(Request{PersonAccess: staffAccess, Action: ActionRead, Resource: "api:flow.summary"}) {
t.Fatal("staff should read Flow summary")
if !CanAccess(Request{PersonAccess: memberAccess, Action: ActionRead, Resource: "api:flow.summary"}) {
t.Fatal("a member should read Flow summary")
}
if !CanAccess(Request{PersonAccess: staffAccess, Action: "create", Resource: "api:flow.task"}) {
t.Fatal("staff should create Flow task")
if !CanAccess(Request{PersonAccess: memberAccess, Action: "create", Resource: "api:flow.task"}) {
t.Fatal("a member should create Flow task")
}
if CanAccess(Request{PersonAccess: staffAccess, Action: ActionManage, Resource: "api:flow.definition"}) {
t.Fatal("staff should not manage Flow definitions")
if CanAccess(Request{PersonAccess: memberAccess, Action: ActionManage, Resource: "api:flow.definition"}) {
t.Fatal("a member should not manage Flow definitions")
}
if !CanAccess(Request{PersonAccess: adminAccess, Action: ActionManage, Resource: "api:flow.definition"}) {
t.Fatal("admin should manage Flow definitions")
}
if !CanAccess(Request{PersonAccess: staffAccess, Action: ActionExecute, Resource: "tool:task_add"}) {
t.Fatal("staff should execute Flow task add tool")
if !CanAccess(Request{PersonAccess: memberAccess, Action: ActionExecute, Resource: "tool:task_add"}) {
t.Fatal("a member should execute Flow task add tool")
}
if !CanAccess(Request{PersonAccess: staffAccess, Action: ActionExecute, Resource: "tool:task_list"}) {
t.Fatal("staff should execute Flow task list tool")
if !CanAccess(Request{PersonAccess: memberAccess, Action: ActionExecute, Resource: "tool:task_list"}) {
t.Fatal("a member should execute Flow task list tool")
}
if !CanAccess(Request{PersonAccess: staffAccess, Action: ActionExecute, Resource: "tool:task_update"}) {
t.Fatal("staff should execute Flow task update tool")
if !CanAccess(Request{PersonAccess: memberAccess, Action: ActionExecute, Resource: "tool:task_update"}) {
t.Fatal("a member should execute Flow task update tool")
}
if CanAccess(Request{PersonAccess: guestAccess, Action: ActionExecute, Resource: "tool:task_add"}) {
t.Fatal("guest should not execute Flow task add tool")
Expand Down
2 changes: 1 addition & 1 deletion internal/adminapi/policy_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func createInvitedPersonPolicy(inviteRequest invitePersonRequest, email string)
securityLevelName := strings.TrimSpace(inviteRequest.SecurityLevelName)
securityLevelRank := inviteRequest.SecurityLevelRank
grantedClasses := append([]string{}, inviteRequest.GrantedClasses...)
circles := normalizeCircles(append([]string{policy.StaffCircleID}, inviteRequest.Circles...))
circles := normalizeCircles(append([]string{policy.MemberCircleID}, inviteRequest.Circles...))
if inviteRequest.IsAdmin {
securityLevelName = "admin"
securityLevelRank = 100
Expand Down
6 changes: 3 additions & 3 deletions internal/agentruntime/active_circle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
func TestResolveActiveCircleIDUsesChannelOrMention(t *testing.T) {
channelCircleID, hasChannelConflict := ResolveActiveCircleID(ToolCatalogRequest{
ConversationChannelName: "circle-hr-compensation",
PersonAccess: policy.PersonAccess{Circles: []string{"staff", "hr-compensation"}},
PersonAccess: policy.PersonAccess{Circles: []string{"member", "hr-compensation"}},
})
mentionedCircleID, hasMentionConflict := ResolveActiveCircleID(ToolCatalogRequest{
Prompt: "please remember this for @hr-compensation",
PersonAccess: policy.PersonAccess{Circles: []string{"staff", "hr-compensation"}},
PersonAccess: policy.PersonAccess{Circles: []string{"member", "hr-compensation"}},
})

if channelCircleID != "hr-compensation" || hasChannelConflict {
Expand All @@ -27,7 +27,7 @@ func TestResolveActiveCircleIDUsesChannelOrMention(t *testing.T) {
func TestResolveActiveCircleIDIgnoresInaccessibleMention(t *testing.T) {
circleID, hasConflict := ResolveActiveCircleID(ToolCatalogRequest{
Prompt: "please remember this for @hr-compensation",
PersonAccess: policy.PersonAccess{Circles: []string{"staff"}},
PersonAccess: policy.PersonAccess{Circles: []string{"member"}},
})

if circleID != "" || hasConflict {
Expand Down
2 changes: 1 addition & 1 deletion internal/agentruntime/agent_turn_request_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func TestLaunchedAgentTurnRequestCarriesHostAssembledContext(t *testing.T) {
if turnRequest.ToolSet == nil || !containsString(turnRequest.ToolSet.ListToolNames(), "memory_search") {
t.Fatalf("expected the launch tool set on the turn request, got %+v", turnRequest.ToolSet)
}
if !containsString(turnRequest.RequesterCircles, "staff") {
if !containsString(turnRequest.RequesterCircles, "member") {
t.Fatalf("expected resolved requester circles on the turn request, got %+v", turnRequest.RequesterCircles)
}
}
16 changes: 8 additions & 8 deletions internal/agentruntime/capability_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestToolCatalogHidesPolicyDeniedCapabilityTools(t *testing.T) {
ProfileName: "default",
PersonAccess: policy.PersonAccess{
PersonID: "person-1",
Circles: []string{"staff"},
Circles: []string{"member"},
ResourceAccessRules: []policy.ResourceAccessPolicy{{
Resource: "tool:site_serve",
Actions: []string{"execute"},
Expand Down Expand Up @@ -275,22 +275,22 @@ func TestCapabilityToolRequestSeparatesModelInputFromTransport(t *testing.T) {

func TestImageReadUsesExactPathInput(t *testing.T) {
workspacePath := t.TempDir()
imagePath := filepath.Join(workspacePath, "circles", "staff", "inbox", "mattermost", "thread-1", "post-1", "mascot.png")
imagePath := filepath.Join(workspacePath, "circles", "member", "inbox", "mattermost", "thread-1", "post-1", "mascot.png")
writeTestFile(t, imagePath, "image")
httpClient := &recordingHTTPClient{responseBody: `{"provider":"internkim","selectedBackend":"device","toolName":"image_read","outcome":"succeeded","status":"ok","result":{"status":"ok","path":"/workspace/circles/staff/inbox/mattermost/thread-1/post-1/mascot.png","attachments":[{"devicePath":"/workspace/circles/staff/inbox/mattermost/thread-1/post-1/mascot.png","filename":"mascot.png","contentType":"image/png","sizeBytes":5,"contentBase64":"aW1hZ2U="}]}}`}
httpClient := &recordingHTTPClient{responseBody: `{"provider":"internkim","selectedBackend":"device","toolName":"image_read","outcome":"succeeded","status":"ok","result":{"status":"ok","path":"/workspace/circles/member/inbox/mattermost/thread-1/post-1/mascot.png","attachments":[{"devicePath":"/workspace/circles/member/inbox/mattermost/thread-1/post-1/mascot.png","filename":"mascot.png","contentType":"image/png","sizeBytes":5,"contentBase64":"aW1hZ2U="}]}}`}
toolCatalogBuilder := newFileToolTestCatalogBuilder(workspacePath)
toolCatalogBuilder.UseTestCapabilityToolDescriptors(capability.Client{Endpoint: "http://capability.local", HTTPClient: httpClient}, []CapabilityToolDescriptor{canonicalReadDescriptor("image_read")})
toolRegistry := toolCatalogBuilder.BuildToolSet(ToolCatalogRequest{
ProfileName: "default",
PersonAccess: policy.PersonAccess{
PersonID: "person-1",
Circles: []string{"staff"},
Circles: []string{"member"},
},
})

result, errorValue := toolRegistry.Invoke(context.Background(), toolcontract.ToolInvocation{
ToolName: "image_read",
Input: toolcontract.MarshalToolInput(map[string]string{"path": "/workspace/circles/staff/inbox/mattermost/thread-1/post-1/mascot.png"}),
Input: toolcontract.MarshalToolInput(map[string]string{"path": "/workspace/circles/member/inbox/mattermost/thread-1/post-1/mascot.png"}),
})

if errorValue != nil {
Expand All @@ -299,7 +299,7 @@ func TestImageReadUsesExactPathInput(t *testing.T) {
if result.Failed() {
t.Fatalf("expected image_read success, got %s", result.ContentText())
}
if !strings.Contains(httpClient.requestBody, `/workspace/circles/staff/inbox/mattermost/thread-1/post-1/mascot.png`) {
if !strings.Contains(httpClient.requestBody, `/workspace/circles/member/inbox/mattermost/thread-1/post-1/mascot.png`) {
t.Fatalf("expected capability request to use exact path, got %s", httpClient.requestBody)
}
}
Expand All @@ -314,7 +314,7 @@ func TestCanonicalReadRejectsMaterialIDInput(t *testing.T) {
ProfileName: "default",
PersonAccess: policy.PersonAccess{
PersonID: "person-1",
Circles: []string{"staff"},
Circles: []string{"member"},
},
})

Expand Down Expand Up @@ -571,7 +571,7 @@ func TestImageGenerateSendsRequesterWorkspacePathToBridge(t *testing.T) {
RequesterPersonID: "person-1",
PersonAccess: policy.PersonAccess{
PersonID: "person-1",
Circles: []string{"staff"},
Circles: []string{"member"},
},
})

Expand Down
4 changes: 3 additions & 1 deletion internal/agentruntime/conversation_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package agentruntime
import (
"path/filepath"
"strings"

"github.com/yeomyeonggeori/blueclaw/internal/policy"
)

type ConversationResourceScope struct {
Expand Down Expand Up @@ -37,7 +39,7 @@ func ConversationScopeForRequest(workspaceRootPath string, request ToolCatalogRe
}
circleID := circleIDFromConversation(request)
if circleID == "" {
circleID = "staff"
circleID = policy.MemberCircleID
}
return ConversationResourceScope{
Kind: "circle",
Expand Down
8 changes: 4 additions & 4 deletions internal/agentruntime/file_hint_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestFileReadResolvesAttachmentFileHint(t *testing.T) {
toolRegistry := toolCatalogBuilder.BuildToolSet(ToolCatalogRequest{
ProfileName: "default",
RequesterPersonID: "person-1",
PersonAccess: policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff"}},
PersonAccess: policy.PersonAccess{PersonID: "person-1", Circles: []string{"member"}},
VisibleContext: agentcontract.VisibleContext{CurrentMaterials: []agentcontract.VisibleContextMaterial{{
FileHint: "attachment:mattermost:file-1",
MaterialID: "mattermost:file-1",
Expand Down Expand Up @@ -53,7 +53,7 @@ func TestFilePreviewResolvesArtifactFileHint(t *testing.T) {
toolRegistry := toolCatalogBuilder.BuildToolSet(ToolCatalogRequest{
ProfileName: "default",
RequesterPersonID: "person-1",
PersonAccess: policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff"}},
PersonAccess: policy.PersonAccess{PersonID: "person-1", Circles: []string{"member"}},
VisibleContext: agentcontract.VisibleContext{Materials: []agentcontract.VisibleContextMaterial{{
FileHint: fileHint,
Path: filepath.Join(workspacePath, filepath.FromSlash(relativePath)),
Expand Down Expand Up @@ -82,7 +82,7 @@ func TestFileHintRejectsUnknownAndForgedValues(t *testing.T) {
toolRegistry := toolCatalogBuilder.BuildToolSet(ToolCatalogRequest{
ProfileName: "default",
RequesterPersonID: "person-1",
PersonAccess: policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff"}},
PersonAccess: policy.PersonAccess{PersonID: "person-1", Circles: []string{"member"}},
})
for _, fileHint := range []string{
"attachment:mattermost:forged",
Expand Down Expand Up @@ -113,7 +113,7 @@ func TestFileToolsPreserveExplicitPathResolutionAndAccess(t *testing.T) {
toolRegistry := toolCatalogBuilder.BuildToolSet(ToolCatalogRequest{
ProfileName: "default",
RequesterPersonID: "person-1",
PersonAccess: policy.PersonAccess{PersonID: "person-1", Circles: []string{"staff"}},
PersonAccess: policy.PersonAccess{PersonID: "person-1", Circles: []string{"member"}},
})

readResult, errorValue := toolRegistry.Invoke(context.Background(), toolcontract.ToolInvocation{
Expand Down
5 changes: 3 additions & 2 deletions internal/agentruntime/file_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"github.com/yeomyeonggeori/blueclaw/internal/policy"
"github.com/yeomyeonggeori/bluecollar/toolcontract"
"mime"
"path/filepath"
Expand Down Expand Up @@ -361,7 +362,7 @@ func (toolCatalogBuilder *ToolCatalogBuilder) isOptionalControlFilePath(path str

func (toolCatalogBuilder *ToolCatalogBuilder) recommendedSiteControlWritePath(path string) string {
cleanPath := strings.Trim(filepath.ToSlash(strings.TrimSpace(path)), "/")
for _, prefix := range []string{"~/sites/", "home/sites/", "workspace/circles/staff/sites/"} {
for _, prefix := range []string{"~/sites/", "home/sites/", "workspace/circles/member/sites/"} {
if recommendedPath := toolCatalogBuilder.recommendedSiteControlWritePathForPrefix(cleanPath, prefix); recommendedPath != "" {
return recommendedPath
}
Expand All @@ -384,7 +385,7 @@ func (toolCatalogBuilder *ToolCatalogBuilder) recommendedSiteControlWritePathFor
if !toolCatalogBuilder.isOptionalControlFilePath(relativePath) {
return ""
}
return filepath.ToSlash(filepath.Join("/workspace", "circles", "staff", "sites", siteID, "draft", relativePath))
return filepath.ToSlash(filepath.Join("/workspace", "circles", policy.MemberCircleID, "sites", siteID, "draft", relativePath))
}

func fileReadResultMap(base map[string]any, readResult fileReadOutput) map[string]any {
Expand Down
Loading
Loading