@treeseed/sdk 0.8.6 → 0.8.8
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/dist/capacity.d.ts +120 -1
- package/dist/capacity.js +371 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11 -1
- package/dist/market-client.d.ts +32 -0
- package/dist/market-client.js +48 -0
- package/dist/operations/services/config-runtime.d.ts +2 -2
- package/dist/operations/services/config-runtime.js +111 -38
- package/dist/operations/services/deploy.js +68 -22
- package/dist/operations/services/hosting-audit.js +8 -2
- package/dist/operations/services/runtime-tools.js +1 -1
- package/dist/platform/deploy-config.js +1 -1
- package/dist/reconcile/builtin-adapters.js +95 -11
- package/dist/sdk-types.d.ts +5 -4
- package/package.json +1 -1
package/dist/capacity.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CapacityEstimateConfidence, CapacityGrant, CapacityPlan, CapacityProviderLane, CapacityReservation, TaskEstimateProfile } from './sdk-types.ts';
|
|
1
|
+
import type { CapacityEstimateConfidence, CapacityGrant, CapacityPlan, CapacityProvider, CapacityProviderLane, CapacityReservation, CreateCapacityReservationRequest, CreateCapacityRoutingDecisionRequest, RecordCapacityUsageRequest, TaskEstimateProfile } from './sdk-types.ts';
|
|
2
2
|
import type { AgentProviderProfile } from './types/agents.ts';
|
|
3
3
|
export type ProcessingEnvironment = 'local' | 'staging' | 'prod';
|
|
4
4
|
export interface CapacityProviderRegistration {
|
|
@@ -62,6 +62,111 @@ export interface CapacityLaneScore {
|
|
|
62
62
|
costPenalty: number;
|
|
63
63
|
reasons: string[];
|
|
64
64
|
}
|
|
65
|
+
export interface CapacityTaskEstimate {
|
|
66
|
+
taskSignature: string;
|
|
67
|
+
confidence: CapacityEstimateConfidence;
|
|
68
|
+
estimatedCreditsP50: number;
|
|
69
|
+
estimatedCreditsP90: number;
|
|
70
|
+
reservedCredits: number;
|
|
71
|
+
}
|
|
72
|
+
export interface TeamCapacitySummary {
|
|
73
|
+
teamId: string;
|
|
74
|
+
monthlyCredits: number | null;
|
|
75
|
+
monthlyUsedCredits: number;
|
|
76
|
+
monthlyRemainingCredits: number | null;
|
|
77
|
+
dailyCredits: number | null;
|
|
78
|
+
dailyUsedCredits: number;
|
|
79
|
+
dailyReservedCredits: number;
|
|
80
|
+
dailyRemainingCredits: number | null;
|
|
81
|
+
providerCount: number;
|
|
82
|
+
activeProviderCount: number;
|
|
83
|
+
degradedProviderCount: number;
|
|
84
|
+
grantCount: number;
|
|
85
|
+
blockedTaskCount: number;
|
|
86
|
+
approvalRequiredCount: number;
|
|
87
|
+
}
|
|
88
|
+
export interface ProjectCapacitySummary extends TeamCapacitySummary {
|
|
89
|
+
projectId: string;
|
|
90
|
+
environment: ProcessingEnvironment;
|
|
91
|
+
readiness: 'ready' | 'waiting_for_budget' | 'waiting_for_provider' | 'paused_by_policy' | 'needs_approval';
|
|
92
|
+
reasons: string[];
|
|
93
|
+
}
|
|
94
|
+
export interface RouteAndReserveInput {
|
|
95
|
+
plan: CapacityPlan;
|
|
96
|
+
estimate: CapacityTaskEstimate;
|
|
97
|
+
taskId?: string | null;
|
|
98
|
+
workDayId?: string | null;
|
|
99
|
+
taskKind?: string | null;
|
|
100
|
+
requiredCapabilities?: string[];
|
|
101
|
+
modelClass?: string | null;
|
|
102
|
+
priorityClass?: string | null;
|
|
103
|
+
allowDegradedProviders?: boolean;
|
|
104
|
+
repositoryMutation?: boolean;
|
|
105
|
+
production?: boolean;
|
|
106
|
+
selectedModel?: string | null;
|
|
107
|
+
source?: string;
|
|
108
|
+
metadata?: Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
export type RouteAndReserveBlockCode = 'no_capacity_provider' | 'no_capacity_grant' | 'no_eligible_lane' | 'insufficient_budget' | 'approval_required';
|
|
111
|
+
export interface RouteAndReserveCandidate {
|
|
112
|
+
providerId: string;
|
|
113
|
+
laneId: string;
|
|
114
|
+
grantId: string;
|
|
115
|
+
remainingCredits: number | null;
|
|
116
|
+
score: CapacityLaneScore;
|
|
117
|
+
eligible: boolean;
|
|
118
|
+
reasons: string[];
|
|
119
|
+
}
|
|
120
|
+
export type RouteAndReserveResult = {
|
|
121
|
+
ok: true;
|
|
122
|
+
provider: CapacityProvider;
|
|
123
|
+
lane: CapacityProviderLane;
|
|
124
|
+
grant: CapacityGrant;
|
|
125
|
+
estimate: CapacityTaskEstimate;
|
|
126
|
+
remainingCreditsBefore: number | null;
|
|
127
|
+
reservation: CreateCapacityReservationRequest;
|
|
128
|
+
routingDecision: CreateCapacityRoutingDecisionRequest;
|
|
129
|
+
ledgerEntry: RecordCapacityUsageRequest;
|
|
130
|
+
capacityMetadata: {
|
|
131
|
+
providerId: string;
|
|
132
|
+
laneId: string;
|
|
133
|
+
grantId: string;
|
|
134
|
+
reservationId: string | null;
|
|
135
|
+
routingDecisionId: string | null;
|
|
136
|
+
estimatedCreditsP50: number;
|
|
137
|
+
estimatedCreditsP90: number;
|
|
138
|
+
reservedCredits: number;
|
|
139
|
+
};
|
|
140
|
+
candidates: RouteAndReserveCandidate[];
|
|
141
|
+
} | {
|
|
142
|
+
ok: false;
|
|
143
|
+
code: RouteAndReserveBlockCode;
|
|
144
|
+
reason: string;
|
|
145
|
+
estimate: CapacityTaskEstimate;
|
|
146
|
+
candidates: RouteAndReserveCandidate[];
|
|
147
|
+
};
|
|
148
|
+
export interface CapacitySettlementInput {
|
|
149
|
+
reservation: CapacityReservation;
|
|
150
|
+
actualCredits: number;
|
|
151
|
+
actualProviderUnits?: number | null;
|
|
152
|
+
actualUsd?: number | null;
|
|
153
|
+
teamId?: string | null;
|
|
154
|
+
projectId?: string | null;
|
|
155
|
+
workDayId?: string | null;
|
|
156
|
+
taskId?: string | null;
|
|
157
|
+
source?: string;
|
|
158
|
+
metadata?: Record<string, unknown>;
|
|
159
|
+
}
|
|
160
|
+
export interface CapacitySettlement {
|
|
161
|
+
reservationId: string;
|
|
162
|
+
state: 'consumed' | 'overran_pending_approval';
|
|
163
|
+
consumeEntry: RecordCapacityUsageRequest;
|
|
164
|
+
releaseEntry: RecordCapacityUsageRequest | null;
|
|
165
|
+
overrunEntry: RecordCapacityUsageRequest | null;
|
|
166
|
+
consumedCredits: number;
|
|
167
|
+
releasedCredits: number;
|
|
168
|
+
overrunCredits: number;
|
|
169
|
+
}
|
|
65
170
|
export declare function reserveCreditsForEstimate(input: CapacityEstimateInput): {
|
|
66
171
|
taskSignature: string;
|
|
67
172
|
confidence: CapacityEstimateConfidence;
|
|
@@ -78,9 +183,23 @@ export declare function summarizeCapacityPlan(plan: CapacityPlan): {
|
|
|
78
183
|
laneCount: number;
|
|
79
184
|
grantCount: number;
|
|
80
185
|
};
|
|
186
|
+
export declare function summarizeTeamCapacityPlan(plan: CapacityPlan): TeamCapacitySummary;
|
|
187
|
+
export declare function summarizeProjectCapacityPlan(plan: CapacityPlan, options?: {
|
|
188
|
+
workPolicyEnabled?: boolean | null;
|
|
189
|
+
approvalRequiredCount?: number;
|
|
190
|
+
blockedTaskCount?: number;
|
|
191
|
+
}): ProjectCapacitySummary;
|
|
81
192
|
export declare function scoreCapacityLane(input: CapacityLaneCandidate): CapacityLaneScore;
|
|
82
193
|
export declare function selectBestCapacityLane(candidates: CapacityLaneCandidate[]): {
|
|
83
194
|
selected: CapacityLaneScore;
|
|
84
195
|
scores: CapacityLaneScore[];
|
|
85
196
|
};
|
|
86
197
|
export declare function reservationHasCapacity(reservation: CapacityReservation): boolean;
|
|
198
|
+
export declare function createReservationReleaseEntry(input: {
|
|
199
|
+
reservation: CapacityReservation;
|
|
200
|
+
credits?: number | null;
|
|
201
|
+
source?: string;
|
|
202
|
+
metadata?: Record<string, unknown>;
|
|
203
|
+
}): RecordCapacityUsageRequest;
|
|
204
|
+
export declare function settleCapacityActuals(input: CapacitySettlementInput): CapacitySettlement;
|
|
205
|
+
export declare function routeAndReserveCapacity(input: RouteAndReserveInput): RouteAndReserveResult;
|
package/dist/capacity.js
CHANGED
|
@@ -6,6 +6,97 @@ function scarcityPenalty(level) {
|
|
|
6
6
|
if (level === "medium") return 15;
|
|
7
7
|
return 0;
|
|
8
8
|
}
|
|
9
|
+
function metadataStatus(value) {
|
|
10
|
+
const status = value?.status;
|
|
11
|
+
return typeof status === "string" ? status : null;
|
|
12
|
+
}
|
|
13
|
+
function stringArray(value) {
|
|
14
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
15
|
+
}
|
|
16
|
+
function booleanValue(value) {
|
|
17
|
+
return typeof value === "boolean" ? value : null;
|
|
18
|
+
}
|
|
19
|
+
function numberValue(value) {
|
|
20
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
21
|
+
}
|
|
22
|
+
function reservationDebit(reservation) {
|
|
23
|
+
if (reservation.state === "released" || reservation.state === "expired" || reservation.state === "cancelled") {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
if (reservation.state === "consumed" || reservation.state === "failed") {
|
|
27
|
+
return Math.max(0, reservation.consumedCredits);
|
|
28
|
+
}
|
|
29
|
+
return Math.max(reservation.reservedCredits, reservation.consumedCredits, 0);
|
|
30
|
+
}
|
|
31
|
+
function activeReservationDebit(reservation) {
|
|
32
|
+
if (reservation.state === "reserved" || reservation.state === "consuming") {
|
|
33
|
+
return Math.max(reservation.reservedCredits, reservation.consumedCredits, 0);
|
|
34
|
+
}
|
|
35
|
+
if (reservation.state === "consumed" || reservation.state === "failed") {
|
|
36
|
+
return Math.max(reservation.consumedCredits, 0);
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
function grantMatchesReservation(grant, reservation) {
|
|
41
|
+
if (grant.teamId !== reservation.teamId) return false;
|
|
42
|
+
if (grant.capacityProviderId !== reservation.capacityProviderId) return false;
|
|
43
|
+
if (grant.laneId && grant.laneId !== reservation.laneId) return false;
|
|
44
|
+
if (grant.projectId && grant.projectId !== reservation.projectId) return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function grantRemainingCredits(plan, grant) {
|
|
48
|
+
const limit = grant.dailyCreditLimit ?? grant.monthlyCreditLimit;
|
|
49
|
+
if (limit === null || limit === void 0) return null;
|
|
50
|
+
const debits = plan.activeReservations.filter((reservation) => grantMatchesReservation(grant, reservation)).reduce((total, reservation) => total + reservationDebit(reservation), 0);
|
|
51
|
+
return Math.max(0, Number(limit) - debits);
|
|
52
|
+
}
|
|
53
|
+
function providerIsEligible(provider, input) {
|
|
54
|
+
if (provider.status === "active") return true;
|
|
55
|
+
if (provider.status === "degraded" && input.allowDegradedProviders) return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function grantIsEligible(grant, input) {
|
|
59
|
+
if (grant.state !== "active") return false;
|
|
60
|
+
if (grant.teamId !== input.plan.teamId) return false;
|
|
61
|
+
if (grant.environment && grant.environment !== input.plan.environment) return false;
|
|
62
|
+
if (grant.projectId && grant.projectId !== input.plan.projectId) return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
function lanePolicyReasons(lane, input) {
|
|
66
|
+
const reasons = [];
|
|
67
|
+
const laneStatus = metadataStatus(lane.metadata);
|
|
68
|
+
if (laneStatus && laneStatus !== "active") reasons.push(`lane_status:${laneStatus}`);
|
|
69
|
+
const policy = lane.routingPolicy ?? {};
|
|
70
|
+
const taskKinds = stringArray(policy.taskKinds);
|
|
71
|
+
const taskKind = input.taskKind ?? input.estimate.taskSignature;
|
|
72
|
+
if (taskKinds.length > 0 && !taskKinds.includes(taskKind)) reasons.push("task_kind_mismatch");
|
|
73
|
+
const requiredCapabilities = stringArray(policy.requiredCapabilities);
|
|
74
|
+
const missingCapabilities = (input.requiredCapabilities ?? []).filter((capability) => !requiredCapabilities.includes(capability));
|
|
75
|
+
if (requiredCapabilities.length > 0 && missingCapabilities.length > 0) {
|
|
76
|
+
reasons.push("capability_mismatch");
|
|
77
|
+
}
|
|
78
|
+
const allowedEnvironments = stringArray(policy.allowedEnvironments);
|
|
79
|
+
if (allowedEnvironments.length > 0 && !allowedEnvironments.includes(input.plan.environment)) {
|
|
80
|
+
reasons.push("environment_mismatch");
|
|
81
|
+
}
|
|
82
|
+
const maxCreditsPerTask = numberValue(policy.maxCreditsPerTask);
|
|
83
|
+
if (maxCreditsPerTask !== null && input.estimate.reservedCredits > maxCreditsPerTask) {
|
|
84
|
+
reasons.push("task_credit_limit_exceeded");
|
|
85
|
+
}
|
|
86
|
+
const approvalThreshold = numberValue(policy.requiresApprovalAboveCredits);
|
|
87
|
+
if (approvalThreshold !== null && input.estimate.reservedCredits > approvalThreshold) {
|
|
88
|
+
reasons.push("approval_required");
|
|
89
|
+
}
|
|
90
|
+
const repositoryMutationAllowed = booleanValue(policy.repositoryMutationAllowed);
|
|
91
|
+
if (input.repositoryMutation && repositoryMutationAllowed === false) {
|
|
92
|
+
reasons.push("repository_mutation_not_allowed");
|
|
93
|
+
}
|
|
94
|
+
const productionAllowed = booleanValue(policy.productionAllowed);
|
|
95
|
+
if (input.production && productionAllowed === false) {
|
|
96
|
+
reasons.push("production_not_allowed");
|
|
97
|
+
}
|
|
98
|
+
return reasons;
|
|
99
|
+
}
|
|
9
100
|
function reserveCreditsForEstimate(input) {
|
|
10
101
|
const profileP50 = finiteNumber(input.profile?.creditsP50);
|
|
11
102
|
const profileP90 = finiteNumber(input.profile?.creditsP90);
|
|
@@ -26,7 +117,7 @@ function reserveCreditsForEstimate(input) {
|
|
|
26
117
|
};
|
|
27
118
|
}
|
|
28
119
|
function summarizeCapacityPlan(plan) {
|
|
29
|
-
const reservedCredits = plan.activeReservations.filter((reservation) => reservation.state === "reserved").reduce((total, reservation) => total + reservation.reservedCredits, 0);
|
|
120
|
+
const reservedCredits = plan.activeReservations.filter((reservation) => reservation.state === "reserved" || reservation.state === "consuming").reduce((total, reservation) => total + reservation.reservedCredits, 0);
|
|
30
121
|
const consumedCredits = plan.activeReservations.reduce((total, reservation) => total + reservation.consumedCredits, 0);
|
|
31
122
|
const grantedDailyCredits = plan.grants.filter((grant) => grant.state === "active").reduce((total, grant) => total + (grant.dailyCreditLimit ?? 0), 0);
|
|
32
123
|
return {
|
|
@@ -39,6 +130,55 @@ function summarizeCapacityPlan(plan) {
|
|
|
39
130
|
grantCount: plan.grants.length
|
|
40
131
|
};
|
|
41
132
|
}
|
|
133
|
+
function summarizeTeamCapacityPlan(plan) {
|
|
134
|
+
const dailyCredits = plan.grants.filter((grant) => grant.state === "active").reduce((total, grant) => total + (grant.dailyCreditLimit ?? 0), 0);
|
|
135
|
+
const monthlyCredits = plan.grants.filter((grant) => grant.state === "active").reduce((total, grant) => total + (grant.monthlyCreditLimit ?? 0), 0);
|
|
136
|
+
const dailyReservedCredits = plan.activeReservations.reduce((total, reservation) => total + activeReservationDebit(reservation), 0);
|
|
137
|
+
const dailyUsedCredits = plan.activeReservations.reduce((total, reservation) => total + Math.max(0, reservation.consumedCredits), 0);
|
|
138
|
+
return {
|
|
139
|
+
teamId: plan.teamId,
|
|
140
|
+
monthlyCredits: monthlyCredits > 0 ? monthlyCredits : null,
|
|
141
|
+
monthlyUsedCredits: dailyUsedCredits,
|
|
142
|
+
monthlyRemainingCredits: monthlyCredits > 0 ? Math.max(0, monthlyCredits - dailyUsedCredits) : null,
|
|
143
|
+
dailyCredits: dailyCredits > 0 ? dailyCredits : null,
|
|
144
|
+
dailyUsedCredits,
|
|
145
|
+
dailyReservedCredits,
|
|
146
|
+
dailyRemainingCredits: dailyCredits > 0 ? Math.max(0, dailyCredits - dailyReservedCredits - dailyUsedCredits) : null,
|
|
147
|
+
providerCount: plan.providers.length,
|
|
148
|
+
activeProviderCount: plan.providers.filter((provider) => provider.status === "active").length,
|
|
149
|
+
degradedProviderCount: plan.providers.filter((provider) => provider.status === "degraded").length,
|
|
150
|
+
grantCount: plan.grants.length,
|
|
151
|
+
blockedTaskCount: 0,
|
|
152
|
+
approvalRequiredCount: 0
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function summarizeProjectCapacityPlan(plan, options = {}) {
|
|
156
|
+
const summary = summarizeTeamCapacityPlan(plan);
|
|
157
|
+
const reasons = [];
|
|
158
|
+
let readiness = "ready";
|
|
159
|
+
if (options.workPolicyEnabled === false) {
|
|
160
|
+
readiness = "paused_by_policy";
|
|
161
|
+
reasons.push("work_policy_disabled");
|
|
162
|
+
} else if (summary.activeProviderCount <= 0) {
|
|
163
|
+
readiness = "waiting_for_provider";
|
|
164
|
+
reasons.push("no_active_provider");
|
|
165
|
+
} else if (summary.dailyRemainingCredits !== null && summary.dailyRemainingCredits <= 0) {
|
|
166
|
+
readiness = "waiting_for_budget";
|
|
167
|
+
reasons.push("daily_budget_exhausted");
|
|
168
|
+
} else if ((options.approvalRequiredCount ?? 0) > 0) {
|
|
169
|
+
readiness = "needs_approval";
|
|
170
|
+
reasons.push("approval_required");
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
...summary,
|
|
174
|
+
projectId: plan.projectId,
|
|
175
|
+
environment: plan.environment,
|
|
176
|
+
readiness,
|
|
177
|
+
reasons,
|
|
178
|
+
blockedTaskCount: options.blockedTaskCount ?? summary.blockedTaskCount,
|
|
179
|
+
approvalRequiredCount: options.approvalRequiredCount ?? summary.approvalRequiredCount
|
|
180
|
+
};
|
|
181
|
+
}
|
|
42
182
|
function scoreCapacityLane(input) {
|
|
43
183
|
const reasons = [];
|
|
44
184
|
let agentFit = 0;
|
|
@@ -91,10 +231,239 @@ function selectBestCapacityLane(candidates) {
|
|
|
91
231
|
function reservationHasCapacity(reservation) {
|
|
92
232
|
return reservation.state === "reserved" && reservation.reservedCredits > reservation.consumedCredits;
|
|
93
233
|
}
|
|
234
|
+
function createReservationReleaseEntry(input) {
|
|
235
|
+
const credits = Math.max(0, Number(input.credits ?? input.reservation.reservedCredits - input.reservation.consumedCredits));
|
|
236
|
+
return {
|
|
237
|
+
capacityProviderId: input.reservation.capacityProviderId,
|
|
238
|
+
laneId: input.reservation.laneId,
|
|
239
|
+
reservationId: input.reservation.id,
|
|
240
|
+
teamId: input.reservation.teamId,
|
|
241
|
+
projectId: input.reservation.projectId,
|
|
242
|
+
workDayId: input.reservation.workDayId,
|
|
243
|
+
taskId: input.reservation.taskId,
|
|
244
|
+
phase: "reservation_released",
|
|
245
|
+
credits: -credits,
|
|
246
|
+
source: input.source ?? "capacity_coordinator",
|
|
247
|
+
metadata: input.metadata ?? {}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function settleCapacityActuals(input) {
|
|
251
|
+
const consumedCredits = Math.max(0, Number(input.actualCredits ?? 0));
|
|
252
|
+
const releasedCredits = Math.max(0, input.reservation.reservedCredits - consumedCredits);
|
|
253
|
+
const overrunCredits = Math.max(0, consumedCredits - input.reservation.reservedCredits);
|
|
254
|
+
const base = {
|
|
255
|
+
capacityProviderId: input.reservation.capacityProviderId,
|
|
256
|
+
laneId: input.reservation.laneId,
|
|
257
|
+
reservationId: input.reservation.id,
|
|
258
|
+
teamId: input.teamId ?? input.reservation.teamId,
|
|
259
|
+
projectId: input.projectId ?? input.reservation.projectId,
|
|
260
|
+
workDayId: input.workDayId ?? input.reservation.workDayId,
|
|
261
|
+
taskId: input.taskId ?? input.reservation.taskId,
|
|
262
|
+
source: input.source ?? "capacity_coordinator",
|
|
263
|
+
metadata: input.metadata ?? {}
|
|
264
|
+
};
|
|
265
|
+
const consumeEntry = {
|
|
266
|
+
...base,
|
|
267
|
+
phase: "task_completed_actual_settlement",
|
|
268
|
+
credits: consumedCredits,
|
|
269
|
+
providerUnits: input.actualProviderUnits ?? null,
|
|
270
|
+
usd: input.actualUsd ?? null
|
|
271
|
+
};
|
|
272
|
+
const releaseEntry = releasedCredits > 0 ? {
|
|
273
|
+
...base,
|
|
274
|
+
phase: "reservation_released",
|
|
275
|
+
credits: -releasedCredits
|
|
276
|
+
} : null;
|
|
277
|
+
const overrunEntry = overrunCredits > 0 ? {
|
|
278
|
+
...base,
|
|
279
|
+
phase: "overrun_hold",
|
|
280
|
+
credits: overrunCredits
|
|
281
|
+
} : null;
|
|
282
|
+
return {
|
|
283
|
+
reservationId: input.reservation.id,
|
|
284
|
+
state: overrunCredits > 0 ? "overran_pending_approval" : "consumed",
|
|
285
|
+
consumeEntry,
|
|
286
|
+
releaseEntry,
|
|
287
|
+
overrunEntry,
|
|
288
|
+
consumedCredits,
|
|
289
|
+
releasedCredits,
|
|
290
|
+
overrunCredits
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function routeAndReserveCapacity(input) {
|
|
294
|
+
const providers = input.plan.providers.filter((provider2) => providerIsEligible(provider2, input));
|
|
295
|
+
const grants = input.plan.grants.filter((grant2) => grantIsEligible(grant2, input));
|
|
296
|
+
const candidates = [];
|
|
297
|
+
for (const grant2 of grants) {
|
|
298
|
+
const provider2 = providers.find((candidate) => candidate.id === grant2.capacityProviderId);
|
|
299
|
+
if (!provider2) continue;
|
|
300
|
+
const lanes = input.plan.lanes.filter(
|
|
301
|
+
(lane2) => lane2.capacityProviderId === provider2.id && (!grant2.laneId || grant2.laneId === lane2.id)
|
|
302
|
+
);
|
|
303
|
+
for (const lane2 of lanes) {
|
|
304
|
+
const reasons = lanePolicyReasons(lane2, input);
|
|
305
|
+
const remainingCredits = grantRemainingCredits(input.plan, grant2);
|
|
306
|
+
if (remainingCredits !== null && remainingCredits < input.estimate.reservedCredits && (grant2.overflowPolicy === "deny" || grant2.overflowPolicy === "hard_grant")) {
|
|
307
|
+
reasons.push("insufficient_budget");
|
|
308
|
+
}
|
|
309
|
+
if (remainingCredits !== null && remainingCredits < input.estimate.reservedCredits && grant2.overflowPolicy === "approval_required") {
|
|
310
|
+
reasons.push("approval_required");
|
|
311
|
+
}
|
|
312
|
+
const score = scoreCapacityLane({
|
|
313
|
+
lane: lane2,
|
|
314
|
+
grant: grant2,
|
|
315
|
+
remainingCredits,
|
|
316
|
+
taskKind: input.taskKind ?? input.estimate.taskSignature,
|
|
317
|
+
requiredCapabilities: input.requiredCapabilities,
|
|
318
|
+
modelClass: input.modelClass ?? null
|
|
319
|
+
});
|
|
320
|
+
candidates.push({
|
|
321
|
+
providerId: provider2.id,
|
|
322
|
+
laneId: lane2.id,
|
|
323
|
+
grantId: grant2.id,
|
|
324
|
+
remainingCredits,
|
|
325
|
+
score,
|
|
326
|
+
eligible: reasons.length === 0,
|
|
327
|
+
reasons
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (input.plan.providers.length === 0 || providers.length === 0) {
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
code: "no_capacity_provider",
|
|
335
|
+
reason: "No active helper capacity provider is available.",
|
|
336
|
+
estimate: input.estimate,
|
|
337
|
+
candidates
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (grants.length === 0) {
|
|
341
|
+
return {
|
|
342
|
+
ok: false,
|
|
343
|
+
code: "no_capacity_grant",
|
|
344
|
+
reason: "No active capacity grant is available for this team, project, and environment.",
|
|
345
|
+
estimate: input.estimate,
|
|
346
|
+
candidates
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const eligible = candidates.filter((candidate) => candidate.eligible).sort((left, right) => right.score.score - left.score.score || left.laneId.localeCompare(right.laneId));
|
|
350
|
+
const selected = eligible[0] ?? null;
|
|
351
|
+
if (!selected) {
|
|
352
|
+
const hasApprovalBlock = candidates.some((candidate) => candidate.reasons.includes("approval_required"));
|
|
353
|
+
const hasBudgetBlock = candidates.some((candidate) => candidate.reasons.includes("insufficient_budget"));
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
code: hasApprovalBlock ? "approval_required" : hasBudgetBlock ? "insufficient_budget" : "no_eligible_lane",
|
|
357
|
+
reason: hasApprovalBlock ? "The requested helper task needs approval before capacity can be reserved." : hasBudgetBlock ? "The requested helper task is above the remaining approved budget." : "No provider lane matches the task policy and capability requirements.",
|
|
358
|
+
estimate: input.estimate,
|
|
359
|
+
candidates
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const provider = providers.find((candidate) => candidate.id === selected.providerId);
|
|
363
|
+
const lane = input.plan.lanes.find((candidate) => candidate.id === selected.laneId);
|
|
364
|
+
const grant = grants.find((candidate) => candidate.id === selected.grantId);
|
|
365
|
+
if (!provider || !lane || !grant) {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
code: "no_eligible_lane",
|
|
369
|
+
reason: "The selected capacity lane could not be resolved.",
|
|
370
|
+
estimate: input.estimate,
|
|
371
|
+
candidates
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const candidatePayload = candidates.map((candidate) => ({
|
|
375
|
+
providerId: candidate.providerId,
|
|
376
|
+
laneId: candidate.laneId,
|
|
377
|
+
grantId: candidate.grantId,
|
|
378
|
+
remainingCredits: candidate.remainingCredits,
|
|
379
|
+
eligible: candidate.eligible,
|
|
380
|
+
reasons: candidate.reasons,
|
|
381
|
+
score: candidate.score.score
|
|
382
|
+
}));
|
|
383
|
+
const scorePayload = Object.fromEntries(candidates.map((candidate) => [candidate.laneId, candidate.score]));
|
|
384
|
+
const reservation = {
|
|
385
|
+
capacityProviderId: provider.id,
|
|
386
|
+
laneId: lane.id,
|
|
387
|
+
teamId: input.plan.teamId,
|
|
388
|
+
projectId: input.plan.projectId,
|
|
389
|
+
workDayId: input.workDayId ?? null,
|
|
390
|
+
taskId: input.taskId ?? null,
|
|
391
|
+
state: "reserved",
|
|
392
|
+
reservedCredits: input.estimate.reservedCredits,
|
|
393
|
+
metadata: {
|
|
394
|
+
...input.metadata ?? {},
|
|
395
|
+
grantId: grant.id,
|
|
396
|
+
taskSignature: input.estimate.taskSignature,
|
|
397
|
+
estimatedCreditsP50: input.estimate.estimatedCreditsP50,
|
|
398
|
+
estimatedCreditsP90: input.estimate.estimatedCreditsP90
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
const routingDecision = {
|
|
402
|
+
taskId: input.taskId ?? null,
|
|
403
|
+
workDayId: input.workDayId ?? null,
|
|
404
|
+
projectId: input.plan.projectId,
|
|
405
|
+
selectedProviderId: provider.id,
|
|
406
|
+
selectedLaneId: lane.id,
|
|
407
|
+
selectedModel: input.selectedModel ?? null,
|
|
408
|
+
decision: "selected",
|
|
409
|
+
reason: selected.score.reasons.length > 0 ? selected.score.reasons.join(",") : "best_eligible_lane",
|
|
410
|
+
candidates: candidatePayload,
|
|
411
|
+
scores: scorePayload,
|
|
412
|
+
metadata: {
|
|
413
|
+
...input.metadata ?? {},
|
|
414
|
+
grantId: grant.id,
|
|
415
|
+
remainingCreditsBefore: selected.remainingCredits,
|
|
416
|
+
reservedCredits: input.estimate.reservedCredits
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const ledgerEntry = {
|
|
420
|
+
capacityProviderId: provider.id,
|
|
421
|
+
laneId: lane.id,
|
|
422
|
+
teamId: input.plan.teamId,
|
|
423
|
+
projectId: input.plan.projectId,
|
|
424
|
+
workDayId: input.workDayId ?? null,
|
|
425
|
+
taskId: input.taskId ?? null,
|
|
426
|
+
phase: "reservation_created",
|
|
427
|
+
credits: input.estimate.reservedCredits,
|
|
428
|
+
source: input.source ?? "capacity_coordinator",
|
|
429
|
+
metadata: {
|
|
430
|
+
...input.metadata ?? {},
|
|
431
|
+
grantId: grant.id,
|
|
432
|
+
taskSignature: input.estimate.taskSignature
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
return {
|
|
436
|
+
ok: true,
|
|
437
|
+
provider,
|
|
438
|
+
lane,
|
|
439
|
+
grant,
|
|
440
|
+
estimate: input.estimate,
|
|
441
|
+
remainingCreditsBefore: selected.remainingCredits,
|
|
442
|
+
reservation,
|
|
443
|
+
routingDecision,
|
|
444
|
+
ledgerEntry,
|
|
445
|
+
capacityMetadata: {
|
|
446
|
+
providerId: provider.id,
|
|
447
|
+
laneId: lane.id,
|
|
448
|
+
grantId: grant.id,
|
|
449
|
+
reservationId: reservation.id ?? null,
|
|
450
|
+
routingDecisionId: routingDecision.id ?? null,
|
|
451
|
+
estimatedCreditsP50: input.estimate.estimatedCreditsP50,
|
|
452
|
+
estimatedCreditsP90: input.estimate.estimatedCreditsP90,
|
|
453
|
+
reservedCredits: input.estimate.reservedCredits
|
|
454
|
+
},
|
|
455
|
+
candidates
|
|
456
|
+
};
|
|
457
|
+
}
|
|
94
458
|
export {
|
|
459
|
+
createReservationReleaseEntry,
|
|
95
460
|
reservationHasCapacity,
|
|
96
461
|
reserveCreditsForEstimate,
|
|
462
|
+
routeAndReserveCapacity,
|
|
97
463
|
scoreCapacityLane,
|
|
98
464
|
selectBestCapacityLane,
|
|
99
|
-
|
|
465
|
+
settleCapacityActuals,
|
|
466
|
+
summarizeCapacityPlan,
|
|
467
|
+
summarizeProjectCapacityPlan,
|
|
468
|
+
summarizeTeamCapacityPlan
|
|
100
469
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { ContentGraphRuntime } from './graph.ts';
|
|
|
3
3
|
export { projectConnectionModeFromHosting } from './sdk-types.ts';
|
|
4
4
|
export { createControlPlaneReporter } from './control-plane.ts';
|
|
5
5
|
export { ControlPlaneClient } from './control-plane-client.ts';
|
|
6
|
-
export { reservationHasCapacity, reserveCreditsForEstimate, scoreCapacityLane, selectBestCapacityLane, summarizeCapacityPlan, } from './capacity.ts';
|
|
6
|
+
export { reservationHasCapacity, reserveCreditsForEstimate, routeAndReserveCapacity, scoreCapacityLane, selectBestCapacityLane, settleCapacityActuals, createReservationReleaseEntry, summarizeCapacityPlan, summarizeProjectCapacityPlan, summarizeTeamCapacityPlan, } from './capacity.ts';
|
|
7
7
|
export { executeKnowledgeHubProviderLaunch, validateKnowledgeHubProviderLaunchPrerequisites, } from './operations/services/hub-provider-launch.ts';
|
|
8
8
|
export { createKnowledgeHubRepositories, defaultHubContentResolutionPolicy, executeKnowledgeHubLaunch, normalizeKnowledgeHubLaunchIntent, planKnowledgeHubLaunch, planKnowledgeHubRepositories, validateRepositoryHost, type HubContentResolutionPolicy, type KnowledgeHubLaunchIntent, type KnowledgeHubLaunchPhase, type KnowledgeHubLaunchPlan, type KnowledgeHubLaunchResult, type KnowledgeHubRepositoryPlan, type RepositoryHost, } from './operations/services/hub-launch.ts';
|
|
9
9
|
export { ensureRailwayEnvironment, ensureRailwayProject, ensureRailwayService, getRailwayAuthProfile, listRailwayEnvironments, listRailwayProjects, listRailwayServices, listRailwayVariables, railwayGraphqlRequest, resolveRailwayApiToken, resolveRailwayApiUrl, resolveRailwayWorkspace, resolveRailwayWorkspaceContext, upsertRailwayVariables, } from './operations/services/railway-api.ts';
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,14 @@ import { ControlPlaneClient } from "./control-plane-client.js";
|
|
|
6
6
|
import {
|
|
7
7
|
reservationHasCapacity,
|
|
8
8
|
reserveCreditsForEstimate,
|
|
9
|
+
routeAndReserveCapacity,
|
|
9
10
|
scoreCapacityLane,
|
|
10
11
|
selectBestCapacityLane,
|
|
11
|
-
|
|
12
|
+
settleCapacityActuals,
|
|
13
|
+
createReservationReleaseEntry,
|
|
14
|
+
summarizeCapacityPlan,
|
|
15
|
+
summarizeProjectCapacityPlan,
|
|
16
|
+
summarizeTeamCapacityPlan
|
|
12
17
|
} from "./capacity.js";
|
|
13
18
|
import {
|
|
14
19
|
executeKnowledgeHubProviderLaunch,
|
|
@@ -250,6 +255,7 @@ export {
|
|
|
250
255
|
createFilesystemContentSource,
|
|
251
256
|
createKnowledgeHubRepositories,
|
|
252
257
|
createPublishedContentPipeline,
|
|
258
|
+
createReservationReleaseEntry,
|
|
253
259
|
createTeamScopedR2OverlayContentPublishProvider,
|
|
254
260
|
createTeamScopedR2OverlayContentRuntimeProvider,
|
|
255
261
|
createTreeseedManagedToolEnv,
|
|
@@ -336,14 +342,18 @@ export {
|
|
|
336
342
|
resolveTreeseedTenantRoot,
|
|
337
343
|
resolveTreeseedToolBinary,
|
|
338
344
|
resolveTreeseedToolCommand,
|
|
345
|
+
routeAndReserveCapacity,
|
|
339
346
|
runTreeseedCopilotTask,
|
|
340
347
|
runTreeseedVerifyDriver,
|
|
341
348
|
scoreCapacityLane,
|
|
342
349
|
selectBestCapacityLane,
|
|
343
350
|
setActiveMarketProfile,
|
|
344
351
|
setMarketSession,
|
|
352
|
+
settleCapacityActuals,
|
|
345
353
|
signEditorialPreviewToken,
|
|
346
354
|
summarizeCapacityPlan,
|
|
355
|
+
summarizeProjectCapacityPlan,
|
|
356
|
+
summarizeTeamCapacityPlan,
|
|
347
357
|
tenantFeatureEnabled,
|
|
348
358
|
tenantModelRendered,
|
|
349
359
|
upsertRailwayVariables,
|
package/dist/market-client.d.ts
CHANGED
|
@@ -165,6 +165,38 @@ export declare class MarketClient {
|
|
|
165
165
|
environments: ProjectEnvironmentAccess[];
|
|
166
166
|
};
|
|
167
167
|
}>;
|
|
168
|
+
teamCapacity(teamId: string): Promise<{
|
|
169
|
+
ok: true;
|
|
170
|
+
payload: Record<string, unknown>;
|
|
171
|
+
}>;
|
|
172
|
+
launchManagedCapacityProvider(teamId: string, body?: Record<string, unknown>): Promise<{
|
|
173
|
+
ok: true;
|
|
174
|
+
payload: Record<string, unknown>;
|
|
175
|
+
}>;
|
|
176
|
+
capacityProvider(providerId: string): Promise<{
|
|
177
|
+
ok: true;
|
|
178
|
+
payload: Record<string, unknown>;
|
|
179
|
+
}>;
|
|
180
|
+
resetCapacityProviderApiKey(providerId: string, body?: Record<string, unknown>): Promise<{
|
|
181
|
+
ok: true;
|
|
182
|
+
payload: Record<string, unknown>;
|
|
183
|
+
}>;
|
|
184
|
+
revokeCapacityProviderApiKey(providerId: string, keyId: string): Promise<{
|
|
185
|
+
ok: true;
|
|
186
|
+
payload: Record<string, unknown>;
|
|
187
|
+
}>;
|
|
188
|
+
capacityGrants(teamId: string): Promise<{
|
|
189
|
+
ok: true;
|
|
190
|
+
payload: unknown[];
|
|
191
|
+
}>;
|
|
192
|
+
createCapacityGrant(teamId: string, body: Record<string, unknown>): Promise<{
|
|
193
|
+
ok: true;
|
|
194
|
+
payload: Record<string, unknown>;
|
|
195
|
+
}>;
|
|
196
|
+
enqueueAgentTask(projectId: string, body: Record<string, unknown>): Promise<{
|
|
197
|
+
ok: true;
|
|
198
|
+
payload: Record<string, unknown>;
|
|
199
|
+
}>;
|
|
168
200
|
catalog(kind?: string | null): Promise<{
|
|
169
201
|
ok: true;
|
|
170
202
|
payload: unknown[];
|
package/dist/market-client.js
CHANGED
|
@@ -315,6 +315,54 @@ class MarketClient {
|
|
|
315
315
|
{ requireAuth: true }
|
|
316
316
|
);
|
|
317
317
|
}
|
|
318
|
+
teamCapacity(teamId) {
|
|
319
|
+
return this.request(
|
|
320
|
+
`/v1/teams/${encodeURIComponent(teamId)}/capacity`,
|
|
321
|
+
{ requireAuth: true }
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
launchManagedCapacityProvider(teamId, body = {}) {
|
|
325
|
+
return this.request(
|
|
326
|
+
`/v1/teams/${encodeURIComponent(teamId)}/capacity/providers/managed`,
|
|
327
|
+
{ method: "POST", body, requireAuth: true }
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
capacityProvider(providerId) {
|
|
331
|
+
return this.request(
|
|
332
|
+
`/v1/capacity/providers/${encodeURIComponent(providerId)}`,
|
|
333
|
+
{ requireAuth: true }
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
resetCapacityProviderApiKey(providerId, body = {}) {
|
|
337
|
+
return this.request(
|
|
338
|
+
`/v1/capacity/providers/${encodeURIComponent(providerId)}/api-keys/reset`,
|
|
339
|
+
{ method: "POST", body, requireAuth: true }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
revokeCapacityProviderApiKey(providerId, keyId) {
|
|
343
|
+
return this.request(
|
|
344
|
+
`/v1/capacity/providers/${encodeURIComponent(providerId)}/api-keys/${encodeURIComponent(keyId)}/revoke`,
|
|
345
|
+
{ method: "POST", requireAuth: true }
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
capacityGrants(teamId) {
|
|
349
|
+
return this.request(
|
|
350
|
+
`/v1/teams/${encodeURIComponent(teamId)}/capacity-grants`,
|
|
351
|
+
{ requireAuth: true }
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
createCapacityGrant(teamId, body) {
|
|
355
|
+
return this.request(
|
|
356
|
+
`/v1/teams/${encodeURIComponent(teamId)}/capacity-grants`,
|
|
357
|
+
{ method: "POST", body, requireAuth: true }
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
enqueueAgentTask(projectId, body) {
|
|
361
|
+
return this.request(
|
|
362
|
+
`/v1/projects/${encodeURIComponent(projectId)}/agent-tasks`,
|
|
363
|
+
{ method: "POST", body, requireAuth: true }
|
|
364
|
+
);
|
|
365
|
+
}
|
|
318
366
|
catalog(kind) {
|
|
319
367
|
const query = kind ? `?kind=${encodeURIComponent(kind)}` : "";
|
|
320
368
|
return this.request(`/v1/catalog${query}`, { requireAuth: Boolean(this.accessToken) });
|
|
@@ -256,7 +256,7 @@ export declare function validateTreeseedCommandEnvironment({ tenantRoot, scope,
|
|
|
256
256
|
}): {
|
|
257
257
|
registry: import("../../platform/environment.ts").TreeseedResolvedEnvironmentRegistry;
|
|
258
258
|
values: {};
|
|
259
|
-
validation:
|
|
259
|
+
validation: any;
|
|
260
260
|
};
|
|
261
261
|
export declare function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }: {
|
|
262
262
|
tenantRoot: any;
|
|
@@ -265,7 +265,7 @@ export declare function assertTreeseedCommandEnvironment({ tenantRoot, scope, pu
|
|
|
265
265
|
}): {
|
|
266
266
|
registry: import("../../platform/environment.ts").TreeseedResolvedEnvironmentRegistry;
|
|
267
267
|
values: {};
|
|
268
|
-
validation:
|
|
268
|
+
validation: any;
|
|
269
269
|
};
|
|
270
270
|
export declare function ensureTreeseedActVerificationTooling({ tenantRoot, installIfMissing, env, write }?: {
|
|
271
271
|
tenantRoot?: string | undefined;
|
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
} from "./railway-deploy.js";
|
|
35
35
|
import {
|
|
36
36
|
normalizeRailwayEnvironmentName,
|
|
37
|
-
resolveRailwayWorkspace
|
|
37
|
+
resolveRailwayWorkspace,
|
|
38
|
+
resolveRailwayWorkspaceContext
|
|
38
39
|
} from "./railway-api.js";
|
|
39
40
|
import {
|
|
40
41
|
createGitHubApiClient,
|
|
@@ -1585,20 +1586,52 @@ function applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override = false
|
|
|
1585
1586
|
function validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
|
|
1586
1587
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1587
1588
|
const values = resolveTreeseedLaunchEnvironment({ tenantRoot, scope });
|
|
1588
|
-
const validation = validateTreeseedEnvironmentValues({
|
|
1589
|
+
const validation = filterValidationByWorkflowPlane(validateTreeseedEnvironmentValues({
|
|
1589
1590
|
values,
|
|
1590
1591
|
scope,
|
|
1591
1592
|
purpose,
|
|
1592
1593
|
deployConfig: registry.context.deployConfig,
|
|
1593
1594
|
tenantConfig: registry.context.tenantConfig,
|
|
1594
1595
|
plugins: registry.context.plugins
|
|
1595
|
-
});
|
|
1596
|
+
}));
|
|
1596
1597
|
return {
|
|
1597
1598
|
registry,
|
|
1598
1599
|
values,
|
|
1599
1600
|
validation
|
|
1600
1601
|
};
|
|
1601
1602
|
}
|
|
1603
|
+
function filterValidationByWorkflowPlane(validation) {
|
|
1604
|
+
const plane = process.env.TREESEED_WORKFLOW_PLANE;
|
|
1605
|
+
if (plane !== "web" && plane !== "processing") {
|
|
1606
|
+
return validation;
|
|
1607
|
+
}
|
|
1608
|
+
const problemApplies = (problem) => doesEntryApplyToWorkflowPlane(problem.entry, plane);
|
|
1609
|
+
const missing = validation.missing.filter(problemApplies);
|
|
1610
|
+
const invalid = validation.invalid.filter(problemApplies);
|
|
1611
|
+
const entries = validation.entries.filter((entry) => doesEntryApplyToWorkflowPlane(entry, plane));
|
|
1612
|
+
const required = validation.required.filter((entry) => doesEntryApplyToWorkflowPlane(entry, plane));
|
|
1613
|
+
return {
|
|
1614
|
+
...validation,
|
|
1615
|
+
ok: missing.length === 0 && invalid.length === 0,
|
|
1616
|
+
entries,
|
|
1617
|
+
required,
|
|
1618
|
+
missing,
|
|
1619
|
+
invalid
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function doesEntryApplyToWorkflowPlane(entry, plane) {
|
|
1623
|
+
const targets = new Set(entry.targets ?? []);
|
|
1624
|
+
const hasProcessingTarget = targets.has("railway-secret") || targets.has("railway-var");
|
|
1625
|
+
const hasWebTarget = targets.has("cloudflare-secret") || targets.has("cloudflare-var") || targets.has("local-cloudflare");
|
|
1626
|
+
const hasWorkflowTarget = targets.has("github-secret") || targets.has("github-variable");
|
|
1627
|
+
if (plane === "web") {
|
|
1628
|
+
return !hasProcessingTarget || hasWebTarget || hasWorkflowTarget;
|
|
1629
|
+
}
|
|
1630
|
+
if (plane === "processing") {
|
|
1631
|
+
return !hasWebTarget || hasProcessingTarget || hasWorkflowTarget;
|
|
1632
|
+
}
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1602
1635
|
function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
|
|
1603
1636
|
const report = validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose });
|
|
1604
1637
|
if (report.validation.ok) {
|
|
@@ -1818,30 +1851,53 @@ function checkGitHubConnection({ tenantRoot, env }) {
|
|
|
1818
1851
|
if (!gh) {
|
|
1819
1852
|
return providerConnectionResult("github", false, "GitHub CLI `gh` is not installed.");
|
|
1820
1853
|
}
|
|
1821
|
-
const
|
|
1822
|
-
const
|
|
1854
|
+
const identityMode = env.TREESEED_GITHUB_IDENTITY_MODE === "account" ? "account" : "repository";
|
|
1855
|
+
const repository = identityMode === "repository" ? maybeResolveGitHubRepositorySlug(tenantRoot) : null;
|
|
1856
|
+
const owner = typeof env.TREESEED_HOSTED_HUBS_GITHUB_OWNER === "string" ? env.TREESEED_HOSTED_HUBS_GITHUB_OWNER.trim() : "";
|
|
1857
|
+
const commandCandidates = repository ? [{
|
|
1858
|
+
args: ["repo", "view", repository, "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
|
|
1859
|
+
successMessage: (resolved) => `GitHub token can access ${resolved || repository}.`
|
|
1860
|
+
}] : owner ? [
|
|
1861
|
+
{
|
|
1862
|
+
args: ["api", `orgs/${owner}`, "--jq", ".login"],
|
|
1863
|
+
successMessage: (resolved) => `GitHub token can access organization ${resolved || owner}.`,
|
|
1864
|
+
optional: true
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
args: ["api", `users/${owner}`, "--jq", ".login"],
|
|
1868
|
+
successMessage: (resolved) => `GitHub token can access user ${resolved || owner}.`,
|
|
1869
|
+
optional: true
|
|
1870
|
+
}
|
|
1871
|
+
] : [
|
|
1872
|
+
{
|
|
1873
|
+
args: ["api", "user", "--jq", ".login"],
|
|
1874
|
+
successMessage: (resolved) => resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
|
|
1875
|
+
}
|
|
1876
|
+
];
|
|
1877
|
+
let lastDetail = "";
|
|
1823
1878
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1824
|
-
const
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1879
|
+
for (const candidate of commandCandidates) {
|
|
1880
|
+
const result = spawnSync(gh, candidate.args, {
|
|
1881
|
+
cwd: tenantRoot,
|
|
1882
|
+
stdio: "pipe",
|
|
1883
|
+
encoding: "utf8",
|
|
1884
|
+
env: createTreeseedManagedToolEnv({ ...process.env, ...env }),
|
|
1885
|
+
timeout: CLI_CHECK_TIMEOUT_MS
|
|
1886
|
+
});
|
|
1887
|
+
if (result.status === 0) {
|
|
1888
|
+
return providerConnectionResult("github", true, candidate.successMessage(result.stdout.trim()));
|
|
1889
|
+
}
|
|
1890
|
+
lastDetail = formatCheckOutput(result) || "GitHub API check failed.";
|
|
1891
|
+
if (candidate.optional && !isTransientProviderConnectionError(lastDetail)) {
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
break;
|
|
1838
1895
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
return providerConnectionResult("github", false, detail);
|
|
1896
|
+
if (attempt >= 2 || !isTransientProviderConnectionError(lastDetail)) {
|
|
1897
|
+
return providerConnectionResult("github", false, lastDetail || "GitHub API check failed.");
|
|
1842
1898
|
}
|
|
1843
1899
|
}
|
|
1844
|
-
return providerConnectionResult("github", false, "GitHub API check failed.");
|
|
1900
|
+
return providerConnectionResult("github", false, lastDetail || "GitHub API check failed.");
|
|
1845
1901
|
}
|
|
1846
1902
|
function checkCloudflareConnection({ tenantRoot, env }) {
|
|
1847
1903
|
if (!env.CLOUDFLARE_API_TOKEN) {
|
|
@@ -1894,23 +1950,26 @@ async function checkRailwayConnection({ tenantRoot, env }) {
|
|
|
1894
1950
|
const checkPromise = (async () => {
|
|
1895
1951
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1896
1952
|
try {
|
|
1897
|
-
const
|
|
1898
|
-
|
|
1899
|
-
if (!whoami.ok) {
|
|
1900
|
-
if (/rate.?limit|too many requests|429/iu.test(whoami.detail || "")) {
|
|
1901
|
-
return providerConnectionResult(
|
|
1902
|
-
"railway",
|
|
1903
|
-
false,
|
|
1904
|
-
"Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
|
|
1905
|
-
{ skipped: true, warning: true, rateLimited: true }
|
|
1906
|
-
);
|
|
1907
|
-
}
|
|
1908
|
-
throw new Error(whoami.detail || "Railway CLI authentication check failed.");
|
|
1909
|
-
}
|
|
1910
|
-
const identity = whoami.stdout.replace(/^logged in as\s+/iu, "").replace(/\s*👋\s*$/u, "").trim() || "an account";
|
|
1911
|
-
return providerConnectionResult("railway", true, `Railway authenticated as ${identity} in workspace ${workspaceName}. Project and service existence will be reconciled during bootstrap.`);
|
|
1953
|
+
const workspace = await resolveRailwayWorkspaceContext({ env, workspace: workspaceName });
|
|
1954
|
+
return providerConnectionResult("railway", true, `Railway API token can access workspace ${workspace.name}. Project and service existence will be reconciled during bootstrap.`);
|
|
1912
1955
|
} catch (error) {
|
|
1913
1956
|
const detail = error instanceof Error ? error.message : "Railway API check failed.";
|
|
1957
|
+
if (/rate.?limit|too many requests|429/iu.test(detail || "")) {
|
|
1958
|
+
return providerConnectionResult(
|
|
1959
|
+
"railway",
|
|
1960
|
+
false,
|
|
1961
|
+
"Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
|
|
1962
|
+
{ skipped: true, warning: true, rateLimited: true }
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
if (attempt >= 2 && isTransientProviderConnectionError(detail)) {
|
|
1966
|
+
return providerConnectionResult(
|
|
1967
|
+
"railway",
|
|
1968
|
+
false,
|
|
1969
|
+
"Railway connectivity preflight hit transient API failures; bootstrap will continue and rely on API-backed reconcile verification.",
|
|
1970
|
+
{ skipped: true, warning: true, transient: true }
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1914
1973
|
if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
|
|
1915
1974
|
return providerConnectionResult("railway", false, detail);
|
|
1916
1975
|
}
|
|
@@ -1928,8 +1987,22 @@ async function checkRailwayConnection({ tenantRoot, env }) {
|
|
|
1928
1987
|
}
|
|
1929
1988
|
async function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env, valuesOverlay = {} } = {}) {
|
|
1930
1989
|
const values = collectTreeseedConfigSeedValues(tenantRoot, scope, env, valuesOverlay);
|
|
1990
|
+
const passthroughValue = (key) => {
|
|
1991
|
+
const overlayValue = valuesOverlay?.[key];
|
|
1992
|
+
if (typeof overlayValue === "string" && overlayValue.trim()) {
|
|
1993
|
+
return overlayValue.trim();
|
|
1994
|
+
}
|
|
1995
|
+
const envValue = env?.[key];
|
|
1996
|
+
if (typeof envValue === "string" && envValue.trim()) {
|
|
1997
|
+
return envValue.trim();
|
|
1998
|
+
}
|
|
1999
|
+
const resolvedValue = values?.[key];
|
|
2000
|
+
return typeof resolvedValue === "string" && resolvedValue.trim() ? resolvedValue.trim() : void 0;
|
|
2001
|
+
};
|
|
1931
2002
|
const rawCommandEnv = {
|
|
1932
2003
|
GH_TOKEN: values.GH_TOKEN,
|
|
2004
|
+
TREESEED_GITHUB_IDENTITY_MODE: passthroughValue("TREESEED_GITHUB_IDENTITY_MODE"),
|
|
2005
|
+
TREESEED_HOSTED_HUBS_GITHUB_OWNER: passthroughValue("TREESEED_HOSTED_HUBS_GITHUB_OWNER"),
|
|
1933
2006
|
CLOUDFLARE_API_TOKEN: values.CLOUDFLARE_API_TOKEN,
|
|
1934
2007
|
CLOUDFLARE_ACCOUNT_ID: values.CLOUDFLARE_ACCOUNT_ID,
|
|
1935
2008
|
RAILWAY_API_TOKEN: values.RAILWAY_API_TOKEN,
|
|
@@ -1044,22 +1044,44 @@ function shouldManageCloudflareWebCacheRules(deployConfig, target) {
|
|
|
1044
1044
|
function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
|
|
1045
1045
|
const requestScript = `import { readFileSync } from 'node:fs';
|
|
1046
1046
|
const input = JSON.parse(readFileSync(0, 'utf8') || '{}');
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
payload = { success: false, errors: [{ message: rawBody || 'empty response' }] };
|
|
1047
|
+
function errorMessage(error) {
|
|
1048
|
+
const parts = [];
|
|
1049
|
+
if (error && typeof error.message === 'string') parts.push(error.message);
|
|
1050
|
+
const cause = error?.cause;
|
|
1051
|
+
if (cause && typeof cause.message === 'string') parts.push(cause.message);
|
|
1052
|
+
if (cause && typeof cause.code === 'string') parts.push(cause.code);
|
|
1053
|
+
if (Array.isArray(cause?.errors)) {
|
|
1054
|
+
for (const entry of cause.errors) {
|
|
1055
|
+
if (entry && typeof entry.message === 'string') parts.push(entry.message);
|
|
1056
|
+
if (entry && typeof entry.code === 'string') parts.push(entry.code);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return [...new Set(parts.filter(Boolean))].join('; ') || String(error);
|
|
1061
1060
|
}
|
|
1062
|
-
|
|
1061
|
+
try {
|
|
1062
|
+
const response = await fetch(input.url, {
|
|
1063
|
+
method: input.method,
|
|
1064
|
+
headers: {
|
|
1065
|
+
authorization: 'Bearer ' + input.token,
|
|
1066
|
+
'content-type': 'application/json',
|
|
1067
|
+
},
|
|
1068
|
+
body: input.body ? JSON.stringify(input.body) : undefined,
|
|
1069
|
+
});
|
|
1070
|
+
const rawBody = await response.text();
|
|
1071
|
+
let payload;
|
|
1072
|
+
try {
|
|
1073
|
+
payload = rawBody ? JSON.parse(rawBody) : {};
|
|
1074
|
+
} catch {
|
|
1075
|
+
payload = { success: false, errors: [{ message: rawBody || 'empty response' }] };
|
|
1076
|
+
}
|
|
1077
|
+
process.stdout.write(JSON.stringify({ ok: response.ok, payload }));
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
process.stdout.write(JSON.stringify({
|
|
1080
|
+
ok: false,
|
|
1081
|
+
transient: true,
|
|
1082
|
+
payload: { success: false, errors: [{ message: errorMessage(error) }] },
|
|
1083
|
+
}));
|
|
1084
|
+
}`;
|
|
1063
1085
|
const requestInput = JSON.stringify({
|
|
1064
1086
|
url: `https://api.cloudflare.com/client/v4${path}`,
|
|
1065
1087
|
method,
|
|
@@ -1067,6 +1089,11 @@ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
|
|
|
1067
1089
|
token: env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
|
|
1068
1090
|
});
|
|
1069
1091
|
const isTransient = (text) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted/iu.test(text || "");
|
|
1092
|
+
const formatPayloadErrors = (payload) => Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).join("; ") : "";
|
|
1093
|
+
const summarizeChildError = (text) => {
|
|
1094
|
+
const lines = String(text || "").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1095
|
+
return lines.find((line) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|typeerror|error/iu.test(line)) ?? lines[0] ?? "";
|
|
1096
|
+
};
|
|
1070
1097
|
let attempt = 0;
|
|
1071
1098
|
for (; ; ) {
|
|
1072
1099
|
const response = spawnSync(
|
|
@@ -1085,29 +1112,48 @@ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
|
|
|
1085
1112
|
}
|
|
1086
1113
|
);
|
|
1087
1114
|
if (response.error?.code === "ETIMEDOUT") {
|
|
1088
|
-
if (attempt <
|
|
1115
|
+
if (attempt < 4) {
|
|
1089
1116
|
attempt += 1;
|
|
1117
|
+
sleepSync(500 * attempt);
|
|
1090
1118
|
continue;
|
|
1091
1119
|
}
|
|
1092
1120
|
if (!allowFailure) {
|
|
1093
|
-
throw new Error(`Cloudflare API request timed out: ${method} ${path}`);
|
|
1121
|
+
throw new Error(`Cloudflare API request timed out after ${attempt + 1} attempts: ${method} ${path}`);
|
|
1094
1122
|
}
|
|
1095
1123
|
return null;
|
|
1096
1124
|
}
|
|
1097
1125
|
const stderr = response.stderr?.trim() || "";
|
|
1098
1126
|
if (response.status !== 0) {
|
|
1099
|
-
if (attempt <
|
|
1127
|
+
if (attempt < 4 && isTransient(stderr)) {
|
|
1100
1128
|
attempt += 1;
|
|
1129
|
+
sleepSync(500 * attempt);
|
|
1101
1130
|
continue;
|
|
1102
1131
|
}
|
|
1103
1132
|
if (!allowFailure) {
|
|
1104
|
-
|
|
1133
|
+
const detail = summarizeChildError(stderr);
|
|
1134
|
+
throw new Error(detail ? `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}: ${detail}` : `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}`);
|
|
1105
1135
|
}
|
|
1106
1136
|
}
|
|
1107
|
-
|
|
1137
|
+
let parsed;
|
|
1138
|
+
try {
|
|
1139
|
+
parsed = JSON.parse(response.stdout?.trim() || '{"ok":false,"payload":{"success":false,"errors":[{"message":"empty response"}]}}');
|
|
1140
|
+
} catch {
|
|
1141
|
+
parsed = {
|
|
1142
|
+
ok: false,
|
|
1143
|
+
payload: {
|
|
1144
|
+
success: false,
|
|
1145
|
+
errors: [{ message: response.stdout?.trim() || stderr || "empty response" }]
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
const details = formatPayloadErrors(parsed.payload);
|
|
1150
|
+
if (!parsed.ok && parsed.transient && attempt < 4 && isTransient(details)) {
|
|
1151
|
+
attempt += 1;
|
|
1152
|
+
sleepSync(500 * attempt);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1108
1155
|
if (!parsed.ok && !allowFailure) {
|
|
1109
|
-
|
|
1110
|
-
throw new Error(details || `Cloudflare API request failed: ${method} ${path}`);
|
|
1156
|
+
throw new Error(details ? `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}: ${details}` : `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}`);
|
|
1111
1157
|
}
|
|
1112
1158
|
return parsed.payload;
|
|
1113
1159
|
}
|
|
@@ -514,8 +514,14 @@ async function runTreeseedHostingAudit({
|
|
|
514
514
|
const connectionReport = await checkTreeseedProviderConnections({
|
|
515
515
|
tenantRoot,
|
|
516
516
|
scope: resolved.scope,
|
|
517
|
-
env:
|
|
518
|
-
|
|
517
|
+
env: {
|
|
518
|
+
...values,
|
|
519
|
+
TREESEED_GITHUB_IDENTITY_MODE: "account"
|
|
520
|
+
},
|
|
521
|
+
valuesOverlay: {
|
|
522
|
+
...values,
|
|
523
|
+
TREESEED_GITHUB_IDENTITY_MODE: "account"
|
|
524
|
+
}
|
|
519
525
|
});
|
|
520
526
|
checks.push(...providerConnectionChecks(connectionReport, hostKinds));
|
|
521
527
|
if (hostKinds.includes("email")) {
|
|
@@ -506,7 +506,7 @@ function parseFallbackDeployConfig(configPath) {
|
|
|
506
506
|
slug: expectString(record.slug, "slug"),
|
|
507
507
|
siteUrl: expectString(record.siteUrl, "siteUrl"),
|
|
508
508
|
contactEmail: expectString(record.contactEmail, "contactEmail"),
|
|
509
|
-
hosting: parsedHosting && record.hub === void 0 && record.runtime === void 0 ? parsedHosting : normalizeLegacyHostingFromPlanes(hub, runtime),
|
|
509
|
+
hosting: parsedHosting?.kind === "market_control_plane" ? { ...parsedHosting, registration: "none" } : parsedHosting && record.hub === void 0 && record.runtime === void 0 ? parsedHosting : normalizeLegacyHostingFromPlanes(hub, runtime),
|
|
510
510
|
hub,
|
|
511
511
|
runtime,
|
|
512
512
|
cloudflare: {
|
|
@@ -541,7 +541,7 @@ function parseDeployConfig(raw) {
|
|
|
541
541
|
const turnstile = optionalRecord(parsed.turnstile, "turnstile") ?? {};
|
|
542
542
|
optionalBoolean(turnstile.enabled, "turnstile.enabled");
|
|
543
543
|
const normalizedHosting = normalizeLegacyHostingFromPlanes(hub, runtime);
|
|
544
|
-
const compatibilityHosting = hosting && !parsed.hub && !parsed.runtime ? hosting : normalizedHosting;
|
|
544
|
+
const compatibilityHosting = hosting?.kind === "market_control_plane" ? { ...hosting, registration: "none" } : hosting && !parsed.hub && !parsed.runtime ? hosting : normalizedHosting;
|
|
545
545
|
return {
|
|
546
546
|
name: expectString(parsed.name, "name"),
|
|
547
547
|
slug: expectString(parsed.slug, "slug"),
|
|
@@ -48,6 +48,8 @@ import {
|
|
|
48
48
|
resolveRailwayWorkspaceContext,
|
|
49
49
|
upsertRailwayVariables
|
|
50
50
|
} from "../operations/services/railway-api.js";
|
|
51
|
+
import { loadTreeseedReconcileState } from "./state.js";
|
|
52
|
+
import { createTreeseedReconcileUnitId } from "./units.js";
|
|
51
53
|
function toDeployTarget(target) {
|
|
52
54
|
return target.kind === "persistent" ? createPersistentDeployTarget(target.scope) : createBranchPreviewDeployTarget(target.branchName);
|
|
53
55
|
}
|
|
@@ -289,6 +291,28 @@ function storeCustomDomainState(input, provider, domain, value) {
|
|
|
289
291
|
function getCustomDomainState(input, provider, domain) {
|
|
290
292
|
return input.context.session.get(customDomainStateKey(provider, domain));
|
|
291
293
|
}
|
|
294
|
+
function getPersistedCustomDomainState(input, provider, domain) {
|
|
295
|
+
if (!domain) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
if (provider === "railway") {
|
|
299
|
+
try {
|
|
300
|
+
const state = loadTreeseedReconcileState(input.context.tenantRoot, input.context.target);
|
|
301
|
+
const unitId = createTreeseedReconcileUnitId("custom-domain:api", domain);
|
|
302
|
+
const unit = state.units[unitId];
|
|
303
|
+
const reconciled = unit?.lastReconciledState;
|
|
304
|
+
if (reconciled && typeof reconciled === "object" && reconciled.domain === domain) {
|
|
305
|
+
return reconciled;
|
|
306
|
+
}
|
|
307
|
+
const observed = unit?.lastObservedState;
|
|
308
|
+
if (observed && typeof observed === "object" && observed.domain === domain) {
|
|
309
|
+
return observed;
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
292
316
|
function listCloudflareDnsRecords(env, zoneId, recordName) {
|
|
293
317
|
const query = recordName ? `?name=${encodeURIComponent(recordName)}` : "";
|
|
294
318
|
const payload = cloudflareApiRequest(`/zones/${encodeURIComponent(zoneId)}/dns_records${query}`, {
|
|
@@ -402,22 +426,70 @@ function normalizeRailwayDomainDnsRecord(value) {
|
|
|
402
426
|
status: typeof record.status === "string" ? record.status.trim().toUpperCase() : ""
|
|
403
427
|
};
|
|
404
428
|
}
|
|
429
|
+
function firstRailwayDomainString(...values) {
|
|
430
|
+
for (const value of values) {
|
|
431
|
+
if (typeof value === "string" && value.trim()) {
|
|
432
|
+
return value.trim();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
function firstRailwayDomainArray(...values) {
|
|
438
|
+
for (const value of values) {
|
|
439
|
+
if (Array.isArray(value)) {
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
405
445
|
function normalizeRailwayDomainPayload(value) {
|
|
406
446
|
if (!value || typeof value !== "object") {
|
|
407
447
|
return null;
|
|
408
448
|
}
|
|
409
449
|
const record = value;
|
|
450
|
+
const status = record.status && typeof record.status === "object" ? record.status : {};
|
|
410
451
|
const domain = typeof record.domain === "string" ? record.domain.trim() : typeof record.name === "string" ? record.name.trim() : "";
|
|
411
|
-
const
|
|
452
|
+
const serviceDomain = firstRailwayDomainString(
|
|
453
|
+
record.serviceDomain,
|
|
454
|
+
record.target,
|
|
455
|
+
record.targetDomain,
|
|
456
|
+
record.cnameTarget,
|
|
457
|
+
record.cname,
|
|
458
|
+
record.dnsTarget,
|
|
459
|
+
status.serviceDomain,
|
|
460
|
+
status.target,
|
|
461
|
+
status.targetDomain,
|
|
462
|
+
status.cnameTarget,
|
|
463
|
+
status.cname,
|
|
464
|
+
status.dnsTarget
|
|
465
|
+
);
|
|
466
|
+
const dnsRecordCandidates = firstRailwayDomainArray(
|
|
467
|
+
record.dnsRecords,
|
|
468
|
+
record.requiredDnsRecords,
|
|
469
|
+
record.requiredRecords,
|
|
470
|
+
record.records,
|
|
471
|
+
record.dns,
|
|
472
|
+
status.dnsRecords,
|
|
473
|
+
status.requiredDnsRecords,
|
|
474
|
+
status.requiredRecords,
|
|
475
|
+
status.records,
|
|
476
|
+
status.dns
|
|
477
|
+
);
|
|
412
478
|
const dnsRecords = dnsRecordCandidates.map((entry) => normalizeRailwayDomainDnsRecord(entry)).filter(Boolean);
|
|
479
|
+
const effectiveDnsRecords = dnsRecords.length > 0 || !domain || !serviceDomain || serviceDomain === domain ? dnsRecords : [{
|
|
480
|
+
type: "CNAME",
|
|
481
|
+
name: domain,
|
|
482
|
+
content: serviceDomain,
|
|
483
|
+
status: ""
|
|
484
|
+
}];
|
|
413
485
|
return {
|
|
414
486
|
id: typeof record.id === "string" ? record.id.trim() : null,
|
|
415
487
|
domain,
|
|
416
|
-
serviceDomain
|
|
417
|
-
certificateStatus: typeof
|
|
418
|
-
verificationDnsHost: typeof record.verificationDnsHost === "string" ? record.verificationDnsHost.trim() : typeof
|
|
419
|
-
verificationToken: typeof record.verificationToken === "string" ? record.verificationToken.trim() : typeof
|
|
420
|
-
dnsRecords
|
|
488
|
+
serviceDomain,
|
|
489
|
+
certificateStatus: typeof status.certificateStatus === "string" ? String(status.certificateStatus).trim().toUpperCase() : null,
|
|
490
|
+
verificationDnsHost: typeof record.verificationDnsHost === "string" ? record.verificationDnsHost.trim() : typeof status.verificationDnsHost === "string" ? String(status.verificationDnsHost).trim() : null,
|
|
491
|
+
verificationToken: typeof record.verificationToken === "string" ? record.verificationToken.trim() : typeof status.verificationToken === "string" ? String(status.verificationToken).trim() : null,
|
|
492
|
+
dnsRecords: effectiveDnsRecords
|
|
421
493
|
};
|
|
422
494
|
}
|
|
423
495
|
async function ensureRailwayCustomDomain(input, service, domain, env, identifiers) {
|
|
@@ -1581,8 +1653,16 @@ function resolveDesiredDnsRecords(input) {
|
|
|
1581
1653
|
if (!domain) {
|
|
1582
1654
|
return [];
|
|
1583
1655
|
}
|
|
1584
|
-
const railwayState = getCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState;
|
|
1656
|
+
const railwayState = getCustomDomainState(input, "railway", domain) ?? getPersistedCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState;
|
|
1585
1657
|
const records = Array.isArray(railwayState?.dnsRecords) ? railwayState.dnsRecords.map((entry) => normalizeRailwayDomainDnsRecord(entry)).filter(Boolean) : [];
|
|
1658
|
+
if (records.length === 0 && typeof railwayState?.serviceDomain === "string" && railwayState.serviceDomain.trim() && railwayState.serviceDomain.trim() !== domain) {
|
|
1659
|
+
records.push({
|
|
1660
|
+
type: "CNAME",
|
|
1661
|
+
name: domain,
|
|
1662
|
+
content: railwayState.serviceDomain.trim(),
|
|
1663
|
+
status: ""
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1586
1666
|
const desiredRecords = records.map((record) => ({
|
|
1587
1667
|
...record,
|
|
1588
1668
|
proxied: false
|
|
@@ -1685,8 +1765,9 @@ function verifyCustomDomainUnit(input) {
|
|
|
1685
1765
|
}
|
|
1686
1766
|
case "custom-domain:api": {
|
|
1687
1767
|
const domain = String(input.unit.spec.domain ?? "").trim();
|
|
1688
|
-
const live = getCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
|
|
1768
|
+
const live = getCustomDomainState(input, "railway", domain) ?? getPersistedCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
|
|
1689
1769
|
const dnsRecords = Array.isArray(live?.dnsRecords) ? live.dnsRecords : [];
|
|
1770
|
+
const hasDnsRequirements = dnsRecords.length > 0 || typeof live?.serviceDomain === "string" && live.serviceDomain.trim().length > 0 || typeof live?.verificationDnsHost === "string" && live.verificationDnsHost.trim().length > 0 && typeof live?.verificationToken === "string" && live.verificationToken.trim().length > 0;
|
|
1690
1771
|
return summarizeVerification(input.unit.unitId, [
|
|
1691
1772
|
verificationCheck("custom-domain.exists", "Railway custom domain attachment exists", "cli", {
|
|
1692
1773
|
exists: Boolean(live?.domain),
|
|
@@ -1695,10 +1776,13 @@ function verifyCustomDomainUnit(input) {
|
|
|
1695
1776
|
issues: live?.domain ? [] : [`Railway custom domain ${domain || "(unset)"} is missing.`]
|
|
1696
1777
|
}),
|
|
1697
1778
|
verificationCheck("custom-domain.dns-requirements", "Railway custom domain exposes DNS requirements", "api", {
|
|
1698
|
-
exists:
|
|
1779
|
+
exists: hasDnsRequirements,
|
|
1699
1780
|
expected: true,
|
|
1700
|
-
observed: dnsRecords.length
|
|
1701
|
-
|
|
1781
|
+
observed: dnsRecords.length > 0 ? dnsRecords.length : {
|
|
1782
|
+
serviceDomain: typeof live?.serviceDomain === "string" ? live.serviceDomain : null,
|
|
1783
|
+
verificationDnsHost: typeof live?.verificationDnsHost === "string" ? live.verificationDnsHost : null
|
|
1784
|
+
},
|
|
1785
|
+
issues: hasDnsRequirements ? [] : [`Railway custom domain ${domain || "(unset)"} did not expose DNS requirements.`]
|
|
1702
1786
|
})
|
|
1703
1787
|
]);
|
|
1704
1788
|
}
|
package/dist/sdk-types.d.ts
CHANGED
|
@@ -394,21 +394,21 @@ export interface TaskCreditLedgerEntry {
|
|
|
394
394
|
projectId: string;
|
|
395
395
|
workDayId: string;
|
|
396
396
|
taskId: string | null;
|
|
397
|
-
phase: 'seed' | 'settle' | 'refund' | 'grant' | 'reserve' | 'consume' | 'release' | 'adjustment';
|
|
397
|
+
phase: 'seed' | 'settle' | 'refund' | 'grant' | 'reserve' | 'consume' | 'release' | 'adjustment' | 'grant_created' | 'reservation_created' | 'reservation_released' | 'task_started' | 'task_completed_estimate_settlement' | 'task_completed_actual_settlement' | 'task_failed_refund' | 'manual_adjustment' | 'monthly_rollover' | 'overrun_hold';
|
|
398
398
|
credits: number;
|
|
399
399
|
metadata?: Record<string, unknown>;
|
|
400
400
|
createdAt: string;
|
|
401
401
|
}
|
|
402
402
|
export type CapacityProviderKind = 'treeseed_managed' | 'team_owned' | 'external' | 'hybrid';
|
|
403
|
-
export type CapacityProviderStatus = 'pending' | 'active' | 'paused' | 'configuration_required' | 'disabled';
|
|
403
|
+
export type CapacityProviderStatus = 'pending' | 'credential_required' | 'registering' | 'active' | 'degraded' | 'draining' | 'paused' | 'configuration_required' | 'disabled' | 'failed';
|
|
404
404
|
export type CapacityProviderBillingScope = 'treeseed' | 'team' | 'external';
|
|
405
405
|
export type CapacityBusinessModel = 'subscription_quota' | 'token_metered' | 'hybrid_usage_based' | 'infrastructure_runtime' | 'custom';
|
|
406
406
|
export type CapacityLaneUnit = 'treeseed_credit' | 'quota_minute' | 'token_usd' | 'github_ai_credit' | 'worker_second' | 'request' | 'custom';
|
|
407
407
|
export type CapacityScarcityLevel = 'low' | 'medium' | 'high';
|
|
408
408
|
export type CapacityGrantScope = 'team' | 'project' | 'workday' | 'overflow_pool';
|
|
409
409
|
export type CapacityGrantState = 'active' | 'paused' | 'expired' | 'disabled';
|
|
410
|
-
export type CapacityOverflowPolicy = 'hard_grant' | 'soft_grant' | 'weighted_fair_share' | 'approval_required';
|
|
411
|
-
export type CapacityReservationState = 'reserved' | 'consumed' | 'released' | 'expired' | 'cancelled';
|
|
410
|
+
export type CapacityOverflowPolicy = 'deny' | 'hard_grant' | 'soft_grant' | 'weighted_fair_share' | 'approval_required' | 'fallback_lane' | 'platform_subsidy';
|
|
411
|
+
export type CapacityReservationState = 'reserved' | 'consuming' | 'consumed' | 'released' | 'expired' | 'cancelled' | 'failed' | 'overran_pending_approval';
|
|
412
412
|
export type CapacityEstimatePhase = 'intent' | 'discovery' | 'plan' | 'execution' | 'actual';
|
|
413
413
|
export type CapacityEstimateConfidence = 'low' | 'medium' | 'high';
|
|
414
414
|
export type CapacityApprovalState = 'pending' | 'approved' | 'rejected' | 'expired' | 'superseded';
|
|
@@ -1667,6 +1667,7 @@ export interface RecordCapacityUsageRequest {
|
|
|
1667
1667
|
usd?: number | null;
|
|
1668
1668
|
source?: string;
|
|
1669
1669
|
metadata?: Record<string, unknown> | null;
|
|
1670
|
+
usageActual?: Record<string, unknown> | null;
|
|
1670
1671
|
}
|
|
1671
1672
|
export interface CreateCapacityRoutingDecisionRequest {
|
|
1672
1673
|
id?: string;
|