chore: add aibridge database resources & define RBAC policies (#19796)

Closes https://github.com/coder/internal/issues/986
This commit is contained in:
Danny Kopping
2025-09-16 21:31:17 +02:00
committed by GitHub
parent 348a2e0285
commit 422bba44d9
26 changed files with 770 additions and 0 deletions
+2
View File
@@ -16001,6 +16001,7 @@ const docTemplate = `{
"type": "string",
"enum": [
"*",
"aibridge_interception",
"api_key",
"assign_org_role",
"assign_role",
@@ -16043,6 +16044,7 @@ const docTemplate = `{
],
"x-enum-varnames": [
"ResourceWildcard",
"ResourceAibridgeInterception",
"ResourceApiKey",
"ResourceAssignOrgRole",
"ResourceAssignRole",
+2
View File
@@ -14536,6 +14536,7 @@
"type": "string",
"enum": [
"*",
"aibridge_interception",
"api_key",
"assign_org_role",
"assign_role",
@@ -14578,6 +14579,7 @@
],
"x-enum-varnames": [
"ResourceWildcard",
"ResourceAibridgeInterception",
"ResourceApiKey",
"ResourceAssignOrgRole",
"ResourceAssignRole",
+77
View File
@@ -175,6 +175,22 @@ func (q *querier) authorizePrebuiltWorkspace(ctx context.Context, action policy.
return xerrors.Errorf("authorize context: %w", workspaceErr)
}
// authorizeAIBridgeInterceptionUpdate validates that the context's actor matches the initiator of the AIBridgeInterception.
// This is used by all of the sub-resources which fall under the [ResourceAibridgeInterception] umbrella.
func (q *querier) authorizeAIBridgeInterceptionUpdate(ctx context.Context, interceptionID uuid.UUID) error {
inter, err := q.db.GetAIBridgeInterceptionByID(ctx, interceptionID)
if err != nil {
return xerrors.Errorf("fetch aibridge interception %q: %w", interceptionID, err)
}
err = q.authorizeContext(ctx, policy.ActionUpdate, inter.RBACObject())
if err != nil {
return err
}
return nil
}
type authContextKey struct{}
// ActorFromContext returns the authorization subject from the context.
@@ -542,6 +558,29 @@ var (
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
// See aibridged package.
subjectAibridged = rbac.Subject{
Type: rbac.SubjectAibridged,
FriendlyName: "AIBridge Daemon",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "aibridged"},
DisplayName: "AIBridge Daemon",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceUser.Type: {
policy.ActionReadPersonal, // Required to read users' external auth links. // TODO: this is too broad; reduce scope to just external_auth_links by creating separate resource.
},
rbac.ResourceApiKey.Type: {policy.ActionRead}, // Validate API keys.
rbac.ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
)
// AsProvisionerd returns a context with an actor that has permissions required
@@ -624,6 +663,12 @@ func AsUsagePublisher(ctx context.Context) context.Context {
return As(ctx, subjectUsagePublisher)
}
// AsAIBridged returns a context with an actor that has permissions
// required for creating, reading, and updating aibridge-related resources.
func AsAIBridged(ctx context.Context) context.Context {
return As(ctx, subjectAibridged)
}
var AsRemoveActor = rbac.Subject{
ID: "remove-actor",
}
@@ -1878,6 +1923,10 @@ func (q *querier) FindMatchingPresetID(ctx context.Context, arg database.FindMat
return q.db.FindMatchingPresetID(ctx, arg)
}
func (q *querier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (database.AIBridgeInterception, error) {
return fetch(q.log, q.auth, q.db.GetAIBridgeInterceptionByID)(ctx, id)
}
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
}
@@ -3757,6 +3806,34 @@ func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now ti
return q.db.GetWorkspacesEligibleForTransition(ctx, now)
}
func (q *querier) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) {
return insert(q.log, q.auth, rbac.ResourceAibridgeInterception.WithOwner(arg.InitiatorID.String()), q.db.InsertAIBridgeInterception)(ctx, arg)
}
func (q *querier) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error {
// All aibridge_token_usages records belong to the initiator of their associated interception.
if err := q.authorizeAIBridgeInterceptionUpdate(ctx, arg.InterceptionID); err != nil {
return err
}
return q.db.InsertAIBridgeTokenUsage(ctx, arg)
}
func (q *querier) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error {
// All aibridge_tool_usages records belong to the initiator of their associated interception.
if err := q.authorizeAIBridgeInterceptionUpdate(ctx, arg.InterceptionID); err != nil {
return err
}
return q.db.InsertAIBridgeToolUsage(ctx, arg)
}
func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error {
// All aibridge_user_prompts records belong to the initiator of their associated interception.
if err := q.authorizeAIBridgeInterceptionUpdate(ctx, arg.InterceptionID); err != nil {
return err
}
return q.db.InsertAIBridgeUserPrompt(ctx, arg)
}
func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
// TODO(Cian): ideally this would be encoded in the policy, but system users are just members and we
// don't currently have a capability to conditionally deny creating resources by owner ID in a role.
+52
View File
@@ -4332,3 +4332,55 @@ func TestInsertAPIKey_AsPrebuildsUser(t *testing.T) {
_, err := dbz.InsertAPIKey(ctx, testutil.Fake(t, faker, database.InsertAPIKeyParams{}))
require.True(t, dbauthz.IsNotAuthorizedError(err))
}
func (s *MethodTestSuite) TestAIBridge() {
s.Run("GetAIBridgeInterceptionByID", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
sessID := uuid.UUID{2}
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID})
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), sessID).Return(sess, nil).AnyTimes()
check.Args(sessID).Asserts(sess, policy.ActionRead).Returns(sess)
}))
s.Run("InsertAIBridgeInterception", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
initID := uuid.UUID{3}
user := testutil.Fake(s.T(), faker, database.User{ID: initID})
// testutil.Fake cannot distinguish between a zero value and an explicitly requested value which is equivalent.
user.IsSystem = false
user.Deleted = false
sessID := uuid.UUID{2}
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID, InitiatorID: initID})
params := database.InsertAIBridgeInterceptionParams{ID: sess.ID, InitiatorID: sess.InitiatorID, Provider: sess.Provider, Model: sess.Model}
db.EXPECT().GetUserByID(gomock.Any(), initID).Return(user, nil).AnyTimes() // Validation.
db.EXPECT().InsertAIBridgeInterception(gomock.Any(), params).Return(sess, nil).AnyTimes()
check.Args(params).Asserts(sess, policy.ActionCreate)
}))
s.Run("InsertAIBridgeTokenUsage", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
sessID := uuid.UUID{2}
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID})
params := database.InsertAIBridgeTokenUsageParams{InterceptionID: sess.ID}
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), sessID).Return(sess, nil).AnyTimes() // Validation.
db.EXPECT().InsertAIBridgeTokenUsage(gomock.Any(), params).Return(nil).AnyTimes()
check.Args(params).Asserts(sess, policy.ActionUpdate)
}))
s.Run("InsertAIBridgeToolUsage", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
sessID := uuid.UUID{2}
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID})
params := database.InsertAIBridgeToolUsageParams{InterceptionID: sess.ID}
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), sessID).Return(sess, nil).AnyTimes() // Validation.
db.EXPECT().InsertAIBridgeToolUsage(gomock.Any(), params).Return(nil).AnyTimes()
check.Args(params).Asserts(sess, policy.ActionUpdate)
}))
s.Run("InsertAIBridgeUserPrompt", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
sessID := uuid.UUID{2}
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID})
params := database.InsertAIBridgeUserPromptParams{InterceptionID: sess.ID}
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), sessID).Return(sess, nil).AnyTimes() // Validation.
db.EXPECT().InsertAIBridgeUserPrompt(gomock.Any(), params).Return(nil).AnyTimes()
check.Args(params).Asserts(sess, policy.ActionUpdate)
}))
}
+35
View File
@@ -586,6 +586,13 @@ func (m queryMetricsStore) FindMatchingPresetID(ctx context.Context, arg databas
return r0, r1
}
func (m queryMetricsStore) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (database.AIBridgeInterception, error) {
start := time.Now()
r0, r1 := m.s.GetAIBridgeInterceptionByID(ctx, id)
m.queryLatencies.WithLabelValues("GetAIBridgeInterceptionByID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
start := time.Now()
apiKey, err := m.s.GetAPIKeyByID(ctx, id)
@@ -2168,6 +2175,34 @@ func (m queryMetricsStore) GetWorkspacesEligibleForTransition(ctx context.Contex
return workspaces, err
}
func (m queryMetricsStore) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) {
start := time.Now()
r0, r1 := m.s.InsertAIBridgeInterception(ctx, arg)
m.queryLatencies.WithLabelValues("InsertAIBridgeInterception").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error {
start := time.Now()
r0 := m.s.InsertAIBridgeTokenUsage(ctx, arg)
m.queryLatencies.WithLabelValues("InsertAIBridgeTokenUsage").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error {
start := time.Now()
r0 := m.s.InsertAIBridgeToolUsage(ctx, arg)
m.queryLatencies.WithLabelValues("InsertAIBridgeToolUsage").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error {
start := time.Now()
r0 := m.s.InsertAIBridgeUserPrompt(ctx, arg)
m.queryLatencies.WithLabelValues("InsertAIBridgeUserPrompt").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
start := time.Now()
key, err := m.s.InsertAPIKey(ctx, arg)
+72
View File
@@ -1094,6 +1094,21 @@ func (mr *MockStoreMockRecorder) FindMatchingPresetID(ctx, arg any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindMatchingPresetID", reflect.TypeOf((*MockStore)(nil).FindMatchingPresetID), ctx, arg)
}
// GetAIBridgeInterceptionByID mocks base method.
func (m *MockStore) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (database.AIBridgeInterception, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAIBridgeInterceptionByID", ctx, id)
ret0, _ := ret[0].(database.AIBridgeInterception)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAIBridgeInterceptionByID indicates an expected call of GetAIBridgeInterceptionByID.
func (mr *MockStoreMockRecorder) GetAIBridgeInterceptionByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIBridgeInterceptionByID", reflect.TypeOf((*MockStore)(nil).GetAIBridgeInterceptionByID), ctx, id)
}
// GetAPIKeyByID mocks base method.
func (m *MockStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
m.ctrl.T.Helper()
@@ -4633,6 +4648,63 @@ func (mr *MockStoreMockRecorder) InTx(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InTx", reflect.TypeOf((*MockStore)(nil).InTx), arg0, arg1)
}
// InsertAIBridgeInterception mocks base method.
func (m *MockStore) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertAIBridgeInterception", ctx, arg)
ret0, _ := ret[0].(database.AIBridgeInterception)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertAIBridgeInterception indicates an expected call of InsertAIBridgeInterception.
func (mr *MockStoreMockRecorder) InsertAIBridgeInterception(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeInterception", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeInterception), ctx, arg)
}
// InsertAIBridgeTokenUsage mocks base method.
func (m *MockStore) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertAIBridgeTokenUsage", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// InsertAIBridgeTokenUsage indicates an expected call of InsertAIBridgeTokenUsage.
func (mr *MockStoreMockRecorder) InsertAIBridgeTokenUsage(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeTokenUsage", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeTokenUsage), ctx, arg)
}
// InsertAIBridgeToolUsage mocks base method.
func (m *MockStore) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertAIBridgeToolUsage", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// InsertAIBridgeToolUsage indicates an expected call of InsertAIBridgeToolUsage.
func (mr *MockStoreMockRecorder) InsertAIBridgeToolUsage(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeToolUsage", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeToolUsage), ctx, arg)
}
// InsertAIBridgeUserPrompt mocks base method.
func (m *MockStore) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertAIBridgeUserPrompt", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// InsertAIBridgeUserPrompt indicates an expected call of InsertAIBridgeUserPrompt.
func (mr *MockStoreMockRecorder) InsertAIBridgeUserPrompt(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeUserPrompt", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeUserPrompt), ctx, arg)
}
// InsertAPIKey mocks base method.
func (m *MockStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
m.ctrl.T.Helper()
+88
View File
@@ -847,6 +847,68 @@ BEGIN
END;
$$;
CREATE TABLE aibridge_interceptions (
id uuid NOT NULL,
initiator_id uuid NOT NULL,
provider text NOT NULL,
model text NOT NULL,
started_at timestamp with time zone NOT NULL
);
COMMENT ON TABLE aibridge_interceptions IS 'Audit log of requests intercepted by AI Bridge';
COMMENT ON COLUMN aibridge_interceptions.initiator_id IS 'Relates to a users record, but FK is elided for performance.';
CREATE TABLE aibridge_token_usages (
id uuid NOT NULL,
interception_id uuid NOT NULL,
provider_response_id text NOT NULL,
input_tokens bigint NOT NULL,
output_tokens bigint NOT NULL,
metadata jsonb,
created_at timestamp with time zone NOT NULL
);
COMMENT ON TABLE aibridge_token_usages IS 'Audit log of tokens used by intercepted requests in AI Bridge';
COMMENT ON COLUMN aibridge_token_usages.provider_response_id IS 'The ID for the response in which the tokens were used, produced by the provider.';
CREATE TABLE aibridge_tool_usages (
id uuid NOT NULL,
interception_id uuid NOT NULL,
provider_response_id text NOT NULL,
server_url text,
tool text NOT NULL,
input text NOT NULL,
injected boolean DEFAULT false NOT NULL,
invocation_error text,
metadata jsonb,
created_at timestamp with time zone NOT NULL
);
COMMENT ON TABLE aibridge_tool_usages IS 'Audit log of tool calls in intercepted requests in AI Bridge';
COMMENT ON COLUMN aibridge_tool_usages.provider_response_id IS 'The ID for the response in which the tools were used, produced by the provider.';
COMMENT ON COLUMN aibridge_tool_usages.server_url IS 'The name of the MCP server against which this tool was invoked. May be NULL, in which case the tool was defined by the client, not injected.';
COMMENT ON COLUMN aibridge_tool_usages.injected IS 'Whether this tool was injected; i.e. Bridge injected these tools into the request from an MCP server. If false it means a tool was defined by the client and already existed in the request (MCP or built-in).';
COMMENT ON COLUMN aibridge_tool_usages.invocation_error IS 'Only injected tools are invoked.';
CREATE TABLE aibridge_user_prompts (
id uuid NOT NULL,
interception_id uuid NOT NULL,
provider_response_id text NOT NULL,
prompt text NOT NULL,
metadata jsonb,
created_at timestamp with time zone NOT NULL
);
COMMENT ON TABLE aibridge_user_prompts IS 'Audit log of prompts used by intercepted requests in AI Bridge';
COMMENT ON COLUMN aibridge_user_prompts.provider_response_id IS 'The ID for the response to the given prompt, produced by the provider.';
CREATE TABLE api_keys (
id text NOT NULL,
hashed_secret bytea NOT NULL,
@@ -2597,6 +2659,18 @@ ALTER TABLE ONLY workspace_resource_metadata ALTER COLUMN id SET DEFAULT nextval
ALTER TABLE ONLY workspace_agent_stats
ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
ALTER TABLE ONLY aibridge_interceptions
ADD CONSTRAINT aibridge_interceptions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY aibridge_token_usages
ADD CONSTRAINT aibridge_token_usages_pkey PRIMARY KEY (id);
ALTER TABLE ONLY aibridge_tool_usages
ADD CONSTRAINT aibridge_tool_usages_pkey PRIMARY KEY (id);
ALTER TABLE ONLY aibridge_user_prompts
ADD CONSTRAINT aibridge_user_prompts_pkey PRIMARY KEY (id);
ALTER TABLE ONLY api_keys
ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id);
@@ -2896,6 +2970,20 @@ CREATE INDEX idx_agent_stats_created_at ON workspace_agent_stats USING btree (cr
CREATE INDEX idx_agent_stats_user_id ON workspace_agent_stats USING btree (user_id);
CREATE INDEX idx_aibridge_interceptions_initiator_id ON aibridge_interceptions USING btree (initiator_id);
CREATE INDEX idx_aibridge_token_usages_interception_id ON aibridge_token_usages USING btree (interception_id);
CREATE INDEX idx_aibridge_token_usages_provider_response_id ON aibridge_token_usages USING btree (provider_response_id);
CREATE INDEX idx_aibridge_tool_usages_interception_id ON aibridge_tool_usages USING btree (interception_id);
CREATE INDEX idx_aibridge_tool_usagesprovider_response_id ON aibridge_tool_usages USING btree (provider_response_id);
CREATE INDEX idx_aibridge_user_prompts_interception_id ON aibridge_user_prompts USING btree (interception_id);
CREATE INDEX idx_aibridge_user_prompts_provider_response_id ON aibridge_user_prompts USING btree (provider_response_id);
CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type);
CREATE INDEX idx_api_keys_user ON api_keys USING btree (user_id);
@@ -0,0 +1,4 @@
DROP TABLE IF EXISTS aibridge_tool_usages CASCADE;
DROP TABLE IF EXISTS aibridge_user_prompts CASCADE;
DROP TABLE IF EXISTS aibridge_token_usages CASCADE;
DROP TABLE IF EXISTS aibridge_interceptions CASCADE;
@@ -0,0 +1,68 @@
CREATE TABLE IF NOT EXISTS aibridge_interceptions (
id UUID PRIMARY KEY,
initiator_id uuid NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
started_at TIMESTAMP WITH TIME ZONE NOT NULL
);
COMMENT ON TABLE aibridge_interceptions IS 'Audit log of requests intercepted by AI Bridge';
COMMENT ON COLUMN aibridge_interceptions.initiator_id IS 'Relates to a users record, but FK is elided for performance.';
CREATE INDEX idx_aibridge_interceptions_initiator_id ON aibridge_interceptions (initiator_id);
CREATE TABLE IF NOT EXISTS aibridge_token_usages (
id UUID PRIMARY KEY,
interception_id UUID NOT NULL,
provider_response_id TEXT NOT NULL,
input_tokens BIGINT NOT NULL,
output_tokens BIGINT NOT NULL,
metadata JSONB DEFAULT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL
);
COMMENT ON TABLE aibridge_token_usages IS 'Audit log of tokens used by intercepted requests in AI Bridge';
COMMENT ON COLUMN aibridge_token_usages.provider_response_id IS 'The ID for the response in which the tokens were used, produced by the provider.';
CREATE INDEX idx_aibridge_token_usages_interception_id ON aibridge_token_usages (interception_id);
CREATE INDEX idx_aibridge_token_usages_provider_response_id ON aibridge_token_usages (provider_response_id);
CREATE TABLE IF NOT EXISTS aibridge_user_prompts (
id UUID PRIMARY KEY,
interception_id UUID NOT NULL,
provider_response_id TEXT NOT NULL,
prompt TEXT NOT NULL,
metadata JSONB DEFAULT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL
);
COMMENT ON TABLE aibridge_user_prompts IS 'Audit log of prompts used by intercepted requests in AI Bridge';
COMMENT ON COLUMN aibridge_user_prompts.provider_response_id IS 'The ID for the response to the given prompt, produced by the provider.';
CREATE INDEX idx_aibridge_user_prompts_interception_id ON aibridge_user_prompts (interception_id);
CREATE INDEX idx_aibridge_user_prompts_provider_response_id ON aibridge_user_prompts (provider_response_id);
CREATE TABLE IF NOT EXISTS aibridge_tool_usages (
id UUID PRIMARY KEY,
interception_id UUID NOT NULL,
provider_response_id TEXT NOT NULL,
server_url TEXT NULL,
tool TEXT NOT NULL,
input TEXT NOT NULL,
injected BOOLEAN NOT NULL DEFAULT FALSE,
invocation_error TEXT NULL,
metadata JSONB DEFAULT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL
);
COMMENT ON TABLE aibridge_tool_usages IS 'Audit log of tool calls in intercepted requests in AI Bridge';
COMMENT ON COLUMN aibridge_tool_usages.provider_response_id IS 'The ID for the response in which the tools were used, produced by the provider.';
COMMENT ON COLUMN aibridge_tool_usages.server_url IS 'The name of the MCP server against which this tool was invoked. May be NULL, in which case the tool was defined by the client, not injected.';
COMMENT ON COLUMN aibridge_tool_usages.injected IS 'Whether this tool was injected; i.e. Bridge injected these tools into the request from an MCP server. If false it means a tool was defined by the client and already existed in the request (MCP or built-in).';
COMMENT ON COLUMN aibridge_tool_usages.invocation_error IS 'Only injected tools are invoked.';
CREATE INDEX idx_aibridge_tool_usages_interception_id ON aibridge_tool_usages (interception_id);
CREATE INDEX idx_aibridge_tool_usagesprovider_response_id ON aibridge_tool_usages (provider_response_id);
@@ -0,0 +1,79 @@
INSERT INTO
aibridge_interceptions (
id,
initiator_id,
provider,
model,
started_at
)
VALUES (
'be003e1e-b38f-43bf-847d-928074dd0aa8',
'30095c71-380b-457a-8995-97b8ee6e5307',
'openai',
'gpt-5',
'2025-09-15 12:45:13.921148+00'
);
INSERT INTO
aibridge_token_usages (
id,
interception_id,
provider_response_id,
input_tokens,
output_tokens,
metadata,
created_at
)
VALUES (
'c56ca89d-af65-47b0-871f-0b9cd2af6575',
'be003e1e-b38f-43bf-847d-928074dd0aa8',
'chatcmpl-CG2s28QlpKIoooUtXuLTmGbdtyS1k',
10950,
118,
'{"prompt_audio": 0, "prompt_cached": 5376, "completion_audio": 0, "completion_reasoning": 64, "completion_accepted_prediction": 0, "completion_rejected_prediction": 0}',
'2025-09-15 12:45:21.674413+00'
);
INSERT INTO
aibridge_tool_usages (
id,
interception_id,
provider_response_id,
server_url,
tool,
input,
injected,
invocation_error,
metadata,
created_at
)
VALUES (
'613b4cfa-a257-4e88-99e6-4d2e99ea25f0',
'be003e1e-b38f-43bf-847d-928074dd0aa8',
'chatcmpl-CG2ryDxMp6n53aMjgo7P6BHno3fTr',
'http://localhost:3000/api/experimental/mcp/http',
'coder_list_workspaces',
'{}',
true,
NULL,
'{}',
'2025-09-15 12:45:17.65274+00'
);
INSERT INTO
aibridge_user_prompts (
id,
interception_id,
provider_response_id,
prompt,
metadata,
created_at
)
VALUES (
'ac1ea8c3-5109-4105-9b62-489fca220ef7',
'be003e1e-b38f-43bf-847d-928074dd0aa8',
'chatcmpl-CG2s28QlpKIoooUtXuLTmGbdtyS1k',
'how many workspaces do i have',
'{}',
'2025-09-15 12:45:21.674335+00'
);
+4
View File
@@ -636,3 +636,7 @@ func (m WorkspaceAgentVolumeResourceMonitor) Debounce(
func (s UserSecret) RBACObject() rbac.Object {
return rbac.ResourceUserSecret.WithID(s.ID).WithOwner(s.UserID.String())
}
func (s AIBridgeInterception) RBACObject() rbac.Object {
return rbac.ResourceAibridgeInterception.WithOwner(s.InitiatorID.String())
}
+51
View File
@@ -2955,6 +2955,57 @@ func AllWorkspaceTransitionValues() []WorkspaceTransition {
}
}
// Audit log of requests intercepted by AI Bridge
type AIBridgeInterception struct {
ID uuid.UUID `db:"id" json:"id"`
// Relates to a users record, but FK is elided for performance.
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
StartedAt time.Time `db:"started_at" json:"started_at"`
}
// Audit log of tokens used by intercepted requests in AI Bridge
type AIBridgeTokenUsage struct {
ID uuid.UUID `db:"id" json:"id"`
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
// The ID for the response in which the tokens were used, produced by the provider.
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
InputTokens int64 `db:"input_tokens" json:"input_tokens"`
OutputTokens int64 `db:"output_tokens" json:"output_tokens"`
Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// Audit log of tool calls in intercepted requests in AI Bridge
type AIBridgeToolUsage struct {
ID uuid.UUID `db:"id" json:"id"`
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
// The ID for the response in which the tools were used, produced by the provider.
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
// The name of the MCP server against which this tool was invoked. May be NULL, in which case the tool was defined by the client, not injected.
ServerUrl sql.NullString `db:"server_url" json:"server_url"`
Tool string `db:"tool" json:"tool"`
Input string `db:"input" json:"input"`
// Whether this tool was injected; i.e. Bridge injected these tools into the request from an MCP server. If false it means a tool was defined by the client and already existed in the request (MCP or built-in).
Injected bool `db:"injected" json:"injected"`
// Only injected tools are invoked.
InvocationError sql.NullString `db:"invocation_error" json:"invocation_error"`
Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// Audit log of prompts used by intercepted requests in AI Bridge
type AIBridgeUserPrompt struct {
ID uuid.UUID `db:"id" json:"id"`
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
// The ID for the response to the given prompt, produced by the provider.
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
Prompt string `db:"prompt" json:"prompt"`
Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
type APIKey struct {
ID string `db:"id" json:"id"`
// hashed_secret contains a SHA256 hash of the key secret. This is considered a secret and MUST NOT be returned from the API as it is used for API key encryption in app proxying code.
+5
View File
@@ -148,6 +148,7 @@ type sqlcQuerier interface {
// The query finds presets where all preset parameters are present in the provided parameters,
// and returns the preset with the most parameters (largest subset).
FindMatchingPresetID(ctx context.Context, arg FindMatchingPresetIDParams) (uuid.UUID, error)
GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (AIBridgeInterception, error)
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
// there is no unique constraint on empty token names
GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
@@ -502,6 +503,10 @@ type sqlcQuerier interface {
GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]GetWorkspacesAndAgentsByOwnerIDRow, error)
GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error)
GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error)
InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error)
InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) error
InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) error
InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) error
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
// We use the organization_id as the id
// for simplicity since all users is
+147
View File
@@ -111,6 +111,153 @@ func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBump
return err
}
const getAIBridgeInterceptionByID = `-- name: GetAIBridgeInterceptionByID :one
SELECT id, initiator_id, provider, model, started_at FROM aibridge_interceptions WHERE id = $1::uuid
`
func (q *sqlQuerier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (AIBridgeInterception, error) {
row := q.db.QueryRowContext(ctx, getAIBridgeInterceptionByID, id)
var i AIBridgeInterception
err := row.Scan(
&i.ID,
&i.InitiatorID,
&i.Provider,
&i.Model,
&i.StartedAt,
)
return i, err
}
const insertAIBridgeInterception = `-- name: InsertAIBridgeInterception :one
INSERT INTO aibridge_interceptions (id, initiator_id, provider, model, started_at)
VALUES ($1::uuid, $2::uuid, $3, $4, $5)
RETURNING id, initiator_id, provider, model, started_at
`
type InsertAIBridgeInterceptionParams struct {
ID uuid.UUID `db:"id" json:"id"`
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
StartedAt time.Time `db:"started_at" json:"started_at"`
}
func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error) {
row := q.db.QueryRowContext(ctx, insertAIBridgeInterception,
arg.ID,
arg.InitiatorID,
arg.Provider,
arg.Model,
arg.StartedAt,
)
var i AIBridgeInterception
err := row.Scan(
&i.ID,
&i.InitiatorID,
&i.Provider,
&i.Model,
&i.StartedAt,
)
return i, err
}
const insertAIBridgeTokenUsage = `-- name: InsertAIBridgeTokenUsage :exec
INSERT INTO aibridge_token_usages (
id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at
) VALUES (
$1, $2, $3, $4, $5, COALESCE($6::jsonb, '{}'::jsonb), $7
)
`
type InsertAIBridgeTokenUsageParams struct {
ID uuid.UUID `db:"id" json:"id"`
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
InputTokens int64 `db:"input_tokens" json:"input_tokens"`
OutputTokens int64 `db:"output_tokens" json:"output_tokens"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) error {
_, err := q.db.ExecContext(ctx, insertAIBridgeTokenUsage,
arg.ID,
arg.InterceptionID,
arg.ProviderResponseID,
arg.InputTokens,
arg.OutputTokens,
arg.Metadata,
arg.CreatedAt,
)
return err
}
const insertAIBridgeToolUsage = `-- name: InsertAIBridgeToolUsage :exec
INSERT INTO aibridge_tool_usages (
id, interception_id, provider_response_id, tool, server_url, input, injected, invocation_error, metadata, created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, COALESCE($9::jsonb, '{}'::jsonb), $10
)
`
type InsertAIBridgeToolUsageParams struct {
ID uuid.UUID `db:"id" json:"id"`
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
Tool string `db:"tool" json:"tool"`
ServerUrl sql.NullString `db:"server_url" json:"server_url"`
Input string `db:"input" json:"input"`
Injected bool `db:"injected" json:"injected"`
InvocationError sql.NullString `db:"invocation_error" json:"invocation_error"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) error {
_, err := q.db.ExecContext(ctx, insertAIBridgeToolUsage,
arg.ID,
arg.InterceptionID,
arg.ProviderResponseID,
arg.Tool,
arg.ServerUrl,
arg.Input,
arg.Injected,
arg.InvocationError,
arg.Metadata,
arg.CreatedAt,
)
return err
}
const insertAIBridgeUserPrompt = `-- name: InsertAIBridgeUserPrompt :exec
INSERT INTO aibridge_user_prompts (
id, interception_id, provider_response_id, prompt, metadata, created_at
) VALUES (
$1, $2, $3, $4, COALESCE($5::jsonb, '{}'::jsonb), $6
)
`
type InsertAIBridgeUserPromptParams struct {
ID uuid.UUID `db:"id" json:"id"`
InterceptionID uuid.UUID `db:"interception_id" json:"interception_id"`
ProviderResponseID string `db:"provider_response_id" json:"provider_response_id"`
Prompt string `db:"prompt" json:"prompt"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) error {
_, err := q.db.ExecContext(ctx, insertAIBridgeUserPrompt,
arg.ID,
arg.InterceptionID,
arg.ProviderResponseID,
arg.Prompt,
arg.Metadata,
arg.CreatedAt,
)
return err
}
const deleteAPIKeyByID = `-- name: DeleteAPIKeyByID :exec
DELETE FROM
api_keys
+28
View File
@@ -0,0 +1,28 @@
-- name: InsertAIBridgeInterception :one
INSERT INTO aibridge_interceptions (id, initiator_id, provider, model, started_at)
VALUES (@id::uuid, @initiator_id::uuid, @provider, @model, @started_at)
RETURNING *;
-- name: InsertAIBridgeTokenUsage :exec
INSERT INTO aibridge_token_usages (
id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at
) VALUES (
@id, @interception_id, @provider_response_id, @input_tokens, @output_tokens, COALESCE(@metadata::jsonb, '{}'::jsonb), @created_at
);
-- name: InsertAIBridgeUserPrompt :exec
INSERT INTO aibridge_user_prompts (
id, interception_id, provider_response_id, prompt, metadata, created_at
) VALUES (
@id, @interception_id, @provider_response_id, @prompt, COALESCE(@metadata::jsonb, '{}'::jsonb), @created_at
);
-- name: InsertAIBridgeToolUsage :exec
INSERT INTO aibridge_tool_usages (
id, interception_id, provider_response_id, tool, server_url, input, injected, invocation_error, metadata, created_at
) VALUES (
@id, @interception_id, @provider_response_id, @tool, @server_url, @input, @injected, @invocation_error, COALESCE(@metadata::jsonb, '{}'::jsonb), @created_at
);
-- name: GetAIBridgeInterceptionByID :one
SELECT * FROM aibridge_interceptions WHERE id = @id::uuid;
+4
View File
@@ -163,6 +163,10 @@ sql:
ai_task_sidebar_app_id: AITaskSidebarAppID
latest_build_has_ai_task: LatestBuildHasAITask
cors_behavior: CorsBehavior
aibridge_interception: AIBridgeInterception
aibridge_tool_usage: AIBridgeToolUsage
aibridge_token_usage: AIBridgeTokenUsage
aibridge_user_prompt: AIBridgeUserPrompt
rules:
- name: do-not-use-public-schema-in-queries
message: "do not use public schema in queries"
+4
View File
@@ -7,6 +7,10 @@ type UniqueConstraint string
// UniqueConstraint enums.
const (
UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
UniqueAibridgeInterceptionsPkey UniqueConstraint = "aibridge_interceptions_pkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_pkey PRIMARY KEY (id);
UniqueAibridgeTokenUsagesPkey UniqueConstraint = "aibridge_token_usages_pkey" // ALTER TABLE ONLY aibridge_token_usages ADD CONSTRAINT aibridge_token_usages_pkey PRIMARY KEY (id);
UniqueAibridgeToolUsagesPkey UniqueConstraint = "aibridge_tool_usages_pkey" // ALTER TABLE ONLY aibridge_tool_usages ADD CONSTRAINT aibridge_tool_usages_pkey PRIMARY KEY (id);
UniqueAibridgeUserPromptsPkey UniqueConstraint = "aibridge_user_prompts_pkey" // ALTER TABLE ONLY aibridge_user_prompts ADD CONSTRAINT aibridge_user_prompts_pkey PRIMARY KEY (id);
UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id);
UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id);
UniqueConnectionLogsPkey UniqueConstraint = "connection_logs_pkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_pkey PRIMARY KEY (id);
+1
View File
@@ -77,6 +77,7 @@ const (
SubjectTypeSubAgentAPI SubjectType = "sub_agent_api"
SubjectTypeFileReader SubjectType = "file_reader"
SubjectTypeUsagePublisher SubjectType = "usage_publisher"
SubjectAibridged SubjectType = "aibridged"
)
const (
+10
View File
@@ -15,6 +15,15 @@ var (
Type: "*",
}
// ResourceAibridgeInterception
// Valid Actions
// - "ActionCreate" :: create aibridge interceptions & related records
// - "ActionRead" :: read aibridge interceptions & related records
// - "ActionUpdate" :: update aibridge interceptions & related records
ResourceAibridgeInterception = Object{
Type: "aibridge_interception",
}
// ResourceApiKey
// Valid Actions
// - "ActionCreate" :: create an api key
@@ -391,6 +400,7 @@ var (
func AllResources() []Objecter {
return []Objecter{
ResourceWildcard,
ResourceAibridgeInterception,
ResourceApiKey,
ResourceAssignOrgRole,
ResourceAssignRole,
+7
View File
@@ -358,4 +358,11 @@ var RBACPermissions = map[string]PermissionDefinition{
ActionUpdate: "update usage events",
},
},
"aibridge_interception": {
Actions: map[Action]ActionDefinition{
ActionRead: "read aibridge interceptions & related records",
ActionUpdate: "update aibridge interceptions & related records",
ActionCreate: "create aibridge interceptions & related records",
},
},
}
+15
View File
@@ -888,6 +888,21 @@ func TestRolePermissions(t *testing.T) {
},
},
},
{
Name: "AIBridgeInterceptions",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceAibridgeInterception.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, memberMe, orgMemberMe},
false: {
otherOrgMember,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
}
// We expect every permission to be tested above.
+2
View File
@@ -5,6 +5,7 @@ type RBACResource string
const (
ResourceWildcard RBACResource = "*"
ResourceAibridgeInterception RBACResource = "aibridge_interception"
ResourceApiKey RBACResource = "api_key"
ResourceAssignOrgRole RBACResource = "assign_org_role"
ResourceAssignRole RBACResource = "assign_role"
@@ -71,6 +72,7 @@ const (
// said resource type.
var RBACResourceActions = map[RBACResource][]RBACAction{
ResourceWildcard: {},
ResourceAibridgeInterception: {ActionCreate, ActionRead, ActionUpdate},
ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUnassign, ActionUpdate},
ResourceAssignRole: {ActionAssign, ActionRead, ActionUnassign},
+5
View File
@@ -183,6 +183,7 @@ Status Code **200**
| `action` | `start` |
| `action` | `stop` |
| `resource_type` | `*` |
| `resource_type` | `aibridge_interception` |
| `resource_type` | `api_key` |
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
@@ -355,6 +356,7 @@ Status Code **200**
| `action` | `start` |
| `action` | `stop` |
| `resource_type` | `*` |
| `resource_type` | `aibridge_interception` |
| `resource_type` | `api_key` |
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
@@ -527,6 +529,7 @@ Status Code **200**
| `action` | `start` |
| `action` | `stop` |
| `resource_type` | `*` |
| `resource_type` | `aibridge_interception` |
| `resource_type` | `api_key` |
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
@@ -668,6 +671,7 @@ Status Code **200**
| `action` | `start` |
| `action` | `stop` |
| `resource_type` | `*` |
| `resource_type` | `aibridge_interception` |
| `resource_type` | `api_key` |
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
@@ -1031,6 +1035,7 @@ Status Code **200**
| `action` | `start` |
| `action` | `stop` |
| `resource_type` | `*` |
| `resource_type` | `aibridge_interception` |
| `resource_type` | `api_key` |
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
+1
View File
@@ -6519,6 +6519,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
| Value |
|------------------------------------|
| `*` |
| `aibridge_interception` |
| `api_key` |
| `assign_org_role` |
| `assign_role` |
+5
View File
@@ -8,6 +8,11 @@ import type { RBACAction, RBACResource } from "./typesGenerated";
export const RBACResourceActions: Partial<
Record<RBACResource, Partial<Record<RBACAction, string>>>
> = {
aibridge_interception: {
create: "create aibridge interceptions & related records",
read: "read aibridge interceptions & related records",
update: "update aibridge interceptions & related records",
},
api_key: {
create: "create an api key",
delete: "delete an api key",
+2
View File
@@ -2415,6 +2415,7 @@ export const RBACActions: RBACAction[] = [
// From codersdk/rbacresources_gen.go
export type RBACResource =
| "aibridge_interception"
| "api_key"
| "assign_org_role"
| "assign_role"
@@ -2457,6 +2458,7 @@ export type RBACResource =
| "workspace_proxy";
export const RBACResources: RBACResource[] = [
"aibridge_interception",
"api_key",
"assign_org_role",
"assign_role",