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
46 changes: 46 additions & 0 deletions .design/project-visibility-design-note.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## Design Note: Project Access via Membership (replacing dead visibility field)

**From:** proj-visibility-dev
**Context:** The project visibility field (private/team/public) has been eradicated -- it was dead code re-introduced by a stale-branch merge. This note proposes the net-new mechanism to capture the same intent using the current role/group/policy model.

### How it works today

1. hub-member-read-all policy grants read+list on ResourceType:"*" to the hub-members group -- every authenticated user can see every project.
2. Per-project project:<slug>:member-create-agents policy grants create, stop_all, message on agents, bound to the project:<slug>:members group.
3. isProjectOwnerOrAdmin bypass (via RoleBindings) gates admin/owner actions.

### Proposed changes -- using existing Policy system (group-aware, fully wired)

Step 1: Narrow the global read-all grant. Replace the ResourceType:"*" wildcard in hub-member-read-all with explicit per-type allows for directory/catalog types only:
- KEEP globally readable: user, group, template, harness_config, broker, runtime_broker, gcp_service_account, policy, skill, quota, role, role_binding, hub
- GATE (remove from global read): project, agent

After this, a project is only visible to users who have a matching policy grant.

Step 2: Add project-scoped read policy. Modify createProjectMembersGroupAndPolicy to also seed a policy granting read, list on project and agent resource types, bound to the project:<slug>:members group. Members can see the project and its agents.

Step 3: Three access levels emerge from membership:
- Private (default): Only owner + explicitly added members. Members group has only the creator.
- Team: Collaborators added to the members group (users and/or nested groups).
- Everyone: Add hub-members group to the project's members group. The project-scoped read policy then applies to all hub users transitively.

No creation-time selector needed. The Members panel IS the access control surface.

Step 4: Enforcement gaps to close:
- getProject (single GET) -- add CheckAccess(ActionRead) gate
- listProjects/listAgents -- fail closed on nil identity (empty/401)

Step 5: Members-card hint. Add hint text to the group member editor when it is a project members group: "To make this project visible to all hub users, add the hub-members group."

### Role tiers (subtractive approach, no policy-engine changes)
- member = read-only (from the project-scoped read policy bound to members group)
- admin/owner = create/manage agents (from isProjectOwnerOrAdmin RoleBinding bypass -- already works)
- Existing members bumped to admin in one-time backfill to preserve current create-agent ability.

### Known gaps (not blockers, flagging for awareness)
1. RoleBinding sync gap: Users added via groups API dont get a corresponding RoleBinding until hub restart.
2. Governance role not carried through group expansion: GetEffectiveGroups drops role. A user who is admin only via a nested group gets read but not create.
3. No policy management UI: Per-project policy changes happen programmatically.

### Implementation approach
This is a direct extension of existing patterns -- the per-project members group exists, hub-members auto-enrollment works, and the Policy systems group-aware enforcement is production-proven. Low-risk, all existing infrastructure being wired together. Will proceed unless you flag concerns.
36 changes: 36 additions & 0 deletions changelog/2026-08-29-changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Release Notes (2026-08-29)

Per-agent messaging authorization ships end-to-end (D1-D10 message modes, conversation model at handler ingress, CLI grammar, admin UI), a credential-leak prevention sweep removes secrets from error output, argv, and deploy scripts, and the conversation model advances through tranches C4-C6 with dual-write for webchat topics and reachability guards.

## 🔒 Security
* **Credential leak prevention (#1364, #1387, #1385, #1383):** Runtime error and debug paths no longer expose secrets — argv and env redacted from sandbox error output, access token removed from curl's argv in deploy.sh, and agent secrets now fetched from the hub at init rather than passed via command-line arguments.
* **Caller-supplied conversation_id authorization (#1403):** Server now authorizes conversation_id values supplied by callers, preventing unauthorized access to conversations by ID guessing.
* **Broker inbound sender authorization unified (#1411):** All broker inbound senders authorized through a single uniform path, eliminating inconsistencies across entry points.
* **@email path validation (#1407):** ResolveConversation now validates @email paths before processing, preventing malformed email addresses from reaching the resolver.

## 🚀 Features
* **Per-agent messaging authorization (D1-D10) (#1371, #1382, #1374):** Complete message-mode system with per-agent authorization controls, conversation envelope/delivery/validation library, and admin UI with mode badges, reachability indicators, mode controls, and templates.
* **Conversation model at handler ingress (#1391, #1380, #1381):** Conversation model enforced at handler ingress with authz reachability guard (C5), webchat topics dual-written into the conversation model (C4), and CLI conversation-reference grammar with flag deprecation (C6).
* **Per-type UAT manage scope aliases (#1404):** Adds manage scope aliases per resource type, reducing the number of individual scopes users need to remember.
* **Agent env var provenance classification (#1384):** Each agent environment variable now carries provenance metadata (hub-injected, user-defined, runtime-derived), enabling audit and debugging of variable origins.
* **Permissions UI polish (#1395, #1394, #1396):** Role-binding display names with reusable principal picker, view-permissions modal for roles, and admin nav visibility fixed for plain members with hub-admin route guard.
* **CountUnbackfilledMessages store method (#1373):** New MessageStore method to count messages needing backfill, supporting the conversation model migration.

## 🐛 Fixes
* **Race condition guards (#1409, #1405):** channelRegistry, pluginManager, dispatcher, and webChatStore reads now guarded with s.mu, fixing data races surfaced by the new nightly race detection job.
* **Guard scope and cross-project sentinel defects (#1379):** Closed DEF-39b and DEF-40 — guard scope was too narrow in some paths, and cross-project sentinels could leak.
* **Legacy-pending sentinel removed (#1401):** Removed the legacy-pending sentinel and split the validation choke point, cleaning up a source of confusion in the messaging pipeline (DEF-41).
* **Backfill 'message' action into existing policies (#1377):** Existing project member policies now include the 'message' action, ensuring messaging works for projects created before the authorization system.
* **Agent token file write check (#1386):** Agent refuses to start when the token file cannot be written, failing fast instead of silently running without credentials.
* **CLI precondition ordering (#1402):** Preconditions hoisted above conversation resolve, preventing confusing errors when prerequisites are missing (DEF-48).
* **Extras module repair (#1398):** Six broken modules in extras/ repaired with a new CI gate to prevent future breakage.
* **Hook response contract (#1400):** Data-driven hook response contract for antigravity, replacing ad-hoc response parsing.
* **Harness provider routing (#1397):** OpenCode vertex-ai provider routing fixed — GITHUB_TOKEN collision no longer breaks routing.

## 🔧 CI & Infrastructure
* **Nightly race detection (#1408):** New CI job runs tests with `-race` flag nightly, surfacing data races before they hit production.
* **Full test suite reporting job (#1388, #1392):** Reporting-only full test suite job added; pipefail ensures the test step reports the real exit code.
* **Authz gate hardening (#1410, #1376, #1372):** AST validation added for DEF-50/DEF-37/DEF-56 authz gates; security marker gate updated for authorizeAgentMessage; conversation upsert guard widened for kind='group' topic mints.

## 📖 Docs
* **Message mode glossary (#1389):** Glossary entries added for message modes and related messaging authorization terminology.
18 changes: 6 additions & 12 deletions cmd/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,9 @@ Examples:
}

var (
hubProjectCreateSlug string
hubProjectCreateName string
hubProjectCreateBranch string
hubProjectCreateVisibility string
hubProjectCreateSlug string
hubProjectCreateName string
hubProjectCreateBranch string
)

// hubProjectCreateCmd creates a project on the Hub from a git URL
Expand Down Expand Up @@ -341,7 +340,6 @@ func init() {
hubProjectCreateCmd.Flags().StringVar(&hubProjectCreateSlug, "slug", "", "Override the auto-derived slug")
hubProjectCreateCmd.Flags().StringVar(&hubProjectCreateName, "name", "", "Human-friendly display name (defaults to repo name)")
hubProjectCreateCmd.Flags().StringVar(&hubProjectCreateBranch, "branch", "", "Base branch for the project (defaults to detected default branch, or main)")
hubProjectCreateCmd.Flags().StringVar(&hubProjectCreateVisibility, "visibility", "", "Project visibility: private, team, or public (default: private)")
hubProjectCreateCmd.Flags().BoolVar(&hubOutputJSON, "json", false, "Output in JSON format")

// Also link flags to the hidden alias subcommands so they work too
Expand All @@ -351,7 +349,6 @@ func init() {
hubGrovesCreateCmd.Flags().StringVar(&hubProjectCreateSlug, "slug", "", "Override the auto-derived slug")
hubGrovesCreateCmd.Flags().StringVar(&hubProjectCreateName, "name", "", "Human-friendly display name (defaults to repo name)")
hubGrovesCreateCmd.Flags().StringVar(&hubProjectCreateBranch, "branch", "", "Base branch for the project (defaults to detected default branch, or main)")
hubGrovesCreateCmd.Flags().StringVar(&hubProjectCreateVisibility, "visibility", "", "Project visibility: private, team, or public (default: private)")
hubGrovesCreateCmd.Flags().BoolVar(&hubOutputJSON, "json", false, "Output in JSON format")

// Broker subcommand flags
Expand Down Expand Up @@ -1326,7 +1323,6 @@ func runHubProjectsInfo(cmd *cobra.Command, args []string) error {
"name": project.Name,
"slug": project.Slug,
"gitRemote": project.GitRemote,
"visibility": project.Visibility,
"agentCount": project.AgentCount,
"created": project.Created,
"updated": project.Updated,
Expand Down Expand Up @@ -1354,7 +1350,6 @@ func runHubProjectsInfo(cmd *cobra.Command, args []string) error {
if project.GitRemote != "" {
fmt.Printf("Git Remote: %s\n", project.GitRemote)
}
fmt.Printf("Visibility: %s\n", valueOrDefault(project.Visibility, "private"))
fmt.Printf("Agents: %d\n", project.AgentCount)
fmt.Printf("Created: %s\n", project.Created.Format(time.RFC3339))
if !project.Updated.IsZero() && project.Updated != project.Created {
Expand Down Expand Up @@ -1607,10 +1602,9 @@ func runHubProjectCreate(cmd *cobra.Command, args []string) error {

// Create project on the hub (server assigns ID)
project, err := client.Projects().Create(ctx, &hubclient.CreateProjectRequest{
Name: displayName,
Slug: slug,
GitRemote: normalized,
Visibility: hubProjectCreateVisibility,
Name: displayName,
Slug: slug,
GitRemote: normalized,
Labels: map[string]string{
"scion.dev/default-branch": defaultBranch,
"scion.dev/clone-url": util.ToHTTPSCloneURL(gitURL),
Expand Down
7 changes: 3 additions & 4 deletions cmd/server_broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,9 @@ func registerGlobalProjectAndBroker(ctx context.Context, s store.Store, brokerID
projectNeedsDefaultBroker := false
if globalProject == nil {
globalProject = &store.Project{
ID: api.NewUUID(),
Name: "Global",
Slug: GlobalProjectName,
Visibility: store.VisibilityPrivate,
ID: api.NewUUID(),
Name: "Global",
Slug: GlobalProjectName,
Labels: map[string]string{
"scion.io/system": "true",
"scion.io/global": "true",
Expand Down
21 changes: 21 additions & 0 deletions docs-site/src/content/docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,30 @@ A seeded system or custom limit configuration that defines a quota boundary with

## Messaging

### Branch mode (message mode)
A message mode that permits messaging from ancestry users (like lineage) plus the agent's direct parent and child agents. Project owners can pierce branch mode.

### Lineage mode (message mode)
A message mode that restricts messaging to users in the agent's ancestry chain — the creating user and their ancestors. No agent-to-agent messaging is permitted for lineage-mode agents. Project owners can pierce lineage mode.

### Message Mode
A per-agent setting that controls which actors (users and agents) can send messages to that agent. One of four values: `none`, `lineage`, `branch`, or `project` (the default). Set by the agent's owner or a project admin via the `set_message_mode` action; changeable at any time with immediate effect. Stored on the agent record as `message_mode`.

### Messageability
A server-computed assessment of whether a specific viewer can message a specific agent, considering the agent's message mode, the viewer's identity, ancestry relationship, and permissions. Exposed in API responses as `_messageability` with `canMessage` and `canReachViewer` booleans. Used by the UI to gate message buttons and show reachability indicators.

### Native Web Chat
The built-in interactive messaging interface in the Web Dashboard (enabled via the `web.native_chat` feature flag) that promotes chat to a top-level fourth ShellType (alongside standalone, profile, and app). It features a dedicated thread rail, unread indicators, three-state visibility filtering (Conversation/Verbose/Full), @-mention autocomplete, and cross-channel reply coherence.

### None mode (message mode)
A message mode that seals the agent from all messaging except system-plane notices and super-admin piercing. No users and no agents can message a none-mode agent through normal paths.

### Piercing (message authorization)
The ability of a privileged user to bypass an agent's message mode restrictions. Super-admins pierce all modes including none. Project owners pierce lineage and branch modes. Piercing applies only to user identities — it is never inherited by an owner's agents.

### Project mode (message mode)
The default message mode. Any user with the `agent:message` permission in the project scope can message the agent, and any same-project agent in project or branch mode can message it. The most permissive mode.

### Message Group
A set of recipients addressed by a single send, correlated by a shared `group_id`, as opposed to a direct message to one recipient or a broadcast to all agents in a project. Distinct from **Group** (Hub users).

Expand Down
17 changes: 17 additions & 0 deletions docs-site/src/content/docs/hosted/user/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,23 @@ When an agent uses the `ask_user` tool (or similar mechanism depending on the ha

Messages are delivered in real-time to the Web Dashboard via Server-Sent Events (SSE). The **Messages Tab** on the individual agent detail page provides a real-time stream of all communication with that specific agent.

## Message Authorization & Modes

Every agent is protected by a **Message Mode** that controls which users and other agents can send messages to it. An agent's message mode can be set via the Web Dashboard or via the `set_message_mode` action. The available modes are:

- **Project Mode (Default)**: Any user with the `agent:message` permission in the project can message the agent. Any peer agent in the project (that is not restricted by lineage mode) can also message it. The most permissive mode.
- **Branch Mode**: Only users in the agent's ancestry chain (its creator and their ancestors), plus the agent's direct parent and child agents, can message it.
- **Lineage Mode**: Strictly restricts messaging to users in the agent's ancestry chain (its creator and their ancestors). No agent-to-agent messaging is permitted.
- **None Mode**: Seals the agent from all messaging except system-plane notices. No users and no agents can message a none-mode agent through normal paths.

### Piercing
Highly privileged users can bypass an agent's message mode restrictions. This is called **piercing**.
- **Project Owners** can pierce Branch and Lineage modes.
- **Super-admins** pierce all modes, including None mode.
Piercing applies only to user identities — it is never inherited by an owner's agents.

The Web Dashboard displays reachability indicators (e.g., whether you can message a specific agent) based on the computed messageability, which takes into account the agent's mode, your ancestry relationship to it, and any piercing privileges.

---

## Developer Guide & Best Practices
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ permissions). Available scopes:
| `agent:attach` | Attach to agent sessions |
| `agent:dispatch` | Dispatch agents (create + start) |
| `agent:manage` | All agent scopes (convenience alias) |
| `project:manage` | All project scopes (convenience alias) |

In addition to project and agent scopes, Scion supports UAT scopes for 7 other resource types: `skill`, `template`, `harness_config`, `group`, `user`, `broker`, and `gcp_service_account`.
In addition to project and agent scopes, Scion supports UAT scopes for 7 other resource types: `skill`, `template`, `harness_config`, `group`, `user`, `broker`, and `gcp_service_account`. Each resource type provides a `*:manage` convenience alias (e.g., `skill:manage`, `template:manage`) that grants all available actions for that resource.

You can dynamically discover all available scopes and their descriptions by querying the API:
```bash
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/hosted/user/secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Enabling progeny propagation dynamically registers implicit access policies (e.g

## Managing Environment Variables

Use the `scion hub env` command suite to manage non-sensitive configuration.
Use the `scion hub env` command suite to manage non-sensitive configuration. Each agent environment variable carries **provenance metadata** (hub-injected, user-defined, or runtime-derived) to help you audit and debug the origin of specific values.

### Setting Variables
```bash
Expand Down
2 changes: 1 addition & 1 deletion internal/fixturegen/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func Spec() []TableFixture {
"git_remote": "https://github.com/example/platform.git",
"labels": unicodeJSON, "annotations": `{"note":"primary"}`,
"created_at": baseTime, "updated_at": baseTime,
"owner_id": userID, "visibility": "private",
"owner_id": userID,
},
{ // minimal row: nullable optionals (git_remote, labels, owner...) left NULL
"id": "11111111-1111-1111-1111-1111111111aa", "name": "Minimal Project",
Expand Down
7 changes: 3 additions & 4 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,7 @@ type StatusEvent struct {
Timestamp string `json:"timestamp"`
}

// Visibility constants for agent and project access control.
// Visibility constants for resource access control (skills, templates, harness configs).
const (
VisibilityPrivate = "private" // Only the owner can access
VisibilityTeam = "team" // Team members can access
Expand All @@ -1029,9 +1029,8 @@ type ProjectInfo struct {
Updated time.Time `json:"updated,omitempty"` // Last modification timestamp

// Ownership
CreatedBy string `json:"createdBy,omitempty"` // User/system that created the project
OwnerID string `json:"ownerId,omitempty"` // Current owner user ID
Visibility string `json:"visibility,omitempty"` // Access level: private, team, public
CreatedBy string `json:"createdBy,omitempty"` // User/system that created the project
OwnerID string `json:"ownerId,omitempty"` // Current owner user ID

// Metadata
Labels map[string]string `json:"labels,omitempty"`
Expand Down
1 change: 0 additions & 1 deletion pkg/ent/migrate/schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading