Skip to content

Commit d8b0738

Browse files
authored
Merge branch 'main' into image-registry-overrides
2 parents f4f3434 + d7028e2 commit d8b0738

23 files changed

Lines changed: 2130 additions & 733 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
`trigger skills` installs Trigger.dev agent skills into your coding agent so it knows how to write tasks, schedules, realtime, and chat.agent code. The skills ship with the CLI and are copied into each tool's native skills directory (Claude Code, Cursor, GitHub Copilot, and Codex / AGENTS.md), and `trigger dev` offers to install them on first run.
6+
7+
```bash
8+
trigger skills --target claude-code
9+
```
10+
11+
Replaces the previous `install-rules` command, which stays as an alias.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: supervisor
3+
type: feature
4+
---
5+
6+
Forward per-run identity labels to the compute provider on create and restore, letting network policy select runs (e.g. private link).
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Add bounded `enrolled` and `org` labels to the `mollifier.decisions` metric so per-enrolled-org pass-through vs mollify is visible (the `org` label is attached only for the enrolled cohort to keep cardinality bounded).

apps/supervisor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ class ManagedSupervisor {
376376
envId: message.environment.id,
377377
orgId: message.organization.id,
378378
projectId: message.project.id,
379+
hasPrivateLink: message.organization.hasPrivateLink,
379380
dequeuedAt: message.dequeuedAt,
380381
});
381382
recordPhaseSince("restore", restoreStart, undefined);

apps/supervisor/src/workloadManager/compute.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ export class ComputeWorkloadManager implements WorkloadManager {
133133
// Strip image digest - resolve by tag, not digest
134134
const imageRef = stripImageDigest(opts.image);
135135

136+
// Labels forwarded to the compute provider for network-policy selection;
137+
// the provider promotes a configured subset to its network layer. Mirrors
138+
// the privatelink label the Kubernetes workload manager sets on the run pod.
139+
const labels: Record<string, string> = {};
140+
if (opts.hasPrivateLink) {
141+
labels.privatelink = opts.orgId;
142+
}
143+
136144
// Wide event: single canonical log line emitted in finally
137145
const event: Record<string, unknown> = {
138146
// High-cardinality identifiers
@@ -173,6 +181,7 @@ export class ComputeWorkloadManager implements WorkloadManager {
173181
deploymentVersion: opts.deploymentVersion,
174182
machine: opts.machine.name,
175183
},
184+
...(Object.keys(labels).length > 0 ? { labels } : {}),
176185
})
177186
);
178187

@@ -297,6 +306,7 @@ export class ComputeWorkloadManager implements WorkloadManager {
297306
envId?: string;
298307
orgId?: string;
299308
projectId?: string;
309+
hasPrivateLink?: boolean;
300310
dequeuedAt?: Date;
301311
}): Promise<boolean> {
302312
const metadata: Record<string, string> = {
@@ -309,6 +319,14 @@ export class ComputeWorkloadManager implements WorkloadManager {
309319
TRIGGER_WORKER_INSTANCE_NAME: this.opts.runner.instanceName,
310320
};
311321

322+
// Resupply the same labels on restore (mirror of the create path); the
323+
// provider doesn't persist them across a snapshot, so without this a
324+
// restored run would lose its policy-based network selection.
325+
const labels: Record<string, string> = {};
326+
if (opts.hasPrivateLink && opts.orgId) {
327+
labels.privatelink = opts.orgId;
328+
}
329+
312330
this.logger.verbose("restore request body", {
313331
snapshotId: opts.snapshotId,
314332
runnerId: opts.runnerId,
@@ -322,6 +340,7 @@ export class ComputeWorkloadManager implements WorkloadManager {
322340
metadata,
323341
cpu: opts.machine.cpu,
324342
memory_gb: opts.machine.memory,
343+
...(Object.keys(labels).length > 0 ? { labels } : {}),
325344
})
326345
);
327346

apps/webapp/app/v3/mollifier/mollifierGate.server.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
recordDecision,
88
type DecisionOutcome,
99
type DecisionReason,
10+
type RecordDecisionOptions,
1011
} from "./mollifierTelemetry.server";
1112

1213
// `count` is the fleet-wide fixed-window counter for the env (INCR with a
@@ -80,7 +81,7 @@ export type GateDependencies = {
8081
inputs: GateInputs,
8182
decision: Extract<TripDecision, { divert: true }>,
8283
) => void;
83-
recordDecision: (outcome: DecisionOutcome, reason?: DecisionReason) => void;
84+
recordDecision: (outcome: DecisionOutcome, opts: RecordDecisionOptions) => void;
8485
};
8586

8687
// `options` is a thunk so env reads happen per-evaluation, not at module load.
@@ -152,52 +153,59 @@ export async function evaluateGate(
152153
): Promise<GateOutcome> {
153154
const d = { ...defaultGateDependencies, ...deps };
154155

156+
// Resolve the per-org flag up front so every decision below — including
157+
// the bypasses — can be labelled enrolled vs not on the
158+
// `mollifier.decisions` counter. Fail open: a transient error must not
159+
// block triggers. The resolver is purely in-memory (reads
160+
// `Organization.featureFlags`); it adds no DB round-trip to the hot path.
161+
let orgFlagEnabled: boolean;
162+
try {
163+
orgFlagEnabled = await d.resolveOrgFlag(inputs);
164+
} catch (error) {
165+
logger.warn("mollifier.resolve_org_flag_failed", {
166+
envId: inputs.envId,
167+
orgId: inputs.orgId,
168+
taskId: inputs.taskId,
169+
error: error instanceof Error ? error.message : String(error),
170+
});
171+
orgFlagEnabled = false;
172+
}
173+
// Passed to every `recordDecision`. `org` only becomes a label for the
174+
// (operationally capped) enrolled cohort — the guard is in
175+
// `decisionLabels`, so passing orgId unconditionally here is safe.
176+
const labels: RecordDecisionOptions = { enrolled: orgFlagEnabled, orgId: inputs.orgId };
177+
155178
// Debounce bypass. onDebounced is a closure over webapp state and
156179
// can't be snapshotted into the buffer for drainer replay. Skip before the
157180
// trip evaluator so debounce traffic is never counted against the rate.
158181
if (inputs.options?.debounce) {
159-
d.recordDecision("pass_through");
182+
d.recordDecision("pass_through", labels);
160183
return { action: "pass_through" };
161184
}
162185
// OneTimeUseToken bypass. OTU is a security feature on the PUBLIC_JWT
163186
// auth path; its synchronous-rejection contract is materially worse to
164187
// break than the idempotency-key contract.
165188
if (inputs.options?.oneTimeUseToken) {
166-
d.recordDecision("pass_through");
189+
d.recordDecision("pass_through", labels);
167190
return { action: "pass_through" };
168191
}
169192
// Single triggerAndWait bypass. batchTriggerAndWait still funnels
170193
// through TriggerTaskService.call per item so the dominant burst pattern
171194
// remains covered.
172195
if (inputs.options?.parentTaskRunId && inputs.options?.resumeParentOnCompletion) {
173-
d.recordDecision("pass_through");
196+
d.recordDecision("pass_through", labels);
174197
return { action: "pass_through" };
175198
}
176199

177200
if (!d.isMollifierEnabled()) {
178-
d.recordDecision("pass_through");
201+
d.recordDecision("pass_through", labels);
179202
return { action: "pass_through" };
180203
}
181204

182-
// Fail open: a transient DB error resolving the per-org flag must not
183-
// block triggers. Mirror the evaluator's fail-open posture in
184-
// `mollifierTripEvaluator.server.ts`.
185-
let orgFlagEnabled: boolean;
186-
try {
187-
orgFlagEnabled = await d.resolveOrgFlag(inputs);
188-
} catch (error) {
189-
logger.warn("mollifier.resolve_org_flag_failed", {
190-
envId: inputs.envId,
191-
orgId: inputs.orgId,
192-
taskId: inputs.taskId,
193-
error: error instanceof Error ? error.message : String(error),
194-
});
195-
orgFlagEnabled = false;
196-
}
197205
const shadowOn = d.isShadowModeOn();
198206

199207
if (!orgFlagEnabled && !shadowOn) {
200-
d.recordDecision("pass_through");
208+
d.recordDecision("pass_through", labels);
201209
return { action: "pass_through" };
202210
}
203211

@@ -226,17 +234,17 @@ export async function evaluateGate(
226234
decision = { divert: false };
227235
}
228236
if (!decision.divert) {
229-
d.recordDecision("pass_through");
237+
d.recordDecision("pass_through", labels);
230238
return { action: "pass_through" };
231239
}
232240

233241
if (orgFlagEnabled) {
234242
d.logMollified(inputs, decision);
235-
d.recordDecision("mollify", decision.reason);
243+
d.recordDecision("mollify", { ...labels, reason: decision.reason });
236244
return { action: "mollify", decision };
237245
}
238246

239247
d.logShadow(inputs, decision);
240-
d.recordDecision("shadow_log", decision.reason);
248+
d.recordDecision("shadow_log", { ...labels, reason: decision.reason });
241249
return { action: "shadow_log", decision };
242250
}

