@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2
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/CHANGELOG.md +36 -0
- package/dist/auth.d.ts +2 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +401 -314
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/connection.js +1 -1
- package/dist/capabilities/intake.js +1 -1
- package/dist/capabilities/version-control.js +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +441 -39455
- package/dist/factory.js.map +1 -1
- package/dist/index.js +2 -39466
- package/dist/integrations/base.js +1 -1
- package/dist/integrations/github/config.js +36 -22
- package/dist/integrations/github/config.js.map +1 -1
- package/dist/integrations/github/factory-session.js +33 -37
- package/dist/integrations/github/factory-session.js.map +1 -1
- package/dist/integrations/github/integration.js +1037 -3624
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-triage.js +71 -71
- package/dist/integrations/github/issue-triage.js.map +1 -1
- package/dist/integrations/github/pat.js +47 -36
- package/dist/integrations/github/pat.js.map +1 -1
- package/dist/integrations/github/provenance.js +72 -101
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +1272 -1833
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +203 -471
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +2 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +401 -326
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +165 -223
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.js +43 -51
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.js +15 -18
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/github/webhook.js +359 -343
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/agent-tools.js +77 -100
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js +611 -1309
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.js +227 -230
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.js +126 -346
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/linear/storage.js +1 -1
- package/dist/integrations/platform/api-client.js +131 -135
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +275 -573
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +845 -3942
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js +457 -1309
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/workos/integration.js +80 -85
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +933 -915
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/custom-provider-source.js +110 -93
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.js +401 -349
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +165 -165
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +447 -25166
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +437 -376
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.js +76 -58
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/route.js +12 -8
- package/dist/routes/route.js.map +1 -1
- package/dist/routes/skills.js +160 -173
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/surface.js +250 -28618
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js +141 -166
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js +654 -862
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +23 -35
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.js +200 -309
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +438 -677
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.js +5 -599
- package/dist/rules/processor.js +344 -596
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.js +21 -18
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.js +168 -157
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +50 -85
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js +183 -454
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.js +33 -29
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.js +266 -281
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/fleet.js +272 -250
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/sandbox/reattach.js +13 -6
- package/dist/sandbox/reattach.js.map +1 -1
- package/dist/server-error.js +12 -16
- package/dist/server-error.js.map +1 -1
- package/dist/skills/service.js +30 -35
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.d.ts.map +1 -1
- package/dist/spa-static.js +91 -64
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +73 -44
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +60 -59
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js +169 -112
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +155 -153
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/credentials/base.js +259 -243
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +138 -143
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js +75 -57
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js +310 -245
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js +117 -102
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.js +89 -88
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js +97 -92
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js +111 -76
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +9 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +777 -702
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +322 -344
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +1266 -1132
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js +202 -184
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/timing.d.ts +15 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +34 -0
- package/dist/timing.js.map +1 -0
- package/dist/workspace.d.ts +2 -3
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +236 -438
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-review/SKILL.md +9 -2
- package/package.json +9 -9
- package/dist/capabilities/connection.js.map +0 -1
- package/dist/capabilities/intake.js.map +0 -1
- package/dist/capabilities/version-control.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/base.js.map +0 -1
- package/dist/integrations/linear/storage.js.map +0 -1
- package/dist/rules/index.js.map +0 -1
|
@@ -1,501 +1,233 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// src/rules/types.ts
|
|
7
|
-
var FACTORY_RULE_STAGES = ["intake", "triage", "planning", "execute", "review", "done", "canceled"];
|
|
8
|
-
var FACTORY_RULE_BOARDS = ["work", "review"];
|
|
9
|
-
|
|
10
|
-
// src/rules/validation.ts
|
|
11
|
-
var MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;
|
|
12
|
-
var MAX_IDEMPOTENCY_KEY_LENGTH = 256;
|
|
13
|
-
var MAX_REASON_LENGTH = 512;
|
|
14
|
-
var MAX_TITLE_LENGTH = 512;
|
|
15
|
-
var MAX_MESSAGE_LENGTH = 8192;
|
|
16
|
-
var MAX_ARGUMENTS_LENGTH = 4096;
|
|
17
|
-
var MAX_ROLE_LENGTH = 32;
|
|
18
|
-
var MAX_SKILL_NAME_LENGTH = 128;
|
|
19
|
-
var MAX_SOURCE_KEY_LENGTH = 256;
|
|
20
|
-
var MAX_URL_LENGTH = 2048;
|
|
21
|
-
var MAX_METADATA_JSON_LENGTH = 16384;
|
|
22
|
-
var MAX_JSON_DEPTH = 8;
|
|
23
|
-
var MAX_JSON_COLLECTION_SIZE = 100;
|
|
24
|
-
var IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
25
|
-
var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
26
|
-
var SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;
|
|
27
|
-
var WORK_ITEM_SOURCES = ["github-issue", "github-pr", "linear-issue", "manual"];
|
|
28
|
-
var REJECTION_CODES = [
|
|
29
|
-
"forbidden",
|
|
30
|
-
"invalid_transition",
|
|
31
|
-
"missing_binding",
|
|
32
|
-
"stale",
|
|
33
|
-
"timeout",
|
|
34
|
-
"rule_error",
|
|
35
|
-
"causal_depth_exceeded",
|
|
36
|
-
"repeated_transition"
|
|
37
|
-
];
|
|
38
|
-
var FactoryRuleValidationError = class extends Error {
|
|
39
|
-
code = "invalid_factory_rule";
|
|
40
|
-
constructor(message) {
|
|
41
|
-
super(message);
|
|
42
|
-
this.name = "FactoryRuleValidationError";
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
function isPlainObject(value) {
|
|
46
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
47
|
-
const prototype = Object.getPrototypeOf(value);
|
|
48
|
-
return prototype === Object.prototype || prototype === null;
|
|
49
|
-
}
|
|
50
|
-
function assertExactKeys(value, keys, label) {
|
|
51
|
-
const allowed = new Set(keys);
|
|
52
|
-
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
|
53
|
-
throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function boundedString(value, label, max, pattern) {
|
|
57
|
-
if (typeof value !== "string") throw new FactoryRuleValidationError(`${label} must be a string.`);
|
|
58
|
-
const normalized = value.trim();
|
|
59
|
-
if (normalized.length === 0 || normalized.length > max || pattern && !pattern.test(normalized)) {
|
|
60
|
-
throw new FactoryRuleValidationError(`${label} is invalid.`);
|
|
61
|
-
}
|
|
62
|
-
return normalized;
|
|
63
|
-
}
|
|
64
|
-
function optionalBoundedString(value, label, max) {
|
|
65
|
-
if (value === void 0) return void 0;
|
|
66
|
-
return boundedString(value, label, max);
|
|
67
|
-
}
|
|
68
|
-
function enumValue(value, allowed, label) {
|
|
69
|
-
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
70
|
-
throw new FactoryRuleValidationError(`${label} is invalid.`);
|
|
71
|
-
}
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
function normalizeFactoryRuleJsonValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
75
|
-
if (value === null || typeof value === "boolean" || typeof value === "string") return value;
|
|
76
|
-
if (typeof value === "number") {
|
|
77
|
-
if (!Number.isFinite(value)) throw new FactoryRuleValidationError("Rule metadata must contain finite numbers.");
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
if (depth >= MAX_JSON_DEPTH || typeof value !== "object" && !Array.isArray(value)) {
|
|
81
|
-
throw new FactoryRuleValidationError("Rule metadata is not bounded JSON.");
|
|
82
|
-
}
|
|
83
|
-
if (seen.has(value)) throw new FactoryRuleValidationError("Rule metadata must not contain cycles.");
|
|
84
|
-
seen.add(value);
|
|
85
|
-
try {
|
|
86
|
-
if (Array.isArray(value)) {
|
|
87
|
-
if (value.length > MAX_JSON_COLLECTION_SIZE) {
|
|
88
|
-
throw new FactoryRuleValidationError("Rule metadata contains too many entries.");
|
|
89
|
-
}
|
|
90
|
-
return value.map((entry) => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));
|
|
91
|
-
}
|
|
92
|
-
if (!isPlainObject(value)) throw new FactoryRuleValidationError("Rule metadata must use plain objects.");
|
|
93
|
-
const entries = Object.entries(value);
|
|
94
|
-
if (entries.length > MAX_JSON_COLLECTION_SIZE) {
|
|
95
|
-
throw new FactoryRuleValidationError("Rule metadata contains too many fields.");
|
|
96
|
-
}
|
|
97
|
-
const sanitized = {};
|
|
98
|
-
for (const [key, entry] of entries) {
|
|
99
|
-
const normalizedKey = boundedString(key, "Rule metadata key", 128, IDENTIFIER_RE);
|
|
100
|
-
sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey) ? "[REDACTED]" : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
|
|
101
|
-
}
|
|
102
|
-
return sanitized;
|
|
103
|
-
} finally {
|
|
104
|
-
seen.delete(value);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function sanitizeMetadata(value) {
|
|
108
|
-
if (value === void 0) return void 0;
|
|
109
|
-
const sanitized = normalizeFactoryRuleJsonValue(value);
|
|
110
|
-
if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError("Rule metadata must be an object.");
|
|
111
|
-
if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
|
|
112
|
-
throw new FactoryRuleValidationError("Rule metadata is too large.");
|
|
113
|
-
}
|
|
114
|
-
return sanitized;
|
|
115
|
-
}
|
|
116
|
-
function commonCommitFields(value) {
|
|
117
|
-
return {
|
|
118
|
-
idempotencyKey: boundedString(value.idempotencyKey, "Factory decision idempotencyKey", MAX_IDEMPOTENCY_KEY_LENGTH)
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
function validateFactoryRuleDecision(value, causalDepth = 0) {
|
|
122
|
-
if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {
|
|
123
|
-
throw new FactoryRuleValidationError("Factory rule causal depth exceeded.");
|
|
124
|
-
}
|
|
125
|
-
if (!isPlainObject(value)) throw new FactoryRuleValidationError("Factory rule decision must be an object.");
|
|
126
|
-
const type = value.type;
|
|
127
|
-
if (typeof type !== "string") throw new FactoryRuleValidationError("Factory rule decision type is required.");
|
|
128
|
-
switch (type) {
|
|
129
|
-
case "reject": {
|
|
130
|
-
assertExactKeys(value, ["type", "code", "reason"], "Factory reject decision");
|
|
131
|
-
return {
|
|
132
|
-
type,
|
|
133
|
-
code: enumValue(value.code, REJECTION_CODES, "Factory rejection code"),
|
|
134
|
-
reason: boundedString(value.reason, "Factory rejection reason", MAX_REASON_LENGTH)
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
case "transition": {
|
|
138
|
-
assertExactKeys(value, ["type", "idempotencyKey", "board", "stage"], "Factory transition decision");
|
|
139
|
-
return {
|
|
140
|
-
type,
|
|
141
|
-
...commonCommitFields(value),
|
|
142
|
-
board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory transition board"),
|
|
143
|
-
stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory transition stage")
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
case "upsertLinkedWorkItem": {
|
|
147
|
-
assertExactKeys(
|
|
148
|
-
value,
|
|
149
|
-
["type", "idempotencyKey", "board", "source", "sourceKey", "title", "url", "stage", "metadata"],
|
|
150
|
-
"Factory linked work item decision"
|
|
151
|
-
);
|
|
152
|
-
const url = value.url;
|
|
153
|
-
if (url !== null && (typeof url !== "string" || url.length > MAX_URL_LENGTH || !/^https?:\/\//.test(url))) {
|
|
154
|
-
throw new FactoryRuleValidationError("Factory linked work item URL is invalid.");
|
|
155
|
-
}
|
|
156
|
-
const metadata = sanitizeMetadata(value.metadata);
|
|
157
|
-
return {
|
|
158
|
-
type,
|
|
159
|
-
...commonCommitFields(value),
|
|
160
|
-
board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory linked work item board"),
|
|
161
|
-
source: enumValue(value.source, WORK_ITEM_SOURCES, "Factory linked work item source"),
|
|
162
|
-
sourceKey: boundedString(value.sourceKey, "Factory linked work item sourceKey", MAX_SOURCE_KEY_LENGTH),
|
|
163
|
-
title: boundedString(value.title, "Factory linked work item title", MAX_TITLE_LENGTH),
|
|
164
|
-
url,
|
|
165
|
-
stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory linked work item stage"),
|
|
166
|
-
...metadata ? { metadata } : {}
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
case "invokeSkill": {
|
|
170
|
-
assertExactKeys(
|
|
171
|
-
value,
|
|
172
|
-
["type", "idempotencyKey", "role", "skillName", "arguments", "precedingMessage"],
|
|
173
|
-
"Factory invoke skill decision"
|
|
174
|
-
);
|
|
175
|
-
const args = optionalBoundedString(value.arguments, "Factory skill arguments", MAX_ARGUMENTS_LENGTH);
|
|
176
|
-
const precedingMessage = optionalBoundedString(
|
|
177
|
-
value.precedingMessage,
|
|
178
|
-
"Factory skill preceding message",
|
|
179
|
-
MAX_MESSAGE_LENGTH
|
|
180
|
-
);
|
|
181
|
-
return {
|
|
182
|
-
type,
|
|
183
|
-
...commonCommitFields(value),
|
|
184
|
-
role: boundedString(value.role, "Factory skill role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
|
|
185
|
-
skillName: boundedString(value.skillName, "Factory skill name", MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE),
|
|
186
|
-
...args ? { arguments: args } : {},
|
|
187
|
-
...precedingMessage ? { precedingMessage } : {}
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
case "sendMessage": {
|
|
191
|
-
assertExactKeys(
|
|
192
|
-
value,
|
|
193
|
-
["type", "idempotencyKey", "role", "message", "priority", "idleBehavior", "prepareBinding"],
|
|
194
|
-
"Factory send message decision"
|
|
195
|
-
);
|
|
196
|
-
const priority = value.priority === void 0 ? void 0 : enumValue(value.priority, ["medium", "high", "urgent"], "Factory message priority");
|
|
197
|
-
const idleBehavior = value.idleBehavior === void 0 ? void 0 : enumValue(value.idleBehavior, ["persist", "wake"], "Factory message idle behavior");
|
|
198
|
-
if (value.prepareBinding !== void 0 && typeof value.prepareBinding !== "boolean") {
|
|
199
|
-
throw new FactoryRuleValidationError("Factory message prepareBinding must be a boolean.");
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
type,
|
|
203
|
-
...commonCommitFields(value),
|
|
204
|
-
role: boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
|
|
205
|
-
message: boundedString(value.message, "Factory message", MAX_MESSAGE_LENGTH),
|
|
206
|
-
...priority ? { priority } : {},
|
|
207
|
-
...idleBehavior ? { idleBehavior } : {},
|
|
208
|
-
...value.prepareBinding === true ? { prepareBinding: true } : {}
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
case "notify": {
|
|
212
|
-
assertExactKeys(value, ["type", "idempotencyKey", "title", "body", "level"], "Factory notify decision");
|
|
213
|
-
const body = optionalBoundedString(value.body, "Factory notification body", MAX_MESSAGE_LENGTH);
|
|
214
|
-
const level = value.level === void 0 ? void 0 : enumValue(value.level, ["info", "warning", "error"], "Factory notification level");
|
|
215
|
-
return {
|
|
216
|
-
type,
|
|
217
|
-
...commonCommitFields(value),
|
|
218
|
-
title: boundedString(value.title, "Factory notification title", MAX_TITLE_LENGTH),
|
|
219
|
-
...body ? { body } : {},
|
|
220
|
-
...level ? { level } : {}
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
default:
|
|
224
|
-
throw new FactoryRuleValidationError("Factory rule decision type is unsupported.");
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
function validateFactoryRuleDecisions(values, causalDepth = 0) {
|
|
228
|
-
if (values.length > MAX_JSON_COLLECTION_SIZE) {
|
|
229
|
-
throw new FactoryRuleValidationError("Factory rule produced too many decisions.");
|
|
230
|
-
}
|
|
231
|
-
const decisions = [];
|
|
232
|
-
for (const value of values) {
|
|
233
|
-
const decision = validateFactoryRuleDecision(value, causalDepth);
|
|
234
|
-
if (decision.type === "reject") {
|
|
235
|
-
throw new FactoryRuleValidationError("A rejection cannot be persisted with commit decisions.");
|
|
236
|
-
}
|
|
237
|
-
decisions.push(decision);
|
|
238
|
-
}
|
|
239
|
-
const keys = decisions.map((decision) => decision.idempotencyKey);
|
|
240
|
-
if (new Set(keys).size !== keys.length) {
|
|
241
|
-
throw new FactoryRuleValidationError("Factory decisions require unique idempotency keys.");
|
|
242
|
-
}
|
|
243
|
-
return decisions;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// src/integrations/github/rules.ts
|
|
247
|
-
var TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["write", "admin"]);
|
|
248
|
-
var RULE_TIMEOUT_MS = 5e3;
|
|
1
|
+
import { resolveFactoryGithubRule } from "../../rules/resolve.js";
|
|
2
|
+
import { validateFactoryRuleDecisions } from "../../rules/validation.js";
|
|
3
|
+
//#region src/integrations/github/rules.ts
|
|
4
|
+
const TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["write", "admin"]);
|
|
5
|
+
const RULE_TIMEOUT_MS = 5e3;
|
|
249
6
|
async function withRuleTimeout(promise) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} finally {
|
|
259
|
-
if (timeout) clearTimeout(timeout);
|
|
260
|
-
}
|
|
7
|
+
let timeout;
|
|
8
|
+
try {
|
|
9
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
10
|
+
timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("FACTORY_RULE_TIMEOUT")), RULE_TIMEOUT_MS);
|
|
11
|
+
})]);
|
|
12
|
+
} finally {
|
|
13
|
+
if (timeout) clearTimeout(timeout);
|
|
14
|
+
}
|
|
261
15
|
}
|
|
262
16
|
function object(value) {
|
|
263
|
-
|
|
17
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
264
18
|
}
|
|
265
19
|
function string(value) {
|
|
266
|
-
|
|
20
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
267
21
|
}
|
|
268
22
|
function number(value) {
|
|
269
|
-
|
|
23
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
270
24
|
}
|
|
271
25
|
function boolean(value) {
|
|
272
|
-
|
|
26
|
+
return typeof value === "boolean" ? value : void 0;
|
|
273
27
|
}
|
|
274
28
|
function eventName(parsed) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
if (parsed.event === "pull_request" && action === "review_requested") return "pullRequestReviewRequested";
|
|
283
|
-
return void 0;
|
|
29
|
+
const action = string(parsed.payload.action);
|
|
30
|
+
if (parsed.event === "issues" && action === "opened") return "issueOpened";
|
|
31
|
+
if (parsed.event === "pull_request" && action === "opened") return "pullRequestOpened";
|
|
32
|
+
if (parsed.event === "pull_request" && action === "synchronize") return "pullRequestUpdated";
|
|
33
|
+
if (parsed.event === "pull_request" && action === "closed" && boolean(object(parsed.payload.pull_request)?.merged)) return "pullRequestMerged";
|
|
34
|
+
if (parsed.event === "pull_request" && action === "review_requested") return "pullRequestReviewRequested";
|
|
284
35
|
}
|
|
285
36
|
function canonicalSourceKey(kind, itemNumber) {
|
|
286
|
-
|
|
37
|
+
return kind === "issue" ? `github-issue:${itemNumber}` : `github-pr:${itemNumber}`;
|
|
287
38
|
}
|
|
288
39
|
function legacySourceKey(repositoryId, kind, itemNumber) {
|
|
289
|
-
|
|
40
|
+
return `github:${repositoryId}:${kind}:${itemNumber}`;
|
|
290
41
|
}
|
|
291
42
|
function provenanceTarget(repositoryId, pullRequestNumber) {
|
|
292
|
-
|
|
43
|
+
return `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;
|
|
293
44
|
}
|
|
294
45
|
function workItemSource(item) {
|
|
295
|
-
|
|
296
|
-
|
|
46
|
+
if (!item.externalSource) return "manual";
|
|
47
|
+
return item.externalSource.type === "pull-request" ? "github-pr" : "github-issue";
|
|
297
48
|
}
|
|
298
49
|
function workItemSourceKey(item) {
|
|
299
|
-
|
|
50
|
+
return item.externalSource?.externalId ?? null;
|
|
300
51
|
}
|
|
301
52
|
async function githubActor(github, input) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
53
|
+
let trusted = false;
|
|
54
|
+
try {
|
|
55
|
+
const permission = await github.getRepositoryCollaboratorPermission(input.installationId, input.repository, input.login);
|
|
56
|
+
trusted = permission !== void 0 && TRUSTED_PERMISSIONS.has(permission);
|
|
57
|
+
} catch {
|
|
58
|
+
trusted = false;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
type: "github",
|
|
62
|
+
login: input.login,
|
|
63
|
+
trusted,
|
|
64
|
+
factoryAuthored: input.factoryAuthored
|
|
65
|
+
};
|
|
314
66
|
}
|
|
315
67
|
function pullRequestProvenance(data) {
|
|
316
|
-
|
|
317
|
-
|
|
68
|
+
if (!data || data.kind !== "factory-pr-provenance" || typeof data.workItemId !== "string") return null;
|
|
69
|
+
return {
|
|
70
|
+
kind: "factory-pr-provenance",
|
|
71
|
+
workItemId: data.workItemId
|
|
72
|
+
};
|
|
318
73
|
}
|
|
319
74
|
var GithubRules = class {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
return { status: committed.status };
|
|
463
|
-
}
|
|
464
|
-
async #relatedItem(orgId, projectId, repositoryId, issueNumber, pullRequestNumber, pullRequestHeadBranch, provenance) {
|
|
465
|
-
const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });
|
|
466
|
-
if (provenance) return items.find((item) => item.id === provenance.workItemId);
|
|
467
|
-
if (issueNumber) {
|
|
468
|
-
return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("issue", issueNumber)) ?? items.find((item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "issue", issueNumber));
|
|
469
|
-
}
|
|
470
|
-
if (pullRequestNumber) {
|
|
471
|
-
return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("pull-request", pullRequestNumber)) ?? items.find(
|
|
472
|
-
(item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "pull-request", pullRequestNumber)
|
|
473
|
-
) ?? // Provenance fallback: a PR pushed from a work item's session branch
|
|
474
|
-
// belongs to that item even when no gh-pr-create provenance was
|
|
475
|
-
// recorded (session predating state seeding, or the PR was opened
|
|
476
|
-
// outside the tracked tool call). Session branches are per-item
|
|
477
|
-
// (`factory/issue-N`), so a head-branch match is unambiguous.
|
|
478
|
-
(pullRequestHeadBranch ? items.find(
|
|
479
|
-
(item) => item.externalSource?.type !== "pull-request" && Object.values(item.sessions).some((session) => session.branch === pullRequestHeadBranch)
|
|
480
|
-
) : void 0);
|
|
481
|
-
}
|
|
482
|
-
return void 0;
|
|
483
|
-
}
|
|
75
|
+
options;
|
|
76
|
+
constructor(options) {
|
|
77
|
+
this.options = options;
|
|
78
|
+
}
|
|
79
|
+
async ingest(parsed) {
|
|
80
|
+
const event = eventName(parsed);
|
|
81
|
+
const repository = object(parsed.payload.repository);
|
|
82
|
+
const installationId = number(object(parsed.payload.installation)?.id);
|
|
83
|
+
const repositoryId = number(repository?.id);
|
|
84
|
+
const repositoryName = string(repository?.full_name);
|
|
85
|
+
const login = string(object(parsed.payload.sender)?.login);
|
|
86
|
+
if (!event || !installationId || !repositoryId || !repositoryName || !login) return { status: "ignored" };
|
|
87
|
+
const projects = await this.options.sourceControl.projectRepositories.listByExternalRepository({
|
|
88
|
+
installationExternalId: String(installationId),
|
|
89
|
+
repositoryExternalId: String(repositoryId)
|
|
90
|
+
});
|
|
91
|
+
if (projects.length === 0) return { status: "ignored" };
|
|
92
|
+
const results = [];
|
|
93
|
+
for (const project of projects) results.push(await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project));
|
|
94
|
+
if (results.some((result) => result.status === "committed")) return { status: "committed" };
|
|
95
|
+
if (results.some((result) => result.status === "replayed")) return { status: "replayed" };
|
|
96
|
+
return results[0] ?? { status: "ignored" };
|
|
97
|
+
}
|
|
98
|
+
async #ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project) {
|
|
99
|
+
const factoryProject = await this.options.projects.get({
|
|
100
|
+
orgId: project.orgId,
|
|
101
|
+
id: project.factoryProjectId
|
|
102
|
+
});
|
|
103
|
+
if (!factoryProject) return { status: "missing" };
|
|
104
|
+
const issue = object(parsed.payload.issue);
|
|
105
|
+
const pullRequest = object(parsed.payload.pull_request);
|
|
106
|
+
const issueNumber = number(issue?.number);
|
|
107
|
+
const pullRequestNumber = number(pullRequest?.number);
|
|
108
|
+
const provenance = pullRequestNumber ? pullRequestProvenance((await this.options.integrationStorage.subscriptions.listByTarget(provenanceTarget(repositoryId, pullRequestNumber), { status: "active" })).find((subscription) => subscription.orgId === project.orgId)?.data) : null;
|
|
109
|
+
const relatedItem = await this.#relatedItem(project.orgId, project.factoryProjectId, repositoryId, issueNumber, pullRequestNumber, string(object(pullRequest?.head)?.ref), provenance);
|
|
110
|
+
const actor = await githubActor(this.options.github, {
|
|
111
|
+
installationId,
|
|
112
|
+
repository: repositoryName,
|
|
113
|
+
login,
|
|
114
|
+
factoryAuthored: provenance !== null
|
|
115
|
+
});
|
|
116
|
+
const context = {
|
|
117
|
+
tenant: {
|
|
118
|
+
orgId: project.orgId,
|
|
119
|
+
projectId: project.factoryProjectId
|
|
120
|
+
},
|
|
121
|
+
actor,
|
|
122
|
+
ingress: {
|
|
123
|
+
type: "github",
|
|
124
|
+
id: `${installationId}:${parsed.deliveryId}`
|
|
125
|
+
},
|
|
126
|
+
cause: `github.${event}`,
|
|
127
|
+
causalChain: [],
|
|
128
|
+
ruleSetVersion: this.options.rules.version,
|
|
129
|
+
...relatedItem ? {
|
|
130
|
+
item: {
|
|
131
|
+
id: relatedItem.id,
|
|
132
|
+
source: workItemSource(relatedItem),
|
|
133
|
+
sourceKey: workItemSourceKey(relatedItem),
|
|
134
|
+
parentWorkItemId: relatedItem.parentWorkItemId,
|
|
135
|
+
title: relatedItem.title,
|
|
136
|
+
url: relatedItem.externalSource?.url ?? null,
|
|
137
|
+
stages: relatedItem.stages
|
|
138
|
+
},
|
|
139
|
+
board: relatedItem.externalSource?.type === "pull-request" ? "review" : "work",
|
|
140
|
+
itemRevision: relatedItem.revision
|
|
141
|
+
} : {},
|
|
142
|
+
event,
|
|
143
|
+
deliveryId: parsed.deliveryId,
|
|
144
|
+
factory: { createdAt: factoryProject.createdAt.toISOString() },
|
|
145
|
+
repository: {
|
|
146
|
+
id: repositoryId,
|
|
147
|
+
fullName: repositoryName
|
|
148
|
+
},
|
|
149
|
+
...issueNumber && string(issue?.title) && string(issue?.html_url) ? { issue: {
|
|
150
|
+
number: issueNumber,
|
|
151
|
+
title: string(issue?.title),
|
|
152
|
+
url: string(issue?.html_url),
|
|
153
|
+
...string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {}
|
|
154
|
+
} } : {},
|
|
155
|
+
...pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url) ? { pullRequest: {
|
|
156
|
+
number: pullRequestNumber,
|
|
157
|
+
title: string(pullRequest?.title),
|
|
158
|
+
url: string(pullRequest?.html_url),
|
|
159
|
+
...string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {},
|
|
160
|
+
state: string(pullRequest?.state) === "closed" ? "closed" : "open",
|
|
161
|
+
merged: boolean(pullRequest?.merged) ?? false,
|
|
162
|
+
headBranch: string(object(pullRequest?.head)?.ref) ?? "",
|
|
163
|
+
baseBranch: string(object(pullRequest?.base)?.ref) ?? ""
|
|
164
|
+
} } : {},
|
|
165
|
+
...object(parsed.payload.review) ? { review: {
|
|
166
|
+
id: number(object(parsed.payload.review)?.id) ?? 0,
|
|
167
|
+
state: string(object(parsed.payload.review)?.state) ?? "unknown",
|
|
168
|
+
url: string(object(parsed.payload.review)?.html_url) ?? ""
|
|
169
|
+
} } : {}
|
|
170
|
+
};
|
|
171
|
+
const rule = resolveFactoryGithubRule(this.options.rules, event);
|
|
172
|
+
let decision;
|
|
173
|
+
let decisions = [];
|
|
174
|
+
let outcome = { status: "accepted" };
|
|
175
|
+
try {
|
|
176
|
+
decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : void 0;
|
|
177
|
+
if (decision?.type === "reject") outcome = {
|
|
178
|
+
status: "rejected",
|
|
179
|
+
code: decision.code,
|
|
180
|
+
reason: decision.reason
|
|
181
|
+
};
|
|
182
|
+
else if (decision) decisions = validateFactoryRuleDecisions([decision]).map((entry) => ({ ...entry }));
|
|
183
|
+
} catch (error) {
|
|
184
|
+
const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
|
|
185
|
+
outcome = {
|
|
186
|
+
status: "rejected",
|
|
187
|
+
code: timedOut ? "timeout" : "rule_error",
|
|
188
|
+
reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory GitHub rule failed."
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return { status: (await this.options.storage.commitRuleEvaluation({
|
|
192
|
+
orgId: project.orgId,
|
|
193
|
+
factoryProjectId: project.factoryProjectId,
|
|
194
|
+
workItemId: relatedItem?.id ?? null,
|
|
195
|
+
ingress: {
|
|
196
|
+
identity: `${installationId}:${parsed.deliveryId}`,
|
|
197
|
+
triggerType: `github.${event}`
|
|
198
|
+
},
|
|
199
|
+
ruleSetVersion: this.options.rules.version,
|
|
200
|
+
expectedRevision: relatedItem?.revision ?? null,
|
|
201
|
+
actor: { ...actor },
|
|
202
|
+
outcome,
|
|
203
|
+
decisions,
|
|
204
|
+
causalChain: [],
|
|
205
|
+
now: /* @__PURE__ */ new Date()
|
|
206
|
+
})).status };
|
|
207
|
+
}
|
|
208
|
+
async #relatedItem(orgId, projectId, repositoryId, issueNumber, pullRequestNumber, pullRequestHeadBranch, provenance) {
|
|
209
|
+
const items = await this.options.storage.list({
|
|
210
|
+
orgId,
|
|
211
|
+
factoryProjectId: projectId
|
|
212
|
+
});
|
|
213
|
+
if (provenance) return items.find((item) => item.id === provenance.workItemId);
|
|
214
|
+
if (issueNumber) return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("issue", issueNumber)) ?? items.find((item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "issue", issueNumber));
|
|
215
|
+
if (pullRequestNumber) return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("pull-request", pullRequestNumber)) ?? items.find((item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "pull-request", pullRequestNumber)) ?? (pullRequestHeadBranch ? items.find((item) => item.externalSource?.type !== "pull-request" && Object.values(item.sessions).some((session) => session.branch === pullRequestHeadBranch)) : void 0);
|
|
216
|
+
}
|
|
484
217
|
};
|
|
485
218
|
function attachGithubRules(github, context) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
};
|
|
219
|
+
if (!context.rules) return void 0;
|
|
220
|
+
const rules = new GithubRules({
|
|
221
|
+
github,
|
|
222
|
+
sourceControl: context.storage.sourceControl,
|
|
223
|
+
integrationStorage: context.storage.generic,
|
|
224
|
+
projects: context.storage.projects,
|
|
225
|
+
storage: context.rules.workItems,
|
|
226
|
+
rules: context.rules.config
|
|
227
|
+
});
|
|
228
|
+
return (event) => rules.ingest(event);
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
export { GithubRules, attachGithubRules };
|
|
232
|
+
|
|
501
233
|
//# sourceMappingURL=rules.js.map
|