Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
35f980a
feat(coderd): rank chat workspace templates
jaaydenh May 7, 2026
f8882fc
test(coderd/database): cover workspace usage authz
jaaydenh May 7, 2026
34c7fac
fix(coderd): address template ranking feedback
jaaydenh May 15, 2026
b8ae639
fix(coderd): follow up on template ranking feedback
jaaydenh May 15, 2026
ee89c16
Merge branch 'main' into workspace-bdje
jaaydenh May 18, 2026
15acaf5
Merge remote-tracking branch 'origin/main' into merge-workspace-bdje-…
jaaydenh May 27, 2026
0fcc777
Merge branch 'main' into workspace-bdje
jaaydenh May 27, 2026
4d357de
fix(coderd): address template ranking feedback
jaaydenh Jun 1, 2026
f8e7819
Merge branch 'main' into workspace-bdje
jaaydenh Jun 1, 2026
9d1ee50
fix(coderd/x/chatd): require template selection on weak matches
jaaydenh Jun 1, 2026
4c20d3f
fix(coderd): rank chat templates by frecency score
jaaydenh Jun 4, 2026
dfc293d
fix(coderd): address template ranking review feedback
jaaydenh Jun 5, 2026
5a7641f
fix(coderd/x/chatd/chattool): add list_templates auth error context
jaaydenh Jun 5, 2026
42fed81
test(coderd): update chat system prompt expectation
jaaydenh Jun 5, 2026
58f73db
Merge branch 'main' into workspace-bdje
jaaydenh Jun 9, 2026
d7fe770
chore: update comments
jaaydenh Jun 9, 2026
95f7beb
test(coderd/database/dbauthz): cover template ranking signals deny path
jaaydenh Jun 10, 2026
71eca18
refactor(coderd/x/chatd/chattool): simplify templateQueryScore tier m…
jaaydenh Jun 10, 2026
206c738
feat(coderd): add hourly hb_agent_runtime_v1 usage events for Coder A…
jaaydenh Jul 17, 2026
4a2c001
Merge remote-tracking branch 'origin/main' into billing-4c3y
jaaydenh Jul 17, 2026
a02ddde
docs(coderd/database): document usage_events.created_at semantics
jaaydenh Jul 22, 2026
8c3e9b4
Merge remote-tracking branch 'origin/main' into billing-4c3y
jaaydenh Jul 22, 2026
f1d30c9
refactor: strip narrative comments from usage event generator change
jaaydenh Jul 23, 2026
b3eacc7
fix: register ai-seats usage cron and grant usage-publisher create pe…
jaaydenh Jul 27, 2026
f444daa
Merge branch 'fix-ai-seats-cron-registration' into billing-4c3y
jaaydenh Jul 27, 2026
446d77c
refactor(enterprise): drop ai-seats cron fix in favor of standalone PR
jaaydenh Jul 27, 2026
7ad3f84
fix(enterprise/coderd/usage): address generator review findings
jaaydenh Jul 27, 2026
8cc70e6
chore: updates for PR review
jaaydenh Jul 28, 2026
abd74ba
chore: address review findings for hb_agent_runtime_v1 usage events
jaaydenh Jul 28, 2026
2d99562
Merge branch 'main' into billing-4c3y
jaaydenh Jul 28, 2026
4678b3c
Merge remote-tracking branch 'origin/main' into billing-4c3y
jaaydenh Jul 28, 2026
18e82e9
chore(coderd/database/migrations): renumber usage events agent runtim…
jaaydenh Jul 28, 2026
cf943ca
Merge remote-tracking branch 'origin/billing-4c3y' into billing-4c3y
jaaydenh Jul 28, 2026
2dc61ef
Merge branch 'main' into billing-4c3y
jaaydenh Jul 29, 2026
206f034
fix(coderd/database/migrations): renumber agent runtime migration to …
jaaydenh Jul 29, 2026
c07c96c
Merge branch 'main' into billing-4c3y
jaaydenh Jul 30, 2026
b72d53c
fix(coderd/database/migrations): renumber agent runtime migration to …
jaaydenh Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions coderd/coderdtest/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package coderdtest
import (
"context"
"sync"
"time"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/usage"
Expand All @@ -11,18 +12,24 @@ import (

var _ usage.Inserter = (*UsageInserter)(nil)

type HeartbeatEvent struct {
ID string
CreatedAt time.Time
Event usagetypes.HeartbeatEvent
}

type UsageInserter struct {
sync.Mutex
discreteEvents []usagetypes.DiscreteEvent
heartbeatEvents []usagetypes.HeartbeatEvent
heartbeatEvents []HeartbeatEvent
seenHeartbeats map[string]struct{}
}

func NewUsageInserter() *UsageInserter {
return &UsageInserter{
discreteEvents: []usagetypes.DiscreteEvent{},
seenHeartbeats: map[string]struct{}{},
heartbeatEvents: []usagetypes.HeartbeatEvent{},
heartbeatEvents: []HeartbeatEvent{},
}
}

Expand All @@ -33,22 +40,26 @@ func (u *UsageInserter) InsertDiscreteUsageEvent(_ context.Context, _ database.S
return nil
}

func (u *UsageInserter) InsertHeartbeatUsageEvent(_ context.Context, _ database.Store, id string, event usagetypes.HeartbeatEvent) error {
func (u *UsageInserter) InsertHeartbeatUsageEvent(_ context.Context, _ database.Store, id string, createdAt time.Time, event usagetypes.HeartbeatEvent) error {
u.Lock()
defer u.Unlock()
if _, seen := u.seenHeartbeats[id]; seen {
return nil
}

u.seenHeartbeats[id] = struct{}{}
u.heartbeatEvents = append(u.heartbeatEvents, event)
u.heartbeatEvents = append(u.heartbeatEvents, HeartbeatEvent{
ID: id,
CreatedAt: createdAt,
Event: event,
})
return nil
}

func (u *UsageInserter) GetHeartbeatEvents() []usagetypes.HeartbeatEvent {
func (u *UsageInserter) GetHeartbeatEvents() []HeartbeatEvent {
u.Lock()
defer u.Unlock()
eventsCopy := make([]usagetypes.HeartbeatEvent, len(u.heartbeatEvents))
eventsCopy := make([]HeartbeatEvent, len(u.heartbeatEvents))
copy(eventsCopy, u.heartbeatEvents)
return eventsCopy
}
Expand All @@ -72,5 +83,5 @@ func (u *UsageInserter) Reset() {
defer u.Unlock()
u.seenHeartbeats = map[string]struct{}{}
u.discreteEvents = []usagetypes.DiscreteEvent{}
u.heartbeatEvents = []usagetypes.HeartbeatEvent{}
u.heartbeatEvents = []HeartbeatEvent{}
}
23 changes: 20 additions & 3 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -671,9 +671,9 @@ var (
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceLicense.Type: {policy.ActionRead},
rbac.ResourceAiSeat.Type: {policy.ActionRead}, // Required for GetActiveAISeatCount.
// The usage publisher doesn't create events, just
// reads/processes them.
rbac.ResourceUsageEvent.Type: {policy.ActionRead, policy.ActionUpdate},
// Create is required to insert heartbeat usage events
// under this subject.
rbac.ResourceUsageEvent.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
Expand Down Expand Up @@ -4992,6 +4992,16 @@ func (q *querier) GetTemplatesWithFilter(ctx context.Context, arg database.GetTe
return q.db.GetAuthorizedTemplates(ctx, arg, prep)
}

func (q *querier) GetTotalChatMessageRuntimeMsInRange(ctx context.Context, arg database.GetTotalChatMessageRuntimeMsInRangeParams) (int64, error) {
Comment thread
jaaydenh marked this conversation as resolved.
// This query exists solely to compute hb_agent_runtime_v1 usage event
// payloads and returns a bare sum with no chat content, so it is gated
// on usage event creation rather than on reading chats.
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceUsageEvent); err != nil {
return 0, err
}
return q.db.GetTotalChatMessageRuntimeMsInRange(ctx, arg)
}

func (q *querier) GetTotalUsageDCManagedAgentsV1(ctx context.Context, arg database.GetTotalUsageDCManagedAgentsV1Params) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUsageEvent); err != nil {
return 0, err
Expand Down Expand Up @@ -6901,6 +6911,13 @@ func (q *querier) ListTasks(ctx context.Context, arg database.ListTasksParams) (
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListTasks)(ctx, arg)
}

func (q *querier) ListUsageEventCreatedAtsByTypeSince(ctx context.Context, arg database.ListUsageEventCreatedAtsByTypeSinceParams) ([]time.Time, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUsageEvent); err != nil {
return nil, err
}
return q.db.ListUsageEventCreatedAtsByTypeSince(ctx, arg)
}

func (q *querier) ListUserChatCompactionThresholds(ctx context.Context, userID uuid.UUID) ([]database.UserConfig, error) {
u, err := q.db.GetUserByID(ctx, userID)
if err != nil {
Expand Down
56 changes: 56 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6717,6 +6717,27 @@ func (s *MethodTestSuite) TestUsageEvents() {
EndDate: time.Time{},
}).Asserts(rbac.ResourceUsageEvent, policy.ActionRead)
}))