apps/webapp/app/v3/mollifier/mollifierTelemetry.server.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,39 @@ export const mollifierDecisionsCounter = meter.createCounter("mollifier.decision
99
export type DecisionOutcome = "pass_through" | "shadow_log" | "mollify";
1010
export type DecisionReason = "per_env_rate";
1111

12-
export function recordDecision(outcome: DecisionOutcome, reason?: DecisionReason): void {
13-
mollifierDecisionsCounter.add(1, {
12+
export type RecordDecisionOptions = {
13+
reason?: DecisionReason;
14+
// Whether the org has the per-org mollifier flag enabled. Emitted as the
15+
// bounded `enrolled` label so we can see how often enrolled orgs pass
16+
// through instead of mollifying — the whole point of this instrumentation.
17+
enrolled: boolean;
18+
// Org id, attached as the `org` label ONLY when `enrolled` is true. The
19+
// enrolled cohort is capped operationally (<= 10 orgs), so this stays
20+
// low-cardinality. It must NEVER be attached for non-enrolled orgs — that
21+
// would fan the metric out across every org id in production (unbounded;
22+
// the same high-cardinality ban that keeps envId/orgId off the other
23+
// mollifier metrics). The guard lives in `decisionLabels`, so callers can
24+
// pass orgId unconditionally.
25+
orgId?: string;
26+
};
27+
28+
// Pure: builds the metric label set for a gate decision. Extracted from
29+
// `recordDecision` so the org-only-when-enrolled cardinality guard is
30+
// unit-testable without standing up an OTel meter.
31+
export function decisionLabels(
32+
outcome: DecisionOutcome,
33+
opts: RecordDecisionOptions,
34+
): Record<string, string> {
35+
return {
1436
outcome,
15-
...(reason ? { reason } : {}),
16-
});
37+
enrolled: opts.enrolled ? "true" : "false",
38+
...(opts.reason ? { reason: opts.reason } : {}),
39+
...(opts.enrolled && opts.orgId ? { org: opts.orgId } : {}),
40+
};
41+
}
42+
43+
export function recordDecision(outcome: DecisionOutcome, opts: RecordDecisionOptions): void {
44+
mollifierDecisionsCounter.add(1, decisionLabels(outcome, opts));
1745
}
1846

1947
// Counts subscriptions hitting `/realtime/v1/runs/<id>` for a run that
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { decisionLabels } from "~/v3/mollifier/mollifierTelemetry.server";
4+
5+
// The cardinality guard. `org` is a bounded label (enrolled cohort is capped
6+
// at <= 10 orgs operationally), so it may ONLY be attached when the org is
7+
// enrolled. Attaching it for non-enrolled orgs would fan `mollifier.decisions`
8+
// out across every org id in production — the high-cardinality blow-up these
9+
// labels are explicitly designed to avoid.
10+
describe("decisionLabels", () => {
11+
it("always emits a bounded `enrolled` label (true/false)", () => {
12+
expect(decisionLabels("pass_through", { enrolled: false })).toEqual({
13+
outcome: "pass_through",
14+
enrolled: "false",
15+
});
16+
expect(decisionLabels("pass_through", { enrolled: true, orgId: "org_1" })).toMatchObject({
17+
enrolled: "true",
18+
});
19+
});
20+
21+
it("attaches the `org` label ONLY when enrolled — never for non-enrolled, even if orgId is passed", () => {
22+
// Non-enrolled: orgId passed but MUST be dropped (cardinality guard).
23+
expect(decisionLabels("pass_through", { enrolled: false, orgId: "org_unbounded" })).toEqual({
24+
outcome: "pass_through",
25+
enrolled: "false",
26+
});
27+
28+
// Enrolled: org label present.
29+
expect(
30+
decisionLabels("mollify", { enrolled: true, orgId: "org_1", reason: "per_env_rate" }),
31+
).toEqual({
32+
outcome: "mollify",
33+
enrolled: "true",
34+
reason: "per_env_rate",
35+
org: "org_1",
36+
});
37+
});
38+
39+
it("omits `org` when enrolled but no orgId is supplied", () => {
40+
expect(decisionLabels("pass_through", { enrolled: true })).toEqual({
41+
outcome: "pass_through",
42+
enrolled: "true",
43+
});
44+
});
45+
46+
it("includes `reason` only when supplied", () => {
47+
expect(decisionLabels("pass_through", { enrolled: true, orgId: "org_1" })).not.toHaveProperty(
48+
"reason",
49+
);
50+
expect(
51+
decisionLabels("shadow_log", { enrolled: false, reason: "per_env_rate" }),
52+
).toMatchObject({ reason: "per_env_rate" });
53+
});
54+
});

0 commit comments

Comments
 (0)