Skip to content
Merged
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
10 changes: 10 additions & 0 deletions agentteams-controller/api/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,16 @@ type HumanSpec struct {
AccessibleWorkers []string `json:"accessibleWorkers,omitempty"`
IdentitySource *IdentitySourceSpec `json:"identitySource,omitempty"`
Note string `json:"note,omitempty"`
// WorkspaceFileAccess controls what this human may do to the knowledge
// base files (workspace-files endpoints) of workers in their own teams:
// "read" (the default, including when empty) allows the read endpoints
// (tree / file-metadata / file-content / file-download); "readwrite"
// additionally allows PUT file-content. Write is an explicit opt-in so
// that a controller upgrade cannot silently grant pre-existing humans
// the new ability to modify worker knowledge files. Admin (L1) callers
// are never restricted, and team leaders always stay read-only on this
// API.
WorkspaceFileAccess string `json:"workspaceFileAccess,omitempty"`
}

type IdentitySourceSpec struct {
Expand Down
6 changes: 6 additions & 0 deletions agentteams-controller/config/crd/humans.agentteams.io.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ spec:
required: [issuer, subject]
note:
type: string
workspaceFileAccess:
type: string
enum:
- read
- readwrite
description: "Knowledge base file access for this human's own teams: read (default when empty) or readwrite (explicit write opt-in); L1 admin is never restricted, team leaders stay read-only"
status:
type: object
properties:
Expand Down
38 changes: 25 additions & 13 deletions agentteams-controller/internal/auth/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ import "fmt"
type Action string

const (
ActionCreate Action = "create"
ActionUpdate Action = "update"
ActionDelete Action = "delete"
ActionGet Action = "get"
ActionList Action = "list"
ActionWake Action = "wake"
ActionSleep Action = "sleep"
ActionEnsureReady Action = "ensure-ready"
ActionReady Action = "ready"
ActionSTS Action = "sts"
ActionStatus Action = "status"
ActionRefreshMatrixToken Action = "refresh-matrix-token"
ActionGateway Action = "gateway"
ActionCreate Action = "create"
ActionUpdate Action = "update"
ActionDelete Action = "delete"
ActionGet Action = "get"
ActionList Action = "list"
ActionWake Action = "wake"
ActionSleep Action = "sleep"
ActionEnsureReady Action = "ensure-ready"
ActionReady Action = "ready"
ActionSTS Action = "sts"
ActionStatus Action = "status"
ActionRefreshMatrixToken Action = "refresh-matrix-token"
ActionGateway Action = "gateway"
ActionWorkspaceFilesWrite Action = "workspace-files-write"
)

// AuthzRequest describes the resource being accessed.
Expand Down Expand Up @@ -114,6 +115,17 @@ func (a *Authorizer) authorizeHuman(caller *CallerIdentity, req AuthzRequest) er
if req.Action == ActionUpdate {
return a.requireSameTeam(caller, req)
}
if req.Action == ActionWorkspaceFilesWrite {
// W3②-rw: L2 humans may write knowledge base files of workers
// in their own teams. Like ActionGet/ActionList this action is
// NOT rejected cross-team at the authorizer level: the
// middleware resolves the worker's team, and a 403 here would
// let a scoped caller probe which workers exist in other teams
// (W8 anti-probing). The handler is the real boundary — it
// hides cross-team workers as 404 and enforces the per-user
// workspaceFileAccess flag and the knowledge base allowlist.
return nil
}
return deny(caller, req)

default:
Expand Down
6 changes: 6 additions & 0 deletions agentteams-controller/internal/auth/authorizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ func TestAuthorizer_HumanScoped(t *testing.T) {
{Action: ActionUpdate, ResourceKind: "worker", ResourceTeam: "market-team"},
{Action: ActionUpdate, ResourceKind: "worker"},
{Action: ActionGet, ResourceKind: "status"},
// W3②-rw: the knowledge base write action is allowed at the
// authorizer level even cross-team (ResourceTeam filled by the
// middleware) — a denial here would leak worker existence via 403
// (W8 anti-probing). The handler is the real boundary (404).
{Action: ActionWorkspaceFilesWrite, ResourceKind: "worker"},
{Action: ActionWorkspaceFilesWrite, ResourceKind: "worker", ResourceTeam: "another-team"},
}
for _, req := range allowed {
if err := az.Authorize(caller, req); err != nil {
Expand Down
5 changes: 5 additions & 0 deletions agentteams-controller/internal/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ func NewHTTPServer(addr string, deps ServerDeps) *HTTPServer {
ckh := NewCheckpointHandler(deps.Client, deps.Namespace, deps.KubeMode, deps.ContainerPrefix)
mux.Handle("GET /api/v1/workers/{name}/checkpoints/{sub}", mw.RequireAuthz(authpkg.ActionGet, "worker", nameFn)(http.HandlerFunc(ckh.proxyCheckpoint)))

// --- Worker knowledge base files (read-only MEMORY.md / memory/** / digest/** inspection; proxy to the worker's qwenpaw app) ---
wfh := NewWorkspaceFilesHandler(deps.Client, deps.Namespace, deps.KubeMode, deps.ContainerPrefix)
mux.Handle("GET /api/v1/workers/{name}/workspace-files/{sub}", mw.RequireAuthz(authpkg.ActionGet, "worker", nameFn)(http.HandlerFunc(wfh.proxyWorkspaceFiles)))
mux.Handle("PUT /api/v1/workers/{name}/workspace-files/file-content", mw.RequireAuthz(authpkg.ActionWorkspaceFilesWrite, "worker", nameFn)(http.HandlerFunc(wfh.proxyWorkspaceFileWrite)))

// W-PR-2: human intervention + lifecycle (write endpoints). All writes go
// through RequireAuthz ActionUpdate + "project" so the authorizer's
// requireSameTeam (TeamLeader / L2) rejects cross-team writes at the code
Expand Down
Loading
Loading