s.Run("ListUsageEventCreatedAtsByTypeSince", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
params := database.ListUsageEventCreatedAtsByTypeSinceParams{
EventType: "hb_agent_runtime_v1",
Since: dbtime.Now(),
}
db.EXPECT().ListUsageEventCreatedAtsByTypeSince(gomock.Any(), params).Return([]time.Time{}, nil)
check.Args(params).Asserts(rbac.ResourceUsageEvent, policy.ActionRead)
}))

// GetTotalChatMessageRuntimeMsInRange exists solely to compute usage
// event payloads, so it asserts usage event creation rather than chat
// read permissions.
s.Run("GetTotalChatMessageRuntimeMsInRange", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
params := database.GetTotalChatMessageRuntimeMsInRangeParams{
StartTime: time.Time{},
EndTime: time.Time{},
}
db.EXPECT().GetTotalChatMessageRuntimeMsInRange(gomock.Any(), params).Return(int64(0), nil)
check.Args(params).Asserts(rbac.ResourceUsageEvent, policy.ActionCreate)
}))
}

// Ensures that the prebuilds actor may never insert an api key.
Expand All @@ -6735,6 +6756,41 @@ func TestInsertAPIKey_AsPrebuildsUser(t *testing.T) {
require.True(t, dbauthz.IsNotAuthorizedError(err))
}

