space

package
v0.2.0-alpha.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

Documentation

Overview

Package space owns what a space's roles may do.

The decision is here, and only here, because it has two enforcers: the HTTP guard refuses a request before a handler runs, and the space service refuses a command whatever called it. Defence in depth is deliberate; two definitions of the same rule were not, and the one nobody remembered to update would have been the permissive one.

The role values, the Space it belongs to, and the store contract are in space.go beside it.

Index

Constants

View Source
const (
	// RoleOwner is the initial role for the user who creates a space.
	RoleOwner = "owner"
	// RoleAdmin can manage shared automation assets but not membership ownership.
	RoleAdmin = "admin"
	// RoleMember is the basic collaboration role for invited members.
	RoleMember = "member"
	// DefaultPersonalName is the initial UX-facing name for a user's own space.
	DefaultPersonalName = "My Space"
)
View Source
const InvitationTTLDefault = 72 * time.Hour

InvitationTTLDefault is how long a pending invitation stays acceptable when nobody accepts it. Three days rather than a login code's shorter window: an invitation is meant to be acted on whenever the recipient next opens Portal, not inside the exchange that sent it. See docs/design/space-membership-lifecycle.md.

Variables

This section is empty.

Functions

func Allows

func Allows(role string, action Action) bool

Allows reports whether a member holding role may perform action.

It answers about a stated role. An unknown role and an unknown action are both refused: a caller with neither has not been given permission, and defaulting either to true would make a typo an escalation. An empty role is not stated, so it is refused here too — EffectiveRole is what turns a stored row into the role to ask about.

func EffectiveRole

func EffectiveRole(role string) string

EffectiveRole is what a membership row's stored role means.

A row with no role is a member. The row is what says somebody belongs to the space; the role only says how much they may do, and the least of the three is what a row that never got one has been given. Reading it as "not a member" instead would make a data defect look like an absent membership, and reading it as anything higher would let a missing value grant something.

Callers normalize before asking Allows, which answers about a stated role.

Types

type Action

type Action string

An Action is something a caller wants to do to a space, named at the coarseness the role rules actually distinguish. It is not one action per route: several routes share a permission, and naming the permission rather than the route is what stops a new route from arriving without one.

const (
	// ActionManageSpaceMembers covers adding and removing members. Granting
	// ownership is not part of it: the service refuses any role but member, so
	// an escalation cannot look like a routine invitation.
	ActionManageSpaceMembers Action = "manage_space_members"
	// ActionInviteSpaceMember covers creating a pending invitation and revoking
	// one before it is accepted. It is deliberately separate from
	// ActionManageSpaceMembers: admin holds this one too, but only at the
	// member role -- a restriction Allows cannot express since it only knows
	// the caller's own role, so the service enforces it. See
	// docs/design/space-membership-lifecycle.md §5.1.
	ActionInviteSpaceMember Action = "invite_space_member"
	// ActionChangeMemberRole covers promoting or demoting a member, including
	// the transfer that results from setting a target's role to owner. It is
	// its own action rather than folded into ActionManageSpaceMembers so that a
	// future change to who may invite or remove does not silently also change
	// who may reshuffle roles. See docs/design/space-membership-lifecycle.md
	// §5.2.
	ActionChangeMemberRole Action = "change_member_role"
	ActionManageAgents     Action = "manage_agents"
	ActionManageWorkflows  Action = "manage_workflows"
	// ActionAssignIssueWorkflow is assigning work to a workflow, which is a
	// change to what the space automates rather than a use of it.
	ActionAssignIssueWorkflow Action = "assign_issue_workflow"
	ActionRunWorkflow         Action = "run_workflow"
	ActionReadAuditTrail      Action = "read_audit_trail"
	ActionCommentIssue        Action = "comment_issue"
	// ActionModerateIssueComments covers deleting a comment the caller did not
	// write. Editing another author's comment is permitted to nobody, so it is
	// not an action here — see internal/service/issue.
	ActionModerateIssueComments Action = "moderate_issue_comments"
	// ActionManageSecrets covers creating, editing, disabling, and destroying
	// Space Secrets. Owner-only: value authority stays with the owner until
	// BuildMax has finer space grants. See docs/design/space-secrets.md §10.
	ActionManageSecrets Action = "manage_secrets"
	// ActionReadSecrets covers listing Secret metadata and item names -- never a
	// value, which no role can read. Owner or admin, because an admin editing an
	// Agent needs to see which Secrets exist to configure its consumption. See
	// docs/design/space-secrets.md §10.
	ActionReadSecrets Action = "read_secrets"
)

