diff --git a/.changeset/agent-threads-runtime.md b/.changeset/agent-threads-runtime.md new file mode 100644 index 000000000..5920f46c6 --- /dev/null +++ b/.changeset/agent-threads-runtime.md @@ -0,0 +1,6 @@ +--- +"github.com/livekit/protocol": minor +"@livekit/protocol": minor +--- + +Agent threads on the agent runtime: `Job.thread_id` and `AgentHttp.StreamPreamble.thread_id` / `thread_grant` (`AgentHttp.AgentThreadGrant`). `auth.AgentThreadGrant.ToProto` converts the claim for the preamble. diff --git a/auth/grants.go b/auth/grants.go index 12fe65847..12b038a31 100644 --- a/auth/grants.go +++ b/auth/grants.go @@ -779,6 +779,23 @@ func (s *AgentThreadGrant) Clone() *AgentThreadGrant { return &clone } +// ToProto is the grant as the node forwards it to the worker in +// AgentHttp.StreamPreamble.thread_grant; nil stays nil. +func (s *AgentThreadGrant) ToProto() *livekit.AgentHttp_AgentThreadGrant { + if s == nil { + return nil + } + return &livekit.AgentHttp_AgentThreadGrant{ + Scope: s.Scope, + ThreadIds: append([]string{}, s.ThreadIDs...), + List: s.List, + Create: s.Create, + Write: s.Write, + Read: s.Read, + Delete: s.Delete, + } +} + func (s *AgentThreadGrant) MarshalLogObject(e zapcore.ObjectEncoder) error { if s == nil { return nil diff --git a/livekit/agent/livekit_agent_thread.pb.go b/livekit/agent/livekit_agent_thread.pb.go index 44f60b38b..572e810f7 100644 --- a/livekit/agent/livekit_agent_thread.pb.go +++ b/livekit/agent/livekit_agent_thread.pb.go @@ -21,7 +21,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.35.1 // source: agent/livekit_agent_thread.proto package agent diff --git a/livekit/livekit_agent.pb.go b/livekit/livekit_agent.pb.go index 7ce1c942c..6ca991501 100644 --- a/livekit/livekit_agent.pb.go +++ b/livekit/livekit_agent.pb.go @@ -317,8 +317,10 @@ type Job struct { Deployment string `protobuf:"bytes,11,opt,name=deployment,proto3" json:"deployment,omitempty"` Attributes map[string]string `protobuf:"bytes,12,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` EnableRedaction bool `protobuf:"varint,13,opt,name=enable_redaction,json=enableRedaction,proto3" json:"enable_redaction,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // the thread ("AT_...") this job continues; empty when dispatch made none + ThreadId string `protobuf:"bytes,14,opt,name=thread_id,json=threadId,proto3" json:"thread_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Job) Reset() { @@ -443,6 +445,13 @@ func (x *Job) GetEnableRedaction() bool { return false } +func (x *Job) GetThreadId() string { + if x != nil { + return x.ThreadId + } + return "" +} + type JobState struct { state protoimpl.MessageState `protogen:"open.v1"` Status JobStatus `protobuf:"varint,1,opt,name=status,proto3,enum=livekit.JobStatus" json:"status,omitempty"` @@ -1864,7 +1873,13 @@ type AgentHttp_StreamPreamble struct { // end client address, host only ClientAddr string `protobuf:"bytes,6,opt,name=client_addr,json=clientAddr,proto3" json:"client_addr,omitempty"` // "http" or "https" as the end client saw it; not derivable worker-side - Scheme string `protobuf:"bytes,7,opt,name=scheme,proto3" json:"scheme,omitempty"` // NEXT_ID: 8 + Scheme string `protobuf:"bytes,7,opt,name=scheme,proto3" json:"scheme,omitempty"` + // the thread ("AT_...") the node resolved for this request; empty when + // none, and the worker resolves it from the body + ThreadId string `protobuf:"bytes,8,opt,name=thread_id,json=threadId,proto3" json:"thread_id,omitempty"` + // the verified agentThread claim of the caller's token, which never + // reaches the worker; the worker enforces it on the addressed thread + ThreadGrant *AgentHttp_AgentThreadGrant `protobuf:"bytes,9,opt,name=thread_grant,json=threadGrant,proto3" json:"thread_grant,omitempty"` // NEXT_ID: 10 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1948,6 +1963,114 @@ func (x *AgentHttp_StreamPreamble) GetScheme() string { return "" } +func (x *AgentHttp_StreamPreamble) GetThreadId() string { + if x != nil { + return x.ThreadId + } + return "" +} + +func (x *AgentHttp_StreamPreamble) GetThreadGrant() *AgentHttp_AgentThreadGrant { + if x != nil { + return x.ThreadGrant + } + return nil +} + +// auth.AgentThreadGrant on the wire: threads by scope or by id, with +// per-action flags +type AgentHttp_AgentThreadGrant struct { + state protoimpl.MessageState `protogen:"open.v1"` + Scope string `protobuf:"bytes,2,opt,name=scope,proto3" json:"scope,omitempty"` + ThreadIds []string `protobuf:"bytes,3,rep,name=thread_ids,json=threadIds,proto3" json:"thread_ids,omitempty"` + List bool `protobuf:"varint,4,opt,name=list,proto3" json:"list,omitempty"` + Create bool `protobuf:"varint,5,opt,name=create,proto3" json:"create,omitempty"` + Write bool `protobuf:"varint,6,opt,name=write,proto3" json:"write,omitempty"` + Read bool `protobuf:"varint,7,opt,name=read,proto3" json:"read,omitempty"` + Delete bool `protobuf:"varint,8,opt,name=delete,proto3" json:"delete,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentHttp_AgentThreadGrant) Reset() { + *x = AgentHttp_AgentThreadGrant{} + mi := &file_livekit_agent_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentHttp_AgentThreadGrant) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentHttp_AgentThreadGrant) ProtoMessage() {} + +func (x *AgentHttp_AgentThreadGrant) ProtoReflect() protoreflect.Message { + mi := &file_livekit_agent_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentHttp_AgentThreadGrant.ProtoReflect.Descriptor instead. +func (*AgentHttp_AgentThreadGrant) Descriptor() ([]byte, []int) { + return file_livekit_agent_proto_rawDescGZIP(), []int{16, 3} +} + +func (x *AgentHttp_AgentThreadGrant) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *AgentHttp_AgentThreadGrant) GetThreadIds() []string { + if x != nil { + return x.ThreadIds + } + return nil +} + +func (x *AgentHttp_AgentThreadGrant) GetList() bool { + if x != nil { + return x.List + } + return false +} + +func (x *AgentHttp_AgentThreadGrant) GetCreate() bool { + if x != nil { + return x.Create + } + return false +} + +func (x *AgentHttp_AgentThreadGrant) GetWrite() bool { + if x != nil { + return x.Write + } + return false +} + +func (x *AgentHttp_AgentThreadGrant) GetRead() bool { + if x != nil { + return x.Read + } + return false +} + +func (x *AgentHttp_AgentThreadGrant) GetDelete() bool { + if x != nil { + return x.Delete + } + return false +} + // the server is draining: the worker should re-register elsewhere; in-flight // HTTP exchanges run to completion type AgentHttp_GoAway struct { @@ -1959,7 +2082,7 @@ type AgentHttp_GoAway struct { func (x *AgentHttp_GoAway) Reset() { *x = AgentHttp_GoAway{} - mi := &file_livekit_agent_proto_msgTypes[22] + mi := &file_livekit_agent_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1971,7 +2094,7 @@ func (x *AgentHttp_GoAway) String() string { func (*AgentHttp_GoAway) ProtoMessage() {} func (x *AgentHttp_GoAway) ProtoReflect() protoreflect.Message { - mi := &file_livekit_agent_proto_msgTypes[22] + mi := &file_livekit_agent_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1984,7 +2107,7 @@ func (x *AgentHttp_GoAway) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentHttp_GoAway.ProtoReflect.Descriptor instead. func (*AgentHttp_GoAway) Descriptor() ([]byte, []int) { - return file_livekit_agent_proto_rawDescGZIP(), []int{16, 3} + return file_livekit_agent_proto_rawDescGZIP(), []int{16, 4} } func (x *AgentHttp_GoAway) GetReason() string { @@ -1998,7 +2121,7 @@ var File_livekit_agent_proto protoreflect.FileDescriptor const file_livekit_agent_proto_rawDesc = "" + "\n" + - "\x13livekit_agent.proto\x12\alivekit\x1a\x14livekit_models.proto\x1a\x14logger/options.proto\"\xe2\x04\n" + + "\x13livekit_agent.proto\x12\alivekit\x1a\x14livekit_models.proto\x1a\x14logger/options.proto\"\x8c\x05\n" + "\x03Job\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12.\n" + "\vdispatch_id\x18\t \x01(\tB\r\xbaP\n" + @@ -2020,7 +2143,8 @@ const file_livekit_agent_proto_rawDesc = "" + "\n" + "attributes\x18\f \x03(\v2\x1c.livekit.Job.AttributesEntryB\x03\xc0P\x01R\n" + "attributes\x12)\n" + - "\x10enable_redaction\x18\r \x01(\bR\x0fenableRedaction\x1a=\n" + + "\x10enable_redaction\x18\r \x01(\bR\x0fenableRedaction\x12(\n" + + "\tthread_id\x18\x0e \x01(\tB\v\xbaP\bthreadIDR\bthreadId\x1a=\n" + "\x0fAttributesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x0e\n" + @@ -2127,7 +2251,7 @@ const file_livekit_agent_proto_rawDesc = "" + "\x05token\x18\x03 \x01(\tR\x05tokenB\x06\n" + "\x04_url\"1\n" + "\x0eJobTermination\x12\x1f\n" + - "\x06job_id\x18\x01 \x01(\tB\b\xbaP\x05jobIDR\x05jobId\"\xaa\x05\n" + + "\x06job_id\x18\x01 \x01(\tB\b\xbaP\x05jobIDR\x05jobId\"\xee\a\n" + "\tAgentHttp\x1a\x8f\x01\n" + "\rAgentEndpoint\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x18\n" + @@ -2135,7 +2259,7 @@ const file_livekit_agent_proto_rawDesc = "" + "\x04kind\x18\x03 \x01(\x0e2$.livekit.AgentHttp.AgentEndpointKindR\x04kind\x12\x16\n" + "\x06public\x18\x04 \x01(\bR\x06public\x1a?\n" + "\x15AgentEndpointSettings\x12\x1a\n" + - "\bprotocol\x18\x01 \x01(\rR\bprotocolJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04\x1a\x8a\x02\n" + + "\bprotocol\x18\x01 \x01(\rR\bprotocolJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04\x1a\xfc\x02\n" + "\x0eStreamPreamble\x128\n" + "\x04kind\x18\x01 \x01(\x0e2$.livekit.AgentHttp.AgentEndpointKindR\x04kind\x12+\n" + "\n" + @@ -2148,7 +2272,19 @@ const file_livekit_agent_proto_rawDesc = "" + "timeout_ms\x18\x05 \x01(\rR\ttimeoutMs\x12$\n" + "\vclient_addr\x18\x06 \x01(\tB\x03\xc0P\x01R\n" + "clientAddr\x12\x16\n" + - "\x06scheme\x18\a \x01(\tR\x06scheme\x1a \n" + + "\x06scheme\x18\a \x01(\tR\x06scheme\x12(\n" + + "\tthread_id\x18\b \x01(\tB\v\xbaP\bthreadIDR\bthreadId\x12F\n" + + "\fthread_grant\x18\t \x01(\v2#.livekit.AgentHttp.AgentThreadGrantR\vthreadGrant\x1a\xcf\x01\n" + + "\x10AgentThreadGrant\x12\x14\n" + + "\x05scope\x18\x02 \x01(\tR\x05scope\x12+\n" + + "\n" + + "thread_ids\x18\x03 \x03(\tB\f\xbaP\tthreadIDsR\tthreadIds\x12\x12\n" + + "\x04list\x18\x04 \x01(\bR\x04list\x12\x16\n" + + "\x06create\x18\x05 \x01(\bR\x06create\x12\x14\n" + + "\x05write\x18\x06 \x01(\bR\x05write\x12\x12\n" + + "\x04read\x18\a \x01(\bR\x04read\x12\x16\n" + + "\x06delete\x18\b \x01(\bR\x06deleteJ\x04\b\x01\x10\x02J\x04\b\t\x10\n" + + "\x1a \n" + "\x06GoAway\x12\x16\n" + "\x06reason\x18\x01 \x01(\tR\x06reason\"/\n" + "\x11AgentEndpointKind\x12\f\n" + @@ -2189,7 +2325,7 @@ func file_livekit_agent_proto_rawDescGZIP() []byte { } var file_livekit_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_livekit_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_livekit_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_livekit_agent_proto_goTypes = []any{ (JobType)(0), // 0: livekit.JobType (WorkerStatus)(0), // 1: livekit.WorkerStatus @@ -2218,16 +2354,17 @@ var file_livekit_agent_proto_goTypes = []any{ (*AgentHttp_AgentEndpoint)(nil), // 24: livekit.AgentHttp.AgentEndpoint (*AgentHttp_AgentEndpointSettings)(nil), // 25: livekit.AgentHttp.AgentEndpointSettings (*AgentHttp_StreamPreamble)(nil), // 26: livekit.AgentHttp.StreamPreamble - (*AgentHttp_GoAway)(nil), // 27: livekit.AgentHttp.GoAway - (*Room)(nil), // 28: livekit.Room - (*ParticipantInfo)(nil), // 29: livekit.ParticipantInfo - (*ParticipantPermission)(nil), // 30: livekit.ParticipantPermission - (*ServerInfo)(nil), // 31: livekit.ServerInfo + (*AgentHttp_AgentThreadGrant)(nil), // 27: livekit.AgentHttp.AgentThreadGrant + (*AgentHttp_GoAway)(nil), // 28: livekit.AgentHttp.GoAway + (*Room)(nil), // 29: livekit.Room + (*ParticipantInfo)(nil), // 30: livekit.ParticipantInfo + (*ParticipantPermission)(nil), // 31: livekit.ParticipantPermission + (*ServerInfo)(nil), // 32: livekit.ServerInfo } var file_livekit_agent_proto_depIdxs = []int32{ 0, // 0: livekit.Job.type:type_name -> livekit.JobType - 28, // 1: livekit.Job.room:type_name -> livekit.Room - 29, // 2: livekit.Job.participant:type_name -> livekit.ParticipantInfo + 29, // 1: livekit.Job.room:type_name -> livekit.Room + 30, // 2: livekit.Job.participant:type_name -> livekit.ParticipantInfo 6, // 3: livekit.Job.state:type_name -> livekit.JobState 22, // 4: livekit.Job.attributes:type_name -> livekit.Job.AttributesEntry 2, // 5: livekit.JobState.status:type_name -> livekit.JobStatus @@ -2243,14 +2380,14 @@ var file_livekit_agent_proto_depIdxs = []int32{ 19, // 15: livekit.ServerMessage.assignment:type_name -> livekit.JobAssignment 20, // 16: livekit.ServerMessage.termination:type_name -> livekit.JobTermination 11, // 17: livekit.ServerMessage.pong:type_name -> livekit.WorkerPong - 27, // 18: livekit.ServerMessage.go_away:type_name -> livekit.AgentHttp.GoAway + 28, // 18: livekit.ServerMessage.go_away:type_name -> livekit.AgentHttp.GoAway 0, // 19: livekit.SimulateJobRequest.type:type_name -> livekit.JobType - 28, // 20: livekit.SimulateJobRequest.room:type_name -> livekit.Room - 29, // 21: livekit.SimulateJobRequest.participant:type_name -> livekit.ParticipantInfo + 29, // 20: livekit.SimulateJobRequest.room:type_name -> livekit.Room + 30, // 21: livekit.SimulateJobRequest.participant:type_name -> livekit.ParticipantInfo 0, // 22: livekit.RegisterWorkerRequest.type:type_name -> livekit.JobType - 30, // 23: livekit.RegisterWorkerRequest.allowed_permissions:type_name -> livekit.ParticipantPermission + 31, // 23: livekit.RegisterWorkerRequest.allowed_permissions:type_name -> livekit.ParticipantPermission 24, // 24: livekit.RegisterWorkerRequest.endpoints:type_name -> livekit.AgentHttp.AgentEndpoint - 31, // 25: livekit.RegisterWorkerResponse.server_info:type_name -> livekit.ServerInfo + 32, // 25: livekit.RegisterWorkerResponse.server_info:type_name -> livekit.ServerInfo 25, // 26: livekit.RegisterWorkerResponse.endpoint_settings:type_name -> livekit.AgentHttp.AgentEndpointSettings 5, // 27: livekit.AvailabilityRequest.job:type_name -> livekit.Job 23, // 28: livekit.AvailabilityResponse.participant_attributes:type_name -> livekit.AvailabilityResponse.ParticipantAttributesEntry @@ -2259,11 +2396,12 @@ var file_livekit_agent_proto_depIdxs = []int32{ 5, // 31: livekit.JobAssignment.job:type_name -> livekit.Job 3, // 32: livekit.AgentHttp.AgentEndpoint.kind:type_name -> livekit.AgentHttp.AgentEndpointKind 3, // 33: livekit.AgentHttp.StreamPreamble.kind:type_name -> livekit.AgentHttp.AgentEndpointKind - 34, // [34:34] is the sub-list for method output_type - 34, // [34:34] is the sub-list for method input_type - 34, // [34:34] is the sub-list for extension type_name - 34, // [34:34] is the sub-list for extension extendee - 0, // [0:34] is the sub-list for field type_name + 27, // 34: livekit.AgentHttp.StreamPreamble.thread_grant:type_name -> livekit.AgentHttp.AgentThreadGrant + 35, // [35:35] is the sub-list for method output_type + 35, // [35:35] is the sub-list for method input_type + 35, // [35:35] is the sub-list for extension type_name + 35, // [35:35] is the sub-list for extension extendee + 0, // [0:35] is the sub-list for field type_name } func init() { file_livekit_agent_proto_init() } @@ -2299,7 +2437,7 @@ func file_livekit_agent_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_livekit_agent_proto_rawDesc), len(file_livekit_agent_proto_rawDesc)), NumEnums: 5, - NumMessages: 23, + NumMessages: 24, NumExtensions: 0, NumServices: 0, }, diff --git a/protobufs/livekit_agent.proto b/protobufs/livekit_agent.proto index 4747feb11..4a844f844 100644 --- a/protobufs/livekit_agent.proto +++ b/protobufs/livekit_agent.proto @@ -36,6 +36,8 @@ message Job { string deployment = 11; map attributes = 12 [(logger.sensitivity) = SENSITIVITY_PII]; bool enable_redaction = 13; + // the thread ("AT_...") this job continues; empty when dispatch made none + string thread_id = 14 [(logger.name) = "threadID"]; } message JobState { @@ -297,7 +299,26 @@ message AgentHttp { string client_addr = 6 [(logger.sensitivity) = SENSITIVITY_PII]; // "http" or "https" as the end client saw it; not derivable worker-side string scheme = 7; - // NEXT_ID: 8 + // the thread ("AT_...") the node resolved for this request; empty when + // none, and the worker resolves it from the body + string thread_id = 8 [(logger.name) = "threadID"]; + // the verified agentThread claim of the caller's token, which never + // reaches the worker; the worker enforces it on the addressed thread + AgentThreadGrant thread_grant = 9; + // NEXT_ID: 10 + } + + // auth.AgentThreadGrant on the wire: threads by scope or by id, with + // per-action flags + message AgentThreadGrant { + reserved 1, 9; + string scope = 2; + repeated string thread_ids = 3 [(logger.name) = "threadIDs"]; + bool list = 4; + bool create = 5; + bool write = 6; + bool read = 7; + bool delete = 8; } // why a stream was reset before any HTTP bytes flowed. This travels as the