// TestGetTotalChatMessageRuntimeMsInRange_HumanRolesDenied mechanically
// checks the invariant the query's authz gate relies on: it exposes a
// deployment-wide aggregate behind usage_event create at site scope, which no
// human-assignable role holds. Owner is excluded from usage_event via
// allPermsExcept in roles.go; org roles such as org-admin do carry
// usage_event permissions, but only at org scope, which cannot satisfy a
// site-scoped check. If either of those ever changes, this test fails.
func TestGetTotalChatMessageRuntimeMsInRange_HumanRolesDenied(t *testing.T) {
Comment thread
jaaydenh marked this conversation as resolved.
t.Parallel()

orgID := uuid.New()
var roles []rbac.RoleIdentifier
for _, role := range rbac.SiteBuiltInRoles() {
roles = append(roles, role.Identifier)
}
for _, role := range rbac.OrganizationRoles(orgID) {
roles = append(roles, role.Identifier)
}
require.NotEmpty(t, roles)

for _, role := range roles {
subj := rbac.Subject{
ID: uuid.NewString(),
Roles: rbac.RoleIdentifiers{role},
Scope: rbac.ScopeAll,
}
ctx := dbauthz.As(testutil.Context(t, testutil.WaitShort), subj)
mDB := dbmock.NewMockStore(gomock.NewController(t))
mDB.EXPECT().Wrappers().Times(1).Return([]string{})
dbz := dbauthz.New(mDB, rbac.NewStrictAuthorizer(prometheus.NewRegistry()), slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
_, err := dbz.GetTotalChatMessageRuntimeMsInRange(ctx, database.GetTotalChatMessageRuntimeMsInRangeParams{})
require.True(t, dbauthz.IsNotAuthorizedError(err), "role %s must be denied", role)
}
}

func (s *MethodTestSuite) TestAIBridge() {
s.Run("InsertAIBridgeInterception", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
initID := uuid.UUID{3}
Expand Down
16 changes: 16 additions & 0 deletions coderd/database/dbmetrics/querymetrics.go

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

30 changes: 30 additions & 0 deletions coderd/database/dbmock/dbmock.go

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

15 changes: 13 additions & 2 deletions coderd/database/dump.sql

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder: check migration number before merge

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
COMMENT ON COLUMN usage_events.created_at IS NULL;

DROP INDEX IF EXISTS idx_usage_events_agent_runtime;

-- Remove hb_agent_runtime_v1 rows so the previous constraint can be restored.
DELETE FROM usage_events WHERE event_type = 'hb_agent_runtime_v1';
DELETE FROM usage_events_daily WHERE event_type = 'hb_agent_runtime_v1';

ALTER TABLE usage_events
DROP CONSTRAINT usage_event_type_check,
ADD CONSTRAINT usage_event_type_check CHECK (event_type IN ('dc_managed_agents_v1', 'hb_ai_seats_v1'));

-- Restores the 000444 version of the function.
CREATE OR REPLACE FUNCTION aggregate_usage_event()
RETURNS TRIGGER AS $$
BEGIN
-- Check for supported event types and throw error for unknown types.
IF NEW.event_type NOT IN ('dc_managed_agents_v1', 'hb_ai_seats_v1') THEN
RAISE EXCEPTION 'Unhandled usage event type in aggregate_usage_event: %', NEW.event_type;
END IF;

INSERT INTO usage_events_daily (day, event_type, usage_data)
VALUES (
date_trunc('day', NEW.created_at AT TIME ZONE 'UTC')::date,
NEW.event_type,
NEW.event_data
)
ON CONFLICT (day, event_type) DO UPDATE SET
usage_data = CASE
-- Handle simple counter events by summing the count.
WHEN NEW.event_type IN ('dc_managed_agents_v1') THEN
jsonb_build_object(
'count',
COALESCE((usage_events_daily.usage_data->>'count')::bigint, 0) +
COALESCE((NEW.event_data->>'count')::bigint, 0)
)
-- Heartbeat events: keep the max value seen that day
WHEN NEW.event_type IN ('hb_ai_seats_v1') THEN
jsonb_build_object(
'count',
GREATEST(
COALESCE((usage_events_daily.usage_data->>'count')::bigint, 0),
COALESCE((NEW.event_data->>'count')::bigint, 0)
)
)
END;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Loading
Loading