func Actions

func Actions() []Action

Actions returns every action, so a test can prove the matrix covers each one rather than only the ones somebody remembered.

type Invitation

type Invitation struct {
	ID      string `json:"id"`
	SpaceID string `json:"space_id"`
	UserID  string `json:"user_id"`
	Role    string `json:"role"`
	// InvitedBy is the user who sent the invitation.
	InvitedBy string    `json:"invited_by"`
	ExpiresAt time.Time `json:"expires_at"`
	// AcceptedAt and RevokedAt are mutually exclusive; both nil means still
	// pending. There is no separate status column -- these two timestamps plus
	// ExpiresAt are the whole state, the same shape user.disabled_at and
	// system_grant.revoked_at already use for "off until proven otherwise".
	AcceptedAt *time.Time `json:"accepted_at,omitempty"`
	RevokedAt  *time.Time `json:"revoked_at,omitempty"`
	CreatedAt  time.Time  `json:"created_at"`
}

Invitation is a pending offer of space membership against an account that already exists — space-scoped invitation never creates one. See docs/design/space-membership-lifecycle.md §1 for why account creation and space membership are kept as two different authorities.

func (Invitation) Pending

func (i Invitation) Pending(now time.Time) bool

Pending reports whether the invitation may still be accepted: neither answered nor withdrawn, and not past its offer window.

type Member

type Member struct {
	SpaceID   string    `json:"space_id"`
	UserID    string    `json:"user_id"`
	Role      string    `json:"role"`
	CreatedAt time.Time `json:"created_at"`
}

Member is one user's membership in a space.

type Space

type Space struct {
	ID                string  `json:"id"`
	Name              string  `json:"name"`
	PersonalForUserID *string `json:"personal_for_user_id,omitempty"`
	QuotaTier         string  `json:"quota_tier,omitempty"`
	// PluginCuration is who fills this space's plugin activation list; empty
	// reads as plugin.CurationOpen. See core/plugin/activation.go.
	PluginCuration coreplugin.Curation `json:"plugin_curation,omitempty"`
	// AgentInstructions are shared guidance appended to every background agent
	// run in this space. Revision changes whenever the text changes so a TaskRun
	// can record exactly which space-level instructions it received.
	AgentInstructions         string `json:"agent_instructions,omitempty"`
	AgentInstructionsRevision int    `json:"agent_instructions_revision,omitempty"`
	// DefaultSandboxNetworkTier and DefaultSandboxFilesystemTier are the
	// config.SandboxNetworkTier / config.SandboxFilesystemTier values an agent
	// that declares neither tier inherits. Empty means the surface baseline
	// applies instead -- see docs/design/agent-sandbox-policy.md §9 M3.
	DefaultSandboxNetworkTier    string    `json:"default_sandbox_network_tier,omitempty"`
	DefaultSandboxFilesystemTier string    `json:"default_sandbox_filesystem_tier,omitempty"`
	CreatedBy                    string    `json:"created_by"`
	CreatedAt                    time.Time `json:"created_at"`
	UpdatedAt                    time.Time `json:"updated_at"`
}

Space is the ownership and collaboration boundary for working resources. A user's default personal space is represented by personal_for_user_id.

type Store

type Store interface {
	// GetSpace returns the space by space_id, or (nil, nil) when not found.
	GetSpace(ctx context.Context, spaceID string) (*Space, error)
	// GetPersonalSpaceByUser returns the default personal space for the user, or (nil, nil) when not found.
	GetPersonalSpaceByUser(ctx context.Context, userID string) (*Space, error)
	// ListSpacesByUser returns all spaces the user belongs to, ordered by created_at ASC.
	ListSpacesByUser(ctx context.Context, userID string) ([]Space, error)
	// CreateSpace creates a new space and owner membership.
	CreateSpace(ctx context.Context, name, createdBy, quotaTier string) (*Space, error)
	// AddSpaceMember adds or updates a space membership.
	AddSpaceMember(ctx context.Context, spaceID, userID, role string) (*Member, error)
	// RemoveSpaceMember removes one membership from a space.
	RemoveSpaceMember(ctx context.Context, spaceID, userID string) error
	// ListSpaceMembers returns members of the space ordered by created_at ASC.
	ListSpaceMembers(ctx context.Context, spaceID string) ([]Member, error)
	// ListAllSpaces returns every space newest first, with the total count. A
	// non-empty query filters on name as a substring.
	//
	// It is the one method here that ignores membership, so only
	// deployment-scoped callers may reach it. It returns spaces, never their
	// contents: an administrator learns that a space exists and how large it is,
	// not what is in it.
	ListAllSpaces(ctx context.Context, query string, limit, offset int) ([]Space, int, error)
	// CountSpaceMembers returns member counts for the given spaces, keyed by
	// space id. It exists so listing spaces is two queries rather than one per
	// row.
	CountSpaceMembers(ctx context.Context, spaceIDs []string) (map[string]int, error)
	// SetSpacePluginCuration records who fills the space's plugin activation
	// list, or returns ErrNotFound. The value is validated above this layer.
	SetSpacePluginCuration(ctx context.Context, spaceID string, mode coreplugin.Curation) error
	// SetSpaceSandboxDefaults records the tiers an agent that declares neither
	// inherits, or returns ErrNotFound. The values are validated above this
	// layer, the same way SetSpacePluginCuration's mode is.
	SetSpaceSandboxDefaults(ctx context.Context, spaceID, networkTier, filesystemTier string) error
	// SetSpaceAgentInstructions replaces the shared instructions for future
	// background agent runs. An identical value is a no-op; a change advances
	// AgentInstructionsRevision atomically.
	SetSpaceAgentInstructions(ctx context.Context, spaceID, instructions string) error

	// CreateInvitation creates a pending invitation for userID to join spaceID
	// at role, sent by invitedBy, acceptable until expiresAt.
	CreateInvitation(ctx context.Context, spaceID, userID, role, invitedBy string, expiresAt time.Time) (*Invitation, error)
	// GetInvitation returns one invitation by its handle, or (nil, nil) when
	// not found.
	GetInvitation(ctx context.Context, invitationID string) (*Invitation, error)
	// ListPendingInvitationsBySpace returns a space's still-pending invitations,
	// newest first.
	ListPendingInvitationsBySpace(ctx context.Context, spaceID string, now time.Time) ([]Invitation, error)
	// ListPendingInvitationsByUser returns one account's still-pending
	// invitations across every space, newest first -- what GET /api/invitations
	// answers.
	ListPendingInvitationsByUser(ctx context.Context, userID string, now time.Time) ([]Invitation, error)
	// AcceptInvitation marks a pending invitation accepted and creates the
	// resulting space membership in the same transaction, or returns (nil, nil)
	// when the row does not exist or is no longer pending -- an invitation
	// marked accepted with no membership to show for it would be evidence of a
	// bug no caller could act on.
	AcceptInvitation(ctx context.Context, invitationID string, now time.Time) (*Invitation, error)
	// RevokeInvitation marks a pending invitation revoked. A row that does not
	// exist or is no longer pending is not an error -- withdrawing an offer
	// that already resolved itself asks for nothing this store has to refuse.
	RevokeInvitation(ctx context.Context, invitationID string, now time.Time) error

	// TransferOwnership makes toUserID the space's owner and demotes fromUserID
	// to admin, atomically -- a space must never be read with two owners or
	// none because a caller observed the change half-applied. Both must
	// already be members; the service enforces that, and the last-owner
	// invariant, before calling this. See
	// docs/design/space-membership-lifecycle.md §5.2-§5.3.
	TransferOwnership(ctx context.Context, spaceID, fromUserID, toUserID string) error
}

Store provides space persistence and membership lookup.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL