Created
March 10, 2026 14:09
-
-
Save markturansky/1e80a5733a026a22d9633a71b7e94da2 to your computer and use it in GitHub Desktop.
Agent data model for Ambient
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Spec: Blackboard Coordination API | |
| **Date:** 2026-03-10 | |
| **Status:** Approved for Implementation | |
| **Author:** API Agent | |
| --- | |
| ## Overview | |
| The Ambient API server gains a **Blackboard** coordination layer: persistent agents, coordination check-ins, project-scoped documents, agent-to-agent messaging, a live SSE fleet dashboard, and a native RBAC system. | |
| The central model: **`Agent` and `Session` are distinct entities.** An Agent is a persistent definition that can be ignited into many Sessions over its lifetime. Sessions are ephemeral Kubernetes execution runs. This separation enables re-ignition, run history, fleet persistence, and collaborative sharing. | |
| --- | |
| ## Data Model | |
| ### Entity Relationship Diagram | |
| ```mermaid | |
| erDiagram | |
| %% ── Existing (unchanged) ───────────────────────────────────────────────── | |
| User { | |
| string ID PK | |
| string username | |
| string name | |
| string email | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| Project { | |
| string ID PK "name-as-ID" | |
| string name | |
| string display_name | |
| string description | |
| string labels | |
| string annotations | |
| string status | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| ProjectSettings { | |
| string ID PK | |
| string project_id FK | |
| string group_access | |
| string repositories | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| %% ── Agent (persistent definition) ─────────────────────────────────────── | |
| Agent { | |
| string ID PK | |
| string project_id FK | |
| string parent_agent_id FK "nullable — fleet hierarchy" | |
| string owner_user_id FK | |
| string name | |
| string display_name | |
| string description | |
| string prompt | |
| string repo_url | |
| string workflow_id | |
| string llm_model | |
| float llm_temperature | |
| int llm_max_tokens | |
| string bot_account_name | |
| string resource_overrides | |
| string environment_variables | |
| string labels | |
| string annotations | |
| string current_session_id FK "nullable — denormalized for fast reads" | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| %% ── Session (ephemeral run) ────────────────────────────────────────────── | |
| Session { | |
| string ID PK | |
| string agent_id FK | |
| string triggered_by_user_id FK "who pressed ignite" | |
| string parent_session_id FK "nullable — sub-session spawning" | |
| string phase | |
| time start_time | |
| time completion_time | |
| string kube_cr_name | |
| string kube_cr_uid | |
| string kube_namespace | |
| string sdk_session_id | |
| int sdk_restart_count | |
| string conditions | |
| string reconciled_repos | |
| string reconciled_workflow | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| %% ── SessionMessage (AG-UI event stream) ───────────────────────────────── | |
| SessionMessage { | |
| string ID PK | |
| string session_id FK | |
| int seq | |
| string event_type | |
| string payload | |
| time created_at | |
| } | |
| %% ── Blackboard ─────────────────────────────────────────────────────────── | |
| SessionCheckIn { | |
| string ID PK | |
| string session_id FK | |
| string agent_id FK "denormalized — enables O(agents) Blackboard queries" | |
| string summary | |
| string branch | |
| string worktree | |
| string pr | |
| string phase | |
| int test_count | |
| jsonb items | |
| jsonb questions | |
| jsonb blockers | |
| string next_steps | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| ProjectDocument { | |
| string ID PK | |
| string project_id FK | |
| string slug | |
| string title | |
| text content | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| AgentMessage { | |
| string ID PK | |
| string recipient_agent_id FK | |
| string sender_agent_id FK | |
| string sender_name "denormalized" | |
| text body | |
| bool read | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| %% ── RBAC ───────────────────────────────────────────────────────────────── | |
| Role { | |
| string ID PK | |
| string name | |
| string display_name | |
| string description | |
| jsonb permissions | |
| bool built_in | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| RoleBinding { | |
| string ID PK | |
| string user_id FK | |
| string role_id FK | |
| string scope "global | project | agent | session" | |
| string scope_id "empty for global" | |
| time created_at | |
| time updated_at | |
| time deleted_at | |
| } | |
| %% ── Relationships ──────────────────────────────────────────────────────── | |
| Project ||--o{ ProjectSettings : "has" | |
| Project ||--o{ Agent : "contains" | |
| Project ||--o{ ProjectDocument : "documents" | |
| User ||--o{ Agent : "owns" | |
| Agent ||--o{ Agent : "parent_of" | |
| Agent ||--o{ Session : "runs" | |
| Agent ||--o| Session : "current_session" | |
| Agent ||--o{ AgentMessage : "receives" | |
| Agent ||--o{ AgentMessage : "sends" | |
| Agent ||--o{ SessionCheckIn : "reports" | |
| Session ||--o{ SessionMessage : "streams" | |
| Session ||--o{ SessionCheckIn : "has" | |
| Session ||--o| Session : "parent_of" | |
| User ||--o{ RoleBinding : "bound_to" | |
| Role ||--o{ RoleBinding : "granted_by" | |
| ``` | |
| --- | |
| ## Agent vs Session | |
| The existing `Session` model conflates two distinct concerns. This spec separates them: | |
| | Category | Fields | Entity | | |
| |---|---|---| | |
| | **Identity** | `name`, `prompt`, `repo_url`, `llm_model`, `llm_temperature`, `llm_max_tokens`, `bot_account_name`, `owner_user_id`, `project_id`, `labels`, `annotations`, `resource_overrides`, `environment_variables`, `workflow_id` | `Agent` — persists forever | | |
| | **Run state** | `phase`, `start_time`, `completion_time`, `kube_cr_name`, `kube_cr_uid`, `kube_namespace`, `sdk_session_id`, `sdk_restart_count`, `conditions`, `reconciled_repos`, `reconciled_workflow` | `Session` — ephemeral | | |
| ### Agent Lifecycle | |
| ``` | |
| Agent ──ignite──► Session ──runs──► completes / fails | |
| │ │ | |
| │◄── current_session_id (denormalized) | |
| │ | |
| └── ignite again ──► new Session | |
| │ | |
| prior Session preserved in history | |
| ``` | |
| `current_session_id` is a denormalized pointer updated on every ignite and on session completion. It enables the Blackboard snapshot to read `Agent + latest SessionCheckIn` without joining through sessions. | |
| ### Hierarchical Fleets | |
| `parent_agent_id` makes fleet topologies first-class: | |
| ``` | |
| Project | |
| └── Agent: "Overlord" | |
| ├── Agent: "API" | |
| ├── Agent: "FE" | |
| └── Agent: "CP" | |
| └── Agent: "Reviewer" | |
| ``` | |
| Each Agent has its own run history, inbox, and check-in timeline. The Blackboard renders this as a collapsible tree. | |
| ### Sessions are not directly creatable | |
| Sessions are created exclusively via `POST /agents/{id}/ignite`. They are run artifacts, not first-class resources. Direct `POST /sessions` is removed. | |
| --- | |
| ## Models | |
| ### `Agent` | |
| ```go | |
| type Agent struct { | |
| api.Meta | |
| ProjectID string `json:"project_id" gorm:"not null;index"` | |
| ParentAgentID *string `json:"parent_agent_id" gorm:"index"` | |
| OwnerUserID string `json:"owner_user_id" gorm:"not null"` | |
| Name string `json:"name" gorm:"not null"` | |
| DisplayName string `json:"display_name"` | |
| Description string `json:"description"` | |
| Prompt *string `json:"prompt" gorm:"type:text"` | |
| RepoURL *string `json:"repo_url"` | |
| WorkflowID *string `json:"workflow_id"` | |
| LlmModel string `json:"llm_model" gorm:"default:'sonnet'"` | |
| LlmTemperature float64 `json:"llm_temperature" gorm:"default:0.7"` | |
| LlmMaxTokens int32 `json:"llm_max_tokens" gorm:"default:4000"` | |
| BotAccountName *string `json:"bot_account_name"` | |
| ResourceOverrides *string `json:"resource_overrides"` | |
| EnvironmentVariables *string `json:"environment_variables"` | |
| Labels *string `json:"labels"` | |
| Annotations *string `json:"annotations"` | |
| CurrentSessionID *string `json:"current_session_id"` | |
| } | |
| ``` | |
| ### `Session` | |
| ```go | |
| type Session struct { | |
| api.Meta | |
| AgentID string `json:"agent_id" gorm:"not null;index"` | |
| TriggeredByUserID *string `json:"triggered_by_user_id"` | |
| ParentSessionID *string `json:"parent_session_id" gorm:"index"` | |
| Phase *string `json:"phase"` | |
| StartTime *time.Time `json:"start_time"` | |
| CompletionTime *time.Time `json:"completion_time"` | |
| SdkSessionID *string `json:"sdk_session_id"` | |
| SdkRestartCount *int32 `json:"sdk_restart_count"` | |
| Conditions *string `json:"conditions"` | |
| ReconciledRepos *string `json:"reconciled_repos"` | |
| ReconciledWorkflow *string `json:"reconciled_workflow"` | |
| KubeCrName *string `json:"kube_cr_name"` | |
| KubeCrUID *string `json:"kube_cr_uid"` | |
| KubeNamespace *string `json:"kube_namespace"` | |
| } | |
| ``` | |
| ### `SessionCheckIn` | |
| `agent_id` is denormalized so the Blackboard snapshot queries `session_checkins` directly by `agent_id` — no join through `sessions`. | |
| ```go | |
| type SessionCheckIn struct { | |
| api.Meta | |
| SessionID string `json:"session_id" gorm:"not null;index"` | |
| AgentID string `json:"agent_id" gorm:"not null;index"` | |
| Summary string `json:"summary"` | |
| Branch string `json:"branch"` | |
| Worktree string `json:"worktree"` | |
| PR string `json:"pr"` | |
| Phase string `json:"phase"` | |
| TestCount *int `json:"test_count"` | |
| Items []string `json:"items" gorm:"serializer:json"` | |
| Questions []string `json:"questions" gorm:"serializer:json"` | |
| Blockers []string `json:"blockers" gorm:"serializer:json"` | |
| NextSteps string `json:"next_steps"` | |
| } | |
| ``` | |
| ### `ProjectDocument` | |
| Project-scoped markdown pages, upserted by slug. Reserved slugs: `protocol` (coordination rules), `archive` (historical records). | |
| ```go | |
| type ProjectDocument struct { | |
| api.Meta | |
| ProjectID string `json:"project_id" gorm:"not null;uniqueIndex:idx_project_slug"` | |
| Slug string `json:"slug" gorm:"not null;uniqueIndex:idx_project_slug"` | |
| Title string `json:"title"` | |
| Content string `json:"content" gorm:"type:text"` | |
| } | |
| ``` | |
| ### `AgentMessage` | |
| Agent-to-agent inbox. Distinct from `SessionMessage` (AG-UI runtime events). Messages persist across re-ignitions — the inbox belongs to the Agent, not the run. Either `SenderAgentID` or `SenderUserID` is set (not both); human users can send via the API. | |
| ```go | |
| type AgentMessage struct { | |
| api.Meta | |
| RecipientAgentID string `json:"recipient_agent_id" gorm:"not null;index"` | |
| SenderAgentID *string `json:"sender_agent_id"` | |
| SenderUserID *string `json:"sender_user_id"` | |
| SenderName string `json:"sender_name"` | |
| Body string `json:"body" gorm:"type:text"` | |
| Read bool `json:"read" gorm:"default:false"` | |
| } | |
| ``` | |
| ### `Role` | |
| ```go | |
| type Role struct { | |
| api.Meta | |
| Name string `json:"name" gorm:"uniqueIndex;not null"` | |
| DisplayName string `json:"display_name"` | |
| Description string `json:"description"` | |
| Permissions []string `json:"permissions" gorm:"serializer:json"` | |
| BuiltIn bool `json:"built_in" gorm:"default:false"` | |
| } | |
| ``` | |
| Permissions stored as `[]string` of canonical `"resource:action"` keys. Resolved to typed `Permission` structs in the service layer — no string matching at auth time. | |
| ### `RoleBinding` | |
| ```go | |
| type RoleBinding struct { | |
| api.Meta | |
| UserID string `json:"user_id" gorm:"not null;index:idx_binding_lookup"` | |
| RoleID string `json:"role_id" gorm:"not null;index:idx_binding_lookup"` | |
| Scope string `json:"scope" gorm:"not null;index:idx_binding_lookup"` | |
| ScopeID string `json:"scope_id" gorm:"index:idx_binding_lookup"` | |
| } | |
| ``` | |
| `Scope` values: `"global"` | `"project"` | `"agent"` | `"session"`. `ScopeID` is empty for global bindings. Composite index on `(user_id, scope, scope_id)` makes the authorization hot path a single indexed query. | |
| --- | |
| ## RBAC | |
| ### Scopes | |
| | Scope | Meaning | | |
| |---|---| | |
| | `global` | Applies across the entire platform | | |
| | `project` | Applies to all agents and sessions in a project | | |
| | `agent` | Applies to one agent and all its sessions | | |
| | `session` | Applies to one session run only | | |
| Effective permissions = union of all applicable bindings (global ∪ project ∪ agent ∪ session). No deny rules. | |
| ### Resources and Permissions | |
| ```go | |
| package rbac | |
| type Resource string | |
| const ( | |
| ResourceUser Resource = "user" | |
| ResourceProject Resource = "project" | |
| ResourceProjectSettings Resource = "project_settings" | |
| ResourceProjectDocument Resource = "project_document" | |
| ResourceAgent Resource = "agent" | |
| ResourceSession Resource = "session" | |
| ResourceSessionMessage Resource = "session_message" | |
| ResourceSessionCheckIn Resource = "session_checkin" | |
| ResourceAgentMessage Resource = "agent_message" | |
| ResourceBlackboard Resource = "blackboard" | |
| ResourceRole Resource = "role" | |
| ResourceRoleBinding Resource = "role_binding" | |
| ) | |
| type Action string | |
| const ( | |
| ActionCreate Action = "create" | |
| ActionRead Action = "read" | |
| ActionUpdate Action = "update" | |
| ActionDelete Action = "delete" | |
| ActionList Action = "list" | |
| ActionWatch Action = "watch" | |
| ActionIgnite Action = "ignite" | |
| ActionCheckin Action = "checkin" | |
| ActionMessage Action = "message" | |
| ) | |
| type Permission struct { | |
| Resource Resource | |
| Action Action | |
| } | |
| func (p Permission) String() string { | |
| return string(p.Resource) + ":" + string(p.Action) | |
| } | |
| var ( | |
| PermUserRead = Permission{ResourceUser, ActionRead} | |
| PermUserList = Permission{ResourceUser, ActionList} | |
| PermUserCreate = Permission{ResourceUser, ActionCreate} | |
| PermUserUpdate = Permission{ResourceUser, ActionUpdate} | |
| PermUserDelete = Permission{ResourceUser, ActionDelete} | |
| PermProjectCreate = Permission{ResourceProject, ActionCreate} | |
| PermProjectRead = Permission{ResourceProject, ActionRead} | |
| PermProjectUpdate = Permission{ResourceProject, ActionUpdate} | |
| PermProjectDelete = Permission{ResourceProject, ActionDelete} | |
| PermProjectList = Permission{ResourceProject, ActionList} | |
| PermProjectSettingsRead = Permission{ResourceProjectSettings, ActionRead} | |
| PermProjectSettingsUpdate = Permission{ResourceProjectSettings, ActionUpdate} | |
| PermProjectDocumentRead = Permission{ResourceProjectDocument, ActionRead} | |
| PermProjectDocumentCreate = Permission{ResourceProjectDocument, ActionCreate} | |
| PermProjectDocumentUpdate = Permission{ResourceProjectDocument, ActionUpdate} | |
| PermProjectDocumentDelete = Permission{ResourceProjectDocument, ActionDelete} | |
| PermProjectDocumentList = Permission{ResourceProjectDocument, ActionList} | |
| PermAgentCreate = Permission{ResourceAgent, ActionCreate} | |
| PermAgentRead = Permission{ResourceAgent, ActionRead} | |
| PermAgentUpdate = Permission{ResourceAgent, ActionUpdate} | |
| PermAgentDelete = Permission{ResourceAgent, ActionDelete} | |
| PermAgentList = Permission{ResourceAgent, ActionList} | |
| PermAgentIgnite = Permission{ResourceAgent, ActionIgnite} | |
| PermSessionRead = Permission{ResourceSession, ActionRead} | |
| PermSessionList = Permission{ResourceSession, ActionList} | |
| PermSessionDelete = Permission{ResourceSession, ActionDelete} | |
| PermSessionMessageWatch = Permission{ResourceSessionMessage, ActionWatch} | |
| PermSessionCheckInCreate = Permission{ResourceSessionCheckIn, ActionCreate} | |
| PermSessionCheckInRead = Permission{ResourceSessionCheckIn, ActionRead} | |
| PermSessionCheckInList = Permission{ResourceSessionCheckIn, ActionList} | |
| PermAgentMessageSend = Permission{ResourceAgentMessage, ActionMessage} | |
| PermAgentMessageRead = Permission{ResourceAgentMessage, ActionRead} | |
| PermAgentMessageDelete = Permission{ResourceAgentMessage, ActionDelete} | |
| PermBlackboardWatch = Permission{ResourceBlackboard, ActionWatch} | |
| PermBlackboardRead = Permission{ResourceBlackboard, ActionRead} | |
| PermRoleRead = Permission{ResourceRole, ActionRead} | |
| PermRoleList = Permission{ResourceRole, ActionList} | |
| PermRoleCreate = Permission{ResourceRole, ActionCreate} | |
| PermRoleUpdate = Permission{ResourceRole, ActionUpdate} | |
| PermRoleDelete = Permission{ResourceRole, ActionDelete} | |
| PermRoleBindingRead = Permission{ResourceRoleBinding, ActionRead} | |
| PermRoleBindingList = Permission{ResourceRoleBinding, ActionList} | |
| PermRoleBindingCreate = Permission{ResourceRoleBinding, ActionCreate} | |
| PermRoleBindingDelete = Permission{ResourceRoleBinding, ActionDelete} | |
| ) | |
| ``` | |
| ### Built-in Roles | |
| Seeded at migration time. `built_in = true` prevents deletion. | |
| ```go | |
| const ( | |
| RolePlatformAdmin = "platform:admin" | |
| RolePlatformViewer = "platform:viewer" | |
| RoleProjectOwner = "project:owner" | |
| RoleProjectEditor = "project:editor" | |
| RoleProjectViewer = "project:viewer" | |
| RoleAgentOperator = "agent:operator" | |
| RoleAgentObserver = "agent:observer" | |
| RoleAgentRunner = "agent:runner" | |
| ) | |
| ``` | |
| ### Permission Matrix | |
| | Role | Projects | Agents | Sessions | Documents | Check-ins | Inbox | Blackboard | RBAC | | |
| |---|---|---|---|---|---|---|---|---| | |
| | `platform:admin` | full | full | full | full | full | full | full | full | | |
| | `platform:viewer` | read/list | read/list | read/list | read/list | read/list | — | watch/read | read/list | | |
| | `project:owner` | full | full | full | full | full | full | watch/read | project+agent bindings | | |
| | `project:editor` | read | create/update/ignite | read/list | create/update | create/read | send/read | watch/read | — | | |
| | `project:viewer` | read | read/list | read/list | read/list | read/list | — | watch/read | — | | |
| | `agent:operator` | — | update/ignite | read/list | — | create/read | send/read | — | — | | |
| | `agent:observer` | — | read | read/list | — | read/list | — | — | — | | |
| | `agent:runner` | — | read | read | read | create | send | — | — | | |
| ### Authorization Flow | |
| ``` | |
| Request → JWT middleware → RBAC middleware | |
| │ | |
| resolve user from token | |
| │ | |
| fetch bindings: | |
| global | |
| + project (resource's project) | |
| + agent (resource's agent) | |
| + session (resource's session) | |
| │ | |
| union → effective permissions | |
| │ | |
| check required Permission constant | |
| │ | |
| allowed / 403 | |
| ``` | |
| Each handler declares its required `Permission` as a constant. No string literals in authorization checks. | |
| --- | |
| ## API | |
| ### Agents | |
| ``` | |
| GET /api/ambient/v1/projects/{id}/agents list agents in project (tree) | |
| GET /api/ambient/v1/agents/{id} read | |
| POST /api/ambient/v1/agents create | |
| PATCH /api/ambient/v1/agents/{id} update definition | |
| DELETE /api/ambient/v1/agents/{id} delete | |
| GET /api/ambient/v1/agents/{id}/sessions run history | |
| GET /api/ambient/v1/agents/{id}/ignition ignition prompt (no session created) | |
| POST /api/ambient/v1/agents/{id}/ignite create session + return prompt + session | |
| GET /api/ambient/v1/agents/{id}/checkins full check-in history across all sessions | |
| GET /api/ambient/v1/agents/{id}/inbox read inbox (unread first) | |
| POST /api/ambient/v1/agents/{id}/inbox send message to this agent | |
| PATCH /api/ambient/v1/agents/{id}/inbox/{msg_id} mark read | |
| DELETE /api/ambient/v1/agents/{id}/inbox/{msg_id} delete | |
| GET /api/ambient/v1/agents/{id}/role_bindings bindings scoped to this agent | |
| ``` | |
| `POST /agents/{id}/ignite` response body: | |
| ```json | |
| { | |
| "session": { "id": "...", "agent_id": "...", "phase": "pending", ... }, | |
| "ignition_prompt": "# Agent: API\n\nYou are API, working in project..." | |
| } | |
| ``` | |
| Ignition prompt assembles: agent identity + definition, peer agent roster with latest check-ins, project protocol document, check-in POST template. | |
| ### Sessions | |
| ``` | |
| GET /api/ambient/v1/sessions/{id} read | |
| DELETE /api/ambient/v1/sessions/{id} cancel/delete | |
| GET /api/ambient/v1/sessions/{id}/messages SSE event stream (AG-UI) | |
| POST /api/ambient/v1/sessions/{id}/checkin submit check-in | |
| GET /api/ambient/v1/sessions/{id}/checkin latest check-in | |
| GET /api/ambient/v1/sessions/{id}/checkins full check-in history | |
| GET /api/ambient/v1/sessions/{id}/role_bindings bindings scoped to this session | |
| ``` | |
| ### Project Documents | |
| ``` | |
| GET /api/ambient/v1/projects/{id}/documents list | |
| GET /api/ambient/v1/projects/{id}/documents/{slug} read | |
| PUT /api/ambient/v1/projects/{id}/documents/{slug} upsert | |
| DELETE /api/ambient/v1/projects/{id}/documents/{slug} delete | |
| ``` | |
| ### Blackboard | |
| ``` | |
| GET /api/ambient/v1/projects/{id}/blackboard SSE — streams check-in events for all agents in project | |
| GET /api/ambient/v1/projects/{id}/blackboard/snapshot JSON — latest check-in per agent (dashboard bootstrap) | |
| ``` | |
| Snapshot query: | |
| ```sql | |
| WITH latest_checkins AS ( | |
| SELECT DISTINCT ON (agent_id) * | |
| FROM session_checkins | |
| ORDER BY agent_id, created_at DESC | |
| ) | |
| SELECT a.*, lc.* | |
| FROM agents a | |
| LEFT JOIN latest_checkins lc ON lc.agent_id = a.id | |
| WHERE a.project_id = ? | |
| ORDER BY a.name | |
| ``` | |
| ### RBAC | |
| ``` | |
| GET /api/ambient/v1/roles | |
| GET /api/ambient/v1/roles/{id} | |
| POST /api/ambient/v1/roles | |
| PATCH /api/ambient/v1/roles/{id} | |
| DELETE /api/ambient/v1/roles/{id} | |
| GET /api/ambient/v1/role_bindings | |
| POST /api/ambient/v1/role_bindings | |
| DELETE /api/ambient/v1/role_bindings/{id} | |
| GET /api/ambient/v1/users/{id}/role_bindings | |
| GET /api/ambient/v1/projects/{id}/role_bindings | |
| GET /api/ambient/v1/agents/{id}/role_bindings | |
| GET /api/ambient/v1/sessions/{id}/role_bindings | |
| ``` | |
| --- | |
| ## Frontend: Blackboard View | |
| Project-scoped fleet dashboard. Agents as rows, current check-in as columns, SSE-live. | |
| ``` | |
| ┌──────────────────────────────────────────────────────────────────────────────────┐ | |
| │ PROJECT: sdk-backend-replacement [Blackboard] │ | |
| ├──────────────┬──────────┬──────────────┬────────┬─────────┬─────────────────────┤ | |
| │ Agent │ Status │ Branch │ PR │ Tests │ Summary │ | |
| ├──────────────┼──────────┼──────────────┼────────┼─────────┼─────────────────────┤ | |
| │ Overlord │ 🟢 active│ docs/ocp.. │ — │ — │ Infrastructure.. │ | |
| │ ├─ API │ 🟢 active│ feat/sess.. │ — │ 25 │ Session messages.. │ | |
| │ ├─ FE │ 🟢 active│ feat/front..│ — │ — │ Frontend running.. │ | |
| │ └─ CP │ 🟢 active│ feat/grpc.. │ #815 │ 28 │ Runner gRPC AG-UI. │ | |
| │ └─ Reviewer │ 🟡 idle │ — │ — │ — │ Awaiting CP response │ | |
| ├──────────────┴──────────┴──────────────┴────────┴─────────┴─────────────────────┤ | |
| │ [?] questions in amber [!] blockers in red │ | |
| │ [▶ Ignite] [✉ Message] [⊕ New Agent] [📄 Protocol] │ | |
| └──────────────────────────────────────────────────────────────────────────────────┘ | |
| ``` | |
| - Tree rows: `parent_agent_id` hierarchy, collapsible | |
| - SSE via `GET /projects/{id}/blackboard` | |
| - Click row → agent detail: definition, run history, session event stream | |
| - Ignite → `POST /agents/{id}/ignite` | |
| - Message → `POST /agents/{id}/inbox` | |
| - Protocol sidebar → `GET /projects/{id}/documents/protocol` | |
| --- | |
| ## Implementation Order | |
| | # | Feature | Effort | | |
| |---|---|---| | |
| | 1 | `pkg/rbac` — Resource/Action/Permission constants package | Small | | |
| | 2 | `Agent` plugin — model, CRUD, ignite, ignition prompt | Large | | |
| | 3 | `Role` + `RoleBinding` plugins + built-in role seeding | Medium | | |
| | 4 | RBAC middleware — DB-backed, wired after JWT middleware | Small | | |
| | 5 | `Session` migration — add `agent_id`, remove identity fields | Medium | | |
| | 6 | `SessionCheckIn` plugin — with `agent_id` denormalization | Medium | | |
| | 7 | `ProjectDocument` plugin — upsert by slug | Small | | |
| | 8 | Blackboard snapshot + SSE endpoints | Medium | | |
| | 9 | `AgentMessage` plugin | Medium | | |
| | 10 | Frontend Blackboard view | Large | | |
| --- | |
| ## Design Decisions | |
| | Decision | Rationale | | |
| |---|---| | |
| | Agent is persistent, Session is ephemeral | Agent identity survives runs; execution state does not | | |
| | `parent_agent_id` on Agent | Fleet hierarchies are first-class in the data model | | |
| | `current_session_id` denormalized on Agent | Blackboard reads Agent + check-in without joining through sessions | | |
| | `agent_id` denormalized on `SessionCheckIn` | Snapshot query is O(agents), not O(sessions × checkins) | | |
| | `AgentMessage` inbox on Agent, not Session | Messages persist across re-ignitions | | |
| | Sessions created only via ignite | Sessions are run artifacts; direct POST /sessions is removed | | |
| | Four-scope RBAC | Agent scope enables sharing one agent without exposing the whole project | | |
| | `agent:runner` role | Pods get the minimum viable credential: read agent, create check-ins, send messages | | |
| | Permissions as Go constants | Every resource/action pair known at compile time — typo-proof, grep-able | | |
| | Union-only permissions | No deny rules — simpler mental model for fleet operators | | |
| | `ProjectDocument` upserted by slug | Pages are edited in-place; version history is future scope | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment