mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: persist agent-pushed workspace context snapshots in coderd (#26145)
Replaces the v2.10 `PushContextState` stub with a real coderd write path. Phase 1 of the chat-side persistence story; nothing reads these rows yet. Follows [#25983](https://github.com/coder/coder/pull/25983) and unblocks [CODAGT-569](https://linear.app/codercom/issue/CODAGT-569/enable-agent-api-v210-pushcontextstate-bump-currentminor-wire-coderd). ## What ships ### Schema (`000517_workspace_agent_context.{up,down}.sql`) Two new tables plus `api_key_scope` enum extensions: - `workspace_agent_context_snapshots` (PK `workspace_agent_id` to `workspace_agents(id) ON DELETE CASCADE`): one row per agent, overwritten per push. Holds `version`, `schema_version`, `aggregate_hash`, `snapshot_error`, `received_at`. - `workspace_agent_context_resources` (PK `(workspace_agent_id, source)`): per-resource state. `body_kind` and `status` are `TEXT` + `CHECK` so adding new wire kinds (the RFC's reserved PLUGIN/HOOK/SUBAGENT/COMMAND) is a one-line CHECK update plus a Go switch case. ### SQLC queries (`coderd/database/queries/workspaceagentcontext.sql`) - `UpsertWorkspaceAgentContextSnapshot` - `UpsertWorkspaceAgentContextResource` - `DeleteStaleWorkspaceAgentContextResources` (delete-where-source-not-in) - `GetLatestWorkspaceAgentContextSnapshot` - `ListWorkspaceAgentContextResources` ### Handler (`coderd/agentapi/context.go`) `ContextAPI` is a new sub-API. `PushContextState`: 1. Rejects `schema_version > 1` with a non-`Unimplemented` error so a forward-incompatible agent fails loudly during rollout instead of slipping into the permanent fallback path the `Unimplemented` translation reserves for old coderd deployments. 2. Validates resources: no empty/duplicate sources, every variant maps to a known body kind, every status maps to a known enum value, the `Body` oneof is set (even when status is non-OK, mirroring the wire guarantee so coderd can attribute failures to a known kind). 3. Inside `Database.InTx`, reads the existing snapshot. If the push is not `initial` and `version` is not strictly greater, returns `accepted = false` and leaves stored state untouched. Otherwise upserts the snapshot row, upserts each resource, then runs the stale-source prune so the snapshot and resource rows always agree. 4. Returns `accepted = true` on success. Resource bodies are stored as `protojson(body oneof variant)` in `body JSONB` with `body_kind` as the discriminator. Adding a new field to an existing variant is zero work since `protojson` tolerates new fields; adding a new variant is a CHECK + switch case. ### RBAC + dbauthz - New `ResourceWorkspaceAgentContext` (Create/Read/Update/Delete). - New `SubjectTypeAgentContext` plus `subjectAgentContext` system role and `dbauthz.AsAgentContext` helper. The push handler elevates to this subject; the agent's own role does not get direct write access to the table. - New `workspace_agent_context:*` API key scopes registered in the enum migration; internal-only (not added to `externalLowLevel`). ### Audit These rows are agent-pushed state, not user-authored. They are intentionally not added to `AuditActionMap` and not enumerated in `enterprise/audit/table.go`, matching `boundary_logs`, `workspace_agent_memory_resource_monitor`, etc. `enterprise/audit` tests pass unchanged. ## Tests - `coderd/agentapi/context_test.go`: 12 subtests covering accepts/rejects (schema version, empty/duplicate source, unknown status, missing body), version semantics (stale dropped, same-version replay dropped, `initial=true` overwrites lower version), variant coverage, non-OK status persistence, and the empty-active-set prune case. - `coderd/database/dbauthz/dbauthz_test.go`: 5 `MethodTestSuite` cases covering the new queries. - `coderd/rbac/roles_test.go`: `WorkspaceAgentContext` permission row asserting no human role currently has access. - `coderd/database/migrations/testdata/fixtures/000517_workspace_agent_context.up.sql`: one snapshot + one resource per known body kind plus a non-OK status, so the migration test suite never lands with these tables empty. ## Out of scope (later phases) - Chat hydration (`chats.context_aggregate_hash`, `last_injected_context`). - Dirty-bit fan-out and `PUT /chats/{id}/context`. - Agent-side `POST /api/v0/context/resync` barrier and the `coder exp chat context` CLI. - `codersdk` chat-context wire types and the dashboard Sources drawer. - Removal of the chatd per-turn pull fallback. ## Compat property This is a pure write path. If anything here returns errors the agent's `RunPush` loop backs off, no chat behavior changes, and the workspace keeps behaving exactly like it did before v2.10. <details> <summary>Implementation plan and decision log</summary> Key design calls: 1. **Concurrency**: Accept iff `req.Initial || req.Version > existing.Version`. The strict RFC reading ("version comparison is authoritative") locks restarted agents out because their per-process counter resets to 1; honoring `initial=true` reflects the real reboot reality while still rejecting steady-state replays/out-of-order pushes. 2. **Body encoding**: `protojson` over the oneof variant body proto, stored in JSONB with `body_kind` discriminator. Structured at the API/Go layer, schema-tolerant at the storage layer, and Phase 2 readers round-trip back via `protojson.Unmarshal`. 3. **Schema version rejection**: returns a normal error, not `Unimplemented`. The agent's `RunPush` loop only short-circuits on `Unimplemented`; that escape hatch is reserved for old coderd deployments. A forward-incompatible agent should retry-and-back-off, not flip the connection into permanent fallback. 4. **Validation strictness**: empty sources, duplicate sources, `STATUS_UNSPECIFIED`, and missing `Body` oneof variants are rejected before any write so a misbehaving agent cannot poison the snapshot table. Phase 2 readers can trust every row maps to a known proto variant. </details> _This PR was authored by Coder Agents on Kyle Carberry's behalf._
This commit is contained in:
@@ -148,6 +148,31 @@ func (q *querier) authorizeContext(ctx context.Context, action policy.Action, ob
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorizeWorkspaceByAgentID authorizes an action against the workspace
|
||||
// that owns the given agent.
|
||||
//
|
||||
// Fast path: a workspace RBAC object cached in the context by the agent
|
||||
// API connection avoids the GetWorkspaceByAgentID query. The cached
|
||||
// object is refreshed every 5 minutes in agentapi/api.go; authorization
|
||||
// failures fall back to the slow path in case it is stale.
|
||||
//
|
||||
// Slow path: fetch the workspace by agent ID and authorize against it.
|
||||
func (q *querier) authorizeWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID, action policy.Action) error {
|
||||
if rbacObj, ok := WorkspaceRBACFromContext(ctx); ok {
|
||||
if err := q.authorizeContext(ctx, action, rbacObj); err == nil {
|
||||
return nil
|
||||
}
|
||||
q.log.Debug(ctx, "fast path authorization failed for workspace by agent ID, using slow path",
|
||||
slog.F("agent_id", agentID))
|
||||
}
|
||||
|
||||
workspace, err := q.db.GetWorkspaceByAgentID(ctx, agentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return q.authorizeContext(ctx, action, workspace)
|
||||
}
|
||||
|
||||
// authorizePrebuiltWorkspace handles authorization for workspace resource types.
|
||||
// prebuilt_workspaces are a subset of workspaces, currently limited to
|
||||
// supporting delete operations. This function first attempts normal workspace
|
||||
@@ -2377,6 +2402,16 @@ func (q *querier) DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds in
|
||||
return q.db.DeleteStaleChatHeartbeats(ctx, staleSeconds)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteStaleWorkspaceAgentContextResources(ctx context.Context, arg database.DeleteStaleWorkspaceAgentContextResourcesParams) error {
|
||||
// Deleting stale context resources is part of updating the agent's
|
||||
// pushed context state, so it authorizes as an update on the
|
||||
// workspace rather than a delete of the workspace itself.
|
||||
if err := q.authorizeWorkspaceByAgentID(ctx, arg.WorkspaceAgentID, policy.ActionUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.DeleteStaleWorkspaceAgentContextResources(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteTailnetPeer(ctx context.Context, arg database.DeleteTailnetPeerParams) (database.DeleteTailnetPeerRow, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
|
||||
return database.DeleteTailnetPeerRow{}, err
|
||||
@@ -3812,6 +3847,13 @@ func (q *querier) GetLatestCryptoKeyByFeature(ctx context.Context, feature datab
|
||||
return q.db.GetLatestCryptoKeyByFeature(ctx, feature)
|
||||
}
|
||||
|
||||
func (q *querier) GetLatestWorkspaceAgentContextSnapshot(ctx context.Context, workspaceAgentID uuid.UUID) (database.WorkspaceAgentContextSnapshot, error) {
|
||||
if err := q.authorizeWorkspaceByAgentID(ctx, workspaceAgentID, policy.ActionRead); err != nil {
|
||||
return database.WorkspaceAgentContextSnapshot{}, err
|
||||
}
|
||||
return q.db.GetLatestWorkspaceAgentContextSnapshot(ctx, workspaceAgentID)
|
||||
}
|
||||
|
||||
func (q *querier) GetLatestWorkspaceAppStatusByAppID(ctx context.Context, appID uuid.UUID) (database.WorkspaceAppStatus, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return database.WorkspaceAppStatus{}, err
|
||||
@@ -6557,6 +6599,13 @@ func (q *querier) ListUserSkillMetadataByUserID(ctx context.Context, userID uuid
|
||||
return q.db.ListUserSkillMetadataByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) ListWorkspaceAgentContextResources(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentContextResource, error) {
|
||||
if err := q.authorizeWorkspaceByAgentID(ctx, workspaceAgentID, policy.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.ListWorkspaceAgentContextResources(ctx, workspaceAgentID)
|
||||
}
|
||||
|
||||
func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||
workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID)
|
||||
if err != nil {
|
||||
@@ -8773,6 +8822,20 @@ func (q *querier) UpsertWebpushVAPIDKeys(ctx context.Context, arg database.Upser
|
||||
return q.db.UpsertWebpushVAPIDKeys(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertWorkspaceAgentContextResource(ctx context.Context, arg database.UpsertWorkspaceAgentContextResourceParams) (database.WorkspaceAgentContextResource, error) {
|
||||
if err := q.authorizeWorkspaceByAgentID(ctx, arg.WorkspaceAgentID, policy.ActionUpdate); err != nil {
|
||||
return database.WorkspaceAgentContextResource{}, err
|
||||
}
|
||||
return q.db.UpsertWorkspaceAgentContextResource(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertWorkspaceAgentContextSnapshot(ctx context.Context, arg database.UpsertWorkspaceAgentContextSnapshotParams) (database.WorkspaceAgentContextSnapshot, error) {
|
||||
if err := q.authorizeWorkspaceByAgentID(ctx, arg.WorkspaceAgentID, policy.ActionUpdate); err != nil {
|
||||
return database.WorkspaceAgentContextSnapshot{}, err
|
||||
}
|
||||
return q.db.UpsertWorkspaceAgentContextSnapshot(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||
workspace, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
|
||||
if err != nil {
|
||||
|
||||
@@ -6073,6 +6073,60 @@ func (s *MethodTestSuite) TestResourcesMonitor() {
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestWorkspaceAgentContext() {
|
||||
s.Run("UpsertWorkspaceAgentContextSnapshot", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
w := testutil.Fake(s.T(), faker, database.Workspace{})
|
||||
agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{})
|
||||
arg := database.UpsertWorkspaceAgentContextSnapshotParams{
|
||||
WorkspaceAgentID: agt.ID,
|
||||
}
|
||||
dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agt.ID).Return(w, nil).AnyTimes()
|
||||
dbm.EXPECT().UpsertWorkspaceAgentContextSnapshot(gomock.Any(), arg).Return(database.WorkspaceAgentContextSnapshot{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(w, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("UpsertWorkspaceAgentContextResource", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
w := testutil.Fake(s.T(), faker, database.Workspace{})
|
||||
agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{})
|
||||
arg := database.UpsertWorkspaceAgentContextResourceParams{
|
||||
WorkspaceAgentID: agt.ID,
|
||||
Source: "/workspace/AGENTS.md",
|
||||
BodyKind: database.WorkspaceAgentContextBodyKindInstructionFile,
|
||||
Body: []byte(`{}`),
|
||||
Status: database.WorkspaceAgentContextResourceStatusOk,
|
||||
}
|
||||
dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agt.ID).Return(w, nil).AnyTimes()
|
||||
dbm.EXPECT().UpsertWorkspaceAgentContextResource(gomock.Any(), arg).Return(database.WorkspaceAgentContextResource{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(w, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("DeleteStaleWorkspaceAgentContextResources", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
w := testutil.Fake(s.T(), faker, database.Workspace{})
|
||||
agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{})
|
||||
arg := database.DeleteStaleWorkspaceAgentContextResourcesParams{
|
||||
WorkspaceAgentID: agt.ID,
|
||||
ActiveSources: []string{"/workspace/AGENTS.md"},
|
||||
}
|
||||
dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agt.ID).Return(w, nil).AnyTimes()
|
||||
dbm.EXPECT().DeleteStaleWorkspaceAgentContextResources(gomock.Any(), arg).Return(nil).AnyTimes()
|
||||
// Stale-resource deletion is part of updating the agent's
|
||||
// context state, so it asserts ActionUpdate on the workspace.
|
||||
check.Args(arg).Asserts(w, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("GetLatestWorkspaceAgentContextSnapshot", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
w := testutil.Fake(s.T(), faker, database.Workspace{})
|
||||
agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{})
|
||||
dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agt.ID).Return(w, nil).AnyTimes()
|
||||
dbm.EXPECT().GetLatestWorkspaceAgentContextSnapshot(gomock.Any(), agt.ID).Return(database.WorkspaceAgentContextSnapshot{}, nil).AnyTimes()
|
||||
check.Args(agt.ID).Asserts(w, policy.ActionRead)
|
||||
}))
|
||||
s.Run("ListWorkspaceAgentContextResources", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
w := testutil.Fake(s.T(), faker, database.Workspace{})
|
||||
agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{})
|
||||
dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agt.ID).Return(w, nil).AnyTimes()
|
||||
dbm.EXPECT().ListWorkspaceAgentContextResources(gomock.Any(), agt.ID).Return(nil, nil).AnyTimes()
|
||||
check.Args(agt.ID).Asserts(w, policy.ActionRead)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestResourcesProvisionerdserver() {
|
||||
createAgent := func(t *testing.T, db database.Store) (database.WorkspaceAgent, database.WorkspaceTable) {
|
||||
t.Helper()
|
||||
|
||||
+40
@@ -834,6 +834,14 @@ func (m queryMetricsStore) DeleteStaleChatHeartbeats(ctx context.Context, staleS
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteStaleWorkspaceAgentContextResources(ctx context.Context, arg database.DeleteStaleWorkspaceAgentContextResourcesParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteStaleWorkspaceAgentContextResources(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("DeleteStaleWorkspaceAgentContextResources").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteStaleWorkspaceAgentContextResources").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteTailnetPeer(ctx context.Context, arg database.DeleteTailnetPeerParams) (database.DeleteTailnetPeerRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.DeleteTailnetPeer(ctx, arg)
|
||||
@@ -2202,6 +2210,14 @@ func (m queryMetricsStore) GetLatestCryptoKeyByFeature(ctx context.Context, feat
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetLatestWorkspaceAgentContextSnapshot(ctx context.Context, workspaceAgentID uuid.UUID) (database.WorkspaceAgentContextSnapshot, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetLatestWorkspaceAgentContextSnapshot(ctx, workspaceAgentID)
|
||||
m.queryLatencies.WithLabelValues("GetLatestWorkspaceAgentContextSnapshot").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetLatestWorkspaceAgentContextSnapshot").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetLatestWorkspaceAppStatusByAppID(ctx context.Context, appID uuid.UUID) (database.WorkspaceAppStatus, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetLatestWorkspaceAppStatusByAppID(ctx, appID)
|
||||
@@ -4714,6 +4730,14 @@ func (m queryMetricsStore) ListUserSkillMetadataByUserID(ctx context.Context, us
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListWorkspaceAgentContextResources(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentContextResource, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListWorkspaceAgentContextResources(ctx, workspaceAgentID)
|
||||
m.queryLatencies.WithLabelValues("ListWorkspaceAgentContextResources").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListWorkspaceAgentContextResources").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListWorkspaceAgentPortShares(ctx, workspaceID)
|
||||
@@ -6362,6 +6386,22 @@ func (m queryMetricsStore) UpsertWebpushVAPIDKeys(ctx context.Context, arg datab
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpsertWorkspaceAgentContextResource(ctx context.Context, arg database.UpsertWorkspaceAgentContextResourceParams) (database.WorkspaceAgentContextResource, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpsertWorkspaceAgentContextResource(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpsertWorkspaceAgentContextResource").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertWorkspaceAgentContextResource").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpsertWorkspaceAgentContextSnapshot(ctx context.Context, arg database.UpsertWorkspaceAgentContextSnapshotParams) (database.WorkspaceAgentContextSnapshot, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpsertWorkspaceAgentContextSnapshot(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpsertWorkspaceAgentContextSnapshot").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertWorkspaceAgentContextSnapshot").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpsertWorkspaceAgentPortShare(ctx context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpsertWorkspaceAgentPortShare(ctx, arg)
|
||||
|
||||
Generated
+74
@@ -1408,6 +1408,20 @@ func (mr *MockStoreMockRecorder) DeleteStaleChatHeartbeats(ctx, staleSeconds any
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStaleChatHeartbeats", reflect.TypeOf((*MockStore)(nil).DeleteStaleChatHeartbeats), ctx, staleSeconds)
|
||||
}
|
||||
|
||||
// DeleteStaleWorkspaceAgentContextResources mocks base method.
|
||||
func (m *MockStore) DeleteStaleWorkspaceAgentContextResources(ctx context.Context, arg database.DeleteStaleWorkspaceAgentContextResourcesParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteStaleWorkspaceAgentContextResources", ctx, arg)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteStaleWorkspaceAgentContextResources indicates an expected call of DeleteStaleWorkspaceAgentContextResources.
|
||||
func (mr *MockStoreMockRecorder) DeleteStaleWorkspaceAgentContextResources(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStaleWorkspaceAgentContextResources", reflect.TypeOf((*MockStore)(nil).DeleteStaleWorkspaceAgentContextResources), ctx, arg)
|
||||
}
|
||||
|
||||
// DeleteTailnetPeer mocks base method.
|
||||
func (m *MockStore) DeleteTailnetPeer(ctx context.Context, arg database.DeleteTailnetPeerParams) (database.DeleteTailnetPeerRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -4079,6 +4093,21 @@ func (mr *MockStoreMockRecorder) GetLatestCryptoKeyByFeature(ctx, feature any) *
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestCryptoKeyByFeature", reflect.TypeOf((*MockStore)(nil).GetLatestCryptoKeyByFeature), ctx, feature)
|
||||
}
|
||||
|
||||
// GetLatestWorkspaceAgentContextSnapshot mocks base method.
|
||||
func (m *MockStore) GetLatestWorkspaceAgentContextSnapshot(ctx context.Context, workspaceAgentID uuid.UUID) (database.WorkspaceAgentContextSnapshot, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetLatestWorkspaceAgentContextSnapshot", ctx, workspaceAgentID)
|
||||
ret0, _ := ret[0].(database.WorkspaceAgentContextSnapshot)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetLatestWorkspaceAgentContextSnapshot indicates an expected call of GetLatestWorkspaceAgentContextSnapshot.
|
||||
func (mr *MockStoreMockRecorder) GetLatestWorkspaceAgentContextSnapshot(ctx, workspaceAgentID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestWorkspaceAgentContextSnapshot", reflect.TypeOf((*MockStore)(nil).GetLatestWorkspaceAgentContextSnapshot), ctx, workspaceAgentID)
|
||||
}
|
||||
|
||||
// GetLatestWorkspaceAppStatusByAppID mocks base method.
|
||||
func (m *MockStore) GetLatestWorkspaceAppStatusByAppID(ctx context.Context, appID uuid.UUID) (database.WorkspaceAppStatus, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8878,6 +8907,21 @@ func (mr *MockStoreMockRecorder) ListUserSkillMetadataByUserID(ctx, userID any)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUserSkillMetadataByUserID", reflect.TypeOf((*MockStore)(nil).ListUserSkillMetadataByUserID), ctx, userID)
|
||||
}
|
||||
|
||||
// ListWorkspaceAgentContextResources mocks base method.
|
||||
func (m *MockStore) ListWorkspaceAgentContextResources(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentContextResource, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListWorkspaceAgentContextResources", ctx, workspaceAgentID)
|
||||
ret0, _ := ret[0].([]database.WorkspaceAgentContextResource)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListWorkspaceAgentContextResources indicates an expected call of ListWorkspaceAgentContextResources.
|
||||
func (mr *MockStoreMockRecorder) ListWorkspaceAgentContextResources(ctx, workspaceAgentID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWorkspaceAgentContextResources", reflect.TypeOf((*MockStore)(nil).ListWorkspaceAgentContextResources), ctx, workspaceAgentID)
|
||||
}
|
||||
|
||||
// ListWorkspaceAgentPortShares mocks base method.
|
||||
func (m *MockStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -11894,6 +11938,36 @@ func (mr *MockStoreMockRecorder) UpsertWebpushVAPIDKeys(ctx, arg any) *gomock.Ca
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWebpushVAPIDKeys", reflect.TypeOf((*MockStore)(nil).UpsertWebpushVAPIDKeys), ctx, arg)
|
||||
}
|
||||
|
||||
// UpsertWorkspaceAgentContextResource mocks base method.
|
||||
func (m *MockStore) UpsertWorkspaceAgentContextResource(ctx context.Context, arg database.UpsertWorkspaceAgentContextResourceParams) (database.WorkspaceAgentContextResource, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpsertWorkspaceAgentContextResource", ctx, arg)
|
||||
ret0, _ := ret[0].(database.WorkspaceAgentContextResource)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpsertWorkspaceAgentContextResource indicates an expected call of UpsertWorkspaceAgentContextResource.
|
||||
func (mr *MockStoreMockRecorder) UpsertWorkspaceAgentContextResource(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceAgentContextResource", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceAgentContextResource), ctx, arg)
|
||||
}
|
||||
|
||||
// UpsertWorkspaceAgentContextSnapshot mocks base method.
|
||||
func (m *MockStore) UpsertWorkspaceAgentContextSnapshot(ctx context.Context, arg database.UpsertWorkspaceAgentContextSnapshotParams) (database.WorkspaceAgentContextSnapshot, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpsertWorkspaceAgentContextSnapshot", ctx, arg)
|
||||
ret0, _ := ret[0].(database.WorkspaceAgentContextSnapshot)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpsertWorkspaceAgentContextSnapshot indicates an expected call of UpsertWorkspaceAgentContextSnapshot.
|
||||
func (mr *MockStoreMockRecorder) UpsertWorkspaceAgentContextSnapshot(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceAgentContextSnapshot", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceAgentContextSnapshot), ctx, arg)
|
||||
}
|
||||
|
||||
// UpsertWorkspaceAgentPortShare mocks base method.
|
||||
func (m *MockStore) UpsertWorkspaceAgentPortShare(ctx context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+81
@@ -611,6 +611,25 @@ CREATE TYPE user_status AS ENUM (
|
||||
|
||||
COMMENT ON TYPE user_status IS 'Defines the users status: active, dormant, or suspended.';
|
||||
|
||||
CREATE TYPE workspace_agent_context_body_kind AS ENUM (
|
||||
'instruction_file',
|
||||
'skill',
|
||||
'mcp_config',
|
||||
'mcp_server',
|
||||
'plugin',
|
||||
'hook',
|
||||
'subagent',
|
||||
'command'
|
||||
);
|
||||
|
||||
CREATE TYPE workspace_agent_context_resource_status AS ENUM (
|
||||
'ok',
|
||||
'oversize',
|
||||
'unreadable',
|
||||
'invalid',
|
||||
'excluded'
|
||||
);
|
||||
|
||||
CREATE TYPE workspace_agent_lifecycle_state AS ENUM (
|
||||
'created',
|
||||
'starting',
|
||||
@@ -3473,6 +3492,56 @@ CREATE TABLE webpush_subscriptions (
|
||||
endpoint_auth_key text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE workspace_agent_context_resources (
|
||||
workspace_agent_id uuid NOT NULL,
|
||||
source text NOT NULL,
|
||||
body_kind workspace_agent_context_body_kind NOT NULL,
|
||||
body jsonb NOT NULL,
|
||||
content_hash bytea NOT NULL,
|
||||
size_bytes bigint NOT NULL,
|
||||
status workspace_agent_context_resource_status NOT NULL,
|
||||
error text DEFAULT ''::text NOT NULL,
|
||||
source_path text DEFAULT ''::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON TABLE workspace_agent_context_resources IS 'Per-resource state for the latest pushed workspace agent context snapshot.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.source IS 'Resource locator: canonical file path for file-backed kinds, or the MCP server name for mcp_server resources.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.body_kind IS 'Discriminator for the body JSON shape. Matches the proto oneof variant: instruction_file, skill, mcp_config, mcp_server. PLUGIN/HOOK/SUBAGENT/COMMAND are reserved for the Claude Code plugin RFC.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.body IS 'protojson-encoded variant body matching body_kind. Always populated; non-OK statuses use the variant zero value so the wire kind is still attributable.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.content_hash IS 'sha256 over the resource''s original bytes (or transport-encoded server tool list).';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.size_bytes IS 'Original payload size in bytes; populated regardless of status.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.status IS 'Per-resource status. ok carries a populated body; oversize, unreadable, invalid, and excluded carry an empty body plus an error string.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.error IS 'Per-resource error or warning string. Populated whenever status is non-ok; may also carry a non-fatal warning when status is ok.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.source_path IS 'User-declared scan root that produced this resource. Empty for built-in scan roots.';
|
||||
|
||||
CREATE TABLE workspace_agent_context_snapshots (
|
||||
workspace_agent_id uuid NOT NULL,
|
||||
version bigint NOT NULL,
|
||||
aggregate_hash bytea NOT NULL,
|
||||
snapshot_error text DEFAULT ''::text NOT NULL,
|
||||
received_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON TABLE workspace_agent_context_snapshots IS 'Latest workspace agent context snapshot received via PushContextState. One row per workspace agent, overwritten in place.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_snapshots.version IS 'Monotonic per-agent-process push counter. Resets to one when the agent process restarts; combined with the initial flag on the wire to detect agent reboots.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_snapshots.aggregate_hash IS 'sha256 over a canonical encoding of every resource in the snapshot. Identical inputs always produce identical hashes; chat hydration uses this to detect drift.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_snapshots.snapshot_error IS 'Singular snapshot-level error string (count cap exceeded, watcher degraded, etc.). Empty when healthy.';
|
||||
|
||||
COMMENT ON COLUMN workspace_agent_context_snapshots.received_at IS 'Time at which coderd received the push.';
|
||||
|
||||
CREATE TABLE workspace_agent_devcontainers (
|
||||
id uuid NOT NULL,
|
||||
workspace_agent_id uuid NOT NULL,
|
||||
@@ -4267,6 +4336,12 @@ ALTER TABLE ONLY users
|
||||
ALTER TABLE ONLY webpush_subscriptions
|
||||
ADD CONSTRAINT webpush_subscriptions_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY workspace_agent_context_resources
|
||||
ADD CONSTRAINT workspace_agent_context_resources_pkey PRIMARY KEY (workspace_agent_id, source);
|
||||
|
||||
ALTER TABLE ONLY workspace_agent_context_snapshots
|
||||
ADD CONSTRAINT workspace_agent_context_snapshots_pkey PRIMARY KEY (workspace_agent_id);
|
||||
|
||||
ALTER TABLE ONLY workspace_agent_devcontainers
|
||||
ADD CONSTRAINT workspace_agent_devcontainers_pkey PRIMARY KEY (id);
|
||||
|
||||
@@ -5125,6 +5200,12 @@ ALTER TABLE ONLY user_status_changes
|
||||
ALTER TABLE ONLY webpush_subscriptions
|
||||
ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY workspace_agent_context_resources
|
||||
ADD CONSTRAINT workspace_agent_context_resources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY workspace_agent_context_snapshots
|
||||
ADD CONSTRAINT workspace_agent_context_snapshots_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY workspace_agent_devcontainers
|
||||
ADD CONSTRAINT workspace_agent_devcontainers_subagent_id_fkey FOREIGN KEY (subagent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
+2
@@ -121,6 +121,8 @@ const (
|
||||
ForeignKeyUserSkillsUserID ForeignKeyConstraint = "user_skills_user_id_fkey" // ALTER TABLE ONLY user_skills ADD CONSTRAINT user_skills_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
ForeignKeyWebpushSubscriptionsUserID ForeignKeyConstraint = "webpush_subscriptions_user_id_fkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyWorkspaceAgentContextResourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_context_resources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_context_resources ADD CONSTRAINT workspace_agent_context_resources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||
ForeignKeyWorkspaceAgentContextSnapshotsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_context_snapshots_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_context_snapshots ADD CONSTRAINT workspace_agent_context_snapshots_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||
ForeignKeyWorkspaceAgentDevcontainersSubagentID ForeignKeyConstraint = "workspace_agent_devcontainers_subagent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_subagent_id_fkey FOREIGN KEY (subagent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||
ForeignKeyWorkspaceAgentDevcontainersWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_devcontainers_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||
ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
DROP TABLE IF EXISTS workspace_agent_context_resources;
|
||||
DROP TABLE IF EXISTS workspace_agent_context_snapshots;
|
||||
DROP TYPE IF EXISTS workspace_agent_context_resource_status;
|
||||
DROP TYPE IF EXISTS workspace_agent_context_body_kind;
|
||||
@@ -0,0 +1,67 @@
|
||||
-- Discriminator for the body JSON shape stored with each context
|
||||
-- resource. Matches the proto oneof variant names. plugin, hook,
|
||||
-- subagent, and command are reserved for the Claude Code plugin RFC.
|
||||
CREATE TYPE workspace_agent_context_body_kind AS ENUM (
|
||||
'instruction_file',
|
||||
'skill',
|
||||
'mcp_config',
|
||||
'mcp_server',
|
||||
'plugin',
|
||||
'hook',
|
||||
'subagent',
|
||||
'command'
|
||||
);
|
||||
|
||||
-- Per-resource resolution status reported by the agent.
|
||||
CREATE TYPE workspace_agent_context_resource_status AS ENUM (
|
||||
'ok',
|
||||
'oversize',
|
||||
'unreadable',
|
||||
'invalid',
|
||||
'excluded'
|
||||
);
|
||||
|
||||
-- Latest workspace agent context snapshot, one row per agent.
|
||||
-- Overwritten on each PushContextState; no history.
|
||||
CREATE TABLE workspace_agent_context_snapshots (
|
||||
workspace_agent_id UUID PRIMARY KEY REFERENCES workspace_agents(id) ON DELETE CASCADE,
|
||||
version BIGINT NOT NULL,
|
||||
aggregate_hash BYTEA NOT NULL,
|
||||
snapshot_error TEXT NOT NULL DEFAULT '',
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE workspace_agent_context_snapshots IS 'Latest workspace agent context snapshot received via PushContextState. One row per workspace agent, overwritten in place.';
|
||||
COMMENT ON COLUMN workspace_agent_context_snapshots.version IS 'Monotonic per-agent-process push counter. Resets to one when the agent process restarts; combined with the initial flag on the wire to detect agent reboots.';
|
||||
COMMENT ON COLUMN workspace_agent_context_snapshots.aggregate_hash IS 'sha256 over a canonical encoding of every resource in the snapshot. Identical inputs always produce identical hashes; chat hydration uses this to detect drift.';
|
||||
COMMENT ON COLUMN workspace_agent_context_snapshots.snapshot_error IS 'Singular snapshot-level error string (count cap exceeded, watcher degraded, etc.). Empty when healthy.';
|
||||
COMMENT ON COLUMN workspace_agent_context_snapshots.received_at IS 'Time at which coderd received the push.';
|
||||
|
||||
-- Resolved resources within a snapshot. Keyed by (agent, source); a
|
||||
-- subsequent push upserts known sources and the agentapi handler
|
||||
-- deletes any sources absent from the latest push in the same
|
||||
-- transaction.
|
||||
CREATE TABLE workspace_agent_context_resources (
|
||||
workspace_agent_id UUID NOT NULL REFERENCES workspace_agents(id) ON DELETE CASCADE,
|
||||
source TEXT NOT NULL,
|
||||
body_kind workspace_agent_context_body_kind NOT NULL,
|
||||
body JSONB NOT NULL,
|
||||
content_hash BYTEA NOT NULL,
|
||||
size_bytes BIGINT NOT NULL,
|
||||
status workspace_agent_context_resource_status NOT NULL,
|
||||
error TEXT NOT NULL DEFAULT '',
|
||||
source_path TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (workspace_agent_id, source)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE workspace_agent_context_resources IS 'Per-resource state for the latest pushed workspace agent context snapshot.';
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.source IS 'Resource locator: canonical file path for file-backed kinds, or the MCP server name for mcp_server resources.';
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.body_kind IS 'Discriminator for the body JSON shape. Matches the proto oneof variant: instruction_file, skill, mcp_config, mcp_server. PLUGIN/HOOK/SUBAGENT/COMMAND are reserved for the Claude Code plugin RFC.';
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.body IS 'protojson-encoded variant body matching body_kind. Always populated; non-OK statuses use the variant zero value so the wire kind is still attributable.';
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.content_hash IS 'sha256 over the resource''s original bytes (or transport-encoded server tool list).';
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.size_bytes IS 'Original payload size in bytes; populated regardless of status.';
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.status IS 'Per-resource status. ok carries a populated body; oversize, unreadable, invalid, and excluded carry an empty body plus an error string.';
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.error IS 'Per-resource error or warning string. Populated whenever status is non-ok; may also carry a non-fatal warning when status is ok.';
|
||||
COMMENT ON COLUMN workspace_agent_context_resources.source_path IS 'User-declared scan root that produced this resource. Empty for built-in scan roots.';
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
-- Snapshot row and a representative set of resources covering each
|
||||
-- v1 body kind plus a non-OK status. workspace_agent_id matches an
|
||||
-- existing fixture row from 000507_boundary_sessions_and_logs.
|
||||
INSERT INTO workspace_agent_context_snapshots (
|
||||
workspace_agent_id,
|
||||
version,
|
||||
aggregate_hash,
|
||||
snapshot_error,
|
||||
received_at
|
||||
) VALUES (
|
||||
'45e89705-e09d-4850-bcec-f9a937f5d78d',
|
||||
1,
|
||||
'\x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
||||
'',
|
||||
'2026-06-01 12:00:00+00'
|
||||
);
|
||||
|
||||
INSERT INTO workspace_agent_context_resources (
|
||||
workspace_agent_id,
|
||||
source,
|
||||
body_kind,
|
||||
body,
|
||||
content_hash,
|
||||
size_bytes,
|
||||
status,
|
||||
error,
|
||||
source_path,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES
|
||||
(
|
||||
'45e89705-e09d-4850-bcec-f9a937f5d78d',
|
||||
'/home/coder/workspace/AGENTS.md',
|
||||
'instruction_file',
|
||||
'{"content":"aGVsbG8="}',
|
||||
'\x1111111111111111111111111111111111111111111111111111111111111111',
|
||||
5,
|
||||
'ok',
|
||||
'',
|
||||
'',
|
||||
'2026-06-01 12:00:00+00',
|
||||
'2026-06-01 12:00:00+00'
|
||||
),
|
||||
(
|
||||
'45e89705-e09d-4850-bcec-f9a937f5d78d',
|
||||
'/home/coder/workspace/.agents/skills/example/SKILL.md',
|
||||
'skill',
|
||||
'{"meta":"LS0tCm5hbWU6IGV4YW1wbGUKLS0tCmJvZHk=","name":"example","description":"Example skill"}',
|
||||
'\x2222222222222222222222222222222222222222222222222222222222222222',
|
||||
32,
|
||||
'ok',
|
||||
'',
|
||||
'/home/coder/workspace',
|
||||
'2026-06-01 12:00:00+00',
|
||||
'2026-06-01 12:00:00+00'
|
||||
),
|
||||
(
|
||||
'45e89705-e09d-4850-bcec-f9a937f5d78d',
|
||||
'/home/coder/workspace/.mcp.json',
|
||||
'mcp_config',
|
||||
'{}',
|
||||
'\x3333333333333333333333333333333333333333333333333333333333333333',
|
||||
128,
|
||||
'ok',
|
||||
'',
|
||||
'',
|
||||
'2026-06-01 12:00:00+00',
|
||||
'2026-06-01 12:00:00+00'
|
||||
),
|
||||
(
|
||||
'45e89705-e09d-4850-bcec-f9a937f5d78d',
|
||||
'mcp:echo',
|
||||
'mcp_server',
|
||||
'{"server_name":"echo","description":"echoes input"}',
|
||||
'\x4444444444444444444444444444444444444444444444444444444444444444',
|
||||
256,
|
||||
'ok',
|
||||
'',
|
||||
'/home/coder/workspace/.mcp.json',
|
||||
'2026-06-01 12:00:00+00',
|
||||
'2026-06-01 12:00:00+00'
|
||||
),
|
||||
(
|
||||
'45e89705-e09d-4850-bcec-f9a937f5d78d',
|
||||
'/home/coder/workspace/big.md',
|
||||
'instruction_file',
|
||||
'{}',
|
||||
'\x5555555555555555555555555555555555555555555555555555555555555555',
|
||||
99999,
|
||||
'oversize',
|
||||
'file exceeds 64KiB per-resource cap',
|
||||
'',
|
||||
'2026-06-01 12:00:00+00',
|
||||
'2026-06-01 12:00:00+00'
|
||||
);
|
||||
Generated
+179
@@ -3798,6 +3798,149 @@ func AllUserStatusValues() []UserStatus {
|
||||
}
|
||||
}
|
||||
|
||||
type WorkspaceAgentContextBodyKind string
|
||||
|
||||
const (
|
||||
WorkspaceAgentContextBodyKindInstructionFile WorkspaceAgentContextBodyKind = "instruction_file"
|
||||
WorkspaceAgentContextBodyKindSkill WorkspaceAgentContextBodyKind = "skill"
|
||||
WorkspaceAgentContextBodyKindMcpConfig WorkspaceAgentContextBodyKind = "mcp_config"
|
||||
WorkspaceAgentContextBodyKindMcpServer WorkspaceAgentContextBodyKind = "mcp_server"
|
||||
WorkspaceAgentContextBodyKindPlugin WorkspaceAgentContextBodyKind = "plugin"
|
||||
WorkspaceAgentContextBodyKindHook WorkspaceAgentContextBodyKind = "hook"
|
||||
WorkspaceAgentContextBodyKindSubagent WorkspaceAgentContextBodyKind = "subagent"
|
||||
WorkspaceAgentContextBodyKindCommand WorkspaceAgentContextBodyKind = "command"
|
||||
)
|
||||
|
||||
func (e *WorkspaceAgentContextBodyKind) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = WorkspaceAgentContextBodyKind(s)
|
||||
case string:
|
||||
*e = WorkspaceAgentContextBodyKind(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for WorkspaceAgentContextBodyKind: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullWorkspaceAgentContextBodyKind struct {
|
||||
WorkspaceAgentContextBodyKind WorkspaceAgentContextBodyKind `json:"workspace_agent_context_body_kind"`
|
||||
Valid bool `json:"valid"` // Valid is true if WorkspaceAgentContextBodyKind is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullWorkspaceAgentContextBodyKind) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.WorkspaceAgentContextBodyKind, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.WorkspaceAgentContextBodyKind.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullWorkspaceAgentContextBodyKind) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.WorkspaceAgentContextBodyKind), nil
|
||||
}
|
||||
|
||||
func (e WorkspaceAgentContextBodyKind) Valid() bool {
|
||||
switch e {
|
||||
case WorkspaceAgentContextBodyKindInstructionFile,
|
||||
WorkspaceAgentContextBodyKindSkill,
|
||||
WorkspaceAgentContextBodyKindMcpConfig,
|
||||
WorkspaceAgentContextBodyKindMcpServer,
|
||||
WorkspaceAgentContextBodyKindPlugin,
|
||||
WorkspaceAgentContextBodyKindHook,
|
||||
WorkspaceAgentContextBodyKindSubagent,
|
||||
WorkspaceAgentContextBodyKindCommand:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AllWorkspaceAgentContextBodyKindValues() []WorkspaceAgentContextBodyKind {
|
||||
return []WorkspaceAgentContextBodyKind{
|
||||
WorkspaceAgentContextBodyKindInstructionFile,
|
||||
WorkspaceAgentContextBodyKindSkill,
|
||||
WorkspaceAgentContextBodyKindMcpConfig,
|
||||
WorkspaceAgentContextBodyKindMcpServer,
|
||||
WorkspaceAgentContextBodyKindPlugin,
|
||||
WorkspaceAgentContextBodyKindHook,
|
||||
WorkspaceAgentContextBodyKindSubagent,
|
||||
WorkspaceAgentContextBodyKindCommand,
|
||||
}
|
||||
}
|
||||
|
||||
type WorkspaceAgentContextResourceStatus string
|
||||
|
||||
const (
|
||||
WorkspaceAgentContextResourceStatusOk WorkspaceAgentContextResourceStatus = "ok"
|
||||
WorkspaceAgentContextResourceStatusOversize WorkspaceAgentContextResourceStatus = "oversize"
|
||||
WorkspaceAgentContextResourceStatusUnreadable WorkspaceAgentContextResourceStatus = "unreadable"
|
||||
WorkspaceAgentContextResourceStatusInvalid WorkspaceAgentContextResourceStatus = "invalid"
|
||||
WorkspaceAgentContextResourceStatusExcluded WorkspaceAgentContextResourceStatus = "excluded"
|
||||
)
|
||||
|
||||
func (e *WorkspaceAgentContextResourceStatus) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = WorkspaceAgentContextResourceStatus(s)
|
||||
case string:
|
||||
*e = WorkspaceAgentContextResourceStatus(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for WorkspaceAgentContextResourceStatus: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullWorkspaceAgentContextResourceStatus struct {
|
||||
WorkspaceAgentContextResourceStatus WorkspaceAgentContextResourceStatus `json:"workspace_agent_context_resource_status"`
|
||||
Valid bool `json:"valid"` // Valid is true if WorkspaceAgentContextResourceStatus is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullWorkspaceAgentContextResourceStatus) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.WorkspaceAgentContextResourceStatus, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.WorkspaceAgentContextResourceStatus.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullWorkspaceAgentContextResourceStatus) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.WorkspaceAgentContextResourceStatus), nil
|
||||
}
|
||||
|
||||
func (e WorkspaceAgentContextResourceStatus) Valid() bool {
|
||||
switch e {
|
||||
case WorkspaceAgentContextResourceStatusOk,
|
||||
WorkspaceAgentContextResourceStatusOversize,
|
||||
WorkspaceAgentContextResourceStatusUnreadable,
|
||||
WorkspaceAgentContextResourceStatusInvalid,
|
||||
WorkspaceAgentContextResourceStatusExcluded:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AllWorkspaceAgentContextResourceStatusValues() []WorkspaceAgentContextResourceStatus {
|
||||
return []WorkspaceAgentContextResourceStatus{
|
||||
WorkspaceAgentContextResourceStatusOk,
|
||||
WorkspaceAgentContextResourceStatusOversize,
|
||||
WorkspaceAgentContextResourceStatusUnreadable,
|
||||
WorkspaceAgentContextResourceStatusInvalid,
|
||||
WorkspaceAgentContextResourceStatusExcluded,
|
||||
}
|
||||
}
|
||||
|
||||
type WorkspaceAgentLifecycleState string
|
||||
|
||||
const (
|
||||
@@ -5983,6 +6126,42 @@ type WorkspaceAgent struct {
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
}
|
||||
|
||||
// Per-resource state for the latest pushed workspace agent context snapshot.
|
||||
type WorkspaceAgentContextResource struct {
|
||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
||||
// Resource locator: canonical file path for file-backed kinds, or the MCP server name for mcp_server resources.
|
||||
Source string `db:"source" json:"source"`
|
||||
// Discriminator for the body JSON shape. Matches the proto oneof variant: instruction_file, skill, mcp_config, mcp_server. PLUGIN/HOOK/SUBAGENT/COMMAND are reserved for the Claude Code plugin RFC.
|
||||
BodyKind WorkspaceAgentContextBodyKind `db:"body_kind" json:"body_kind"`
|
||||
// protojson-encoded variant body matching body_kind. Always populated; non-OK statuses use the variant zero value so the wire kind is still attributable.
|
||||
Body json.RawMessage `db:"body" json:"body"`
|
||||
// sha256 over the resource's original bytes (or transport-encoded server tool list).
|
||||
ContentHash []byte `db:"content_hash" json:"content_hash"`
|
||||
// Original payload size in bytes; populated regardless of status.
|
||||
SizeBytes int64 `db:"size_bytes" json:"size_bytes"`
|
||||
// Per-resource status. ok carries a populated body; oversize, unreadable, invalid, and excluded carry an empty body plus an error string.
|
||||
Status WorkspaceAgentContextResourceStatus `db:"status" json:"status"`
|
||||
// Per-resource error or warning string. Populated whenever status is non-ok; may also carry a non-fatal warning when status is ok.
|
||||
Error string `db:"error" json:"error"`
|
||||
// User-declared scan root that produced this resource. Empty for built-in scan roots.
|
||||
SourcePath string `db:"source_path" json:"source_path"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// Latest workspace agent context snapshot received via PushContextState. One row per workspace agent, overwritten in place.
|
||||
type WorkspaceAgentContextSnapshot struct {
|
||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
||||
// Monotonic per-agent-process push counter. Resets to one when the agent process restarts; combined with the initial flag on the wire to detect agent reboots.
|
||||
Version int64 `db:"version" json:"version"`
|
||||
// sha256 over a canonical encoding of every resource in the snapshot. Identical inputs always produce identical hashes; chat hydration uses this to detect drift.
|
||||
AggregateHash []byte `db:"aggregate_hash" json:"aggregate_hash"`
|
||||
// Singular snapshot-level error string (count cap exceeded, watcher degraded, etc.). Empty when healthy.
|
||||
SnapshotError string `db:"snapshot_error" json:"snapshot_error"`
|
||||
// Time at which coderd received the push.
|
||||
ReceivedAt time.Time `db:"received_at" json:"received_at"`
|
||||
}
|
||||
|
||||
// Workspace agent devcontainer configuration
|
||||
type WorkspaceAgentDevcontainer struct {
|
||||
// Unique identifier
|
||||
|
||||
Generated
+25
@@ -216,6 +216,10 @@ type sqlcQuerier interface {
|
||||
DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error
|
||||
DeleteRuntimeConfig(ctx context.Context, key string) error
|
||||
DeleteStaleChatHeartbeats(ctx context.Context, staleSeconds int32) (int64, error)
|
||||
// Deletes any resources for the agent whose source is not in the
|
||||
// supplied active set. Atomic alongside the snapshot upsert so the
|
||||
// stored snapshot and resource rows always agree.
|
||||
DeleteStaleWorkspaceAgentContextResources(ctx context.Context, arg DeleteStaleWorkspaceAgentContextResourcesParams) error
|
||||
DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error)
|
||||
DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error)
|
||||
DeleteTask(ctx context.Context, arg DeleteTaskParams) (uuid.UUID, error)
|
||||
@@ -231,6 +235,15 @@ type sqlcQuerier interface {
|
||||
DeleteWorkspaceACLsByOrganization(ctx context.Context, arg DeleteWorkspaceACLsByOrganizationParams) error
|
||||
DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error
|
||||
DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error
|
||||
// Soft-deletes a single sub-agent (a child agent such as a devcontainer
|
||||
// agent). Called from the DeleteSubAgent RPC when a sub-agent is torn
|
||||
// down, which can happen mid-build without a full workspace rebuild.
|
||||
//
|
||||
// Agent context rows are hard-deleted for the same reason as in
|
||||
// SoftDeletePriorWorkspaceAgents: they only describe live agents, the
|
||||
// rebuild-time soft-delete queries skip already-deleted agents, and
|
||||
// agents are never hard-deleted, so the rows would otherwise orphan
|
||||
// forever.
|
||||
DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error
|
||||
// Disable foreign keys and triggers for all tables.
|
||||
// Deprecated: disable foreign keys was created to aid in migrating off
|
||||
@@ -574,6 +587,7 @@ type sqlcQuerier interface {
|
||||
GetLastChatMessageByRole(ctx context.Context, arg GetLastChatMessageByRoleParams) (ChatMessage, error)
|
||||
GetLastUpdateCheck(ctx context.Context) (string, error)
|
||||
GetLatestCryptoKeyByFeature(ctx context.Context, feature CryptoKeyFeature) (CryptoKey, error)
|
||||
GetLatestWorkspaceAgentContextSnapshot(ctx context.Context, workspaceAgentID uuid.UUID) (WorkspaceAgentContextSnapshot, error)
|
||||
GetLatestWorkspaceAppStatusByAppID(ctx context.Context, appID uuid.UUID) (WorkspaceAppStatus, error)
|
||||
GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error)
|
||||
GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error)
|
||||
@@ -1153,6 +1167,7 @@ type sqlcQuerier interface {
|
||||
// (runtime injection).
|
||||
ListUserSecretsWithValues(ctx context.Context, userID uuid.UUID) ([]UserSecret, error)
|
||||
ListUserSkillMetadataByUserID(ctx context.Context, userID uuid.UUID) ([]ListUserSkillMetadataByUserIDRow, error)
|
||||
ListWorkspaceAgentContextResources(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentContextResource, error)
|
||||
ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error)
|
||||
// Locks the chat row with FOR UPDATE and atomically increments its
|
||||
// snapshot_version, returning the post-bump chat. This is the single
|
||||
@@ -1212,12 +1227,20 @@ type sqlcQuerier interface {
|
||||
// provisionerdserver when a workspace build completes, after the new
|
||||
// build's agents have been inserted, so running agents are not
|
||||
// deleted while a build is still queued or provisioning.
|
||||
//
|
||||
// Agent context rows (workspace_agent_context_snapshots and
|
||||
// workspace_agent_context_resources) only describe live agents, and
|
||||
// agents are never un-deleted, so they are hard-deleted here instead
|
||||
// of accumulating alongside the soft-deleted agent rows.
|
||||
SoftDeletePriorWorkspaceAgents(ctx context.Context, arg SoftDeletePriorWorkspaceAgentsParams) error
|
||||
// Marks every non-deleted agent belonging to the given workspace as
|
||||
// deleted. Called alongside UpdateWorkspaceDeletedByID when a workspace
|
||||
// itself is soft-deleted, so the agent instance-identity auth path
|
||||
// (which filters on workspace_agents.deleted) doesn't keep seeing
|
||||
// orphaned rows.
|
||||
//
|
||||
// Agent context rows are hard-deleted for the same reason as in
|
||||
// SoftDeletePriorWorkspaceAgents.
|
||||
SoftDeleteWorkspaceAgentsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) error
|
||||
// Overrides updated_at on the parent run without touching any
|
||||
// other column. Used by tests that need to stamp a run with a
|
||||
@@ -1524,6 +1547,8 @@ type sqlcQuerier interface {
|
||||
UpsertUserChatDebugLoggingEnabled(ctx context.Context, arg UpsertUserChatDebugLoggingEnabledParams) error
|
||||
UpsertUserChatPersonalModelOverride(ctx context.Context, arg UpsertUserChatPersonalModelOverrideParams) error
|
||||
UpsertWebpushVAPIDKeys(ctx context.Context, arg UpsertWebpushVAPIDKeysParams) error
|
||||
UpsertWorkspaceAgentContextResource(ctx context.Context, arg UpsertWorkspaceAgentContextResourceParams) (WorkspaceAgentContextResource, error)
|
||||
UpsertWorkspaceAgentContextSnapshot(ctx context.Context, arg UpsertWorkspaceAgentContextSnapshotParams) (WorkspaceAgentContextSnapshot, error)
|
||||
UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error)
|
||||
UpsertWorkspaceApp(ctx context.Context, arg UpsertWorkspaceAppParams) (WorkspaceApp, error)
|
||||
//
|
||||
|
||||
@@ -15122,6 +15122,139 @@ func TestSoftDeleteWorkspaceAgentsByWorkspaceID(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestSoftDeleteWorkspaceAgentsPurgesContext verifies that both agent
|
||||
// soft-delete queries hard-delete the agents' pushed context rows
|
||||
// (workspace_agent_context_snapshots and
|
||||
// workspace_agent_context_resources). Agents are only ever
|
||||
// soft-deleted, so without this the context rows would accumulate
|
||||
// forever.
|
||||
func TestSoftDeleteWorkspaceAgentsPurgesContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
tpl := dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
tplVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
|
||||
type buildBundle struct {
|
||||
buildID uuid.UUID
|
||||
agentID uuid.UUID
|
||||
agent database.WorkspaceAgent
|
||||
}
|
||||
|
||||
newBuild := func(t *testing.T, wsID uuid.UUID, buildNumber int32) buildBundle {
|
||||
t.Helper()
|
||||
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
OrganizationID: org.ID,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
})
|
||||
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
WorkspaceID: wsID,
|
||||
JobID: job.ID,
|
||||
TemplateVersionID: tplVersion.ID,
|
||||
BuildNumber: buildNumber,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: job.ID})
|
||||
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: resource.ID})
|
||||
return buildBundle{buildID: build.ID, agentID: agent.ID, agent: agent}
|
||||
}
|
||||
|
||||
pushContext := func(t *testing.T, agentID uuid.UUID) {
|
||||
t.Helper()
|
||||
_, err := db.UpsertWorkspaceAgentContextSnapshot(ctx, database.UpsertWorkspaceAgentContextSnapshotParams{
|
||||
WorkspaceAgentID: agentID,
|
||||
Version: 1,
|
||||
AggregateHash: []byte{0x01},
|
||||
ReceivedAt: dbtime.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.UpsertWorkspaceAgentContextResource(ctx, database.UpsertWorkspaceAgentContextResourceParams{
|
||||
WorkspaceAgentID: agentID,
|
||||
Source: "/workspace/AGENTS.md",
|
||||
BodyKind: database.WorkspaceAgentContextBodyKindInstructionFile,
|
||||
Body: []byte(`{}`),
|
||||
ContentHash: []byte{0x02},
|
||||
SizeBytes: 2,
|
||||
Status: database.WorkspaceAgentContextResourceStatusOk,
|
||||
Now: dbtime.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
hasContext := func(t *testing.T, agentID uuid.UUID) bool {
|
||||
t.Helper()
|
||||
_, err := db.GetLatestWorkspaceAgentContextSnapshot(ctx, agentID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
resources, err := db.ListWorkspaceAgentContextResources(ctx, agentID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, resources, "snapshot and resource rows must be deleted together")
|
||||
return false
|
||||
}
|
||||
require.NoError(t, err)
|
||||
return true
|
||||
}
|
||||
|
||||
wsA := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OrganizationID: org.ID,
|
||||
TemplateID: tpl.ID,
|
||||
OwnerID: user.ID,
|
||||
}).ID
|
||||
wsB := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OrganizationID: org.ID,
|
||||
TemplateID: tpl.ID,
|
||||
OwnerID: user.ID,
|
||||
}).ID
|
||||
|
||||
a1 := newBuild(t, wsA, 1)
|
||||
a2 := newBuild(t, wsA, 2)
|
||||
b1 := newBuild(t, wsB, 1)
|
||||
|
||||
pushContext(t, a1.agentID)
|
||||
pushContext(t, a2.agentID)
|
||||
pushContext(t, b1.agentID)
|
||||
|
||||
// Soft-deleting wsA's prior agents purges a1's context but leaves
|
||||
// the current build's agent and other workspaces untouched.
|
||||
err := db.SoftDeletePriorWorkspaceAgents(ctx, database.SoftDeletePriorWorkspaceAgentsParams{
|
||||
WorkspaceID: wsA,
|
||||
CurrentBuildID: a2.buildID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, hasContext(t, a1.agentID), "prior build agent context must be purged")
|
||||
assert.True(t, hasContext(t, a2.agentID), "current build agent context must remain")
|
||||
assert.True(t, hasContext(t, b1.agentID), "other workspace agent context must remain")
|
||||
|
||||
// Soft-deleting all of wsB's agents purges b1's context.
|
||||
err = db.SoftDeleteWorkspaceAgentsByWorkspaceID(ctx, wsB)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasContext(t, a2.agentID), "other workspace agent context must remain")
|
||||
assert.False(t, hasContext(t, b1.agentID), "deleted workspace agent context must be purged")
|
||||
|
||||
// Removing a sub-agent mid-build via DeleteWorkspaceSubAgentByID purges
|
||||
// only that sub-agent's context. The rebuild-time queries skip
|
||||
// already-deleted agents, so this is the sole cleanup opportunity.
|
||||
c1 := newBuild(t, wsA, 3)
|
||||
subAgent := dbgen.WorkspaceSubAgent(t, db, c1.agent, database.WorkspaceAgent{})
|
||||
pushContext(t, c1.agentID)
|
||||
pushContext(t, subAgent.ID)
|
||||
|
||||
err = db.DeleteWorkspaceSubAgentByID(ctx, subAgent.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasContext(t, c1.agentID), "parent agent context must remain")
|
||||
assert.False(t, hasContext(t, subAgent.ID), "deleted sub-agent context must be purged")
|
||||
}
|
||||
|
||||
func TestAIGatewayKeysTableConstraints(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Generated
+273
-27
@@ -31114,6 +31114,214 @@ func (q *sqlQuerier) ValidateUserIDs(ctx context.Context, userIds []uuid.UUID) (
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteStaleWorkspaceAgentContextResources = `-- name: DeleteStaleWorkspaceAgentContextResources :exec
|
||||
DELETE FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id = $1
|
||||
AND NOT (source = ANY($2 :: text[]))
|
||||
`
|
||||
|
||||
type DeleteStaleWorkspaceAgentContextResourcesParams struct {
|
||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
||||
ActiveSources []string `db:"active_sources" json:"active_sources"`
|
||||
}
|
||||
|
||||
// Deletes any resources for the agent whose source is not in the
|
||||
// supplied active set. Atomic alongside the snapshot upsert so the
|
||||
// stored snapshot and resource rows always agree.
|
||||
func (q *sqlQuerier) DeleteStaleWorkspaceAgentContextResources(ctx context.Context, arg DeleteStaleWorkspaceAgentContextResourcesParams) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteStaleWorkspaceAgentContextResources, arg.WorkspaceAgentID, pq.Array(arg.ActiveSources))
|
||||
return err
|
||||
}
|
||||
|
||||
const getLatestWorkspaceAgentContextSnapshot = `-- name: GetLatestWorkspaceAgentContextSnapshot :one
|
||||
SELECT workspace_agent_id, version, aggregate_hash, snapshot_error, received_at FROM workspace_agent_context_snapshots
|
||||
WHERE workspace_agent_id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetLatestWorkspaceAgentContextSnapshot(ctx context.Context, workspaceAgentID uuid.UUID) (WorkspaceAgentContextSnapshot, error) {
|
||||
row := q.db.QueryRowContext(ctx, getLatestWorkspaceAgentContextSnapshot, workspaceAgentID)
|
||||
var i WorkspaceAgentContextSnapshot
|
||||
err := row.Scan(
|
||||
&i.WorkspaceAgentID,
|
||||
&i.Version,
|
||||
&i.AggregateHash,
|
||||
&i.SnapshotError,
|
||||
&i.ReceivedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listWorkspaceAgentContextResources = `-- name: ListWorkspaceAgentContextResources :many
|
||||
SELECT workspace_agent_id, source, body_kind, body, content_hash, size_bytes, status, error, source_path, created_at, updated_at FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id = $1
|
||||
ORDER BY source ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) ListWorkspaceAgentContextResources(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentContextResource, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listWorkspaceAgentContextResources, workspaceAgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []WorkspaceAgentContextResource
|
||||
for rows.Next() {
|
||||
var i WorkspaceAgentContextResource
|
||||
if err := rows.Scan(
|
||||
&i.WorkspaceAgentID,
|
||||
&i.Source,
|
||||
&i.BodyKind,
|
||||
&i.Body,
|
||||
&i.ContentHash,
|
||||
&i.SizeBytes,
|
||||
&i.Status,
|
||||
&i.Error,
|
||||
&i.SourcePath,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const upsertWorkspaceAgentContextResource = `-- name: UpsertWorkspaceAgentContextResource :one
|
||||
INSERT INTO workspace_agent_context_resources (
|
||||
workspace_agent_id,
|
||||
source,
|
||||
body_kind,
|
||||
body,
|
||||
content_hash,
|
||||
size_bytes,
|
||||
status,
|
||||
error,
|
||||
source_path,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9,
|
||||
$10,
|
||||
$10
|
||||
)
|
||||
ON CONFLICT (workspace_agent_id, source) DO UPDATE SET
|
||||
body_kind = EXCLUDED.body_kind,
|
||||
body = EXCLUDED.body,
|
||||
content_hash = EXCLUDED.content_hash,
|
||||
size_bytes = EXCLUDED.size_bytes,
|
||||
status = EXCLUDED.status,
|
||||
error = EXCLUDED.error,
|
||||
source_path = EXCLUDED.source_path,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING workspace_agent_id, source, body_kind, body, content_hash, size_bytes, status, error, source_path, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpsertWorkspaceAgentContextResourceParams struct {
|
||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
||||
Source string `db:"source" json:"source"`
|
||||
BodyKind WorkspaceAgentContextBodyKind `db:"body_kind" json:"body_kind"`
|
||||
Body json.RawMessage `db:"body" json:"body"`
|
||||
ContentHash []byte `db:"content_hash" json:"content_hash"`
|
||||
SizeBytes int64 `db:"size_bytes" json:"size_bytes"`
|
||||
Status WorkspaceAgentContextResourceStatus `db:"status" json:"status"`
|
||||
Error string `db:"error" json:"error"`
|
||||
SourcePath string `db:"source_path" json:"source_path"`
|
||||
Now time.Time `db:"now" json:"now"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpsertWorkspaceAgentContextResource(ctx context.Context, arg UpsertWorkspaceAgentContextResourceParams) (WorkspaceAgentContextResource, error) {
|
||||
row := q.db.QueryRowContext(ctx, upsertWorkspaceAgentContextResource,
|
||||
arg.WorkspaceAgentID,
|
||||
arg.Source,
|
||||
arg.BodyKind,
|
||||
arg.Body,
|
||||
arg.ContentHash,
|
||||
arg.SizeBytes,
|
||||
arg.Status,
|
||||
arg.Error,
|
||||
arg.SourcePath,
|
||||
arg.Now,
|
||||
)
|
||||
var i WorkspaceAgentContextResource
|
||||
err := row.Scan(
|
||||
&i.WorkspaceAgentID,
|
||||
&i.Source,
|
||||
&i.BodyKind,
|
||||
&i.Body,
|
||||
&i.ContentHash,
|
||||
&i.SizeBytes,
|
||||
&i.Status,
|
||||
&i.Error,
|
||||
&i.SourcePath,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const upsertWorkspaceAgentContextSnapshot = `-- name: UpsertWorkspaceAgentContextSnapshot :one
|
||||
INSERT INTO workspace_agent_context_snapshots (
|
||||
workspace_agent_id,
|
||||
version,
|
||||
aggregate_hash,
|
||||
snapshot_error,
|
||||
received_at
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5
|
||||
)
|
||||
ON CONFLICT (workspace_agent_id) DO UPDATE SET
|
||||
version = EXCLUDED.version,
|
||||
aggregate_hash = EXCLUDED.aggregate_hash,
|
||||
snapshot_error = EXCLUDED.snapshot_error,
|
||||
received_at = EXCLUDED.received_at
|
||||
RETURNING workspace_agent_id, version, aggregate_hash, snapshot_error, received_at
|
||||
`
|
||||
|
||||
type UpsertWorkspaceAgentContextSnapshotParams struct {
|
||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
||||
Version int64 `db:"version" json:"version"`
|
||||
AggregateHash []byte `db:"aggregate_hash" json:"aggregate_hash"`
|
||||
SnapshotError string `db:"snapshot_error" json:"snapshot_error"`
|
||||
ReceivedAt time.Time `db:"received_at" json:"received_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpsertWorkspaceAgentContextSnapshot(ctx context.Context, arg UpsertWorkspaceAgentContextSnapshotParams) (WorkspaceAgentContextSnapshot, error) {
|
||||
row := q.db.QueryRowContext(ctx, upsertWorkspaceAgentContextSnapshot,
|
||||
arg.WorkspaceAgentID,
|
||||
arg.Version,
|
||||
arg.AggregateHash,
|
||||
arg.SnapshotError,
|
||||
arg.ReceivedAt,
|
||||
)
|
||||
var i WorkspaceAgentContextSnapshot
|
||||
err := row.Scan(
|
||||
&i.WorkspaceAgentID,
|
||||
&i.Version,
|
||||
&i.AggregateHash,
|
||||
&i.SnapshotError,
|
||||
&i.ReceivedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceAgentDevcontainersByAgentID = `-- name: GetWorkspaceAgentDevcontainersByAgentID :many
|
||||
SELECT
|
||||
id, workspace_agent_id, created_at, workspace_folder, config_path, name, subagent_id
|
||||
@@ -31800,16 +32008,30 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold
|
||||
}
|
||||
|
||||
const deleteWorkspaceSubAgentByID = `-- name: DeleteWorkspaceSubAgentByID :exec
|
||||
UPDATE
|
||||
workspace_agents
|
||||
SET
|
||||
deleted = TRUE
|
||||
WHERE
|
||||
id = $1
|
||||
AND parent_id IS NOT NULL
|
||||
AND deleted = FALSE
|
||||
WITH soft_deleted_agents AS (
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id = $1
|
||||
AND parent_id IS NOT NULL
|
||||
AND deleted = FALSE
|
||||
RETURNING id
|
||||
), purged_context_resources AS (
|
||||
DELETE FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
|
||||
)
|
||||
DELETE FROM workspace_agent_context_snapshots
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
|
||||
`
|
||||
|
||||
// Soft-deletes a single sub-agent (a child agent such as a devcontainer
|
||||
// agent). Called from the DeleteSubAgent RPC when a sub-agent is torn
|
||||
// down, which can happen mid-build without a full workspace rebuild.
|
||||
//
|
||||
// Agent context rows are hard-deleted for the same reason as in
|
||||
// SoftDeletePriorWorkspaceAgents: they only describe live agents, the
|
||||
// rebuild-time soft-delete queries skip already-deleted agents, and
|
||||
// agents are never hard-deleted, so the rows would otherwise orphan
|
||||
// forever.
|
||||
func (q *sqlQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteWorkspaceSubAgentByID, id)
|
||||
return err
|
||||
@@ -33391,17 +33613,25 @@ func (q *sqlQuerier) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg
|
||||
}
|
||||
|
||||
const softDeletePriorWorkspaceAgents = `-- name: SoftDeletePriorWorkspaceAgents :exec
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id IN (
|
||||
SELECT wa.id
|
||||
FROM workspace_agents wa
|
||||
JOIN workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN workspace_builds wb ON wb.job_id = wr.job_id
|
||||
WHERE wb.workspace_id = $1
|
||||
AND wb.id <> $2
|
||||
AND wa.deleted = FALSE
|
||||
WITH soft_deleted_agents AS (
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id IN (
|
||||
SELECT wa.id
|
||||
FROM workspace_agents wa
|
||||
JOIN workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN workspace_builds wb ON wb.job_id = wr.job_id
|
||||
WHERE wb.workspace_id = $1
|
||||
AND wb.id <> $2
|
||||
AND wa.deleted = FALSE
|
||||
)
|
||||
RETURNING id
|
||||
), purged_context_resources AS (
|
||||
DELETE FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
|
||||
)
|
||||
DELETE FROM workspace_agent_context_snapshots
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
|
||||
`
|
||||
|
||||
type SoftDeletePriorWorkspaceAgentsParams struct {
|
||||
@@ -33414,22 +33644,35 @@ type SoftDeletePriorWorkspaceAgentsParams struct {
|
||||
// provisionerdserver when a workspace build completes, after the new
|
||||
// build's agents have been inserted, so running agents are not
|
||||
// deleted while a build is still queued or provisioning.
|
||||
//
|
||||
// Agent context rows (workspace_agent_context_snapshots and
|
||||
// workspace_agent_context_resources) only describe live agents, and
|
||||
// agents are never un-deleted, so they are hard-deleted here instead
|
||||
// of accumulating alongside the soft-deleted agent rows.
|
||||
func (q *sqlQuerier) SoftDeletePriorWorkspaceAgents(ctx context.Context, arg SoftDeletePriorWorkspaceAgentsParams) error {
|
||||
_, err := q.db.ExecContext(ctx, softDeletePriorWorkspaceAgents, arg.WorkspaceID, arg.CurrentBuildID)
|
||||
return err
|
||||
}
|
||||
|
||||
const softDeleteWorkspaceAgentsByWorkspaceID = `-- name: SoftDeleteWorkspaceAgentsByWorkspaceID :exec
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id IN (
|
||||
SELECT wa.id
|
||||
FROM workspace_agents wa
|
||||
JOIN workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN workspace_builds wb ON wb.job_id = wr.job_id
|
||||
WHERE wb.workspace_id = $1
|
||||
AND wa.deleted = FALSE
|
||||
WITH soft_deleted_agents AS (
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id IN (
|
||||
SELECT wa.id
|
||||
FROM workspace_agents wa
|
||||
JOIN workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN workspace_builds wb ON wb.job_id = wr.job_id
|
||||
WHERE wb.workspace_id = $1
|
||||
AND wa.deleted = FALSE
|
||||
)
|
||||
RETURNING id
|
||||
), purged_context_resources AS (
|
||||
DELETE FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
|
||||
)
|
||||
DELETE FROM workspace_agent_context_snapshots
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
|
||||
`
|
||||
|
||||
// Marks every non-deleted agent belonging to the given workspace as
|
||||
@@ -33437,6 +33680,9 @@ WHERE id IN (
|
||||
// itself is soft-deleted, so the agent instance-identity auth path
|
||||
// (which filters on workspace_agents.deleted) doesn't keep seeing
|
||||
// orphaned rows.
|
||||
//
|
||||
// Agent context rows are hard-deleted for the same reason as in
|
||||
// SoftDeletePriorWorkspaceAgents.
|
||||
func (q *sqlQuerier) SoftDeleteWorkspaceAgentsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, softDeleteWorkspaceAgentsByWorkspaceID, workspaceID)
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
-- name: UpsertWorkspaceAgentContextSnapshot :one
|
||||
INSERT INTO workspace_agent_context_snapshots (
|
||||
workspace_agent_id,
|
||||
version,
|
||||
aggregate_hash,
|
||||
snapshot_error,
|
||||
received_at
|
||||
) VALUES (
|
||||
@workspace_agent_id,
|
||||
@version,
|
||||
@aggregate_hash,
|
||||
@snapshot_error,
|
||||
@received_at
|
||||
)
|
||||
ON CONFLICT (workspace_agent_id) DO UPDATE SET
|
||||
version = EXCLUDED.version,
|
||||
aggregate_hash = EXCLUDED.aggregate_hash,
|
||||
snapshot_error = EXCLUDED.snapshot_error,
|
||||
received_at = EXCLUDED.received_at
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpsertWorkspaceAgentContextResource :one
|
||||
INSERT INTO workspace_agent_context_resources (
|
||||
workspace_agent_id,
|
||||
source,
|
||||
body_kind,
|
||||
body,
|
||||
content_hash,
|
||||
size_bytes,
|
||||
status,
|
||||
error,
|
||||
source_path,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@workspace_agent_id,
|
||||
@source,
|
||||
@body_kind,
|
||||
@body,
|
||||
@content_hash,
|
||||
@size_bytes,
|
||||
@status,
|
||||
@error,
|
||||
@source_path,
|
||||
@now,
|
||||
@now
|
||||
)
|
||||
ON CONFLICT (workspace_agent_id, source) DO UPDATE SET
|
||||
body_kind = EXCLUDED.body_kind,
|
||||
body = EXCLUDED.body,
|
||||
content_hash = EXCLUDED.content_hash,
|
||||
size_bytes = EXCLUDED.size_bytes,
|
||||
status = EXCLUDED.status,
|
||||
error = EXCLUDED.error,
|
||||
source_path = EXCLUDED.source_path,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteStaleWorkspaceAgentContextResources :exec
|
||||
-- Deletes any resources for the agent whose source is not in the
|
||||
-- supplied active set. Atomic alongside the snapshot upsert so the
|
||||
-- stored snapshot and resource rows always agree.
|
||||
DELETE FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id = @workspace_agent_id
|
||||
AND NOT (source = ANY(@active_sources :: text[]));
|
||||
|
||||
-- name: GetLatestWorkspaceAgentContextSnapshot :one
|
||||
SELECT * FROM workspace_agent_context_snapshots
|
||||
WHERE workspace_agent_id = @workspace_agent_id;
|
||||
|
||||
-- name: ListWorkspaceAgentContextResources :many
|
||||
SELECT * FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id = @workspace_agent_id
|
||||
ORDER BY source ASC;
|
||||
@@ -514,14 +514,28 @@ WHERE
|
||||
AND deleted = FALSE;
|
||||
|
||||
-- name: DeleteWorkspaceSubAgentByID :exec
|
||||
UPDATE
|
||||
workspace_agents
|
||||
SET
|
||||
deleted = TRUE
|
||||
WHERE
|
||||
id = $1
|
||||
AND parent_id IS NOT NULL
|
||||
AND deleted = FALSE;
|
||||
-- Soft-deletes a single sub-agent (a child agent such as a devcontainer
|
||||
-- agent). Called from the DeleteSubAgent RPC when a sub-agent is torn
|
||||
-- down, which can happen mid-build without a full workspace rebuild.
|
||||
--
|
||||
-- Agent context rows are hard-deleted for the same reason as in
|
||||
-- SoftDeletePriorWorkspaceAgents: they only describe live agents, the
|
||||
-- rebuild-time soft-delete queries skip already-deleted agents, and
|
||||
-- agents are never hard-deleted, so the rows would otherwise orphan
|
||||
-- forever.
|
||||
WITH soft_deleted_agents AS (
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id = @id
|
||||
AND parent_id IS NOT NULL
|
||||
AND deleted = FALSE
|
||||
RETURNING id
|
||||
), purged_context_resources AS (
|
||||
DELETE FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
|
||||
)
|
||||
DELETE FROM workspace_agent_context_snapshots
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents);
|
||||
|
||||
-- name: GetWorkspaceAgentsForMetrics :many
|
||||
SELECT
|
||||
@@ -577,17 +591,30 @@ LIMIT 1;
|
||||
-- provisionerdserver when a workspace build completes, after the new
|
||||
-- build's agents have been inserted, so running agents are not
|
||||
-- deleted while a build is still queued or provisioning.
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id IN (
|
||||
SELECT wa.id
|
||||
FROM workspace_agents wa
|
||||
JOIN workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN workspace_builds wb ON wb.job_id = wr.job_id
|
||||
WHERE wb.workspace_id = @workspace_id
|
||||
AND wb.id <> @current_build_id
|
||||
AND wa.deleted = FALSE
|
||||
);
|
||||
--
|
||||
-- Agent context rows (workspace_agent_context_snapshots and
|
||||
-- workspace_agent_context_resources) only describe live agents, and
|
||||
-- agents are never un-deleted, so they are hard-deleted here instead
|
||||
-- of accumulating alongside the soft-deleted agent rows.
|
||||
WITH soft_deleted_agents AS (
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id IN (
|
||||
SELECT wa.id
|
||||
FROM workspace_agents wa
|
||||
JOIN workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN workspace_builds wb ON wb.job_id = wr.job_id
|
||||
WHERE wb.workspace_id = @workspace_id
|
||||
AND wb.id <> @current_build_id
|
||||
AND wa.deleted = FALSE
|
||||
)
|
||||
RETURNING id
|
||||
), purged_context_resources AS (
|
||||
DELETE FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
|
||||
)
|
||||
DELETE FROM workspace_agent_context_snapshots
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents);
|
||||
|
||||
-- name: SoftDeleteWorkspaceAgentsByWorkspaceID :exec
|
||||
-- Marks every non-deleted agent belonging to the given workspace as
|
||||
@@ -595,13 +622,24 @@ WHERE id IN (
|
||||
-- itself is soft-deleted, so the agent instance-identity auth path
|
||||
-- (which filters on workspace_agents.deleted) doesn't keep seeing
|
||||
-- orphaned rows.
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id IN (
|
||||
SELECT wa.id
|
||||
FROM workspace_agents wa
|
||||
JOIN workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN workspace_builds wb ON wb.job_id = wr.job_id
|
||||
WHERE wb.workspace_id = @workspace_id
|
||||
AND wa.deleted = FALSE
|
||||
);
|
||||
--
|
||||
-- Agent context rows are hard-deleted for the same reason as in
|
||||
-- SoftDeletePriorWorkspaceAgents.
|
||||
WITH soft_deleted_agents AS (
|
||||
UPDATE workspace_agents
|
||||
SET deleted = TRUE
|
||||
WHERE id IN (
|
||||
SELECT wa.id
|
||||
FROM workspace_agents wa
|
||||
JOIN workspace_resources wr ON wr.id = wa.resource_id
|
||||
JOIN workspace_builds wb ON wb.job_id = wr.job_id
|
||||
WHERE wb.workspace_id = @workspace_id
|
||||
AND wa.deleted = FALSE
|
||||
)
|
||||
RETURNING id
|
||||
), purged_context_resources AS (
|
||||
DELETE FROM workspace_agent_context_resources
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents)
|
||||
)
|
||||
DELETE FROM workspace_agent_context_snapshots
|
||||
WHERE workspace_agent_id IN (SELECT id FROM soft_deleted_agents);
|
||||
|
||||
Generated
+2
@@ -110,6 +110,8 @@ const (
|
||||
UniqueUserStatusChangesPkey UniqueConstraint = "user_status_changes_pkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id);
|
||||
UniqueUsersPkey UniqueConstraint = "users_pkey" // ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
||||
UniqueWebpushSubscriptionsPkey UniqueConstraint = "webpush_subscriptions_pkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_pkey PRIMARY KEY (id);
|
||||
UniqueWorkspaceAgentContextResourcesPkey UniqueConstraint = "workspace_agent_context_resources_pkey" // ALTER TABLE ONLY workspace_agent_context_resources ADD CONSTRAINT workspace_agent_context_resources_pkey PRIMARY KEY (workspace_agent_id, source);
|
||||
UniqueWorkspaceAgentContextSnapshotsPkey UniqueConstraint = "workspace_agent_context_snapshots_pkey" // ALTER TABLE ONLY workspace_agent_context_snapshots ADD CONSTRAINT workspace_agent_context_snapshots_pkey PRIMARY KEY (workspace_agent_id);
|
||||
UniqueWorkspaceAgentDevcontainersPkey UniqueConstraint = "workspace_agent_devcontainers_pkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_pkey PRIMARY KEY (id);
|
||||
UniqueWorkspaceAgentLogSourcesPkey UniqueConstraint = "workspace_agent_log_sources_pkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id);
|
||||
UniqueWorkspaceAgentMemoryResourceMonitorsPkey UniqueConstraint = "workspace_agent_memory_resource_monitors_pkey" // ALTER TABLE ONLY workspace_agent_memory_resource_monitors ADD CONSTRAINT workspace_agent_memory_resource_monitors_pkey PRIMARY KEY (agent_id);
|
||||
|
||||
Reference in New Issue
Block a user