@desplega.ai/agent-swarm 1.80.3 → 1.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.json +486 -29
- package/package.json +3 -3
- package/plugin/commands/user-management.md +85 -46
- package/plugin/pi-skills/user-management/SKILL.md +85 -46
- package/src/agentmail/handlers.ts +25 -3
- package/src/be/db.ts +33 -109
- package/src/be/migrations/067_users_first_class.sql +185 -0
- package/src/be/migrations/068_profile_changed_event_type.sql +56 -0
- package/src/be/unmapped-identities.ts +98 -0
- package/src/be/users.ts +531 -0
- package/src/github/handlers.ts +67 -7
- package/src/gitlab/handlers.ts +73 -5
- package/src/http/operator-actor.ts +59 -0
- package/src/http/users.ts +611 -21
- package/src/linear/oauth.ts +61 -1
- package/src/linear/sync.ts +134 -21
- package/src/slack/actions.ts +8 -2
- package/src/slack/assistant.ts +12 -9
- package/src/slack/enrich.ts +162 -0
- package/src/slack/handlers.ts +11 -19
- package/src/tests/agentmail-handlers.test.ts +166 -0
- package/src/tests/github-handlers.test.ts +290 -0
- package/src/tests/gitlab-handlers.test.ts +293 -1
- package/src/tests/http-users.test.ts +605 -0
- package/src/tests/linear-sync-identity.test.ts +427 -0
- package/src/tests/mcp-tools-user.test.ts +292 -0
- package/src/tests/slack-identity-resolution.test.ts +349 -0
- package/src/tests/user-identity.test.ts +351 -81
- package/src/tools/manage-user.ts +119 -24
- package/src/tools/resolve-user.ts +43 -29
- package/src/types.ts +26 -4
- package/src/utils/secret-scrubber.ts +5 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "How to manage the user registry — creating users for new Slack/GitHub/GitLab identities, managing aliases, resolving users across platforms. Use when a new human interacts with the swarm or when user identity needs updating."
|
|
2
|
+
description: "How to manage the user registry — creating users for new Slack/GitHub/GitLab/Linear identities, managing aliases, resolving users across platforms. Use when a new human interacts with the swarm or when user identity needs updating."
|
|
3
3
|
argument-hint: [action]
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -7,16 +7,18 @@ argument-hint: [action]
|
|
|
7
7
|
|
|
8
8
|
Manage the swarm's user registry — creating, updating, resolving, and listing users. Users link human identities across platforms (Slack, GitHub, GitLab, Linear, email) so the swarm can track who requested work.
|
|
9
9
|
|
|
10
|
+
> **Migration note (2026-05)**: the old top-level identity fields (`slackUserId`, `linearUserId`, `githubUsername`, `gitlabUsername`) and the fuzzy `name` lookup were removed in lockstep with the user-identity refactor. Use the new `{kind, externalId}` shape instead. Old payloads now fail Zod validation at runtime — there is no compatibility shim.
|
|
11
|
+
|
|
10
12
|
## When to Create Users
|
|
11
13
|
|
|
12
14
|
Create a new user when:
|
|
13
|
-
- An **unknown Slack user** sends a message to the swarm (
|
|
15
|
+
- An **unknown Slack user** sends a message to the swarm (`resolve-user` with `{kind: "slack", externalId: "<U_X>"}` returns no match)
|
|
14
16
|
- An **unknown GitHub user** opens an issue or PR that triggers a task
|
|
15
17
|
- An **unknown GitLab user** creates an issue or MR
|
|
16
18
|
- An **unknown Linear user** is assigned to or creates a synced issue
|
|
17
19
|
- A human explicitly asks to be registered
|
|
18
20
|
|
|
19
|
-
**Do NOT** create duplicate users. Always call `resolve-user` first to check if the person already exists under a different platform identity.
|
|
21
|
+
**Do NOT** create duplicate users. Always call `resolve-user` first — by `{kind, externalId}` AND, when you have it, by `email` — to check if the person already exists under a different platform identity.
|
|
20
22
|
|
|
21
23
|
## Tools
|
|
22
24
|
|
|
@@ -24,41 +26,62 @@ Two MCP tools handle user management:
|
|
|
24
26
|
|
|
25
27
|
### `resolve-user` — Find an existing user
|
|
26
28
|
|
|
27
|
-
Looks up a user by
|
|
29
|
+
Looks up a user by an `(kind, externalId)` pair OR by email (primary or alias). Use this BEFORE creating a new user. Caller MUST supply either `(kind + externalId)` OR `email` — empty input is rejected.
|
|
28
30
|
|
|
29
31
|
```
|
|
32
|
+
# Lookup by platform identity
|
|
33
|
+
resolve-user with:
|
|
34
|
+
kind: "slack"
|
|
35
|
+
externalId: "U12345"
|
|
36
|
+
|
|
37
|
+
# OR
|
|
38
|
+
resolve-user with:
|
|
39
|
+
kind: "github"
|
|
40
|
+
externalId: "octocat"
|
|
41
|
+
|
|
42
|
+
# OR
|
|
43
|
+
resolve-user with:
|
|
44
|
+
kind: "gitlab"
|
|
45
|
+
externalId: "octocat"
|
|
46
|
+
|
|
47
|
+
# OR
|
|
48
|
+
resolve-user with:
|
|
49
|
+
kind: "linear"
|
|
50
|
+
externalId: "uuid-from-linear"
|
|
51
|
+
|
|
52
|
+
# OR lookup by email (primary or alias)
|
|
30
53
|
resolve-user with:
|
|
31
|
-
|
|
32
|
-
# OR
|
|
33
|
-
githubUsername: "octocat" # GitHub username
|
|
34
|
-
# OR
|
|
35
|
-
gitlabUsername: "octocat" # GitLab username
|
|
36
|
-
# OR
|
|
37
|
-
linearUserId: "uuid" # Linear user UUID
|
|
38
|
-
# OR
|
|
39
|
-
email: "user@example.com" # Primary email or alias
|
|
40
|
-
# OR
|
|
41
|
-
name: "Jane Doe" # Fuzzy name search (least specific)
|
|
54
|
+
email: "user@example.com"
|
|
42
55
|
```
|
|
43
56
|
|
|
44
|
-
|
|
57
|
+
Email lookup is case-insensitive and checks the primary `email` column AND every entry in `emailAliases`.
|
|
45
58
|
|
|
46
59
|
### `manage-user` — CRUD operations
|
|
47
60
|
|
|
61
|
+
Identities are managed via a declarative `identities: [{kind, externalId}, ...]` array:
|
|
62
|
+
|
|
63
|
+
- On **create**: every entry in `identities` is linked (each emits `identity_added`).
|
|
64
|
+
- On **update**: `identities` is treated as the full desired set. Helper computes a diff against the user's current identities — adds emit `identity_added`, removes emit `identity_removed`. Omit the field entirely to leave identities untouched.
|
|
65
|
+
|
|
48
66
|
```
|
|
49
67
|
# Create a new user
|
|
50
68
|
manage-user with:
|
|
51
69
|
action: "create"
|
|
52
|
-
name: "Jane Doe"
|
|
53
|
-
email: "jane@company.com"
|
|
54
|
-
role: "engineering lead"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
name: "Jane Doe" # Required
|
|
71
|
+
email: "jane@company.com" # Optional
|
|
72
|
+
role: "engineering lead" # Optional, free-form
|
|
73
|
+
identities: # Optional
|
|
74
|
+
- kind: "slack"
|
|
75
|
+
externalId: "U12345"
|
|
76
|
+
- kind: "github"
|
|
77
|
+
externalId: "janedoe"
|
|
78
|
+
- kind: "linear"
|
|
79
|
+
externalId: "uuid-from-linear"
|
|
80
|
+
emailAliases: ["jane.doe@company.com"] # Optional
|
|
81
|
+
timezone: "America/New_York" # Optional
|
|
82
|
+
notes: "Prefers async communication" # Optional
|
|
83
|
+
dailyBudgetUsd: 25.0 # Optional — null/omitted = unlimited
|
|
84
|
+
status: "active" # Optional — "invited" | "active" | "suspended"
|
|
62
85
|
|
|
63
86
|
# List all users
|
|
64
87
|
manage-user with:
|
|
@@ -69,11 +92,16 @@ manage-user with:
|
|
|
69
92
|
action: "get"
|
|
70
93
|
userId: "<uuid>"
|
|
71
94
|
|
|
72
|
-
# Update a user (
|
|
95
|
+
# Update a user (declarative — pass the FULL desired set for `identities`)
|
|
73
96
|
manage-user with:
|
|
74
97
|
action: "update"
|
|
75
98
|
userId: "<uuid>"
|
|
76
|
-
|
|
99
|
+
identities: # FULL desired set; diff is applied
|
|
100
|
+
- kind: "slack"
|
|
101
|
+
externalId: "U12345"
|
|
102
|
+
- kind: "github"
|
|
103
|
+
externalId: "janedoe-new" # renamed → identity_added + identity_removed
|
|
104
|
+
emailAliases: ["jane.doe@company.com", "jd@example.com"] # emits email_added/email_removed per delta
|
|
77
105
|
|
|
78
106
|
# Delete a user
|
|
79
107
|
manage-user with:
|
|
@@ -83,37 +111,48 @@ manage-user with:
|
|
|
83
111
|
|
|
84
112
|
## Workflow: New Slack User
|
|
85
113
|
|
|
86
|
-
1. Receive a message from an unknown Slack user (e.g., `
|
|
87
|
-
2. Call `resolve-user` with `
|
|
88
|
-
3. Get the user's Slack profile (name, email) via `slack-read` or from the message metadata
|
|
89
|
-
4. Call `resolve-user` with `email: "<their-email>"` — check if they exist under a different platform
|
|
90
|
-
5. If found: call `manage-user` with `action: "update"
|
|
91
|
-
6. If not found: call `manage-user` with `action: "create"
|
|
114
|
+
1. Receive a message from an unknown Slack user (e.g., external ID `U_NEW123`).
|
|
115
|
+
2. Call `resolve-user` with `{kind: "slack", externalId: "U_NEW123"}` — returns null.
|
|
116
|
+
3. Get the user's Slack profile (name, email) via `slack-read` or from the message metadata.
|
|
117
|
+
4. Call `resolve-user` with `{email: "<their-email>"}` — check if they exist under a different platform.
|
|
118
|
+
5. If found: call `manage-user` with `action: "update"`, passing the user's FULL identity set including the new Slack entry.
|
|
119
|
+
6. If not found: call `manage-user` with `action: "create"`, including `name`, `email`, and `identities: [{kind: "slack", externalId: "U_NEW123"}]`.
|
|
92
120
|
|
|
93
121
|
## Workflow: New GitHub User
|
|
94
122
|
|
|
95
|
-
1. Receive a webhook from an unknown GitHub user (e.g., `
|
|
96
|
-
2. Call `resolve-user` with `
|
|
97
|
-
3. Call `manage-user` with `action: "create"
|
|
98
|
-
4. If you know their email (from the webhook payload), include it
|
|
123
|
+
1. Receive a webhook from an unknown GitHub user (e.g., login `octocat`).
|
|
124
|
+
2. Call `resolve-user` with `{kind: "github", externalId: "octocat"}` — returns null.
|
|
125
|
+
3. Call `manage-user` with `action: "create"`, including at minimum `name` and `identities: [{kind: "github", externalId: "octocat"}]`.
|
|
126
|
+
4. If you know their email (from the webhook payload), include it.
|
|
99
127
|
|
|
100
128
|
## Workflow: Linking Identities
|
|
101
129
|
|
|
102
130
|
When you discover a known user is also active on another platform:
|
|
103
131
|
|
|
104
|
-
1. Call `resolve-user` to find them by their known identity
|
|
105
|
-
2. Call `manage-user` with `action: "update"`
|
|
132
|
+
1. Call `resolve-user` to find them by their known identity.
|
|
133
|
+
2. Call `manage-user` with `action: "update"`, passing the FULL desired `identities` set (existing + the new one).
|
|
134
|
+
|
|
135
|
+
Example: You know "Jane" by Slack ID, and discover her GitHub login:
|
|
106
136
|
|
|
107
|
-
Example: You know "Jane" by Slack ID, and discover her GitHub username:
|
|
108
137
|
```
|
|
109
|
-
resolve-user
|
|
110
|
-
|
|
138
|
+
resolve-user kind: "slack" externalId: "U_JANE"
|
|
139
|
+
→ returns user with id "abc-123" (identities currently: [{kind: "slack", externalId: "U_JANE"}])
|
|
140
|
+
|
|
141
|
+
manage-user action: "update" userId: "abc-123"
|
|
142
|
+
identities:
|
|
143
|
+
- kind: "slack"
|
|
144
|
+
externalId: "U_JANE"
|
|
145
|
+
- kind: "github"
|
|
146
|
+
externalId: "janedoe"
|
|
147
|
+
→ adds GitHub identity (emit identity_added). Slack identity unchanged.
|
|
111
148
|
```
|
|
112
149
|
|
|
113
150
|
## Important Notes
|
|
114
151
|
|
|
115
152
|
- `manage-user` is **lead-only** — workers cannot use it for any action (the lead check happens before action dispatch). Workers must use `resolve-user` for lookups.
|
|
116
|
-
-
|
|
153
|
+
- The `(kind, externalId)` PK on `user_external_ids` means the same identifier cannot be linked to two different users — a re-link to a different user surfaces as a PK collision (the operator can investigate via the People page merge flow).
|
|
117
154
|
- Deleting a user clears `requestedByUserId` on all their associated tasks (sets to null).
|
|
118
|
-
- Email aliases are case-insensitive for resolution.
|
|
119
|
-
- The `preferredChannel` field defaults to `"slack"` and can be `"slack"`, `"email"`, `"github"`,
|
|
155
|
+
- Email aliases are case-insensitive for resolution. Editing them via `manage-user update` emits `email_added` / `email_removed` events per delta.
|
|
156
|
+
- The `preferredChannel` field defaults to `"slack"` and can be `"slack"`, `"email"`, `"github"`, `"gitlab"`, or any custom string.
|
|
157
|
+
- `dailyBudgetUsd` is `null` = unlimited.
|
|
158
|
+
- `status` lifecycle: `invited` → `active` → `suspended`. The CHECK constraint rejects other values.
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: user-management
|
|
3
|
-
description: "How to manage the user registry — creating users for new Slack/GitHub/GitLab identities, managing aliases, resolving users across platforms. Use when a new human interacts with the swarm or when user identity needs updating."
|
|
3
|
+
description: "How to manage the user registry — creating users for new Slack/GitHub/GitLab/Linear identities, managing aliases, resolving users across platforms. Use when a new human interacts with the swarm or when user identity needs updating."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# User Management
|
|
7
7
|
|
|
8
8
|
Manage the swarm's user registry — creating, updating, resolving, and listing users. Users link human identities across platforms (Slack, GitHub, GitLab, Linear, email) so the swarm can track who requested work.
|
|
9
9
|
|
|
10
|
+
> **Migration note (2026-05)**: the old top-level identity fields (`slackUserId`, `linearUserId`, `githubUsername`, `gitlabUsername`) and the fuzzy `name` lookup were removed in lockstep with the user-identity refactor. Use the new `{kind, externalId}` shape instead. Old payloads now fail Zod validation at runtime — there is no compatibility shim.
|
|
11
|
+
|
|
10
12
|
## When to Create Users
|
|
11
13
|
|
|
12
14
|
Create a new user when:
|
|
13
|
-
- An **unknown Slack user** sends a message to the swarm (
|
|
15
|
+
- An **unknown Slack user** sends a message to the swarm (`resolve-user` with `{kind: "slack", externalId: "<U_X>"}` returns no match)
|
|
14
16
|
- An **unknown GitHub user** opens an issue or PR that triggers a task
|
|
15
17
|
- An **unknown GitLab user** creates an issue or MR
|
|
16
18
|
- An **unknown Linear user** is assigned to or creates a synced issue
|
|
17
19
|
- A human explicitly asks to be registered
|
|
18
20
|
|
|
19
|
-
**Do NOT** create duplicate users. Always call `resolve-user` first to check if the person already exists under a different platform identity.
|
|
21
|
+
**Do NOT** create duplicate users. Always call `resolve-user` first — by `{kind, externalId}` AND, when you have it, by `email` — to check if the person already exists under a different platform identity.
|
|
20
22
|
|
|
21
23
|
## Tools
|
|
22
24
|
|
|
@@ -24,41 +26,62 @@ Two MCP tools handle user management:
|
|
|
24
26
|
|
|
25
27
|
### `resolve-user` — Find an existing user
|
|
26
28
|
|
|
27
|
-
Looks up a user by
|
|
29
|
+
Looks up a user by an `(kind, externalId)` pair OR by email (primary or alias). Use this BEFORE creating a new user. Caller MUST supply either `(kind + externalId)` OR `email` — empty input is rejected.
|
|
28
30
|
|
|
29
31
|
```
|
|
32
|
+
# Lookup by platform identity
|
|
33
|
+
resolve-user with:
|
|
34
|
+
kind: "slack"
|
|
35
|
+
externalId: "U12345"
|
|
36
|
+
|
|
37
|
+
# OR
|
|
38
|
+
resolve-user with:
|
|
39
|
+
kind: "github"
|
|
40
|
+
externalId: "octocat"
|
|
41
|
+
|
|
42
|
+
# OR
|
|
43
|
+
resolve-user with:
|
|
44
|
+
kind: "gitlab"
|
|
45
|
+
externalId: "octocat"
|
|
46
|
+
|
|
47
|
+
# OR
|
|
48
|
+
resolve-user with:
|
|
49
|
+
kind: "linear"
|
|
50
|
+
externalId: "uuid-from-linear"
|
|
51
|
+
|
|
52
|
+
# OR lookup by email (primary or alias)
|
|
30
53
|
resolve-user with:
|
|
31
|
-
|
|
32
|
-
# OR
|
|
33
|
-
githubUsername: "octocat" # GitHub username
|
|
34
|
-
# OR
|
|
35
|
-
gitlabUsername: "octocat" # GitLab username
|
|
36
|
-
# OR
|
|
37
|
-
linearUserId: "uuid" # Linear user UUID
|
|
38
|
-
# OR
|
|
39
|
-
email: "user@example.com" # Primary email or alias
|
|
40
|
-
# OR
|
|
41
|
-
name: "Jane Doe" # Fuzzy name search (least specific)
|
|
54
|
+
email: "user@example.com"
|
|
42
55
|
```
|
|
43
56
|
|
|
44
|
-
|
|
57
|
+
Email lookup is case-insensitive and checks the primary `email` column AND every entry in `emailAliases`.
|
|
45
58
|
|
|
46
59
|
### `manage-user` — CRUD operations
|
|
47
60
|
|
|
61
|
+
Identities are managed via a declarative `identities: [{kind, externalId}, ...]` array:
|
|
62
|
+
|
|
63
|
+
- On **create**: every entry in `identities` is linked (each emits `identity_added`).
|
|
64
|
+
- On **update**: `identities` is treated as the full desired set. Helper computes a diff against the user's current identities — adds emit `identity_added`, removes emit `identity_removed`. Omit the field entirely to leave identities untouched.
|
|
65
|
+
|
|
48
66
|
```
|
|
49
67
|
# Create a new user
|
|
50
68
|
manage-user with:
|
|
51
69
|
action: "create"
|
|
52
|
-
name: "Jane Doe"
|
|
53
|
-
email: "jane@company.com"
|
|
54
|
-
role: "engineering lead"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
name: "Jane Doe" # Required
|
|
71
|
+
email: "jane@company.com" # Optional
|
|
72
|
+
role: "engineering lead" # Optional, free-form
|
|
73
|
+
identities: # Optional
|
|
74
|
+
- kind: "slack"
|
|
75
|
+
externalId: "U12345"
|
|
76
|
+
- kind: "github"
|
|
77
|
+
externalId: "janedoe"
|
|
78
|
+
- kind: "linear"
|
|
79
|
+
externalId: "uuid-from-linear"
|
|
80
|
+
emailAliases: ["jane.doe@company.com"] # Optional
|
|
81
|
+
timezone: "America/New_York" # Optional
|
|
82
|
+
notes: "Prefers async communication" # Optional
|
|
83
|
+
dailyBudgetUsd: 25.0 # Optional — null/omitted = unlimited
|
|
84
|
+
status: "active" # Optional — "invited" | "active" | "suspended"
|
|
62
85
|
|
|
63
86
|
# List all users
|
|
64
87
|
manage-user with:
|
|
@@ -69,11 +92,16 @@ manage-user with:
|
|
|
69
92
|
action: "get"
|
|
70
93
|
userId: "<uuid>"
|
|
71
94
|
|
|
72
|
-
# Update a user (
|
|
95
|
+
# Update a user (declarative — pass the FULL desired set for `identities`)
|
|
73
96
|
manage-user with:
|
|
74
97
|
action: "update"
|
|
75
98
|
userId: "<uuid>"
|
|
76
|
-
|
|
99
|
+
identities: # FULL desired set; diff is applied
|
|
100
|
+
- kind: "slack"
|
|
101
|
+
externalId: "U12345"
|
|
102
|
+
- kind: "github"
|
|
103
|
+
externalId: "janedoe-new" # renamed → identity_added + identity_removed
|
|
104
|
+
emailAliases: ["jane.doe@company.com", "jd@example.com"] # emits email_added/email_removed per delta
|
|
77
105
|
|
|
78
106
|
# Delete a user
|
|
79
107
|
manage-user with:
|
|
@@ -83,37 +111,48 @@ manage-user with:
|
|
|
83
111
|
|
|
84
112
|
## Workflow: New Slack User
|
|
85
113
|
|
|
86
|
-
1. Receive a message from an unknown Slack user (e.g., `
|
|
87
|
-
2. Call `resolve-user` with `
|
|
88
|
-
3. Get the user's Slack profile (name, email) via `slack-read` or from the message metadata
|
|
89
|
-
4. Call `resolve-user` with `email: "<their-email>"` — check if they exist under a different platform
|
|
90
|
-
5. If found: call `manage-user` with `action: "update"
|
|
91
|
-
6. If not found: call `manage-user` with `action: "create"
|
|
114
|
+
1. Receive a message from an unknown Slack user (e.g., external ID `U_NEW123`).
|
|
115
|
+
2. Call `resolve-user` with `{kind: "slack", externalId: "U_NEW123"}` — returns null.
|
|
116
|
+
3. Get the user's Slack profile (name, email) via `slack-read` or from the message metadata.
|
|
117
|
+
4. Call `resolve-user` with `{email: "<their-email>"}` — check if they exist under a different platform.
|
|
118
|
+
5. If found: call `manage-user` with `action: "update"`, passing the user's FULL identity set including the new Slack entry.
|
|
119
|
+
6. If not found: call `manage-user` with `action: "create"`, including `name`, `email`, and `identities: [{kind: "slack", externalId: "U_NEW123"}]`.
|
|
92
120
|
|
|
93
121
|
## Workflow: New GitHub User
|
|
94
122
|
|
|
95
|
-
1. Receive a webhook from an unknown GitHub user (e.g., `
|
|
96
|
-
2. Call `resolve-user` with `
|
|
97
|
-
3. Call `manage-user` with `action: "create"
|
|
98
|
-
4. If you know their email (from the webhook payload), include it
|
|
123
|
+
1. Receive a webhook from an unknown GitHub user (e.g., login `octocat`).
|
|
124
|
+
2. Call `resolve-user` with `{kind: "github", externalId: "octocat"}` — returns null.
|
|
125
|
+
3. Call `manage-user` with `action: "create"`, including at minimum `name` and `identities: [{kind: "github", externalId: "octocat"}]`.
|
|
126
|
+
4. If you know their email (from the webhook payload), include it.
|
|
99
127
|
|
|
100
128
|
## Workflow: Linking Identities
|
|
101
129
|
|
|
102
130
|
When you discover a known user is also active on another platform:
|
|
103
131
|
|
|
104
|
-
1. Call `resolve-user` to find them by their known identity
|
|
105
|
-
2. Call `manage-user` with `action: "update"`
|
|
132
|
+
1. Call `resolve-user` to find them by their known identity.
|
|
133
|
+
2. Call `manage-user` with `action: "update"`, passing the FULL desired `identities` set (existing + the new one).
|
|
134
|
+
|
|
135
|
+
Example: You know "Jane" by Slack ID, and discover her GitHub login:
|
|
106
136
|
|
|
107
|
-
Example: You know "Jane" by Slack ID, and discover her GitHub username:
|
|
108
137
|
```
|
|
109
|
-
resolve-user
|
|
110
|
-
|
|
138
|
+
resolve-user kind: "slack" externalId: "U_JANE"
|
|
139
|
+
→ returns user with id "abc-123" (identities currently: [{kind: "slack", externalId: "U_JANE"}])
|
|
140
|
+
|
|
141
|
+
manage-user action: "update" userId: "abc-123"
|
|
142
|
+
identities:
|
|
143
|
+
- kind: "slack"
|
|
144
|
+
externalId: "U_JANE"
|
|
145
|
+
- kind: "github"
|
|
146
|
+
externalId: "janedoe"
|
|
147
|
+
→ adds GitHub identity (emit identity_added). Slack identity unchanged.
|
|
111
148
|
```
|
|
112
149
|
|
|
113
150
|
## Important Notes
|
|
114
151
|
|
|
115
152
|
- `manage-user` is **lead-only** — workers cannot use it for any action (the lead check happens before action dispatch). Workers must use `resolve-user` for lookups.
|
|
116
|
-
-
|
|
153
|
+
- The `(kind, externalId)` PK on `user_external_ids` means the same identifier cannot be linked to two different users — a re-link to a different user surfaces as a PK collision (the operator can investigate via the People page merge flow).
|
|
117
154
|
- Deleting a user clears `requestedByUserId` on all their associated tasks (sets to null).
|
|
118
|
-
- Email aliases are case-insensitive for resolution.
|
|
119
|
-
- The `preferredChannel` field defaults to `"slack"` and can be `"slack"`, `"email"`, `"github"`,
|
|
155
|
+
- Email aliases are case-insensitive for resolution. Editing them via `manage-user update` emits `email_added` / `email_removed` events per delta.
|
|
156
|
+
- The `preferredChannel` field defaults to `"slack"` and can be `"slack"`, `"email"`, `"github"`, `"gitlab"`, or any custom string.
|
|
157
|
+
- `dailyBudgetUsd` is `null` = unlimited.
|
|
158
|
+
- `status` lifecycle: `invited` → `active` → `suspended`. The CHECK constraint rejects other values.
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
getAgentById,
|
|
4
4
|
getAgentMailInboxMapping,
|
|
5
5
|
getAllAgents,
|
|
6
|
-
resolveUser,
|
|
7
6
|
} from "../be/db";
|
|
7
|
+
import { findOrCreateUserByEmail } from "../be/users";
|
|
8
8
|
import { resolveTemplate } from "../prompts/resolver";
|
|
9
9
|
import { createIngressBuffer } from "../tasks/additive-ingress";
|
|
10
10
|
import { agentmailContextKey } from "../tasks/context-key";
|
|
@@ -73,6 +73,16 @@ function extractEmailFromField(from: string): string | undefined {
|
|
|
73
73
|
return bareMatch?.[0]?.toLowerCase();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Extract display name from a from_ field like "Taras Yarema <t@desplega.ai>".
|
|
78
|
+
* Returns undefined if no display-name portion is present (bare address).
|
|
79
|
+
*/
|
|
80
|
+
function extractNameFromField(from: string): string | undefined {
|
|
81
|
+
const angleMatch = from.match(/^\s*"?([^"<]+?)"?\s*<[^>]+@[^>]+>\s*$/);
|
|
82
|
+
const name = angleMatch?.[1]?.trim();
|
|
83
|
+
return name && name.length > 0 ? name : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
/**
|
|
77
87
|
* Check if an inbox domain is allowed by the filter.
|
|
78
88
|
* Returns true if no filter is set or the inbox domain matches.
|
|
@@ -159,9 +169,21 @@ export async function handleMessageReceived(
|
|
|
159
169
|
const subject = message.subject || "(no subject)";
|
|
160
170
|
const body = message.text || message.html || "";
|
|
161
171
|
|
|
162
|
-
// Resolve canonical user from sender email
|
|
172
|
+
// Resolve canonical user from sender email — per Q17.F, email is the
|
|
173
|
+
// primary identifier for inbound email events, so always auto-create when
|
|
174
|
+
// missing. findOrCreateUserByEmail emits identity_added (new row) or
|
|
175
|
+
// auto_merge (existing row matched via primary email or emailAliases).
|
|
163
176
|
const senderEmail = extractEmailFromField(from);
|
|
164
|
-
const
|
|
177
|
+
const senderName = extractNameFromField(from);
|
|
178
|
+
let requestedByUserId: string | undefined;
|
|
179
|
+
if (senderEmail) {
|
|
180
|
+
const { user } = findOrCreateUserByEmail(
|
|
181
|
+
senderEmail,
|
|
182
|
+
{ name: senderName ?? undefined },
|
|
183
|
+
{ kind: "system", id: "webhook:agentmail" },
|
|
184
|
+
);
|
|
185
|
+
requestedByUserId = user.id;
|
|
186
|
+
}
|
|
165
187
|
const preview = body.length > 500 ? `${body.substring(0, 500)}...` : body;
|
|
166
188
|
|
|
167
189
|
// Emit workflow trigger event
|