Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3de047c
Add conversations/messages models for running capabilities integration
noah-tutt-praetorian Sep 15, 2025
fd154fd
feat: simplify conversation and message models
noah-tutt-praetorian Sep 15, 2025
2df2715
feat: simplify conversation/message models for UUID-only approach
noah-tutt-praetorian Sep 15, 2025
bef2fb5
Merge branch 'main' into nmt/planner
noah-tutt-praetorian Sep 18, 2025
d844fcd
feat: add PlannerEvent model
noah-tutt-praetorian Sep 22, 2025
0f9969b
feat: recreate clean planner-events with simplified message formatting
noah-tutt-praetorian Sep 22, 2025
b2f0aa4
Merge branch 'main' into nmt/planner
noah-tutt-praetorian Sep 22, 2025
f4ad8dd
remove
noah-tutt-praetorian Sep 22, 2025
7a0db41
refactor: clean up models - remove TTL, add source field, simplify Pl…
noah-tutt-praetorian Sep 22, 2025
e97c56c
URGENT FIX: Make PlannerEvent pure PoD - remove ALL database code
noah-tutt-praetorian Sep 22, 2025
5c4b8f7
URGENT FIX: Place PlannerEvent in correct location as pure PoD
noah-tutt-praetorian Sep 22, 2025
ed5faad
update
noah-tutt-praetorian Sep 22, 2025
279bc30
rm
noah-tutt-praetorian Sep 22, 2025
1ac0680
fix
noah-tutt-praetorian Sep 22, 2025
3868071
Merge branch 'main' into nmt/planner
noah-tutt-praetorian Sep 22, 2025
72861c7
update
noah-tutt-praetorian Sep 23, 2025
1e9c57e
Merge branch 'main' into nmt/planner
noah-tutt-praetorian Sep 23, 2025
89b4d12
update
noah-tutt-praetorian Sep 24, 2025
607f0fe
test: update message and conversation tests for new fields
noah-tutt-praetorian Sep 24, 2025
541de16
update
noah-tutt-praetorian Sep 24, 2025
9a6fc1a
update
noah-tutt-praetorian Sep 24, 2025
ec5885a
fix: update conversation tests for new model structure
noah-tutt-praetorian Sep 24, 2025
4e893f4
tweak
noah-tutt-praetorian Sep 24, 2025
4041da2
feat: update PlannerJobCompletion to include full result objects
noah-tutt-praetorian Sep 25, 2025
54e3da5
feat: include full objects and complete key list in job results
noah-tutt-praetorian Sep 25, 2025
44d9db2
cleanup: remove explanatory comments and fix tool iteration count
noah-tutt-praetorian Sep 25, 2025
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
94 changes: 94 additions & 0 deletions client/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,46 @@ components:
- username
- value
type: object
conversation:
description: Represents a conversation between a user and AI assistant with
running capabilities.
properties:
BaseModel:
type: object
created:
description: Timestamp when the conversation was created (RFC3339).
example: "2023-10-27T10:00:00Z"
type: string
key:
description: Unique key for the conversation.
example: '#conversation#example-conversation#550e8400-e29b-41d4-a716-446655440000'
type: string
name:
description: Name of the conversation.
example: My AI Assistant Chat
type: string
ttl:
description: Time-to-live for the conversation record (Unix timestamp).
example: "1706353200"
format: int64
type: integer
username:
description: Username who owns the conversation.
example: [email protected]
type: string
uuid:
description: UUID of the conversation for reference.
example: 550e8400-e29b-41d4-a716-446655440000
type: string
required:
- BaseModel
- created
- key
- name
- ttl
- username
- uuid
type: object
cpe:
description: Represents a Common Platform Enumeration (CPE) identifier, used
for naming hardware, software, and operating systems.
Expand Down Expand Up @@ -2094,6 +2134,10 @@ components:
description: Configuration parameters for the job capability.
example: '{"test": "cve-1111-2222"}'
type: object
conversation:
description: UUID of the conversation that initiated this job.
example: 550e8400-e29b-41d4-a716-446655440000
type: string
created:
description: Timestamp when the job was created (RFC3339).
example: "2023-10-27T10:00:00Z"
Expand Down Expand Up @@ -2230,6 +2274,56 @@ components:
- username
- visited
type: object
message:
description: Represents a message within a conversation, with KSUID ordering
for proper sequencing.
properties:
BaseModel:
type: object
content:
description: Content of the message.
example: Hello, how can I help you today?
type: string
conversationId:
description: ID of the conversation this message belongs to.
example: 550e8400-e29b-41d4-a716-446655440000
type: string
key:
description: Unique key for the message.
example: '#message#550e8400-e29b-41d4-a716-446655440000#1sB5tZfLipTVWQWHVKnDFS6kFRK'
type: string
messageId:
description: KSUID for message ordering.
example: 1sB5tZfLipTVWQWHVKnDFS6kFRK
type: string
role:
description: Role of the message sender (user, chariot, system).
example: user
type: string
timestamp:
description: Timestamp when the message was created (RFC3339).
example: "2023-10-27T10:00:00Z"
type: string
ttl:
description: Time-to-live for the message record (Unix timestamp).
example: "1706353200"
format: int64
type: integer
username:
description: Username who sent the message.
example: [email protected]
type: string
required:
- BaseModel
- content
- conversationId
- key
- messageId
- role
- timestamp
- ttl
- username
type: object
Comment on lines +2277 to +2326
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Align message.role description and include tool fields.

Code defines additional roles and tool-use fields; OpenAPI should reflect them.

Apply:

     message:
       description: Represents a message within a conversation, with KSUID ordering
         for proper sequencing.
       properties:
         BaseModel:
           type: object
         content:
           description: Content of the message.
           example: Hello, how can I help you today?
           type: string
         conversationId:
           description: ID of the conversation this message belongs to.
           example: 550e8400-e29b-41d4-a716-446655440000
           type: string
         key:
           description: Unique key for the message.
           example: '#message#550e8400-e29b-41d4-a716-446655440000#1sB5tZfLipTVWQWHVKnDFS6kFRK'
           type: string
         messageId:
           description: KSUID for message ordering.
           example: 1sB5tZfLipTVWQWHVKnDFS6kFRK
           type: string
         role:
-          description: Role of the message sender (user, chariot, system).
+          description: Role of the message sender (user, chariot, system, tool call, tool response, planner-output).
           example: user
           type: string
         timestamp:
           description: Timestamp when the message was created (RFC3339).
           example: "2023-10-27T10:00:00Z"
           type: string
         ttl:
           description: Time-to-live for the message record (Unix timestamp).
           example: "1706353200"
           format: int64
           type: integer
         username:
           description: Username who sent the message.
           example: [email protected]
           type: string
+        toolUseId:
+          description: Tool use ID for tool result messages.
+          example: tooluse_kZJMlvQmRJ6eAyJE5GIl7Q
+          type: string
+        toolUseContent:
+          description: JSON serialized tool use content for assistant tool use messages.
+          example: '{"name":"query","input":{"node":{"labels":["Asset"]}}}'
+          type: string
       required:
         - BaseModel
         - content
         - conversationId
         - key
         - messageId
         - role
         - timestamp
         - ttl
         - username
