@desplega.ai/agent-swarm 1.80.2 → 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/README.md +3 -0
- 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/agentmail/types.ts +1 -0
- 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/http/webhooks.ts +9 -0
- package/src/http/workflows.ts +2 -15
- 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-api-integration.test.ts +8 -4
- 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/tests/workflow-triggers-v2.test.ts +261 -20
- 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
- package/src/workflows/input.ts +7 -2
- package/src/workflows/triggers.ts +89 -9
package/src/gitlab/handlers.ts
CHANGED
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* - Detects bot mentions
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { failTask, findTaskByVcs, getAllAgents,
|
|
11
|
+
import { failTask, findTaskByVcs, getAllAgents, incrKv, upsertKv } from "../be/db";
|
|
12
|
+
import { findOrCreateUserByEmail, findUserByExternalId, linkIdentity } from "../be/users";
|
|
12
13
|
import { resolveTemplate } from "../prompts/resolver";
|
|
13
14
|
import { gitlabContextKey } from "../tasks/context-key";
|
|
14
15
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
15
16
|
import { GITLAB_BOT_NAME } from "./auth";
|
|
16
17
|
import { addGitLabNoteReaction, addGitLabReaction } from "./reactions";
|
|
18
|
+
import type { GitLabUser } from "./types";
|
|
17
19
|
// Side-effect import: registers all GitLab event templates in the in-memory registry
|
|
18
20
|
import "./templates";
|
|
19
21
|
import type { IssueEvent, MergeRequestEvent, NoteEvent, PipelineEvent } from "./types";
|
|
@@ -53,6 +55,62 @@ function findLeadAgent() {
|
|
|
53
55
|
);
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
// ── Identity resolution ──
|
|
59
|
+
|
|
60
|
+
const UNMAPPED_NAMESPACE = "integration:unmapped:gitlab";
|
|
61
|
+
const UNMAPPED_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
62
|
+
const GITLAB_WEBHOOK_ACTOR = { kind: "system", id: "webhook:gitlab" } as const;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a GitLab webhook sender to a `users.id`.
|
|
66
|
+
*
|
|
67
|
+
* Cascade (Q17):
|
|
68
|
+
* 1. Fast path — `findUserByExternalId('gitlab', user.username)`.
|
|
69
|
+
* 2. If missing AND `user.email` is a real non-empty string (some GitLab
|
|
70
|
+
* installations send `email: ""` instead of omitting the field — Q17.E
|
|
71
|
+
* manual spot-check), run `findOrCreateUserByEmail` + `linkIdentity`.
|
|
72
|
+
* 3. Otherwise record an unmapped tracker entry (kv) for operator triage.
|
|
73
|
+
*
|
|
74
|
+
* Returns `undefined` when no mapping could be established — callers pass
|
|
75
|
+
* that straight to `requestedByUserId`.
|
|
76
|
+
*/
|
|
77
|
+
function resolveGitLabSender(
|
|
78
|
+
user: GitLabUser,
|
|
79
|
+
sampleEventType: string,
|
|
80
|
+
sampleContext: string,
|
|
81
|
+
): string | undefined {
|
|
82
|
+
const existing = findUserByExternalId("gitlab", user.username);
|
|
83
|
+
if (existing) return existing.id;
|
|
84
|
+
|
|
85
|
+
// Inline-email cascade — only run when email is a real non-empty string.
|
|
86
|
+
// Some GitLab installations emit `email: ""` instead of omitting the field.
|
|
87
|
+
const inlineEmail = typeof user.email === "string" ? user.email.trim() : "";
|
|
88
|
+
if (inlineEmail !== "") {
|
|
89
|
+
const { user: linked } = findOrCreateUserByEmail(
|
|
90
|
+
inlineEmail,
|
|
91
|
+
{ name: user.name },
|
|
92
|
+
GITLAB_WEBHOOK_ACTOR,
|
|
93
|
+
);
|
|
94
|
+
linkIdentity(linked.id, "gitlab", user.username, GITLAB_WEBHOOK_ACTOR);
|
|
95
|
+
return linked.id;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// No mapping + no inline email → unmapped tracker.
|
|
99
|
+
upsertKv({
|
|
100
|
+
namespace: UNMAPPED_NAMESPACE,
|
|
101
|
+
key: `${user.username}:meta`,
|
|
102
|
+
value: {
|
|
103
|
+
lastSeenAt: new Date().toISOString(),
|
|
104
|
+
sampleEventType,
|
|
105
|
+
sampleContext: sampleContext.slice(0, 100),
|
|
106
|
+
},
|
|
107
|
+
valueType: "json",
|
|
108
|
+
expiresAt: Date.now() + UNMAPPED_TTL_MS,
|
|
109
|
+
});
|
|
110
|
+
incrKv(UNMAPPED_NAMESPACE, `${user.username}:count`, 1);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
56
114
|
// ── Event Handlers ──
|
|
57
115
|
|
|
58
116
|
export async function handleMergeRequest(
|
|
@@ -63,7 +121,11 @@ export async function handleMergeRequest(
|
|
|
63
121
|
const repo = project.path_with_namespace;
|
|
64
122
|
|
|
65
123
|
// Resolve canonical user from GitLab sender
|
|
66
|
-
const requestedByUserId =
|
|
124
|
+
const requestedByUserId = resolveGitLabSender(
|
|
125
|
+
user,
|
|
126
|
+
"merge_request",
|
|
127
|
+
`MR !${mr.iid}: ${mr.title}`,
|
|
128
|
+
);
|
|
67
129
|
|
|
68
130
|
console.log(`[GitLab] MR #${mr.iid} ${action} by ${user.username} in ${repo}`);
|
|
69
131
|
|
|
@@ -163,7 +225,11 @@ export async function handleIssue(
|
|
|
163
225
|
const repo = project.path_with_namespace;
|
|
164
226
|
|
|
165
227
|
// Resolve canonical user from GitLab sender
|
|
166
|
-
const requestedByUserId =
|
|
228
|
+
const requestedByUserId = resolveGitLabSender(
|
|
229
|
+
user,
|
|
230
|
+
"issue",
|
|
231
|
+
`Issue #${issue.iid}: ${issue.title}`,
|
|
232
|
+
);
|
|
167
233
|
|
|
168
234
|
console.log(`[GitLab] Issue #${issue.iid} ${action} by ${user.username} in ${repo}`);
|
|
169
235
|
|
|
@@ -246,8 +312,10 @@ export async function handleNote(event: NoteEvent): Promise<{ created: boolean;
|
|
|
246
312
|
const { user, project, object_attributes: note } = event;
|
|
247
313
|
const repo = project.path_with_namespace;
|
|
248
314
|
|
|
249
|
-
// Resolve canonical user from GitLab sender
|
|
250
|
-
|
|
315
|
+
// Resolve canonical user from GitLab sender — currently dead-coded
|
|
316
|
+
// (underscore prefix). Rewired for parity with live sites; if a future
|
|
317
|
+
// change uses the value, the resolution path is already correct.
|
|
318
|
+
const _requestedByUserId = resolveGitLabSender(user, "note", note.note);
|
|
251
319
|
|
|
252
320
|
// Only handle comments with bot mentions
|
|
253
321
|
if (!detectMention(note.note)) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator-auth middleware producing the `op:<sha256(rawKey)[:16]>` fingerprint
|
|
3
|
+
* (Q16) that is embedded as `actor` in `user_identity_events` rows.
|
|
4
|
+
*
|
|
5
|
+
* The bearer-key check already gates every `route({ auth: { apiKey: true } })`
|
|
6
|
+
* route via `src/http/core.ts::handleCore` — so by the time we reach the route
|
|
7
|
+
* handler we know the caller's `Authorization: Bearer <key>` matches the
|
|
8
|
+
* configured swarm key. This helper just re-reads that key, hashes it with
|
|
9
|
+
* `fingerprintApiKey`, and packages it as an `IdentityActor` for the identity
|
|
10
|
+
* mutation helpers in `src/be/users.ts`.
|
|
11
|
+
*
|
|
12
|
+
* MUST read the swarm key via `getApiKey()` from `src/utils/api-key.ts` —
|
|
13
|
+
* direct `process.env.{API_KEY,AGENT_SWARM_API_KEY}` reads are rejected by
|
|
14
|
+
* `scripts/check-api-key-boundary.sh` (CI).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
18
|
+
import { fingerprintApiKey, type IdentityActor } from "../be/users";
|
|
19
|
+
import { getApiKey } from "../utils/api-key";
|
|
20
|
+
import { jsonError } from "./utils";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract the raw bearer key from the request. Returns null if the header is
|
|
24
|
+
* missing or malformed.
|
|
25
|
+
*/
|
|
26
|
+
function extractBearer(req: IncomingMessage): string | null {
|
|
27
|
+
const raw = req.headers.authorization;
|
|
28
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
29
|
+
if (!header || !header.startsWith("Bearer ")) return null;
|
|
30
|
+
return header.slice(7);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the calling operator's `IdentityActor`. Assumes `handleCore` already
|
|
35
|
+
* 401'd unauthenticated requests; on the off chance the header is missing here
|
|
36
|
+
* (e.g. a route mistakenly opted out of api-key gating), we respond with 401
|
|
37
|
+
* and return null so the caller can short-circuit.
|
|
38
|
+
*
|
|
39
|
+
* Returns null after writing a 401 response. The caller MUST stop processing
|
|
40
|
+
* the request when this returns null.
|
|
41
|
+
*/
|
|
42
|
+
export function getOperatorActor(req: IncomingMessage, res: ServerResponse): IdentityActor | null {
|
|
43
|
+
const rawKey = extractBearer(req);
|
|
44
|
+
const swarmKey = getApiKey();
|
|
45
|
+
|
|
46
|
+
// If no key is configured server-side, the public-route guard already
|
|
47
|
+
// skipped the bearer check. We still need *some* fingerprint for the audit
|
|
48
|
+
// event — fall back to a stable placeholder so callers see consistent shapes.
|
|
49
|
+
if (!swarmKey) {
|
|
50
|
+
return { kind: "operator", id: fingerprintApiKey("") };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!rawKey || rawKey !== swarmKey) {
|
|
54
|
+
jsonError(res, "Unauthorized", 401);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { kind: "operator", id: fingerprintApiKey(rawKey) };
|
|
59
|
+
}
|