deepline 0.3.90 → 0.3.91
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/bundling-sources/sdk/src/client.ts +101 -6
- package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +2 -556
- package/dist/bundling-sources/sdk/src/monitor-fleets.ts +2 -2
- package/dist/bundling-sources/sdk/src/release.ts +7 -12
- package/dist/bundling-sources/sdk/src/types.ts +6 -1
- package/dist/bundling-sources/shared_libs/integration-contracts/monitor-fleet-contract.ts +549 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/errors.ts +96 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/limits.ts +8 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/plan.ts +320 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +106 -0
- package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +5 -1
- package/dist/bundling-sources/shared_libs/monitors/monitor-fleet-limits.ts +2 -12
- package/dist/bundling-sources/shared_libs/monitors/package.json +107 -5
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +12 -7
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +36 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/db-session-lease-store.ts +578 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +521 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-receipt-gateway.ts +1378 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/runtime-topology.ts +1024 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-gateway-route-probe.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machine-lifecycle.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +1844 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/process-tree.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +262 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-surface.ts +98 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +303 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-publication-guardian.ts +70 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-recovery.ts +519 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-fly-rollback-alignment.ts +280 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-production-gate.ts +159 -0
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/memory-store.ts +114 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +44 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/child-play-resolver.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +3662 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +84 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +91 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +424 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +321 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +110 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +625 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +46 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +50 -0
- package/dist/bundling-sources/shared_libs/plays/bundling/repository-adapter.ts +24 -0
- package/dist/bundling-sources/shared_libs/plays/child-run-projection.ts +174 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +267 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/control-plane-types.ts +73 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +162 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +87 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/source-file-path.ts +26 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/target-paths.ts +36 -0
- package/dist/bundling-sources/shared_libs/plays/launch/input-contract.ts +400 -0
- package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +96 -0
- package/dist/bundling-sources/shared_libs/plays/local-file-discovery.ts +385 -0
- package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +79 -6
- package/dist/bundling-sources/shared_libs/plays/play-reference.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/run-input-summary.ts +260 -0
- package/dist/bundling-sources/shared_libs/plays/run-summary-types.ts +49 -0
- package/dist/bundling-sources/shared_libs/plays/secrets/errors.ts +19 -0
- package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +347 -0
- package/dist/bundling-sources/shared_libs/plays/standard-webhooks.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/status-result.ts +388 -0
- package/dist/bundling-sources/shared_libs/plays/transform-factor-structure.ts +377 -0
- package/dist/bundling-sources/shared_libs/plays/webhook-payload.ts +113 -0
- package/dist/cli/index.js +369 -162
- package/dist/cli/index.mjs +369 -162
- package/dist/{compiler-manifest-BaJfE-5H.d.mts → compiler-manifest-x14KL83E.d.mts} +1 -1
- package/dist/{compiler-manifest-BaJfE-5H.d.ts → compiler-manifest-x14KL83E.d.ts} +1 -1
- package/dist/index.d.mts +45 -8
- package/dist/index.d.ts +45 -8
- package/dist/index.js +61 -14
- package/dist/index.mjs +61 -14
- package/dist/install-integrity.json +54 -2
- package/dist/monitor-fleet-contract.d.mts +3 -10
- package/dist/monitor-fleet-contract.d.ts +3 -10
- package/dist/monitor-fleet-contract.js +2 -0
- package/dist/monitor-fleet-contract.mjs +1 -1
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/release.d.mts +3 -3
- package/dist/release.d.ts +3 -3
- package/dist/release.js +7 -12
- package/dist/release.mjs +7 -12
- package/package.json +2 -2
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side enforcement of the play input contract at run creation.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct guards, both intended to run BEFORE any run/billing artifact is
|
|
5
|
+
* created (see `apps/deepline-api/src/app/api/v2/plays/run/route.ts` `startPlayRunForAuth`):
|
|
6
|
+
*
|
|
7
|
+
* 1. `validateScalarInputAgainstSchema` — validates the scalar `--input`
|
|
8
|
+
* envelope against the play's declared `inputSchema`. Enforces
|
|
9
|
+
* `additionalProperties: false` (unknown keys are rejected), the declared
|
|
10
|
+
* JSON-schema `type` of each declared property, declared string
|
|
11
|
+
* `minLength`/`maxLength` (so `csv: ""` against `minLength:1` is a 400 rather
|
|
12
|
+
* than a paid failed run), and CSV-vs-scalar mutual exclusion (a non-empty
|
|
13
|
+
* `csv` path may not coexist with scalar data fields). This is a HARD
|
|
14
|
+
* CUTOVER: no warn phase, env flag, or compatibility shim. Previously nothing
|
|
15
|
+
* validated scalar input server-side, so `--input '{"foo":"bar"}'` against a
|
|
16
|
+
* play with `additionalProperties: false` silently exported `foo`, and a
|
|
17
|
+
* `csv` alongside scalar fields silently dropped the scalars.
|
|
18
|
+
*
|
|
19
|
+
* SCOPE: scalar envelope keys ONLY. CSV column passthrough is BY DESIGN
|
|
20
|
+
* (source-column preservation). CSV columns never appear in the scalar input
|
|
21
|
+
* object — the scalar envelope for a CSV play declares `csv` (a string path)
|
|
22
|
+
* and `columns` (an alias map), while real CSV header names live at the row
|
|
23
|
+
* level (`csvInput.rowInputSchema`, `additionalProperties: true`). Validating
|
|
24
|
+
* the scalar envelope therefore never touches CSV column names.
|
|
25
|
+
*
|
|
26
|
+
* 2. `assertCsvHasDataRows` — rejects a CSV that has a header row but zero data
|
|
27
|
+
* rows, before the run is created/charged. A header-only CSV otherwise
|
|
28
|
+
* creates a paid failed run (~$0.02).
|
|
29
|
+
*
|
|
30
|
+
* Both helpers return a structured failure compatible with the run route's
|
|
31
|
+
* `PlayRunStartFailure` envelope (`{ status, error, details? }`) instead of
|
|
32
|
+
* throwing, so the route can surface a clean 400 to the client.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export type PlayInputContractFailure = {
|
|
36
|
+
status: 400;
|
|
37
|
+
error: string;
|
|
38
|
+
details?: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type DeclaredInputSchema = {
|
|
42
|
+
type?: unknown;
|
|
43
|
+
additionalProperties?: unknown;
|
|
44
|
+
properties?: unknown;
|
|
45
|
+
required?: unknown;
|
|
46
|
+
/**
|
|
47
|
+
* Prebuilt CSV plays carry a top-level `csvInput` descriptor. We read
|
|
48
|
+
* `csvInput.inputField` to learn which declared property is the CSV path
|
|
49
|
+
* (defaults to `"csv"`), so the csv-vs-scalar mutual-exclusion check is driven
|
|
50
|
+
* by the schema rather than a hardcoded key name.
|
|
51
|
+
*/
|
|
52
|
+
csvInput?: unknown;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Declared property names that are CSV-control inputs (not row data): the alias
|
|
57
|
+
* map that remaps source CSV headers onto canonical play fields. These may
|
|
58
|
+
* coexist with the CSV path. Derived structurally below; this constant just
|
|
59
|
+
* names the well-known control key in the prebuilt schema shape.
|
|
60
|
+
*/
|
|
61
|
+
const CSV_CONTROL_KEYS = new Set<string>(['columns']);
|
|
62
|
+
|
|
63
|
+
function csvControlKeys(schema: Record<string, unknown>): Set<string> {
|
|
64
|
+
const keys = new Set(CSV_CONTROL_KEYS);
|
|
65
|
+
const csvInput = schema.csvInput;
|
|
66
|
+
if (!isPlainObject(csvInput)) return keys;
|
|
67
|
+
const runLevelControls = csvInput.runLevelControlFields;
|
|
68
|
+
if (!Array.isArray(runLevelControls)) return keys;
|
|
69
|
+
for (const field of runLevelControls) {
|
|
70
|
+
if (typeof field === 'string' && field.trim()) {
|
|
71
|
+
keys.add(field.trim());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return keys;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve which declared scalar property is the CSV-path input for this schema.
|
|
79
|
+
* Driven by `csvInput.inputField`; absent that descriptor the schema is not a
|
|
80
|
+
* CSV play and there is no csv key.
|
|
81
|
+
*/
|
|
82
|
+
function csvInputFieldName(schema: Record<string, unknown>): string | null {
|
|
83
|
+
const csvInput = schema.csvInput;
|
|
84
|
+
if (!isPlainObject(csvInput)) return null;
|
|
85
|
+
const field = csvInput.inputField;
|
|
86
|
+
return typeof field === 'string' && field.length > 0 ? field : 'csv';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The JSON-schema `type` keyword we know how to check at the scalar envelope
|
|
91
|
+
* level. Anything outside this set (e.g. union schemas via `anyOf`) is left
|
|
92
|
+
* un-type-checked here; presence/`additionalProperties` is still enforced.
|
|
93
|
+
*/
|
|
94
|
+
type SimpleJsonType =
|
|
95
|
+
| 'object'
|
|
96
|
+
| 'array'
|
|
97
|
+
| 'string'
|
|
98
|
+
| 'number'
|
|
99
|
+
| 'integer'
|
|
100
|
+
| 'boolean'
|
|
101
|
+
| 'null';
|
|
102
|
+
|
|
103
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
104
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function jsonTypeOf(value: unknown): SimpleJsonType {
|
|
108
|
+
if (value === null) return 'null';
|
|
109
|
+
if (Array.isArray(value)) return 'array';
|
|
110
|
+
const t = typeof value;
|
|
111
|
+
if (t === 'object') return 'object';
|
|
112
|
+
if (t === 'string') return 'string';
|
|
113
|
+
if (t === 'boolean') return 'boolean';
|
|
114
|
+
if (t === 'number') {
|
|
115
|
+
return Number.isInteger(value as number) ? 'integer' : 'number';
|
|
116
|
+
}
|
|
117
|
+
// Functions / undefined / symbol cannot appear in parsed JSON input.
|
|
118
|
+
return 'object';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function typeMatches(
|
|
122
|
+
received: SimpleJsonType,
|
|
123
|
+
expected: SimpleJsonType,
|
|
124
|
+
): boolean {
|
|
125
|
+
if (received === expected) return true;
|
|
126
|
+
// A JSON `integer` value satisfies a declared `number`.
|
|
127
|
+
if (expected === 'number' && received === 'integer') return true;
|
|
128
|
+
// A whole-valued `number` satisfies a declared `integer`.
|
|
129
|
+
if (expected === 'integer' && received === 'number') return true;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function declaredTypeFor(propertySchema: unknown): SimpleJsonType | null {
|
|
134
|
+
if (!isPlainObject(propertySchema)) return null;
|
|
135
|
+
const declared = propertySchema.type;
|
|
136
|
+
if (typeof declared !== 'string') return null;
|
|
137
|
+
switch (declared) {
|
|
138
|
+
case 'object':
|
|
139
|
+
case 'array':
|
|
140
|
+
case 'string':
|
|
141
|
+
case 'number':
|
|
142
|
+
case 'integer':
|
|
143
|
+
case 'boolean':
|
|
144
|
+
case 'null':
|
|
145
|
+
return declared;
|
|
146
|
+
default:
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validate the scalar `--input` envelope against the play's declared
|
|
153
|
+
* `inputSchema`. Returns `null` when the input is acceptable, otherwise a 400
|
|
154
|
+
* failure describing the first violation class encountered (unknown keys, then
|
|
155
|
+
* type mismatches).
|
|
156
|
+
*
|
|
157
|
+
* When `schema` is null/absent, or does not declare `additionalProperties:
|
|
158
|
+
* false` and has no checkable property types, this is a no-op (returns null) —
|
|
159
|
+
* plays without a strict schema are unaffected, per the task contract.
|
|
160
|
+
*/
|
|
161
|
+
export function validateScalarInputAgainstSchema(input: {
|
|
162
|
+
scalarInput: Record<string, unknown>;
|
|
163
|
+
schema: DeclaredInputSchema | null | undefined;
|
|
164
|
+
playName?: string | null;
|
|
165
|
+
}): PlayInputContractFailure | null {
|
|
166
|
+
const { scalarInput, schema } = input;
|
|
167
|
+
if (!isPlainObject(schema)) return null;
|
|
168
|
+
|
|
169
|
+
const properties = isPlainObject(schema.properties) ? schema.properties : {};
|
|
170
|
+
const allowedKeys = Object.keys(properties);
|
|
171
|
+
const additionalPropsForbidden = schema.additionalProperties === false;
|
|
172
|
+
|
|
173
|
+
const inputKeys = Object.keys(scalarInput);
|
|
174
|
+
|
|
175
|
+
// 1. additionalProperties: false → reject unknown scalar keys.
|
|
176
|
+
if (additionalPropsForbidden) {
|
|
177
|
+
const unknownKeys = inputKeys.filter((key) => !(key in properties));
|
|
178
|
+
if (unknownKeys.length > 0) {
|
|
179
|
+
const playLabel = input.playName ? ` for play "${input.playName}"` : '';
|
|
180
|
+
return {
|
|
181
|
+
status: 400,
|
|
182
|
+
error: `Unknown input ${
|
|
183
|
+
unknownKeys.length === 1 ? 'key' : 'keys'
|
|
184
|
+
}${playLabel}: ${unknownKeys
|
|
185
|
+
.map((key) => `"${key}"`)
|
|
186
|
+
.join(', ')}. Allowed ${
|
|
187
|
+
allowedKeys.length === 1 ? 'key is' : 'keys are'
|
|
188
|
+
}: ${
|
|
189
|
+
allowedKeys.length > 0
|
|
190
|
+
? allowedKeys.map((key) => `"${key}"`).join(', ')
|
|
191
|
+
: '(none)'
|
|
192
|
+
}.`,
|
|
193
|
+
details: {
|
|
194
|
+
unknownKeys,
|
|
195
|
+
allowedKeys,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 2. CSV-vs-scalar mutual exclusion. For a CSV-backed play, the scalar
|
|
202
|
+
// envelope either supplies a CSV path (`csv`) OR per-row scalar data fields
|
|
203
|
+
// — never both. Production run 20260611t145724-650 sent a non-empty `csv`
|
|
204
|
+
// AND scalar data fields (linkedin_url, current_domain, contact_full_name)
|
|
205
|
+
// and the run silently used only the CSV. The csv key and the control keys
|
|
206
|
+
// (e.g. `columns`, the header-alias map) may coexist; any OTHER declared
|
|
207
|
+
// property supplied alongside a non-empty csv is a conflicting data field.
|
|
208
|
+
const csvKey = csvInputFieldName(schema);
|
|
209
|
+
if (csvKey && csvKey in properties) {
|
|
210
|
+
const csvControls = csvControlKeys(schema);
|
|
211
|
+
const csvValue = scalarInput[csvKey];
|
|
212
|
+
const csvSupplied =
|
|
213
|
+
typeof csvValue === 'string'
|
|
214
|
+
? csvValue.length > 0
|
|
215
|
+
: csvValue !== undefined && csvValue !== null;
|
|
216
|
+
if (csvSupplied) {
|
|
217
|
+
const conflictingKeys = inputKeys.filter(
|
|
218
|
+
(key) =>
|
|
219
|
+
key !== csvKey &&
|
|
220
|
+
!csvControls.has(key) &&
|
|
221
|
+
key in properties &&
|
|
222
|
+
scalarInput[key] !== undefined,
|
|
223
|
+
);
|
|
224
|
+
if (conflictingKeys.length > 0) {
|
|
225
|
+
const playLabel = input.playName ? ` for play "${input.playName}"` : '';
|
|
226
|
+
return {
|
|
227
|
+
status: 400,
|
|
228
|
+
error: `Conflicting input${playLabel}: "${csvKey}" and scalar data ${
|
|
229
|
+
conflictingKeys.length === 1 ? 'field' : 'fields'
|
|
230
|
+
} ${conflictingKeys
|
|
231
|
+
.map((key) => `"${key}"`)
|
|
232
|
+
.join(
|
|
233
|
+
', ',
|
|
234
|
+
)} are mutually exclusive. Put the values in the CSV or remove "${csvKey}".`,
|
|
235
|
+
details: {
|
|
236
|
+
csvKey,
|
|
237
|
+
conflictingKeys,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 3. Type mismatches and string-length constraints on declared properties
|
|
245
|
+
// (e.g. custom_fields: string when the schema declares object; an empty
|
|
246
|
+
// `csv: ""` against a declared minLength:1). Only checks keys the caller
|
|
247
|
+
// actually supplied; required-field enforcement is left to downstream
|
|
248
|
+
// runtime validation (the play's own input contract), matching prior
|
|
249
|
+
// behavior.
|
|
250
|
+
for (const key of inputKeys) {
|
|
251
|
+
if (!(key in properties)) continue; // unknown keys handled above
|
|
252
|
+
const propertySchema = properties[key];
|
|
253
|
+
const expected = declaredTypeFor(propertySchema);
|
|
254
|
+
const value = scalarInput[key];
|
|
255
|
+
// Optional fields explicitly set to null/undefined are not checked here;
|
|
256
|
+
// absence is handled by required-field validation downstream.
|
|
257
|
+
if (value === undefined) continue;
|
|
258
|
+
|
|
259
|
+
if (expected) {
|
|
260
|
+
const received = jsonTypeOf(value);
|
|
261
|
+
if (!typeMatches(received, expected)) {
|
|
262
|
+
const playLabel = input.playName ? ` for play "${input.playName}"` : '';
|
|
263
|
+
return {
|
|
264
|
+
status: 400,
|
|
265
|
+
error: `Invalid type for input field "${key}"${playLabel}: expected ${expected}, received ${received}.`,
|
|
266
|
+
details: {
|
|
267
|
+
field: key,
|
|
268
|
+
expectedType: expected,
|
|
269
|
+
receivedType: received,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// String-length constraints. Generalized from the schema — production run
|
|
276
|
+
// 20260611t150001-525 passed `csv: ""` against a declared minLength:1 and
|
|
277
|
+
// created a paid failed run ("no asset binding"). Enforced for any declared
|
|
278
|
+
// string property carrying minLength/maxLength.
|
|
279
|
+
if (typeof value === 'string' && isPlainObject(propertySchema)) {
|
|
280
|
+
const lengthFailure = checkStringLength({
|
|
281
|
+
key,
|
|
282
|
+
value,
|
|
283
|
+
propertySchema,
|
|
284
|
+
playName: input.playName ?? null,
|
|
285
|
+
});
|
|
286
|
+
if (lengthFailure) return lengthFailure;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Enforce declared `minLength` / `maxLength` for a supplied string value.
|
|
295
|
+
* Returns a 400 failure naming the field and the violated constraint, otherwise
|
|
296
|
+
* null. Only fires when the property schema declares the bound as a number.
|
|
297
|
+
*/
|
|
298
|
+
function checkStringLength(input: {
|
|
299
|
+
key: string;
|
|
300
|
+
value: string;
|
|
301
|
+
propertySchema: Record<string, unknown>;
|
|
302
|
+
playName: string | null;
|
|
303
|
+
}): PlayInputContractFailure | null {
|
|
304
|
+
const { key, value, propertySchema, playName } = input;
|
|
305
|
+
const playLabel = playName ? ` for play "${playName}"` : '';
|
|
306
|
+
|
|
307
|
+
const minLength = propertySchema.minLength;
|
|
308
|
+
if (typeof minLength === 'number' && value.length < minLength) {
|
|
309
|
+
return {
|
|
310
|
+
status: 400,
|
|
311
|
+
error:
|
|
312
|
+
minLength === 1
|
|
313
|
+
? `${key} must be a non-empty string (minLength 1)${playLabel}.`
|
|
314
|
+
: `${key} must be at least ${minLength} characters (minLength ${minLength})${playLabel}; received length ${value.length}.`,
|
|
315
|
+
details: {
|
|
316
|
+
field: key,
|
|
317
|
+
constraint: 'minLength',
|
|
318
|
+
minLength,
|
|
319
|
+
receivedLength: value.length,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const maxLength = propertySchema.maxLength;
|
|
325
|
+
if (typeof maxLength === 'number' && value.length > maxLength) {
|
|
326
|
+
return {
|
|
327
|
+
status: 400,
|
|
328
|
+
error: `${key} must be at most ${maxLength} characters (maxLength ${maxLength})${playLabel}; received length ${value.length}.`,
|
|
329
|
+
details: {
|
|
330
|
+
field: key,
|
|
331
|
+
constraint: 'maxLength',
|
|
332
|
+
maxLength,
|
|
333
|
+
receivedLength: value.length,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Inspect raw CSV bytes and reject a CSV that has a header row but zero data
|
|
343
|
+
* rows. Returns null when the CSV has at least one data row (or when the bytes
|
|
344
|
+
* are not parseable as a header-bearing CSV — that is left to the runtime CSV
|
|
345
|
+
* parser to surface, so this guard is strictly additive and never masks other
|
|
346
|
+
* failures).
|
|
347
|
+
*
|
|
348
|
+
* "Data row" = any non-empty line after the first (header) line. Trailing
|
|
349
|
+
* blank lines and a trailing newline after the header are ignored.
|
|
350
|
+
*/
|
|
351
|
+
export function assertCsvHasDataRows(input: {
|
|
352
|
+
csvText: string;
|
|
353
|
+
logicalPath?: string | null;
|
|
354
|
+
}): PlayInputContractFailure | null {
|
|
355
|
+
const { csvText } = input;
|
|
356
|
+
if (typeof csvText !== 'string') return null;
|
|
357
|
+
|
|
358
|
+
// Normalize line endings, then split. A header-only file is one where every
|
|
359
|
+
// line after the first is empty (or there is only a header line).
|
|
360
|
+
const lines = csvText.replace(/\r\n?/g, '\n').split('\n');
|
|
361
|
+
if (lines.length === 0) return null;
|
|
362
|
+
|
|
363
|
+
// No header at all (empty file) — leave to the runtime parser; not our guard.
|
|
364
|
+
const header = lines[0];
|
|
365
|
+
if (!header || header.trim().length === 0) return null;
|
|
366
|
+
|
|
367
|
+
const hasDataRow = lines.slice(1).some((line) => line.trim().length > 0);
|
|
368
|
+
|
|
369
|
+
if (hasDataRow) return null;
|
|
370
|
+
|
|
371
|
+
const pathLabel = input.logicalPath ? ` ("${input.logicalPath}")` : '';
|
|
372
|
+
return {
|
|
373
|
+
status: 400,
|
|
374
|
+
error: `csv has a header row but no data rows${pathLabel}.`,
|
|
375
|
+
details: {
|
|
376
|
+
...(input.logicalPath ? { logicalPath: input.logicalPath } : {}),
|
|
377
|
+
reason: 'csv_header_only',
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Decode a base64-encoded CSV upload to text for the zero-row backstop. Returns
|
|
384
|
+
* null when the content type is not CSV or the payload cannot be decoded
|
|
385
|
+
* (decoding failure is surfaced by the runtime, not masked here).
|
|
386
|
+
*/
|
|
387
|
+
export function decodeCsvUploadText(input: {
|
|
388
|
+
contentType: string;
|
|
389
|
+
contentBase64: string;
|
|
390
|
+
}): string | null {
|
|
391
|
+
const contentType = input.contentType.toLowerCase();
|
|
392
|
+
const isCsv =
|
|
393
|
+
contentType.includes('text/csv') || contentType.includes('application/csv');
|
|
394
|
+
if (!isCsv) return null;
|
|
395
|
+
try {
|
|
396
|
+
return Buffer.from(input.contentBase64, 'base64').toString('utf-8');
|
|
397
|
+
} catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { PlayBundleArtifact } from '../artifact-types';
|
|
2
|
+
import {
|
|
3
|
+
normalizePlayContractCompatibility,
|
|
4
|
+
type NormalizedPlayContractCompatibilitySnapshot,
|
|
5
|
+
type PlayRunContractSnapshot,
|
|
6
|
+
} from '../contracts';
|
|
7
|
+
import { stripInlineSourceMapFromArtifact } from '../artifact-transport';
|
|
8
|
+
import type { PlaySandboxRuntimeLimits } from '../sandbox-runtime-limits';
|
|
9
|
+
import type { AdmittedPlayAuthoringContract } from '../authoring-contract';
|
|
10
|
+
|
|
11
|
+
const MAX_INLINE_PLAY_ARTIFACT_BYTES = 1_500_000;
|
|
12
|
+
|
|
13
|
+
export type PlayRunLaunchValidationSnapshot = {
|
|
14
|
+
sourceCode: string;
|
|
15
|
+
staticPipeline: unknown;
|
|
16
|
+
artifactHash: string;
|
|
17
|
+
graphHash: string;
|
|
18
|
+
maxCreditsPerRun: number | null;
|
|
19
|
+
sandboxRuntimeLimits: PlaySandboxRuntimeLimits;
|
|
20
|
+
authoringContract: AdmittedPlayAuthoringContract;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type PreparedPlayRunContractSnapshot = PlayRunContractSnapshot & {
|
|
24
|
+
compatibility: NormalizedPlayContractCompatibilitySnapshot;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function runtimeArtifactForSchedulerInput(
|
|
28
|
+
artifact: PlayBundleArtifact | null | undefined,
|
|
29
|
+
): PlayBundleArtifact | null {
|
|
30
|
+
if (!artifact) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const bytes = Buffer.byteLength(JSON.stringify(artifact), 'utf-8');
|
|
34
|
+
if (bytes > MAX_INLINE_PLAY_ARTIFACT_BYTES) {
|
|
35
|
+
console.info('[perf][plays.start_run]', {
|
|
36
|
+
phase: 'runtime_artifact_inline_skipped',
|
|
37
|
+
ms: 0,
|
|
38
|
+
artifactBytes: bytes,
|
|
39
|
+
maxBytes: MAX_INLINE_PLAY_ARTIFACT_BYTES,
|
|
40
|
+
});
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// The scheduler prefers this inline artifact to the stored copy. Keep its
|
|
44
|
+
// map so a Daytona runner can map user errors without an extra artifact read;
|
|
45
|
+
// the size gate above already accounts for the complete artifact.
|
|
46
|
+
const compacted = stripInlineSourceMapFromArtifact(artifact);
|
|
47
|
+
console.info('[perf][plays.start_run]', {
|
|
48
|
+
phase: 'runtime_artifact_inline_compacted',
|
|
49
|
+
ms: 0,
|
|
50
|
+
originalArtifactBytes: bytes,
|
|
51
|
+
compactedArtifactBytes: Buffer.byteLength(
|
|
52
|
+
JSON.stringify(compacted),
|
|
53
|
+
'utf-8',
|
|
54
|
+
),
|
|
55
|
+
});
|
|
56
|
+
return compacted;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildPlayRunContractSnapshot(input: {
|
|
60
|
+
source?: 'published' | 'ad_hoc' | 'draft';
|
|
61
|
+
artifactStorageKey: string;
|
|
62
|
+
artifact: Pick<PlayBundleArtifact, 'compatibility'>;
|
|
63
|
+
validatedSnapshot: PlayRunLaunchValidationSnapshot;
|
|
64
|
+
transportStaticPipeline?: unknown;
|
|
65
|
+
}): PreparedPlayRunContractSnapshot {
|
|
66
|
+
// The runtime backend is launch infrastructure, not an artifact contract.
|
|
67
|
+
// Copy the admitted artifact compatibility so resumed work cannot acquire
|
|
68
|
+
// newer semantics merely because the runtime was redeployed.
|
|
69
|
+
const compatibility = normalizePlayContractCompatibility(
|
|
70
|
+
input.artifact.compatibility,
|
|
71
|
+
);
|
|
72
|
+
const staticPipeline =
|
|
73
|
+
input.transportStaticPipeline ?? input.validatedSnapshot.staticPipeline;
|
|
74
|
+
return {
|
|
75
|
+
source: input.source ?? 'published',
|
|
76
|
+
revisionVersion: null,
|
|
77
|
+
staticPipeline,
|
|
78
|
+
billingLimit:
|
|
79
|
+
typeof input.validatedSnapshot.maxCreditsPerRun === 'number'
|
|
80
|
+
? { maxCreditsPerRun: input.validatedSnapshot.maxCreditsPerRun }
|
|
81
|
+
: null,
|
|
82
|
+
runtimeLimit: input.validatedSnapshot.sandboxRuntimeLimits,
|
|
83
|
+
sourceCode: input.validatedSnapshot.sourceCode,
|
|
84
|
+
artifactMetadata: {
|
|
85
|
+
storageKey: input.artifactStorageKey,
|
|
86
|
+
artifactHash: input.validatedSnapshot.artifactHash,
|
|
87
|
+
graphHash: input.validatedSnapshot.graphHash,
|
|
88
|
+
},
|
|
89
|
+
codeFormat: 'cjs_module',
|
|
90
|
+
compatibility,
|
|
91
|
+
authoringContract: {
|
|
92
|
+
...input.validatedSnapshot.authoringContract,
|
|
93
|
+
staticPipeline,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|