+        # toolUseId/toolUseContent are optional
       type: object
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
message:
description: Represents a message within a conversation, with KSUID ordering
for proper sequencing.
properties:
BaseModel:
type: object
content:
description: Content of the message.
example: Hello, how can I help you today?
type: string
conversationId:
description: ID of the conversation this message belongs to.
example: 550e8400-e29b-41d4-a716-446655440000
type: string
key:
description: Unique key for the message.
example: '#message#550e8400-e29b-41d4-a716-446655440000#1sB5tZfLipTVWQWHVKnDFS6kFRK'
type: string
messageId:
description: KSUID for message ordering.
example: 1sB5tZfLipTVWQWHVKnDFS6kFRK
type: string
role:
description: Role of the message sender (user, chariot, system).
example: user
type: string
timestamp:
description: Timestamp when the message was created (RFC3339).
example: "2023-10-27T10:00:00Z"
type: string
ttl:
description: Time-to-live for the message record (Unix timestamp).
example: "1706353200"
format: int64
type: integer
username:
description: Username who sent the message.
example: [email protected]
type: string
required:
- BaseModel
- content
- conversationId
- key
- messageId
- role
- timestamp
- ttl
- username
type: object
message:
description: Represents a message within a conversation, with KSUID ordering
for proper sequencing.
properties:
BaseModel:
type: object
content:
description: Content of the message.
example: Hello, how can I help you today?
type: string
conversationId:
description: ID of the conversation this message belongs to.
example: 550e8400-e29b-41d4-a716-446655440000
type: string
key:
description: Unique key for the message.
example: '#message#550e8400-e29b-41d4-a716-446655440000#1sB5tZfLipTVWQWHVKnDFS6kFRK'
type: string
messageId:
description: KSUID for message ordering.
example: 1sB5tZfLipTVWQWHVKnDFS6kFRK
type: string
role:
description: Role of the message sender (user, chariot, system, tool call, tool response, planner-output).
example: user
type: string
timestamp:
description: Timestamp when the message was created (RFC3339).
example: "2023-10-27T10:00:00Z"
type: string
ttl:
description: Time-to-live for the message record (Unix timestamp).
example: "1706353200"
format: int64
type: integer
username:
description: Username who sent the message.
example: [email protected]
type: string
toolUseId:
description: Tool use ID for tool result messages.
example: tooluse_kZJMlvQmRJ6eAyJE5GIl7Q
type: string
toolUseContent:
description: JSON serialized tool use content for assistant tool use messages.
example: '{"name":"query","input":{"node":{"labels":["Asset"]}}}'
type: string
required:
- BaseModel
- content
- conversationId
- key
- messageId
- role
- timestamp
- ttl
- username
# toolUseId/toolUseContent are optional
type: object

metadata:
description: Contains metadata about an asset, including discovery information,
relationships, and attributes.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
Expand Down
61 changes: 61 additions & 0 deletions pkg/model/model/conversation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package model

import (
"github.com/google/uuid"
"github.com/praetorian-inc/tabularium/pkg/registry"
)

type Conversation struct {
registry.BaseModel
baseTableModel
Username string `dynamodbav:"username" json:"username" desc:"Username who owns the conversation." example:"[email protected]"`
Key string `dynamodbav:"key" json:"key" desc:"Unique key for the conversation." example:"#conversation#example-conversation#550e8400-e29b-41d4-a716-446655440000"`
UUID string `dynamodbav:"uuid" json:"uuid" desc:"UUID of the conversation for reference." example:"550e8400-e29b-41d4-a716-446655440000"`
User string `dynamodbav:"user" json:"user" desc:"Who started the conversation." example:"[email protected]"`
Created string `dynamodbav:"created" json:"created" desc:"Timestamp when the conversation was created (RFC3339)." example:"2023-10-27T10:00:00Z"`
Topic string `dynamodbav:"topic" json:"topic" desc:"Topic extracted from first message (first 256 chars)." example:"Find all active assets in my infrastructure"`
}

func init() {
registry.Registry.MustRegisterModel(&Conversation{})
}

func (c *Conversation) GetKey() string {
return c.Key
}

func (c *Conversation) GetDescription() string {
return "Represents a conversation between a user and AI assistant with running capabilities."
}

func (c *Conversation) Defaulted() {
c.Created = Now()
}

func (c *Conversation) GetHooks() []registry.Hook {
return []registry.Hook{
{
Call: func() error {
if c.Key == "" {
conversationID := uuid.New().String()
c.UUID = conversationID
c.Key = "#conversation#" + conversationID
}
return nil
},
},
}
}

func (c *Conversation) Valid() bool {
return c.Key != ""
}

func NewConversation(topic string) Conversation {
conv := Conversation{
Topic: topic,
}
conv.Defaulted()
registry.CallHooks(&conv)
return conv
}
187 changes: 187 additions & 0 deletions pkg/model/model/conversation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package model

import (
"strings"
"testing"

"github.com/praetorian-inc/tabularium/pkg/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Comment on lines +7 to +10
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Tests must use the standard library (no testify).

Project guideline: “**/*_test.go: Use the standard Go testing framework; do not introduce additional test frameworks.” Replace assert/require with native checks (if/tt.Errorf/tt.Fatal).

Example refactor pattern:

  • assert.Equal(t, got, want) → if got != want { t.Errorf("...") }
  • require.NoError(t, err) → if err != nil { t.Fatal(err) }

Remove these imports:

-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"github.com/praetorian-inc/tabularium/pkg/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
"github.com/praetorian-inc/tabularium/pkg/registry"
)
🤖 Prompt for AI Agents
In pkg/model/model/conversation_test.go around lines 7 to 10, the test imports
testify's assert and require which violates the project guideline to use the
standard library for tests; remove "github.com/stretchr/testify/assert" and
"github.com/stretchr/testify/require" from the import block and replace all
assert/require callsites with native testing checks (e.g., assert.Equal -> if
got != want { t.Errorf("... got=%v want=%v", got, want) }; require.NoError -> if
err != nil { t.Fatal(err) } or t.Fatalf with context). Ensure you keep the
"testing" package imported and update all assertions accordingly.


func TestConversation_NewConversation(t *testing.T) {
topic := "Test Conversation"

conv := NewConversation(topic)

assert.Equal(t, topic, conv.Topic)
assert.NotEmpty(t, conv.UUID)
assert.NotEmpty(t, conv.Created)
assert.NotEmpty(t, conv.Source)

Check failure on line 20 in pkg/model/model/conversation_test.go

View workflow job for this annotation

GitHub Actions / test

conv.Source undefined (type Conversation has no field or method Source)
assert.NotEmpty(t, conv.Key)
assert.True(t, strings.HasPrefix(conv.Key, "#conversation#"))
assert.True(t, conv.Valid())
}

func TestConversation_GetKey(t *testing.T) {
conv := NewConversation("test")
assert.Equal(t, conv.Key, conv.GetKey())
assert.NotEmpty(t, conv.GetKey())
}

func TestConversation_GetDescription(t *testing.T) {
conv := &Conversation{}
expected := "Represents a conversation between a user and AI assistant with running capabilities."
assert.Equal(t, expected, conv.GetDescription())
}

func TestConversation_Defaulted(t *testing.T) {
conv := &Conversation{}
conv.Defaulted()

assert.NotEmpty(t, conv.Created)
assert.NotEmpty(t, conv.Source)

Check failure on line 43 in pkg/model/model/conversation_test.go

View workflow job for this annotation

GitHub Actions / test

conv.Source undefined (type *Conversation has no field or method Source)

// Verify TTL is approximately 30 days from now
assert.NotEmpty(t, conv.Created) // Allow 60 seconds tolerance
}

func TestConversation_Hooks(t *testing.T) {
conv := &Conversation{}

// Call hooks manually
registry.CallHooks(conv)

assert.NotEmpty(t, conv.Key)
assert.True(t, strings.HasPrefix(conv.Key, "#conversation#"))

// Verify UUID format in key (should be 36 characters with dashes)
keyParts := strings.Split(conv.Key, "#")
require.Len(t, keyParts, 3)
uuid := keyParts[2]
assert.Len(t, uuid, 36)
assert.Contains(t, uuid, "-")
}

func TestConversation_Hooks_ExistingKey(t *testing.T) {
existingKey := "#conversation#existing#12345"
conv := &Conversation{
Key: existingKey,
}

registry.CallHooks(conv)

// Should not change existing key
assert.Equal(t, existingKey, conv.Key)
}

func TestConversation_RegistryIntegration(t *testing.T) {
// Test that the conversation is properly registered in the registry
conv := &Conversation{}

// Check that it's registered by calling a registry function
hooks := conv.GetHooks()
assert.Len(t, hooks, 1)

// Verify hook functionality
err := hooks[0].Call()
assert.NoError(t, err)
assert.NotEmpty(t, conv.Key)
}

func TestConversation_KeyGeneration_Uniqueness(t *testing.T) {
// Test that multiple conversations with same topic get different keys
topic := "Same Name"

conv1 := NewConversation(topic)
conv2 := NewConversation(topic)

assert.NotEqual(t, conv1.Key, conv2.Key)
assert.True(t, strings.HasPrefix(conv1.Key, "#conversation#"))
assert.True(t, strings.HasPrefix(conv2.Key, "#conversation#"))
assert.Equal(t, topic, conv1.Topic)
assert.Equal(t, topic, conv2.Topic)
}

func TestConversation_SecurityScenarios(t *testing.T) {
testCases := []struct {
name string
conversationName string
expectValid bool
}{
{
name: "valid standard conversation",
conversationName: "Normal Conversation",
expectValid: true,
},
{
name: "conversation with special characters",
conversationName: "Conv/\\with<>special|chars",
expectValid: true,
},
{
name: "very long conversation name",
conversationName: strings.Repeat("a", 1000),
expectValid: true,
},
{
name: "SQL injection attempt in name",
conversationName: "'; DROP TABLE users; --",
expectValid: true, // Should be treated as regular string
},
{
name: "XSS attempt in name",
conversationName: "<script>alert('xss')</script>",
expectValid: true, // Should be treated as regular string
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conv := NewConversation(tc.conversationName)

assert.Equal(t, tc.expectValid, conv.Valid())
if tc.expectValid {
assert.Equal(t, tc.conversationName, conv.Topic)
assert.NotEmpty(t, conv.Key)
}
})
}
}

func TestConversation_TopicField(t *testing.T) {
testCases := []struct {
name string
topic string
expected string
}{
{
name: "short topic",
topic: "Find all assets",
expected: "Find all assets",
},
{
name: "long topic gets truncated",
topic: strings.Repeat("a", 300),
expected: strings.Repeat("a", 256),
},
{
name: "empty topic",
topic: "",
expected: "",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conv := NewConversation("Test Chat")
conv.Topic = tc.topic

if len(tc.topic) > 256 {
conv.Topic = tc.topic[:256]
}

assert.Equal(t, tc.expected, conv.Topic)
})
}
}
2 changes: 2 additions & 0 deletions pkg/model/model/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type Job struct {
Full bool `dynamodbav:"-" json:"full,omitempty" desc:"Indicates if this is a full scan job." example:"false"`
Capabilities []string `dynamodbav:"-" json:"capabilities,omitempty" desc:"List of specific capabilities to run for this job." example:"[\"portscan\", \"nuclei\"]"`
Queue string `dynamodbav:"-" desc:"Target queue for the job." example:"standard"`
Conversation string `dynamodbav:"conversation,omitempty" json:"conversation,omitempty" desc:"UUID of the conversation that initiated this job." example:"550e8400-e29b-41d4-a716-446655440000"`
User string `dynamodbav:"user,omitempty" json:"user,omitempty" desc:"User who initiated this job." example:"[email protected]"`
Origin TargetWrapper `dynamodbav:"origin" json:"origin" desc:"The job that originally started this chain of jobs."`
Target TargetWrapper `dynamodbav:"target" json:"target" desc:"The primary target of the job."`
Parent TargetWrapper `dynamodbav:"parent" json:"parent,omitempty" desc:"Optional parent target from which this job was spawned."`
Expand Down
Loading
Loading